Skip to content

[Permissions] Capability-based authz refactor. gate org secrets, deleteUser, and ReadMe JWT on MANAGE_ORG#528

Open
juanmrad wants to merge 2 commits into
mainfrom
tighten-capability-authz-for-permissions
Open

[Permissions] Capability-based authz refactor. gate org secrets, deleteUser, and ReadMe JWT on MANAGE_ORG#528
juanmrad wants to merge 2 commits into
mainfrom
tighten-capability-authz-for-permissions

Conversation

@juanmrad
Copy link
Copy Markdown
Member

@juanmrad juanmrad commented May 20, 2026

Context & Requests for Reviewers

Foundation work for #406 (granular permissions).

Replace the remaining role-string authz callsites with user.getPermissions().includes(...) so server-side checks no longer branch on user.role. After this PR there are zero resolver-level role-string checks.

Add a new MANAGE_ROLES capability granted only to ADMIN today — the seam future PRs will gate the role-editing API on so admins can delegate role editing without granting full MANAGE_ORG.

Close several server-side authz gaps where any authenticated user in an org could read or modify org-level secrets. All now require MANAGE_ORG.

The refactor is intentionally a no-op for every role today (verified by permissioning.test.ts); only the security fixes change observable behavior, and only for non-admins.

Summary by CodeRabbit

  • Security Improvements

    • Stronger permission gating for org secrets, API key actions, integrations, user deletion, and org settings.
    • Permissions now drive access checks across review, backtest, retroaction, routing, and manual-review flows.
  • UI Changes

    • Save/management controls and the "Skip to next job" action are now enabled/disabled based on user permissions with clearer tooltip messaging.
  • Bug Fixes

    • Fixed unauthorized access paths so sensitive data and actions are blocked unless proper permissions are present.

Review Change Stack

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 20, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: d956d847-7768-421c-9415-2dc8a3e545f9

📥 Commits

Reviewing files that changed from the base of the PR and between a40c660 and ae1e765.

📒 Files selected for processing (6)
  • client/src/webpages/dashboard/mrt/ManualReviewAppealSettings.tsx
  • client/src/webpages/dashboard/mrt/manual_review_job/ManualReviewJobReview.tsx
  • server/graphql/modules/apiKey.resolver.test.ts
  • server/graphql/modules/user.resolver.test.ts
  • server/services/userManagementService/permissioning.test.ts
  • server/services/userManagementService/permissioning.ts

📝 Walkthrough

Walkthrough

Migrate authorization from role-based checks to permission-based checks: add MANAGE_ROLES, expose me.permissions in GraphQL types, replace hasPermission(role) usage with user.getPermissions().includes(...), and add permission gates and tests across resolvers and client UI.

Changes

Permission-based Authorization Refactor

