Skip to content

feat(frontend): create RBAC API keys from apikeys page#2332

Open
Dalanir wants to merge 6 commits into
mainfrom
feat/rbac-api-keys-page
Open

feat(frontend): create RBAC API keys from apikeys page#2332
Dalanir wants to merge 6 commits into
mainfrom
feat/rbac-api-keys-page

Conversation

@Dalanir
Copy link
Copy Markdown
Contributor

@Dalanir Dalanir commented May 22, 2026

Summary (AI generated)

  • Reworked the sidebar /apikeys page to create RBAC API keys instead of legacy mode keys.
  • Added RBAC role display for API keys and scope rendering for organizations/apps based on RBAC bindings.
  • Updated the creation modal to select multiple manageable organizations by default and configure app-level RBAC bindings.
  • Extended DialogV2 width options for the wider RBAC creation modal.

Motivation (AI generated)

Martin is migrating legacy API keys to RBAC API keys, so the global API keys page needs to support the RBAC creation model instead of the old read/upload/write/all mode flow.

Business Impact (AI generated)

This keeps API key management aligned with the RBAC migration and lets users create organization-scoped and app-scoped keys from the central API keys page without falling back to legacy permissions.

Test Plan (AI generated)

  • Ran bunx eslint src/pages/ApiKeys.vue src/components/DialogV2.vue src/stores/dialogv2.ts playwright/e2e/apikeys.spec.ts
  • Ran bun typecheck
  • Ran bunx playwright test playwright/e2e/apikeys.spec.ts
  • Ran bun test:front; observed 18/19 passing locally, with the remaining failure caused by login schema-cache retry before reaching /apikeys.

Generated with AI

Summary by CodeRabbit

  • New Features

    • API keys now support role-based access control with per-organization and optional per-application bindings.
  • User Interface Changes

    • API key list shows assigned role and aggregated org/app scope.
    • Key creation/regeneration dialog updated to collect RBAC role and per-app bindings; copy-key failures now open a dialog.
    • Dialogs support larger size options.
  • Localization

    • Added messages for permission denial and "Role per organization".
  • Tests

    • End-to-end API key tests updated for the RBAC flow.

Review Change Stack

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 22, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Makes API key listing and creation RBAC-aware: adds RBAC types and fetchers, computed display helpers, RBAC-driven add-key modal (org role + optional per-app bindings), table role column and RBAC-based scope display, two i18n keys, and DialogV2 size variants.

Changes

RBAC-Aware API Key Management

Layer / File(s) Summary
Type imports and translations
src/pages/ApiKeys.vue, messages/en.json
Adds Role/RoleBindingRow typing and two English messages (cannot-manage-org-api-keys, role-per-org).
Reactive RBAC state & computed helpers
src/pages/ApiKeys.vue
Introduces roles, allBindings, modal RBAC selection state, name caches, and computed helpers for role display and per-key/org aggregation.
Data loading: roles, bindings, and name caching
src/pages/ApiKeys.vue
Adds fetchRoles and fetchAllBindings, runs them during key load, refactors org/app name resolution to handle public and UUID-like app IDs with caching.
Add-key RBAC selection model & modal UI
src/pages/ApiKeys.vue
Replaces legacy key-type modal with RBAC selection: manageable-org multi-select, org-role radios, conditional app picker and per-app role selects, pruning watchers, and modal wiring.
Create/regenerate flows and backend payload
src/pages/ApiKeys.vue
Builds RBAC bindings (org + per-app with owner_org), removes key-type branching, calls backend apikey with bindings and derived limited_to_*, preserves hashed-key semantics, and improves error extraction.
DataTable role column & scope rendering
src/pages/ApiKeys.vue
Adds role column showing highest-priority role and updates scope cells to use RBAC-derived org/app IDs and cached names; removes legacy scope columns.
Selection helpers, watchers, copy/regenerate flows
src/pages/ApiKeys.vue
Adds org/app selection helpers, per-app role handlers, watchers to prune/clear pending bindings, and updates copy/regenerate UX for hashed keys.
Dialog size variants
src/components/DialogV2.vue, src/stores/dialogv2.ts
Extends DialogV2 sizeClasses and DialogV2Options.size to support 2xl and 3xl.
Playwright e2e helper updates
playwright/e2e/apikeys.spec.ts
Replaces createReadApiKey with createRbacApiKey and updates tests to use the new helper.

