Skip to content

fix(rbac): gate Auditor View on audit:read instead of role string (CS-189)#2581

Merged
tofikwest merged 12 commits intomainfrom
tofik/cs-189-auditor-view-permission-gating
Apr 20, 2026
Merged

fix(rbac): gate Auditor View on audit:read instead of role string (CS-189)#2581
tofikwest merged 12 commits intomainfrom
tofik/cs-189-auditor-view-permission-gating

Conversation

@tofikwest
Copy link
Copy Markdown
Contributor

@tofikwest tofikwest commented Apr 16, 2026

Summary

Fixes CS-189 — customer's custom "Comp AI" org role couldn't see the Auditor View tab, even though the role was granted audit:read.

Investigation

"Comp AI" is not a role that exists in our code. The customer created a custom organization role literally named "Comp AI" (custom roles are stored in organization_role per-org). The real bug is broader: the tab-visibility gate used roles.includes(Role.auditor) — a literal string match against the built-in "auditor" role name. That check hid the tab from:

  • Custom org roles granted audit:read (including "Comp AI")
  • Owners and admins (who implicitly have all permissions)

Meanwhile the route layout (apps/app/.../auditor/layout.tsx) already enforces audit:read via requireRoutePermission('auditor', orgId), so the literal-role check was both redundant and wrong. This also violates the explicit guidance in CLAUDE.md:

No manual role string parsing (role.includes('admin')) — always use permission checks.

Fix

Switched the four places gating the Auditor View tab to canAccessRoute(permissions, 'auditor'):

  • apps/app/src/app/(app)/[orgId]/components/AppSidebar.tsx (design-system sidebar)
  • apps/app/src/components/main-menu.tsx (legacy sidebar + mobile menu) — now reads permissions via usePermissions() so callers don't need to pass hasAuditorRole
  • apps/app/src/app/(app)/[orgId]/components/app-shell-search-groups.tsx (command palette)
  • apps/app/src/app/(app)/[orgId]/auditor/(overview)/page.tsx — removed redundant roles.includes(Role.auditor) check (layout still enforces audit:read)

Also dropped the now-unused hasAuditorRole prop from MainMenu / sidebar.tsx call sites. The separate isOnlyAuditor concept (for hiding non-auditor tabs for external auditors) is untouched.

Behavioral impact

  • Customer's "Comp AI" custom role with audit:read → tab visible ✅ (was hidden; fixes the ticket)
  • Built-in auditor role → tab visible ✅ (unchanged)
  • Owner / admin → tab visible (was hidden — arguably correct now, they already have access to the route server-side)
  • Custom role without audit:read → tab hidden ✅ (unchanged)
  • Employee / contractor → tab hidden ✅ (unchanged)

Test plan

  • 3 new tests in permissions.test.ts lock in the auditor route mapping (any role with audit:read passes; missing audit:read fails)
  • npx vitest run src/lib/permissions.test.ts — 23/24 pass (1 failure is pre-existing, unrelated to this change)
  • Typecheck clean on all touched files
  • Manual: with a custom role that has audit:read, Auditor View appears in the sidebar, main menu, and command palette, and the page renders
  • Manual: with employee role, tab is hidden in all three surfaces
  • Manual: with built-in auditor role, behavior unchanged

🤖 Generated with Claude Code


Summary by cubic

Scope Auditor View to explicit auditor access: show it only for the built-in auditor role or custom roles with audit:read; hide it from owners/admins unless explicitly granted. Fixes CS‑189 by letting the customer’s “Comp AI” custom role see the tab and page.

  • Bug Fixes

    • Gate visibility with canAccessAuditorView in AppSidebar, MainMenu (via usePermissions()), and command palette; drop hasAuditorRole from MainMenu and its call site.
    • Enforce server guard with requireAuditorViewAccess in auditor/layout.tsx; remove the redundant role check in auditor/(overview)/page.tsx.
    • Resolve visibility in org layout and pass canAccessAuditorView through AppShellWrapper to the sidebar and search groups.
  • Refactors

    • Add canAccessAuditorView, resolveCustomRolePermissions, resolveAuditorViewAccess, and requireAuditorViewAccess; expose customPermissions and canAccessAuditorView from usePermissions().
    • Add unit tests to lock the visibility matrix for built-in roles and custom roles with/without audit:read.

Written for commit 2573c20. Summary will update on new commits.

… (CS-189)

The Auditor View tab was gated on roles.includes(Role.auditor) — a
literal string match against the built-in "auditor" role. That made the
tab invisible to any custom organization role (e.g. a "Comp AI" custom
role a customer set up for our support team) even when that role was
granted audit:read, and also hid it from owners/admins who implicitly
have the permission. The route layout already enforces audit:read via
requireRoutePermission, so the literal-role gate was both redundant and
wrong.

Switched to canAccessRoute(permissions, 'auditor') in:
- AppSidebar nav item (design-system sidebar)
- MainMenu item (legacy sidebar + mobile menu) — reads permissions via
  usePermissions() so callers no longer need to pass hasAuditorRole
- app-shell-search-groups command palette entry

Also removed the redundant roles.includes(Role.auditor) page-level
check in auditor/(overview)/page.tsx; auditor/layout.tsx's existing
requireRoutePermission('auditor', orgId) still enforces audit:read
server-side.

Aligns with CLAUDE.md RBAC guidance: "No manual role string parsing —
always use permission checks."

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@linear
Copy link
Copy Markdown

linear Bot commented Apr 16, 2026

@vercel
Copy link
Copy Markdown

vercel Bot commented Apr 16, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
app Ready Ready Preview, Comment Apr 20, 2026 7:51pm
comp-framework-editor Ready Ready Preview, Comment Apr 20, 2026 7:51pm
portal Ready Ready Preview, Comment Apr 20, 2026 7:51pm

Request Review

Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No issues found across 6 files

Auto-approved: Fixes a specific RBAC UI visibility bug by moving from role-string checks to permission-based checks, following existing architectural guidelines. Includes unit tests.

…w-up)

Follow-up after CTO review: owner/admin should NOT see the Auditor View
tab or access the page, even though their implicit all-permissions
include audit:read. The tab is scoped to audit work; it should only
appear for users whose role explicitly grants audit access.

New rule:
  Show Auditor View iff
    - user has the built-in `auditor` role, OR
    - user has a custom org role that explicitly grants `audit:read`.

Implementation:
  - New pure helper `canAccessAuditorView(roleString, customRolePermissions)`
    in lib/permissions.ts. Separates "permission came from built-in role"
    (owner/admin/everyone) from "permission came from custom role".
  - Server-side helpers in lib/permissions.server.ts:
    - `resolveCustomRolePermissions` fetches only the user's custom-role
      permissions (built-in roles excluded).
    - `resolveAuditorViewAccess` runs the check for the current user.
    - `requireAuditorViewAccess` is the route guard — replaces
      `requireRoutePermission('auditor', orgId)` on the auditor layout.
  - usePermissions() now returns `canAccessAuditorView` + `customPermissions`
    so client components don't have to replicate the rule.
  - Org layout computes the flag server-side and forwards it through
    AppShellWrapper → AppSidebar + getAppShellSearchGroups.
  - main-menu reads the flag directly from usePermissions().