Layer / File(s) Summary
Permission enum & permissioning core
server/services/userManagementService/permissioning.ts, server/services/userManagementService/permissioning.test.ts, server/services/userManagementService/index.ts, server/graphql/generated.ts
Add MANAGE_ROLES to UserPermission, expand JSDoc for canonical seed, remove exported hasPermission, and make getPermissionsForRole return fresh arrays.
Client & Server generated GraphQL types
client/src/graphql/generated.ts, server/graphql/generated.ts
Regenerated types: me now exposes permissions: ReadonlyArray<GQLUserPermission>; GraphQL documents updated to request permissions; GQLUserPermission gains ManageRoles.
Client dashboard permission gating
client/src/webpages/dashboard/mrt/ManualReviewAppealSettings.tsx, client/src/webpages/dashboard/mrt/manual_review_job/ManualReviewJobReview.tsx
Components request me.permissions, import GQLUserPermission, compute capabilities via userHasPermissions(...), and gate UI elements (Save, Skip) on permission checks instead of role comparisons; small data-shaping/formatting refinements included.
Remove hasPermission & refactor imports
server/services/userManagementService/index.ts, various resolvers
Stop exporting/using hasPermission; update imports to reference UserPermission and user.getPermissions() directly.
Backtest / Retroaction / Routing permission checks
server/graphql/modules/backtest.ts, server/graphql/modules/retroaction.ts, server/graphql/modules/routingRule.ts, related tests
Replace hasPermission(..., user.role) with user.getPermissions().includes(...); update tests to mock getPermissions() returning needed capabilities.
API Key resolver & tests
server/graphql/modules/apiKey.ts, server/graphql/modules/apiKey.resolver.test.ts
Query.apiKey now distinguishes unauthenticated (missing orgId) vs unauthorized (lacking MANAGE_ORG) with appropriate errors; rotate mutations’ error messaging clarified; tests added for unauthenticated/forbidden/success cases.
Integration resolvers & tests
server/graphql/modules/integration.ts, server/graphql/modules/integration.resolver.test.ts
Require MANAGE_ORG for Query.integrationConfig and related mutations; tests assert forbidden behavior and that backends are not invoked without permission.
Manual Review Tool updates
server/graphql/modules/manualReviewTool.ts
deleteAllJobsFromQueue now passes user.getPermissions() instead of deriving permissions from user.role.
Org fields & mutation gating
server/graphql/modules/org.ts, server/graphql/modules/org.resolver.test.ts
Gate Org sensitive fields (apiKey, integrationConfigs, publicSigningKey, defaultInterfacePreferences) and certain mutations on MANAGE_ORG; add tests including IDOR guard and permission enforcement.
User resolver: deleteUser & readMeJWT gating + tests
server/graphql/modules/user.ts, server/graphql/modules/user.resolver.test.ts
deleteUser requires MANAGE_ORG. User.readMeJWT includes org secrets (apiKey, publicSigningKey) only when the requesting user has MANAGE_ORG; otherwise secrets are null and org APIs are not called. Tests validate both scenarios and identity guarding.

Sequence Diagram: User.readMeJWT flow

sequenceDiagram
  participant Client
  participant Resolver as User.readMeJWT
  participant Auth as Auth (ctx.getUser)
  participant OrgAPI
  participant JWT as JWT Signer

  Client->>Resolver: request readMeJWT(parentUserId)
  Resolver->>Auth: ctx.getUser() -> user
  Resolver->>Auth: user.getPermissions().includes('MANAGE_ORG')?
  alt Has MANAGE_ORG
    Resolver->>OrgAPI: getActivatedApiKey(user.orgId)
    Resolver->>OrgAPI: getPublicSigningKey(user.orgId)
    OrgAPI-->>Resolver: apiKey, publicSigningKey
    Resolver->>JWT: sign payload with apiKey/publicSigningKey
  else Lacks MANAGE_ORG
    Resolver->>JWT: sign payload with null secrets
  end
  JWT-->>Resolver: token
  Resolver-->>Client: token
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • roostorg/coop#505: Both PRs migrate UI/API authorization to capability-based me.permissions checks and strengthen MANAGE_ORG gating in overlapping GraphQL/server areas.

Suggested reviewers

  • cassidyjames
  • julietshen
  • vinaysrao1
  • dom-notion

🐰 I hopped through code with careful paws,
Swapped roles for permissions, trimmed old claws.
Secrets tucked away for those with the key,
Tests guard the gates — secure as can be.
A tiny rabbit cheers: permissions now free.

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 30.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Description check ❓ Inconclusive The description covers context, objectives, and rationale but lacks explicit testing details and deployment considerations required by the template. Add a Tests section describing how changes were validated (e.g., automated tests added to permissioning.test.ts and resolver test files) and clarify any rollout considerations.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main change: a capability-based authorization refactor that gates org secrets, deleteUser, and ReadMe JWT on MANAGE_ORG permission.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch tighten-capability-authz-for-permissions

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (4)
server/graphql/modules/apiKey.resolver.test.ts (1)

18-36: ⚡ Quick win

Add unauthenticated-path coverage for Query.apiKey.

Line 21 only verifies forbidden/authorized flows. Since the resolver now distinguishes unauthenticated callers, add a case where getUser() is null (or missing orgId) and assert 'Authenticated user required' plus no service invocation.

Proposed test addition
   describe('Query.apiKey', () => {
+    it('throws unauthenticatedError when caller is not authenticated', async () => {
+      const { ctx, getActiveApiKeyForOrg } = makeCtx([UserPermission.MANAGE_ORG]);
+      const unauthenticatedCtx = { ...ctx, getUser: () => null };
+      await expect(Query.apiKey({}, {}, unauthenticatedCtx)).rejects.toThrow(
+        'Authenticated user required',
+      );
+      expect(getActiveApiKeyForOrg).not.toHaveBeenCalled();
+    });
+
     it('throws forbiddenError when caller lacks MANAGE_ORG', async () => {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/graphql/modules/apiKey.resolver.test.ts` around lines 18 - 36, Add a
new unit test for Query.apiKey that covers the unauthenticated path: create a
ctx from makeCtx where getUser() returns null (or a user object without orgId),
call Query.apiKey({}, {}, ctx) and assert it rejects with 'Authenticated user
required', and also assert getActiveApiKeyForOrg was not called; reference
Query.apiKey, getUser, and getActiveApiKeyForOrg to locate where to add the
test.
server/graphql/modules/user.resolver.test.ts (1)

102-133: ⚡ Quick win

Add coverage for the readMeJWT identity guard.

The new JWT tests validate MANAGE_ORG gating, but they don’t exercise the user.id !== authedUser.id check. Add a case asserting null token and zero orgAPI calls when parent user differs from authenticated user.

Proposed test addition
   describe('User.readMeJWT does not leak org secrets to non-admins', () => {
@@
     it('embeds org secrets in the JWT for MANAGE_ORG callers', async () => {
@@
       expect(getPublicSigningKeyPem).toHaveBeenCalledWith('org-1');
     });
+
+    it('returns null when authenticated user does not match parent user', async () => {
+      const { ctx, getActivatedApiKeyForOrg, getPublicSigningKeyPem } =
+        makeReadMeCtx([UserPermission.MANAGE_ORG]);
+      const mismatchedParent = { ...userParent, id: 'other-user' };
+      await expect(User.readMeJWT(mismatchedParent, {}, ctx)).resolves.toBeNull();
+      expect(getActivatedApiKeyForOrg).not.toHaveBeenCalled();
+      expect(getPublicSigningKeyPem).not.toHaveBeenCalled();
+    });
   });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/graphql/modules/user.resolver.test.ts` around lines 102 - 133, Add a
new test exercising the identity guard in User.readMeJWT: use makeReadMeCtx to
create a ctx whose authenticated user id is different from userParent.id (i.e.,
not the same user), call User.readMeJWT(userParent, {}, ctx) and assert the
returned token is null and that getActivatedApiKeyForOrg and
getPublicSigningKeyPem were not called; this ensures the user.id !==
authedUser.id branch is covered and prevents org-secret lookups for mismatched
identities.
client/src/webpages/dashboard/mrt/ManualReviewAppealSettings.tsx (1)

17-17: ⚡ Quick win

Use @/ alias for the new permissions import.

Switch this newly-added relative import to the configured absolute alias for consistency with project standards.

💡 Suggested change
-import { userHasPermissions } from '../../../routing/permissions';
+import { userHasPermissions } from '`@/routing/permissions`';

As per coding guidelines, **/*.{ts,tsx}: Use absolute imports with @/ prefix over relative paths where configured in tsconfig.json.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@client/src/webpages/dashboard/mrt/ManualReviewAppealSettings.tsx` at line 17,
Replace the relative import of userHasPermissions with the project's absolute
alias: update the import statement that currently references
"../../../routing/permissions" to use the "`@/routing/permissions`" path so
ManualReviewAppealSettings.tsx imports userHasPermissions via the configured
tsconfig alias.
client/src/webpages/dashboard/mrt/manual_review_job/ManualReviewJobReview.tsx (1)

47-47: ⚡ Quick win

Use @/ alias for the new permissions utility import.

Please convert this new relative import to the configured absolute alias style.

💡 Suggested change
-import { userHasPermissions } from '../../../../routing/permissions';
+import { userHasPermissions } from '`@/routing/permissions`';

As per coding guidelines, **/*.{ts,tsx}: Use absolute imports with @/ prefix over relative paths where configured in tsconfig.json.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@client/src/webpages/dashboard/mrt/manual_review_job/ManualReviewJobReview.tsx`
at line 47, The import in ManualReviewJobReview.tsx uses a relative path for the
permissions utility; change the import of userHasPermissions from
'../../../../routing/permissions' to use the configured absolute alias (start
with '`@/`') so it resolves via the tsconfig paths (e.g., import {
userHasPermissions } from '`@/routing/permissions`'). Ensure the symbol name
userHasPermissions remains unchanged and update any IDE imports to match the
alias.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@server/services/userManagementService/permissioning.ts`:
- Around line 160-170: getPermissionsForRole currently returns the shared array
from UserPermissionsForRole which allows callers to mutate the canonical
permission set; change getPermissionsForRole to return a defensive copy of the
permission array (e.g., Array.from(...) or spread into a new array) and ensure
when UserPermissionsForRole.get(role) is missing you still return a new empty
array instead of the shared literal so callers cannot mutate the internal map.

---

Nitpick comments:
In
`@client/src/webpages/dashboard/mrt/manual_review_job/ManualReviewJobReview.tsx`:
- Line 47: The import in ManualReviewJobReview.tsx uses a relative path for the
permissions utility; change the import of userHasPermissions from
'../../../../routing/permissions' to use the configured absolute alias (start
with '`@/`') so it resolves via the tsconfig paths (e.g., import {
userHasPermissions } from '`@/routing/permissions`'). Ensure the symbol name
userHasPermissions remains unchanged and update any IDE imports to match the
alias.

In `@client/src/webpages/dashboard/mrt/ManualReviewAppealSettings.tsx`:
- Line 17: Replace the relative import of userHasPermissions with the project's
absolute alias: update the import statement that currently references
"../../../routing/permissions" to use the "`@/routing/permissions`" path so
ManualReviewAppealSettings.tsx imports userHasPermissions via the configured
tsconfig alias.

In `@server/graphql/modules/apiKey.resolver.test.ts`:
- Around line 18-36: Add a new unit test for Query.apiKey that covers the
unauthenticated path: create a ctx from makeCtx where getUser() returns null (or
a user object without orgId), call Query.apiKey({}, {}, ctx) and assert it
rejects with 'Authenticated user required', and also assert
getActiveApiKeyForOrg was not called; reference Query.apiKey, getUser, and
getActiveApiKeyForOrg to locate where to add the test.

In `@server/graphql/modules/user.resolver.test.ts`:
- Around line 102-133: Add a new test exercising the identity guard in
User.readMeJWT: use makeReadMeCtx to create a ctx whose authenticated user id is
different from userParent.id (i.e., not the same user), call
User.readMeJWT(userParent, {}, ctx) and assert the returned token is null and
that getActivatedApiKeyForOrg and getPublicSigningKeyPem were not called; this
ensures the user.id !== authedUser.id branch is covered and prevents org-secret
lookups for mismatched identities.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 648a7844-8e91-48e9-a381-56448ed7aa1a

📥 Commits

Reviewing files that changed from the base of the PR and between 09fe5f0 and a40c660.

📒 Files selected for processing (21)
  • client/src/graphql/generated.ts
  • client/src/webpages/dashboard/mrt/ManualReviewAppealSettings.tsx
  • client/src/webpages/dashboard/mrt/manual_review_job/ManualReviewJobReview.tsx
  • server/graphql/generated.ts
  • server/graphql/modules/apiKey.resolver.test.ts
  • server/graphql/modules/apiKey.ts
  • server/graphql/modules/backtest.resolver.test.ts
  • server/graphql/modules/backtest.ts
  • server/graphql/modules/integration.resolver.test.ts
  • server/graphql/modules/integration.ts
  • server/graphql/modules/manualReviewTool.ts
  • server/graphql/modules/org.resolver.test.ts
  • server/graphql/modules/org.ts
  • server/graphql/modules/retroaction.resolver.test.ts
  • server/graphql/modules/retroaction.ts
  • server/graphql/modules/routingRule.ts
  • server/graphql/modules/user.resolver.test.ts
  • server/graphql/modules/user.ts
  • server/services/userManagementService/index.ts
  • server/services/userManagementService/permissioning.test.ts
  • server/services/userManagementService/permissioning.ts
💤 Files with no reviewable changes (1)
  • server/services/userManagementService/index.ts

Comment thread server/services/userManagementService/permissioning.ts Outdated
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Refactors authorization checks away from role-string branching toward capability checks via user.getPermissions().includes(...), introduces a dedicated MANAGE_ROLES capability, and closes several authorization gaps where org-level secrets could be accessed by any authenticated org user.

Changes:

  • Add MANAGE_ROLES permission and document the capability-driven authz approach in permissioning.ts; remove the legacy hasPermission helper export.
  • Gate sensitive org secret surfaces (API key, webhook signing key, integration configs) plus deleteUser and ReadMe JWT secret embedding behind MANAGE_ORG.
  • Update/extend resolver tests and client UI queries/guards to use me.permissions instead of me.role where applicable.

Reviewed changes

Copilot reviewed 21 out of 21 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
server/services/userManagementService/permissioning.ts Adds MANAGE_ROLES and clarifies the permission-seed map as the canonical source for capabilities.
server/services/userManagementService/permissioning.test.ts Adds tests for MANAGE_ROLES defaults and seed-map invariants.
server/services/userManagementService/index.ts Stops re-exporting the removed hasPermission helper.
server/graphql/modules/user.ts Adds MANAGE_ROLES to the GraphQL enum; gates deleteUser and ReadMe JWT secret embedding on MANAGE_ORG.
server/graphql/modules/user.resolver.test.ts Adds coverage for deleteUser MANAGE_ORG gating and ReadMe JWT secret non-leakage.
server/graphql/modules/routingRule.ts Replaces role-based permission check with user.getPermissions() capability check.
server/graphql/modules/retroaction.ts Replaces role-based permission check with capability check for RUN_RETROACTION.
server/graphql/modules/retroaction.resolver.test.ts Updates resolver test user mocks to provide getPermissions().
server/graphql/modules/org.ts Adds MANAGE_ORG gates to multiple sensitive Org field/mutation resolvers (api key, integration configs, webhook key, safety/SSO settings).
server/graphql/modules/org.resolver.test.ts Adds tests ensuring sensitive Org resolvers require MANAGE_ORG and don’t hit datasources when forbidden.
server/graphql/modules/manualReviewTool.ts Passes user.getPermissions() into service calls instead of resolving via role at callsite.
server/graphql/modules/integration.ts Gates integration config read/write operations behind MANAGE_ORG.
server/graphql/modules/integration.resolver.test.ts Adds forbidden-path tests ensuring MANAGE_ORG is required before datasource/registry access.
server/graphql/modules/backtest.ts Replaces role-based permission check with capability check for RUN_BACKTEST.
server/graphql/modules/backtest.resolver.test.ts Updates resolver test user mocks to provide getPermissions().
server/graphql/modules/apiKey.ts Gates API key existence query behind MANAGE_ORG; retains MANAGE_ORG checks for rotate operations.
server/graphql/modules/apiKey.resolver.test.ts Adds tests verifying Query.apiKey is MANAGE_ORG-gated and doesn’t call the service when forbidden.
server/graphql/generated.ts Regenerates server GraphQL types to include MANAGE_ROLES.
client/src/webpages/dashboard/mrt/ManualReviewAppealSettings.tsx Switches gating UI from me.role to me.permissions (MANAGE_ORG).
client/src/webpages/dashboard/mrt/manual_review_job/ManualReviewJobReview.tsx Switches “admin bypass” logic to permission-based gating and updates queries to request me.permissions.
client/src/graphql/generated.ts Regenerates client GraphQL types/ops reflecting me.permissions and MANAGE_ROLES.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread server/services/userManagementService/permissioning.test.ts
Comment thread server/graphql/modules/apiKey.ts
Comment thread server/graphql/modules/apiKey.ts
@juanmrad
Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 20, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 21 out of 21 changed files in this pull request and generated 1 comment.


describe('getPermissionsForRole', () => {
it('returns an empty array for an unknown role rather than throwing', () => {
expect(getPermissionsForRole('NOT_A_ROLE')).toEqual([]);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants