Skip to content

feat: migrate CLI auth checks to RBAC-aware permissions#571

Open
Dalanir wants to merge 1 commit intomainfrom
feat/rbac-cli-permissions
Open

feat: migrate CLI auth checks to RBAC-aware permissions#571
Dalanir wants to merge 1 commit intomainfrom
feat/rbac-cli-permissions

Conversation

@Dalanir
Copy link
Copy Markdown

@Dalanir Dalanir commented Mar 25, 2026

Summary (AI generated)

  • Migrates CLI permission checks to RBAC-aware backend wrappers with legacy fallback semantics.
  • Makes login, user account, and organization add identity-only instead of gating them on legacy key modes.
  • Uses org.create_app for app creation and init organization selection.
  • Removes now-unused legacy helpers from the CLI auth path.

Motivation (AI generated)

The CLI still contained legacy mode-based checks that could conflict with or bypass the RBAC behavior now implemented in the backend. This change makes the CLI defer permission decisions to backend RBAC-aware checks while preserving the current fallback behavior for legacy keys.

Business Impact (AI generated)

This brings the CLI in line with the console and public API authorization model, reducing surprising permission mismatches for customers using API Keys v2 and improving confidence in RBAC-based automation flows.

Test Plan (AI generated)

  • Run bunx eslint "src/**/*.ts"
  • Run bun run typecheck
  • Verify app add works with org.create_app
  • Verify build request uses app.build_native
  • Verify bundle upload channel updates use RBAC-aware permission checks
  • Verify init only offers organizations with org.create_app
  • Verify login, user account, and organization add still work with a valid API key

Generated with AI

Summary by CodeRabbit

Release Notes

  • Refactor
    • Modernized authentication and authorization system across all operations. Transitioned from role-based access control to permission-based enforcement with more granular scoping at the organization, app, and channel levels.
    • Improved permission validation for organization and app management operations.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 25, 2026

📝 Walkthrough

Walkthrough

This is a major refactoring of the CLI's authentication and authorization system, replacing the legacy verifyUser permission-scope verification pattern with a new approach using resolveUserIdFromApiKey for user resolution and granular hasCliPermission/assertCliPermission functions for scoped permission checks tied to organizations, apps, or channels.

Changes

Cohort / File(s) Summary
Core Permission & Auth Utilities
src/utils.ts
Removed checkKey and verifyUser functions. Added new exported functions: resolveUserIdFromApiKey, hasCliPermission, assertCliPermission, getOrganizationWithPermission, and getAccessibleAppsForApiKey. Updated isAllowedAppOrg to use get_org_perm_for_apikey_v2 RPC and refactored organization lookup to filter by permission dynamically.
App Operations
src/app/add.ts, src/app/delete.ts, src/app/list.ts, src/app/set.ts
Replaced verifyUser calls with resolveUserIdFromApiKey. In add.ts, replaced organization lookup with getOrganizationWithPermission and added explicit assertCliPermission call for org.create_app. In list.ts, refactored getActiveApps to use getAccessibleAppsForApiKey instead of direct table query.
Build Operations
src/build/request.ts
Replaced verifyUser with resolveUserIdFromApiKey followed by assertCliPermission for app.build_native permission scoped to the app.
Bundle Operations
src/bundle/cleanup.ts, src/bundle/compatibility.ts, src/bundle/delete.ts, src/bundle/list.ts, src/bundle/unlink.ts
Replaced verifyUser with resolveUserIdFromApiKey across all files; subsequent org/app permission checks via checkAppExistsAndHasPermissionOrgErr remain unchanged.
Bundle Upload
src/bundle/upload.ts
Replaced verifyUser with resolveUserIdFromApiKey and is_allowed_capgkey RPC with hasCliPermission checks. Updated setVersionInChannel to perform channel lookup and compute API access by querying hasCliPermission with permission keys channel.update_settings or app.create_channel based on channel existence.
Channel Operations
src/channel/add.ts, src/channel/currentBundle.ts, src/channel/delete.ts, src/channel/list.ts, src/channel/set.ts
Replaced verifyUser with resolveUserIdFromApiKey across all channel operations; subsequent org/app permission checks remain in place.
Init Command
src/init/command.ts
Updated selectOrganizationForInit signature from (supabase, roles: string[]) to (supabase, apikey: string); now filters organizations by calling hasCliPermission(..., 'org.create_app', { orgId }) instead of checking role lists. Replaced verifyUser with resolveUserIdFromApiKey in initApp.
Login & User
src/login.ts, src/user/account.ts
Replaced verifyUser calls with resolveUserIdFromApiKey to obtain user identity from API key.
Organization Operations
src/organization/add.ts, src/organization/delete.ts, src/organization/list.ts, src/organization/members.ts, src/organization/set.ts
Replaced verifyUser with resolveUserIdFromApiKey across all files. In delete.ts, added assertCliPermission for org.delete and removed prior ownership-based prompting logic. In members.ts and set.ts, added explicit assertCliPermission calls for org.read_members and org.update_settings respectively.