Behavior matrix:
  - auditor → visible ✅
  - owner,auditor / admin,auditor → visible ✅
  - CompAI custom role with audit:read → visible ✅
  - owner alone / admin alone → hidden (implicit audit:read doesn't count)
  - owner,ReadOnlyCustomRole (no audit) → hidden
  - employee / contractor → hidden ✅

Adds 9 unit tests locking in every row of that matrix.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 10 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="apps/app/src/lib/permissions.ts">

<violation number="1" location="apps/app/src/lib/permissions.ts:119">
P1: Auditor View access is checked against custom-role permissions only, which hides the tab for built-in roles with effective `audit:read` (e.g., owner/admin) and diverges from permission-based route gating.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review, or fix all with cubic.

Linked issue analysis

Linked issue: CS-189: [Improvement] Add Auditor View tab to the Comp AI role

Status Acceptance criteria Notes
Custom org role with audit:read (e.g., “Comp AI”) should see the Auditor View tab Added canAccessAuditorView and server-side custom-role resolution
Auditor View visibility must be based on audit:read permission, not literal 'auditor' role string Removed literal-role checks and use permission-based helpers
Auditor View tab should appear/hidden consistently in sidebar, main menu, and command palette Gated all three surfaces on server/client auditor-view flag
Auditor View page must render for permitted users (no 404 for custom roles with audit:read) Removed 404 role check; layout enforces requireAuditorViewAccess
Follow guidance to avoid manual literal role-string gating when determining tab visibility (use permission checks instead) Replaced literal role gating with permission helpers and server resolution

Comment thread apps/app/src/lib/permissions.ts
Signed-off-by: Tofik Hasanov <72318342+tofikwest@users.noreply.github.com>
@tofikwest
Copy link
Copy Markdown
Contributor Author

@cubic-dev-ai review it

@cubic-dev-ai
Copy link
Copy Markdown
Contributor

cubic-dev-ai Bot commented Apr 20, 2026

@cubic-dev-ai review it

@tofikwest I have started the AI code review. It will take a few minutes to complete.

Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 12 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="apps/app/src/app/(app)/[orgId]/layout.tsx">

<violation number="1" location="apps/app/src/app/(app)/[orgId]/layout.tsx:170">
P2: This adds a second custom-role permission query on every request for custom-role users, duplicating work already done by `resolveUserPermissions` and increasing layout latency.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review, or fix all with cubic.

Comment thread apps/app/src/app/(app)/[orgId]/layout.tsx
tofikwest and others added 2 commits April 20, 2026 15:03
…ew gating

Per CS-189 product decision: owners/admins should not see the Auditor View
tab unless they explicitly opt in via a custom role granting audit:read.
Removing audit:read from the built-in owner/admin roles lets the standard
canAccessRoute('auditor') check do the work — the merged permissions
naturally cover owner,auditor / admin,auditor and custom roles with
audit:read, while hiding owner/admin alone.

audit:read is not referenced anywhere in the codebase outside
ROUTE_PERMISSIONS.auditor, so dropping it is safe. Owner/admin still
retain audit:create and audit:update for SOA management.

Deletes ~100 lines of special-case code that was previously needed to
distinguish implicit vs explicit audit:read:
- canAccessAuditorView
- resolveCustomRolePermissions
- resolveAuditorViewAccess
- requireAuditorViewAccess
- customPermissions plumbing through AppShellWrapper / AppSidebar /
  search groups / MainMenu / usePermissions

Tests updated to exercise the full resolution path
(resolveBuiltInPermissions + canAccessRoute).
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

0 issues found across 12 files (changes from recent commits).

Requires human review: Auto-approval blocked by 1 unresolved issue from previous reviews.

Linked issue analysis

Linked issue: CS-189: [Improvement] Add Auditor View tab to the Comp AI role

Status Acceptance criteria Notes
Gate Auditor View tab in AppSidebar using canAccessRoute(permissions,'auditor') instead of role string check Updated hidden prop to use canAccessRoute in AppSidebar
MainMenu: use usePermissions + canAccessRoute and remove hasAuditorRole prop Imported usePermissions and replaced hasAuditorRole usage
Command palette (app-shell-search-groups) uses permission check instead of hasAuditorRole Replaced hasAuditorRole && can('auditor') with can('auditor')
Remove roles.includes(Role.auditor) check from auditor/(overview)/page.tsx Deleted roles.includes(Role.auditor) and notFound call
Drop hasAuditorRole prop from sidebar callsites Removed hasAuditorRole prop in sidebar.tsx call site
Remove audit:read from built-in owner/admin roles in packages/auth/src/permissions.ts Changed audit perms to ['create','update'] for owner/admin
Add unit tests to lock Auditor View visibility and route mapping Added 'Auditor View visibility' tests exercising audit:read mapping
Update training/permissions-regression.spec.ts to reflect owner audit perms change Adjusted regression test to expect no audit:read for owner
⚠️ Typecheck clean and unit tests pass (CI/test run) as claimed in PR Tests added but no CI logs or artifacts present in diffs
Behavioral: custom role with audit:read sees Auditor View in sidebar, main menu, command palette, and page renders; other roles hidden UI gates changed + tests show custom role with audit:read passes

@vercel vercel Bot temporarily deployed to Preview – portal April 20, 2026 19:22 Inactive
@tofikwest
Copy link
Copy Markdown
Contributor Author

@cubic-dev-ai review it

@cubic-dev-ai
Copy link
Copy Markdown
Contributor

cubic-dev-ai Bot commented Apr 20, 2026

@cubic-dev-ai review it

@tofikwest I have started the AI code review. It will take a few minutes to complete.

Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cubic analysis

1 issue found across 8 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="packages/auth/src/permissions.ts">

<violation number="1" location="packages/auth/src/permissions.ts:77">
P1: According to linked Linear issue CS-189, custom roles (e.g., Comp AI) need Auditor View access, but removing `audit:read` from built-in owner/admin prevents those role managers from granting `audit:read` to custom roles via the privilege-escalation guard.</violation>
</file>

Linked issue analysis

Linked issue: CS-189: [Improvement] Add Auditor View tab to the Comp AI role

Status Acceptance criteria Notes
Gate Auditor View tab in AppSidebar using canAccessRoute(permissions,'auditor') instead of role string check Updated hidden prop to use canAccessRoute in AppSidebar
MainMenu: use usePermissions + canAccessRoute and remove hasAuditorRole prop Imported usePermissions and replaced hasAuditorRole usage
Command palette (app-shell-search-groups) uses permission check instead of hasAuditorRole Replaced hasAuditorRole && can('auditor') with can('auditor')
Remove roles.includes(Role.auditor) check from auditor/(overview)/page.tsx Deleted roles.includes(Role.auditor) and notFound call
Drop hasAuditorRole prop from sidebar callsites Removed hasAuditorRole prop in sidebar.tsx call site
Remove audit:read from built-in owner/admin roles in packages/auth/src/permissions.ts Changed audit perms to ['create','update'] for owner/admin
Add unit tests to lock Auditor View visibility and route mapping Added 'Auditor View visibility' tests exercising audit:read mapping
Update training/permissions-regression.spec.ts to reflect owner audit perms change Adjusted regression test to expect no audit:read for owner
⚠️ Typecheck clean and unit tests pass (CI/test run) as claimed in PR Tests added but no CI logs or artifacts present in diffs
Behavioral: custom role with audit:read sees Auditor View in sidebar, main menu, command palette, and page renders; other roles hidden UI gates changed + tests show custom role with audit:read passes

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review, or fix all with cubic.

Comment thread packages/auth/src/permissions.ts Outdated
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

0 issues found across 12 files (changes from recent commits).

Requires human review: This PR modifies core RBAC and authorization logic across multiple files, including server-side route guards. Permission changes are high-impact and require human verification.

@tofikwest tofikwest merged commit fa16795 into main Apr 20, 2026
7 checks passed
@tofikwest tofikwest deleted the tofik/cs-189-auditor-view-permission-gating branch April 20, 2026 19:52
@claudfuen
Copy link
Copy Markdown
Contributor

🎉 This PR is included in version 3.26.1 🎉

The release is available on GitHub release

Your semantic-release bot 📦🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants