Repository navigation
[P2][security][needs-validation] RBAC silently turns itself off when the licence tier falls below Pro in enforced mode #2092
Description
Source: security-audit skill, run data-boar-run-1 (profile quick, source-only: no target code was executed), snapshot origin/main @ 7f354a34, 2026-10-18. Each trace below was found by a hunter agent and then re-read in the snapshot by an independent verifier. Status: needs validation — the source trace holds, the runtime result is not observed. Please start with the bounded local test in the plan; if it reproduces, turn it into a regression test and fix, otherwise close with the evidence.
RBAC enforcement turns itself off, widening access, when the licence tier falls back to Community or Std
rbac_enforcement_active() returns true only if api.rbac.enabled is set and the runtime tier allows dashboard_rbac (Pro or higher; Tier.OPEN always allows it). In licensing.mode=enforced, get_runtime_tier_for_features() returns Tier.COMMUNITY for any state other than VALID/GRACE. It also returns Community for a valid licence with no dbtier claim, and Std for a std/standard dbtier. When the tier is below Pro, rbac_middleware_handler calls call_next() before any role classification or check, and _authorize_audit_log_download() returns [] with no principal check. A principal that was restricted (a WebAuthn session with a narrow roles_json, or the shared API key with narrow api.rbac.api_key_roles) then has the same access as an unrestricted one on every role-gated route. That includes GET/POST /{locale}/config (the YAML editor, which writes the runtime config) and audit-log download. The loss is reported only as enterprise_surface.access_surface.rbac='not_implemented' in /status and /health. Nothing blocks or refuses access. Source confirms the trace. The docs (USAGE.md, ADR-0082, the rbac_settings docstring and the /logs docstring) say RBAC is inactive when the tier does not allow dashboard_rbac and that Community cannot enable RBAC. They do not address a previously configured RBAC policy silently lapsing when a Pro licence goes EXPIRED beyond grace, REVOKED, MACHINE_MISMATCH, TAMPERED or UNLICENSED, or when a hostname change breaks a bound licence. This contrasts with the module's own stated fail-closed contract (#719) for tier fallback. The effect is a loss of least-privilege separation among principals who already hold the API key or a passkey. It is not a pre-authentication bypass, and the fallback itself is operator or time driven rather than attacker triggered. The finding is not covered by #2077 to #2089. Whether it matters depends on whether any deployment combines enforced mode, api.rbac.enabled and role-restricted principals.
Root cause. A commercial tier fallback is reused as an authorisation switch. rbac_enforcement_active() derives 'is RBAC on' from the licence tier. When the licence state degrades in enforced mode, the configured default-deny role policy fails open (middleware passes through, audit-log gate returns early) instead of continuing to enforce or denying non-public routes.
Trace.
api/rbac.py:253(entrypoint) — Every HTTP request passes through the innermost RBAC middleware handler when it is installed; the caller holds the API key or a passkey session with a restricted role set.api/rbac.py:260(propagation) —if not rbac_enforcement_active(cfg): return await call_next(request)returns before classify_route_rbac and principal_allows, so no role check runs.core/rbac_settings.py:21(propagation) — Returns is_feature_available('dashboard_rbac', get_runtime_tier_for_features(config)) once api.rbac.enabled is set, so RBAC activity depends on the licence-derived tier.core/licensing/runtime_feature_tier.py:105(propagation) — In enforced mode, any state other than VALID/GRACE is audited as tier_fail_closed and returns Tier.COMMUNITY at line 123. A valid licence without a dbtier claim also returns Community (line 100).core/licensing/tier_features.py:120(propagation) — dashboard_rbac maps to Tier.PRO; is_feature_available (line 181) denies Community and Std and allows only Tier.OPEN unconditionally.api/routes.py:981(sink) — When rbac_enforcement_active() is false the function returns [] with no principal or permission check, so /logs download is open to anyone who passes the API-key middleware. The RBAC middleware pass-through at api/rbac.py:260 does the same for all other role-gated routes, including GET/POST /{locale}/config.
What blocks confirmation.
- Whether any deployment runs licensing.mode=enforced with api.rbac.enabled=true and role-restricted principals (narrow roles_json or api_key_roles) is a deployed-configuration fact outside the repository.
- Whether the maintainers intend a lapsed or non-Pro licence to silently drop a configured RBAC policy (documented 'Community cannot enable RBAC' versus the fail-closed contract of [P1][bug] licensing: DATA_BOAR_ENV=development bypassa todo enforcement — sem aviso nem audit log em produção #719) is a product-intent decision that source cannot settle.
- No sandboxed execution is approved for this run, so the observed behaviour with a degraded licence state has not been reproduced.
Bounded local test (dummy data, sandbox, no network). Run only in an approved sandbox with a dummy config, no network and a scratch SQLite database. Build a FastAPI TestClient with api.rbac.enabled=true, api.require_api_key=true, a dummy api.api_key, api.rbac.api_key_roles=['reports_reader'] and api.audit_logs.enabled with a scratch directory. Case A: licensing.mode=open, no effective_tier (Tier.OPEN). Send the dummy key to GET /en/config and GET /logs/; expect 403 (RBAC enforced). Case B: licensing.mode=enforced with no licence file (UNLICENSED, Tier.COMMUNITY), same requests; expect the 403 to disappear (200, or 404 for a missing session). Case C: enforced mode with a Pro licence signed by a test-only anchor (if the test harness supports it); expect 403 again. Do not use POST /scan to compare, because an enforced non-valid licence may block scans for an unrelated reason. Test GET /{locale}/config and /logs instead. Do not POST a config.
Owner-observed check. Owner check, read-only: on any deployment with licensing.mode=enforced and api.rbac.enabled=true, read GET /status -> enterprise_surface.access_surface.rbac and the license state. Search the LICENSE_AUDIT log for 'tier_fail_closed' events. If the licence state is not VALID/GRACE, or the dbtier claim is below pro, and rbac shows 'not_implemented', then the configured roles are not being enforced. Compare with the roles_json or api_key_roles in use. No traffic beyond the operator's own status read is needed.
Suggested direction (recommendation). Decouple 'is RBAC enforced' from the commercial tier once api.rbac.enabled is set: keep default-deny when the licence degrades, or refuse to start / deny non-public routes with a loud audit event. Keep 'Community cannot enable RBAC' for the initial enable, if that is the intent. Product-owner decision needed.
Activity
FabioLeitao commentedon Oct 20, 2026
Resolvida pela PR #2114 (merge 87f1fe0). Commit f1cd0ea. Testes em tests/test_rbac.py: test_rbac_enforcement_stays_closed_when_tier_drops e test_rbac_community_yaml_enabled_stays_fail_closed.