Possibly related PRs

  • Cap-go/CLI#547 — Modifies src/build/request.ts with refactoring to logging and BuildLogger in the same function where authentication flow is being changed.
  • Cap-go/CLI#549 — Modifies src/init/command.ts's organization-selection and user-verification flow which this PR updates to use CLI-permission checks instead of role-based filtering.
  • Cap-go/CLI#452 — Modifies 2FA enforcement mechanisms across utils and permission/org checks, overlapping with the permission infrastructure updates in this PR.

Poem

🐰 The old verifyUser hops away,
As granular permissions save the day!
Each scope now scoped to org or app,
Permission checks fill the auth gap.
CLI commands dance with gated might! ✨


🎯 4 (Complex) | ⏱️ ~45 minutes

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately captures the main objective of the pull request: migrating CLI authentication checks from legacy key-mode verification to RBAC-aware permission checks across multiple files.

✏️ 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 feat/rbac-cli-permissions

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@sonarqubecloud
Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

Failed conditions
7.6% Duplication on New Code (required ≤ 3%)

See analysis details on SonarQube Cloud

@Dalanir Dalanir self-assigned this Mar 25, 2026
Copy link
Copy Markdown

@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.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/channel/set.ts (1)

167-181: ⚠️ Potential issue | 🟠 Major

Remove user_id filters from app_versions queries; rely on org-level permission check instead.

The user_id filters at lines 173 and 223 are inconsistent with the permission model. checkAppExistsAndHasPermissionOrgErr validates org-level access (which should grant all org members equal visibility), but subsequent queries restrict results to versions owned by the specific API key holder. Other app_versions queries across the codebase (getActiveAppVersions, getVersionData, deleteSpecificVersion) do not filter by user_id, suggesting the intended access model is org-scoped, not user-scoped.

Either remove the .eq('user_id', userId) filters to align with the org permission check, or clarify whether versions should be intentionally scoped to individual users despite org access.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/channel/set.ts` around lines 167 - 181, The supabase query that selects
from 'app_versions' (the block using resolvedBundleVersion and variables
resolvedBundleVersion, userId) incorrectly restricts results by .eq('user_id',
userId); remove the user_id filter so the query only filters by app_id, name,
and deleted to honor org-level permission already enforced by
checkAppExistsAndHasPermissionOrgErr; update both places in this file where
.eq('user_id', userId) is applied (the resolvedBundleVersion lookup and the
later version lookup around line 223) to match other functions like
getActiveAppVersions/getVersionData which are org-scoped.
🧹 Nitpick comments (6)
src/channel/add.ts (1)

39-53: Duplicate resolveUserIdFromApiKey call.

resolveUserIdFromApiKey is called twice: once at line 39 (discarded) and again at line 53 to retrieve userId. Consider capturing the result from the first call and reusing it to avoid a redundant RPC roundtrip.

♻️ Proposed fix to eliminate duplicate call
-  await resolveUserIdFromApiKey(supabase, options.apikey)
+  const userId = await resolveUserIdFromApiKey(supabase, options.apikey)
   await checkAppExistsAndHasPermissionOrgErr(supabase, options.apikey, appId, OrganizationPerm.admin, silent, true)

   if (!silent)
     log.info(`Creating channel ${appId}#${channelId} to Capgo`)

   const data = await findUnknownVersion(supabase, appId)
   if (!data) {
     if (!silent)
       log.error('Cannot find default version for channel creation, please contact Capgo support 🤨')
     throw new Error('Cannot find default version for channel creation')
   }

   const orgId = await getOrganizationId(supabase, appId)
-  const userId = await resolveUserIdFromApiKey(supabase, options.apikey)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/channel/add.ts` around lines 39 - 53, The code calls
resolveUserIdFromApiKey twice; capture its returned userId once (call
resolveUserIdFromApiKey(supabase, options.apikey) into a variable) and reuse
that variable later instead of calling the function again. Replace the first
discarded call with an assignment (userId) and remove the second call, ensuring
any subsequent logic that needs userId (e.g., the existing userId usage after
getOrganizationId) uses the captured variable.
src/app/add.ts (1)

94-106: Consider removing unnecessary non-null assertions.

SonarCloud flags the ! assertions on options.apikey at lines 94, 99, and 103 as unnecessary. Since ensureOptions (line 91) throws if apikey is falsy, the value is guaranteed to be defined at this point.

If the type narrowing isn't recognized by TypeScript, consider either:

  1. Extracting apikey to a local const after validation
  2. Using a type guard in ensureOptions that narrows the type
♻️ Proposed fix to eliminate non-null assertions
   ensureOptions(appId, options, silent)
+  const apikey = options.apikey! // Guaranteed by ensureOptions

   const supabase = await createSupabaseClient(options.apikey!, options.supaHost, options.supaAnon)
-  const userId = await resolveUserIdFromApiKey(supabase, options.apikey!)
+  const userId = await resolveUserIdFromApiKey(supabase, apikey)

   await ensureAppDoesNotExist(supabase, appId, silent)

   if (!organization)
-    organization = await getOrganizationWithPermission(supabase, options.apikey!, 'org.create_app')
+    organization = await getOrganizationWithPermission(supabase, apikey, 'org.create_app')

   const organizationUid = organization.gid

-  await assertCliPermission(supabase, options.apikey!, 'org.create_app', { orgId: organizationUid }, {
+  await assertCliPermission(supabase, apikey, 'org.create_app', { orgId: organizationUid }, {
     message: `Insufficient permissions to create an app in organization ${organizationUid}`,
     silent,
   })
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/add.ts` around lines 94 - 106, Remove the unnecessary non-null
assertions on options.apikey by extracting a local const apikey immediately
after ensureOptions returns (e.g., const apikey = options.apikey) and use that
const in resolveUserIdFromApiKey, getOrganizationWithPermission, and
assertCliPermission calls; alternatively make ensureOptions a type guard that
narrows options so apikey is known defined—update the references to use the
narrowed/const apikey instead of options.apikey! (affects
resolveUserIdFromApiKey, getOrganizationWithPermission, assertCliPermission).
src/app/list.ts (1)

35-35: Unused return value from resolveUserIdFromApiKey.