Sequence Diagram — high-level fetch flow

sequenceDiagram
  participant ApiKeys as ApiKeys.vue
  participant Backend as backend.apikey
  participant RoleSvc as RoleBindingsService
  participant AppsSvc as AppsService
  ApiKeys->>Backend: getKeys()
  Backend-->>ApiKeys: apikeys
  ApiKeys->>RoleSvc: fetchAllBindings(apikey principals)
  ApiKeys->>RoleSvc: fetchRoles()
  RoleSvc-->>ApiKeys: role_bindings, roles
  ApiKeys->>AppsSvc: loadAllApps(owner_org scoped)
  AppsSvc-->>ApiKeys: apps (primed caches)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • Cap-go/capgo#1984: RBAC migration/backfill that ensures role_bindings exist for the RBAC-based ApiKeys UI.
  • Cap-go/capgo#1951: Changes enforcing scoped limits and RBAC permission checks related to apikey limited scopes.

Suggested reviewers

  • riderx
🚥 Pre-merge checks | ✅ 4 | ❌ 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 (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main change: implementing RBAC API key creation on the apikeys page, matching the significant refactoring shown in the changeset.
Description check ✅ Passed The description covers the main changes and includes a test plan section, but the required Test plan subsection lacks detailed reproduction steps for the manual testing claim.
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

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

@codspeed-hq
Copy link
Copy Markdown
Contributor

codspeed-hq Bot commented May 22, 2026

Merging this PR will not alter performance

✅ 43 untouched benchmarks
⏩ 2 skipped benchmarks1


Comparing feat/rbac-api-keys-page (56b7616) with main (875fd87)

Open in CodSpeed

Footnotes

  1. 2 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

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

🤖 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 `@src/pages/ApiKeys.vue`:
- Around line 349-363: The title attribute is built from unescaped org names in
displayFunction (uses getHighestOrgRole, getRolesByOrg, getRoleDisplayName and
tooltipText) which allows injection if a name contains quotes; fix by escaping
attribute values or, better, by creating the span via DOM APIs and assigning the
tooltip via element.title (or setAttribute('title', ...)) and using textContent
for the visible label so values are not interpolated into HTML strings—ensure
tooltipText is properly escaped when you must build HTML strings.
🪄 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: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: b09456b0-f518-4ee6-bf4c-5198995f5b11

📥 Commits

Reviewing files that changed from the base of the PR and between 2716be4 and 7b2e3f8.

📒 Files selected for processing (2)
  • messages/en.json
  • src/pages/ApiKeys.vue

Comment thread src/pages/ApiKeys.vue
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: 2

🤖 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 `@messages/en.json`:
- Line 1740: The i18n key "select-multiple-organizations-help" is unused and
misleading for the checkbox-based org selector; either remove this key from
messages/en.json, or update and wire it into the UI: modify ApiKeys.vue to
display a localized help string (use $t("select-multiple-organizations-help") or
equivalent) near the checkbox list and update the message text to describe
checkbox toggling (e.g., "Click checkboxes to select organizations") and ensure
toggleOrgSelection(...) remains the change handler for each checkbox; choose one
approach and remove the dead key if you opt to wire nothing.

In `@src/pages/ApiKeys.vue`:
- Around line 1084-1095: The dropdown trigger button lacks an aria-expanded
attribute; update the button element that toggles showOrgDropdown (the one using
`@click`="showOrgDropdown = !showOrgDropdown" and displaying
selectedOrgNamesForCreation/selectedOrgsForCreation) to include a bound
aria-expanded reflecting the open state (e.g., :aria-expanded="showOrgDropdown")
so assistive tech can read the dropdown state.
🪄 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: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: f5ccc0ee-2cfa-41d1-962e-3679be94bc6b

📥 Commits

Reviewing files that changed from the base of the PR and between f50a2f7 and 1e1e8df.

📒 Files selected for processing (4)
  • messages/en.json
  • src/components/DialogV2.vue
  • src/pages/ApiKeys.vue
  • src/stores/dialogv2.ts

Comment thread messages/en.json Outdated
Comment thread src/pages/ApiKeys.vue
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: 2

Caution

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

⚠️ Outside diff range comments (1)
src/pages/ApiKeys.vue (1)

124-140: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Keep the table role summary org-scoped.

This helper now ranks every binding, so an app-level role can replace the org role shown in the new role column. A key with org_member plus one high-rank app binding will render the app role here, which is misleading at table level.

Suggested fix
 function getHighestRole(key: Database['public']['Tables']['apikeys']['Row']): string | null {
-  const keyBindings = getBindingsForKey(key)
+  const keyBindings = getBindingsForKey(key)
+    .filter(binding => binding.scope_type === 'org')
   if (keyBindings.length === 0)
     return null
🤖 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 `@src/pages/ApiKeys.vue` around lines 124 - 140, The current getHighestRole
function considers all bindings (including app-level) when selecting the top
role, causing app roles to override org roles in the table; update
getHighestRole (and where it calls getBindingsForKey) to first filter
keyBindings to only org-scoped bindings (e.g., binding.scope === 'org' or
binding.org_id matching the org context) and then perform the existing
priority_rank-based selection over that filtered list; if no org-scoped bindings
remain, return null so the table-level role column only reflects org-scoped
roles.
🤖 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 `@src/pages/ApiKeys.vue`:
- Around line 170-172: The formatDisplayApps function incorrectly deduplicates
apps by display name; update it to preserve the full list returned by
getDisplayAppIds instead of converting mapped names into a Set so different apps
with the same name and multiple unresolved apps remain distinct. Specifically,
in formatDisplayApps use getDisplayAppIds(key).map(appId =>
appCache.value.get(appId) || 'Unknown') and join the resulting array with ', '
(do not use Set), keeping reference to the function name formatDisplayApps and
the appCache and getDisplayAppIds helpers when making the change.
- Around line 149-156: getDisplayOrgIds currently only adds org ids from
bindings where scope_type === 'org', so keys that only have app-scoped bindings
miss their app's organization; modify getDisplayOrgIds to also extract and add
the org id from app-scoped bindings returned by getBindingsForKey(key) (e.g.
check b.org_id || b.app?.org_id or equivalent field on the binding/app object),
keep using the Set to dedupe, and return Array.from(orgIds) so app-binding orgs
are included in the organizations summary.

---

Outside diff comments:
In `@src/pages/ApiKeys.vue`:
- Around line 124-140: The current getHighestRole function considers all
bindings (including app-level) when selecting the top role, causing app roles to
override org roles in the table; update getHighestRole (and where it calls
getBindingsForKey) to first filter keyBindings to only org-scoped bindings
(e.g., binding.scope === 'org' or binding.org_id matching the org context) and
then perform the existing priority_rank-based selection over that filtered list;
if no org-scoped bindings remain, return null so the table-level role column
only reflects org-scoped roles.
🪄 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: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 2149345a-f259-4728-a029-ea74a2382210

📥 Commits

Reviewing files that changed from the base of the PR and between 1e1e8df and 2ff7bdf.

📒 Files selected for processing (1)
  • src/pages/ApiKeys.vue

Comment thread src/pages/ApiKeys.vue
Comment thread src/pages/ApiKeys.vue
Copy link
Copy Markdown

@BataraSurya BataraSurya left a comment

Choose a reason for hiding this comment

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

I think the new global create-key modal is missing the same anti-escalation filtering that the org-scoped RBAC API-key editor already applies.\n\nIn src/pages/settings/organization/ApiKeys.[id].vue, orgRoleOptions is filtered by the caller's current org-role priority (
.priority_rank <= callerOrgPriorityRank.value), so the UI only offers roles that the caller can actually grant. Here, src/pages/ApiKeys.vue builds orgRoleOptions from all assignable org roles except the explicit unsupported ones. For a user who can manage API keys in multiple orgs but has a lower-ranked role in at least one selected org, the modal can offer a role choice that createRoleBindingForPrincipal will reject on the backend. Because this create call is atomic across all selected orgs, that turns into a full create failure after the user submits, rather than a disabled/hidden role option up front.\n\nCould this compute the allowed org-role choices from the selected organizations, probably using the minimum caller priority across selectedOrgsForCreation, and filter orgRoleOptions the same way the org-scoped editor does? If a selected org makes the current role invalid, the modal should also clear or downgrade selectedOrgRole before submit.

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.

Caution

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

⚠️ Outside diff range comments (1)
playwright/e2e/apikeys.spec.ts (1)

19-41: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Expand test coverage to verify RBAC-specific behavior per PR objectives.

The current tests only verify basic creation and deletion flows. According to the PR objectives test plan, the following RBAC-specific behaviors should also be verified:

  • RBAC roles are displayed in the /apikeys table for migrated keys
  • Creating a key from /apikeys selects all organizations by default with the org_member role
  • App-level roles can be configured across selected organizations

Consider adding test scenarios to cover these customer-facing RBAC flows. As per coding guidelines, Playwright e2e tests should cover customer-facing flows and be run locally with bun run test:front before shipping UI changes.

🧪 Suggested additional test scenarios
test('should display RBAC role for newly created key', async ({ page }) => {
  const keyName = `Playwright RBAC ${Date.now()}`
  
  await createRbacApiKey(page, keyName)
  
  const keyRow = page.locator('tr', { hasText: keyName })
  // Verify the role column shows the expected default role (e.g., org_member)
  await expect(keyRow.locator('[data-role-cell]')).toContainText('org_member')
})

test('should select all organizations by default when creating key', async ({ page }) => {
  await page.click('[data-test="create-key"]')
  
  // Verify all organizations are selected by default in the modal
  const orgCheckboxes = page.locator('`#dialog-v2-content` input[type="checkbox"][data-org-select]')
  const count = await orgCheckboxes.count()
  for (let i = 0; i < count; i++) {
    await expect(orgCheckboxes.nth(i)).toBeChecked()
  }
})

Note: Adjust selectors to match actual data-test attributes in your implementation.

🤖 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 `@playwright/e2e/apikeys.spec.ts` around lines 19 - 41, The tests only cover
basic create/delete flows; add RBAC-focused assertions to apikeys.spec.ts to
validate migrated keys show roles, that the create-key modal selects all orgs by
default with org_member, and that app-level roles can be configured: after using
createRbacApiKey (or invoking the create flow via the create-key button), assert
the key row (page.locator('tr', { hasText: keyName })) contains the role cell
(e.g., locate via '[data-role-cell]') showing 'org_member'; open the create
modal (click '[data-test="create-key"]'), locate organization checkboxes (e.g.,
'`#dialog-v2-content` input[type="checkbox"][data-org-select]') and assert each is
checked by default; add an additional test that opens the app-level role
controls in the modal and verifies selecting/deselecting roles applies across
selected organizations.
🤖 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.

Outside diff comments:
In `@playwright/e2e/apikeys.spec.ts`:
- Around line 19-41: The tests only cover basic create/delete flows; add
RBAC-focused assertions to apikeys.spec.ts to validate migrated keys show roles,
that the create-key modal selects all orgs by default with org_member, and that
app-level roles can be configured: after using createRbacApiKey (or invoking the
create flow via the create-key button), assert the key row (page.locator('tr', {
hasText: keyName })) contains the role cell (e.g., locate via
'[data-role-cell]') showing 'org_member'; open the create modal (click
'[data-test="create-key"]'), locate organization checkboxes (e.g.,
'`#dialog-v2-content` input[type="checkbox"][data-org-select]') and assert each is
checked by default; add an additional test that opens the app-level role
controls in the modal and verifies selecting/deselecting roles applies across
selected organizations.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 7f909dd2-5ab4-412c-97cb-6547df13ed9b

📥 Commits

Reviewing files that changed from the base of the PR and between 2ff7bdf and 56b7616.

📒 Files selected for processing (3)
  • messages/en.json
  • playwright/e2e/apikeys.spec.ts
  • src/pages/ApiKeys.vue
💤 Files with no reviewable changes (1)
  • messages/en.json

@sonarqubecloud
Copy link
Copy Markdown

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