RBAC V2 — CI lint gates
Issue: #1480
Script: scripts/ci/lint_rbac_legacy_callsites.sh
Workflow job: rbac-legacy-lint in .github/workflows/ci-pr.yml
Gate added: 2026-06-30
Purpose
After the S3–S10 RBAC V2 cutover (#1472, #1498, #1499), the legacy
@require_role decorator and inline users.role == comparisons were removed
from all Console blueprints and Raptor routes. This CI gate prevents those
patterns from reappearing via future PRs.
What the gate checks
Check 1 — Console blueprints + middleware: @require_role active decorator
Scope: console/app/blueprints/ and console/app/middleware/
Pattern: ^[[:space:]]*@require_role\( (extended regex)
Matches only lines where @require_role( is the first non-whitespace token
on the line — i.e. an active Python decorator call. The pattern intentionally
excludes:
- Lines inside docstrings or module-level comments (they do not start with
@) - Commented-out code (
# @require_role(...)) — the#is the first token - The new decorator
@require_rbac_role(— the string@require_role(is not a substring of@require_rbac_role((they diverge at character 10:ovsb)
What to use instead: @require_rbac_role or @require_permission from
app.middleware.rbac.
Check 2 — Raptor routes: users.role == hardcoded comparison
Scope: backend_v2/api/routes/
Pattern: users\.role[[:space:]]*== (extended regex)
Matches direct role comparisons that bypass the RBAC V2 has_permission() /
require_rbac_role model.
What to use instead: has_permission(g.admin, "<permission>") or the
@require_rbac_role decorator from app.middleware.rbac.
Tests directory exclusion
Both checks exclude */tests/* sub-directories via --exclude-dir=tests.
Test fixtures may legitimately reference legacy patterns for
backward-compatibility assertions.
Suppressing a false positive (# rbac-lint-ok)
If a line is a genuine exception (test fixture, migration compat shim, or
external interface constraint), add the trailer comment # rbac-lint-ok to
the same line as the callsite:
@require_role("superadmin") # rbac-lint-ok — compat shim for legacy client, see #9999
Requirements for suppression:
- The
# rbac-lint-okcomment must be on the same line as the callsite. - Include the issue number that approved the exception.
- The PR introducing the suppression requires explicit code-review sign-off on the exception — do not suppress silently.
- Document the exemption in the PR description under a heading
## rbac-lint-ok suppressions.
Suppressions are auditable: git grep '# rbac-lint-ok' shows all active
exceptions at a glance.
Running locally
bash scripts/ci/lint_rbac_legacy_callsites.sh
Expected output on a clean tree:
Check 1: @require_role active decorator in console blueprints + middleware
OK — no active @require_role decorators found in Console scope
Check 2: users.role == hardcoded comparison in Raptor routes
OK — no users.role == comparisons found in Raptor routes
lint_rbac_legacy_callsites: all checks passed.
CI workflow integration
The gate runs as rbac-legacy-lint in ci-pr.yml, triggered when a PR
touches:
console/app/blueprints/**console/app/middleware/**backend_v2/api/routes/**
The job is path-filtered so it does not incur cost on unrelated PRs.
Future expansion (Phase 2+)
When new surfaces are added (Reasonator routes, Velvet endpoints), extend the
script with an additional grep block following the same pattern. Update this
doc to list the new surface and its grep pattern.