The userId returned from resolveUserIdFromApiKey is discarded. If this is only for API key validation, consider adding a comment to clarify intent, or evaluate if downstream code could benefit from the resolved user ID.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/list.ts` at line 35, The call to resolveUserIdFromApiKey(supabase,
options.apikey) discards its return value; either capture and use the returned
userId (e.g., const userId = await resolveUserIdFromApiKey(...)) where
downstream logic needs it, or explicitly document intent by assigning to an
underscore with a comment (e.g., const _userId = await
resolveUserIdFromApiKey(...); // validate API key only) so the purpose is clear
and linters won’t flag an unused value; update any downstream logic to use
userId if needed.
src/init/command.ts (1)

942-953: Consider batching permission checks for organizations.

The current implementation calls hasCliPermission sequentially for each organization via Promise.all. While this works correctly, users with many organizations may experience noticeable latency. Consider whether a batched permission check RPC could improve performance.

For most users this is likely acceptable, but worth noting for future optimization.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/init/command.ts` around lines 942 - 953, The permission checks loop
(permissionChecks using hasCliPermission over allOrganizations) can cause
latency for many orgs; replace the per-org RPC calls with a batched permission
check: either implement a new server-side RPC (e.g., hasCliPermissionBatch) that
accepts an array of org.gid and returns permission booleans, then call that from
this code and map results to allowedOrganizations, or apply client-side
batching/concurrency control (chunk allOrganizations into N-sized batches and
call hasCliPermission in parallel per chunk then await each batch) to avoid
running hundreds of RPCs at once; update the variables permissionChecks and
allowedOrganizations accordingly and ensure the error branch and pLog.error
message remain unchanged.
src/utils.ts (2)

1488-1493: Consider batching permission checks to reduce RPC overhead.

This makes N parallel RPC calls (one cli_check_permission per organization). For users with many organizations, this could be slow and put unnecessary load on the backend. Consider adding a backend endpoint that returns organizations filtered by permission in a single call.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/utils.ts` around lines 1488 - 1493, Current code builds permissionChecks
by mapping allOrganizations and calling hasCliPermission (which triggers
cli_check_permission per org) in parallel, causing N RPCs; replace this with a
single batched permission API on the backend that accepts an array of org IDs
and returns only the allowed orgs, then call that new endpoint (instead of
hasCliPermission per org) to get filtered organizations in one request; update
the logic around permissionChecks/allOrganizations to use the backend response
and remove the Promise.all-per-org pattern to eliminate per-org RPC overhead.

1534-1536: Include error details in log message.

When userIdError is present, the underlying error reason is lost. Consider including it in the log for easier debugging.

Proposed fix
   if (!userId || userIdError) {
-    log.error(`Cannot auth user with apikey`)
+    log.error(`Cannot auth user with apikey${userIdError ? `: ${formatError(userIdError)}` : ''}`)
     throw new Error('Cannot authenticate user with provided API key')
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/utils.ts` around lines 1534 - 1536, The current auth failure log loses
the underlying error; update the block that checks userId and userIdError to
include the error details from userIdError in the log (e.g., log.error message
for the userId/auth attempt should append or interpolate
userIdError.stack/message) while keeping the thrown Error message unchanged or
augmenting it with limited context; locate the conditional that references
userId, userIdError and log.error and change the log call to include the
userIdError details.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@src/channel/set.ts`:
- Around line 167-181: The supabase query that selects from 'app_versions' (the
block using resolvedBundleVersion and variables resolvedBundleVersion, userId)
incorrectly restricts results by .eq('user_id', userId); remove the user_id
filter so the query only filters by app_id, name, and deleted to honor org-level
permission already enforced by checkAppExistsAndHasPermissionOrgErr; update both
places in this file where .eq('user_id', userId) is applied (the
resolvedBundleVersion lookup and the later version lookup around line 223) to
match other functions like getActiveAppVersions/getVersionData which are
org-scoped.

---

Nitpick comments:
In `@src/app/add.ts`:
- Around line 94-106: Remove the unnecessary non-null assertions on
options.apikey by extracting a local const apikey immediately after
ensureOptions returns (e.g., const apikey = options.apikey) and use that const
in resolveUserIdFromApiKey, getOrganizationWithPermission, and
assertCliPermission calls; alternatively make ensureOptions a type guard that
narrows options so apikey is known defined—update the references to use the
narrowed/const apikey instead of options.apikey! (affects
resolveUserIdFromApiKey, getOrganizationWithPermission, assertCliPermission).

In `@src/app/list.ts`:
- Line 35: The call to resolveUserIdFromApiKey(supabase, options.apikey)
discards its return value; either capture and use the returned userId (e.g.,
const userId = await resolveUserIdFromApiKey(...)) where downstream logic needs
it, or explicitly document intent by assigning to an underscore with a comment
(e.g., const _userId = await resolveUserIdFromApiKey(...); // validate API key
only) so the purpose is clear and linters won’t flag an unused value; update any
downstream logic to use userId if needed.

In `@src/channel/add.ts`:
- Around line 39-53: The code calls resolveUserIdFromApiKey twice; capture its
returned userId once (call resolveUserIdFromApiKey(supabase, options.apikey)
into a variable) and reuse that variable later instead of calling the function
again. Replace the first discarded call with an assignment (userId) and remove
the second call, ensuring any subsequent logic that needs userId (e.g., the
existing userId usage after getOrganizationId) uses the captured variable.

In `@src/init/command.ts`:
- Around line 942-953: The permission checks loop (permissionChecks using
hasCliPermission over allOrganizations) can cause latency for many orgs; replace
the per-org RPC calls with a batched permission check: either implement a new
server-side RPC (e.g., hasCliPermissionBatch) that accepts an array of org.gid
and returns permission booleans, then call that from this code and map results
to allowedOrganizations, or apply client-side batching/concurrency control
(chunk allOrganizations into N-sized batches and call hasCliPermission in
parallel per chunk then await each batch) to avoid running hundreds of RPCs at
once; update the variables permissionChecks and allowedOrganizations accordingly
and ensure the error branch and pLog.error message remain unchanged.

In `@src/utils.ts`:
- Around line 1488-1493: Current code builds permissionChecks by mapping
allOrganizations and calling hasCliPermission (which triggers
cli_check_permission per org) in parallel, causing N RPCs; replace this with a
single batched permission API on the backend that accepts an array of org IDs
and returns only the allowed orgs, then call that new endpoint (instead of
hasCliPermission per org) to get filtered organizations in one request; update
the logic around permissionChecks/allOrganizations to use the backend response
and remove the Promise.all-per-org pattern to eliminate per-org RPC overhead.
- Around line 1534-1536: The current auth failure log loses the underlying
error; update the block that checks userId and userIdError to include the error
details from userIdError in the log (e.g., log.error message for the userId/auth
attempt should append or interpolate userIdError.stack/message) while keeping
the thrown Error message unchanged or augmenting it with limited context; locate
the conditional that references userId, userIdError and log.error and change the
log call to include the userIdError details.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: ec62f042-6e5b-4394-9ba5-4a4b0ff59103

📥 Commits

Reviewing files that changed from the base of the PR and between e5bf612 and b3cadf9.

📒 Files selected for processing (25)
  • src/app/add.ts
  • src/app/delete.ts
  • src/app/list.ts
  • src/app/set.ts
  • src/build/request.ts
  • src/bundle/cleanup.ts
  • src/bundle/compatibility.ts
  • src/bundle/delete.ts
  • src/bundle/list.ts
  • src/bundle/unlink.ts
  • src/bundle/upload.ts
  • src/channel/add.ts
  • src/channel/currentBundle.ts
  • src/channel/delete.ts
  • src/channel/list.ts
  • src/channel/set.ts
  • src/init/command.ts
  • src/login.ts
  • src/organization/add.ts
  • src/organization/delete.ts
  • src/organization/list.ts
  • src/organization/members.ts
  • src/organization/set.ts
  • src/user/account.ts
  • src/utils.ts

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.

1 participant