Gate AI Workspace actions on scopes, and align scope matching with the API - #3127
Conversation
… server actions in AI Workspace.
📝 WalkthroughWalkthroughThe PR updates role scopes and adds permission-aware behavior across AI Workspace. It centralizes permission tooltip and inheritance logic, gates gateway policies and resource actions, disables unauthorized controls, and updates documentation links. ChangesPermission enforcement
Estimated code review effort: 4 (Complex) | ~45 minutes Sequence Diagram(s)sequenceDiagram
participant WorkspacePage
participant AppAuth
participant PermissionConstants
participant ProtectedAction
WorkspacePage->>AppAuth: request hasPermission
AppAuth->>PermissionConstants: evaluate required scope
PermissionConstants-->>AppAuth: return permission result
AppAuth-->>WorkspacePage: return canPerform flag
WorkspacePage->>ProtectedAction: enable or disable action
Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
… enhancing user feedback with tooltips and alerts in the GatewayPolicies component.
…, changing secret read access to secret management, enhancing role definitions for better access control.
There was a problem hiding this comment.
Actionable comments posted: 7
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
portals/ai-workspace/src/pages/appShell/appShellPages/serviceProvider/ServiceProviderModelsTab.tsx (1)
493-514: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winUse one permission-aware tooltip for Add Model Provider.
When the update scope is absent and one provider already exists, both
DisabledActionTooltipand the innerTooltipare active. The control can show conflicting permission and single-provider-limit messages.Compute one title with permission taking precedence and use one tooltip wrapper.
🤖 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 `@portals/ai-workspace/src/pages/appShell/appShellPages/serviceProvider/ServiceProviderModelsTab.tsx` around lines 493 - 514, Replace the nested DisabledActionTooltip and Tooltip wrappers around the Button with a single tooltip component. Compute one title value that checks isReadOnlyProvider first and uses lockedActionTooltip if true, otherwise checks disableAddProviderButton and shows the FormattedMessage for the single-provider-limit case, otherwise returns an empty string. This ensures permission-related messages take precedence and prevents conflicting tooltip messages from both wrappers appearing simultaneously.
🧹 Nitpick comments (1)
portals/ai-workspace/src/auth/permissions.ts (1)
236-255: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winAdd table-driven tests for
checkPermission.Test exact matches, own-level wildcards, own-level
:manage, ancestor:manage, and theap:api_key:all:manageexception. This logic now controls permission-aware actions across the workspace. The PR currently has no unit tests for these rules.🤖 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 `@portals/ai-workspace/src/auth/permissions.ts` around lines 236 - 255, Add table-driven unit tests for checkPermission covering exact scope matches, own-level wildcard grants, own-level :manage grants, broader ancestor :manage grants, and the ap:api_key:all:manage exception. Include both granting and non-granting cases where needed, using representative scopes that verify the documented wildcard and hierarchy boundaries.
🤖 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
`@portals/ai-workspace/src/pages/appShell/appShellPages/applications/OverviewTabs/AssociationsTable.tsx`:
- Around line 162-166: Update AssociationsTab’s API-key add/remove action guards
to also require SCOPES.APPLICATION_API_KEY_CREATE and
SCOPES.APPLICATION_API_KEY_DELETE, respectively, alongside the association
permissions. Ensure unauthorized actions are blocked before drawer state is
mutated or addAPIKeys/removeAPIKey are called, with UI gating consistent with
APIKeyTab if applicable.
In
`@portals/ai-workspace/src/pages/appShell/appShellPages/gateways/ViewGateway.tsx`:
- Around line 233-241: Update the detail-page edit control near the header
action in ViewGateway so it checks SCOPES.GATEWAY_UPDATE through hasPermission
before allowing navigation. Match the existing table-level edit gating and
feedback behavior, and verify the edit route and gateway update API enforce the
same permission scope.
In
`@portals/ai-workspace/src/pages/appShell/appShellPages/proxies/LLMProxyGuardrailsTab.tsx`:
- Around line 207-212: The resource-level DisabledActionTooltip wrappers do not
receive the permission-specific tooltip when editing is unauthorized. Update the
wrappers around the resource-level actions, including the occurrence near the
existing action rendering and the additional occurrence noted in the comment, to
pass title={lockedActionTooltip} so unauthorized controls display
NO_PERMISSION_TOOLTIP while preserving gateway-managed behavior.
In
`@portals/ai-workspace/src/pages/appShell/appShellPages/proxies/LLMProxyOverview.tsx`:
- Around line 155-156: In LLMProxyOverview, derive canUpdateProxy from
SCOPES.LLM_PROXY_UPDATE alongside canDeleteProxy, disable the overview Edit
button and Save action when it is false, and guard handleSaveChanges so
updateProxy is only called when canUpdateProxy is true.
In
`@portals/ai-workspace/src/pages/appShell/appShellPages/serviceProvider/ServiceProviderDeploymentsCard.tsx`:
- Around line 581-585: Update the delete action tooltip near the disabled
condition in ServiceProviderDeploymentsCard so it displays NO_PERMISSION_TOOLTIP
when key.id exists but canDeleteProviderApiKey is false, instead of the generic
“Delete API key” text. Ensure the permission message takes precedence over the
identifier check while preserving existing behavior for other disabled states.
In
`@portals/ai-workspace/src/pages/appShell/appShellPages/serviceProvider/ServiceProviderGuardrailsTab.tsx`:
- Around line 222-227: Update the resource-level DisabledActionTooltip around
the Add action to pass title={lockedActionTooltip}, matching the global Add
control. Use the existing lockedActionTooltip derived from canEditProvider so
missing LLM_PROVIDER_UPDATE permission displays NO_PERMISSION_TOOLTIP while
gateway-managed resources retain their existing behavior.
In
`@portals/ai-workspace/src/pages/appShell/appShellPages/serviceProvider/ServiceProviderModelsTab.tsx`:
- Around line 162-167: The ModelPill component at lines 665-675 renders a remove
icon regardless of edit permissions, even when isReadOnlyProvider is true.
Update the ModelPill rendering to either conditionally pass the onRemove prop
only when canEditProvider is true, or wrap the remove icon with
DisabledActionTooltip and pass the lockedActionTooltip constant when the user
lacks LLM_PROVIDER_UPDATE permissions. Use the computed isReadOnlyProvider flag
or canEditProvider boolean to determine which approach to apply.
---
Outside diff comments:
In
`@portals/ai-workspace/src/pages/appShell/appShellPages/serviceProvider/ServiceProviderModelsTab.tsx`:
- Around line 493-514: Replace the nested DisabledActionTooltip and Tooltip
wrappers around the Button with a single tooltip component. Compute one title
value that checks isReadOnlyProvider first and uses lockedActionTooltip if true,
otherwise checks disableAddProviderButton and shows the FormattedMessage for the
single-provider-limit case, otherwise returns an empty string. This ensures
permission-related messages take precedence and prevents conflicting tooltip
messages from both wrappers appearing simultaneously.
---
Nitpick comments:
In `@portals/ai-workspace/src/auth/permissions.ts`:
- Around line 236-255: Add table-driven unit tests for checkPermission covering
exact scope matches, own-level wildcard grants, own-level :manage grants,
broader ancestor :manage grants, and the ap:api_key:all:manage exception.
Include both granting and non-granting cases where needed, using representative
scopes that verify the documented wildcard and hierarchy boundaries.
🪄 Autofix
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: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: 84ed6f45-2144-4510-ad2a-38cdc606ff3b
📒 Files selected for processing (34)
platform-api/resources/role-to-scope-mapping.yamlportals/ai-workspace/src/auth/permissions.tsportals/ai-workspace/src/contexts/GatewayPoliciesContext.tsxportals/ai-workspace/src/contexts/OIDCAppAuthProvider.tsxportals/ai-workspace/src/pages/appShell/QuickStartIntroPopup.tsxportals/ai-workspace/src/pages/appShell/appShellPages/applications/ApplicationNew.tsxportals/ai-workspace/src/pages/appShell/appShellPages/applications/ApplicationsList.tsxportals/ai-workspace/src/pages/appShell/appShellPages/applications/GenAIApplicationsSummaryCardSection.tsxportals/ai-workspace/src/pages/appShell/appShellPages/applications/OverviewTabs/APIKeyTab.tsxportals/ai-workspace/src/pages/appShell/appShellPages/applications/OverviewTabs/AssociationsTable.tsxportals/ai-workspace/src/pages/appShell/appShellPages/externalServers/ExternalServersList.tsxportals/ai-workspace/src/pages/appShell/appShellPages/externalServers/ExternalServersNew.tsxportals/ai-workspace/src/pages/appShell/appShellPages/externalServers/MCPProxiesSummaryCardSection.tsxportals/ai-workspace/src/pages/appShell/appShellPages/gateways/GatewayPolicies.tsxportals/ai-workspace/src/pages/appShell/appShellPages/gateways/GatewaysTable.tsxportals/ai-workspace/src/pages/appShell/appShellPages/gateways/ViewGateway.tsxportals/ai-workspace/src/pages/appShell/appShellPages/projects/AddNewProject.tsxportals/ai-workspace/src/pages/appShell/appShellPages/projects/ExploreMoreCard.tsxportals/ai-workspace/src/pages/appShell/appShellPages/projects/ProjectListView.tsxportals/ai-workspace/src/pages/appShell/appShellPages/providerTemplate/CreateProviderTemplate.tsxportals/ai-workspace/src/pages/appShell/appShellPages/providerTemplate/CreateProviderTemplateVersion.tsxportals/ai-workspace/src/pages/appShell/appShellPages/providerTemplate/ProviderTemplateOverview.tsxportals/ai-workspace/src/pages/appShell/appShellPages/providerTemplate/ProviderTemplatesList.tsxportals/ai-workspace/src/pages/appShell/appShellPages/proxies/LLMProxiesList.tsxportals/ai-workspace/src/pages/appShell/appShellPages/proxies/LLMProxiesSummaryCardSection.tsxportals/ai-workspace/src/pages/appShell/appShellPages/proxies/LLMProxyGuardrailsTab.tsxportals/ai-workspace/src/pages/appShell/appShellPages/proxies/LLMProxyNew.tsxportals/ai-workspace/src/pages/appShell/appShellPages/proxies/LLMProxyOverview.tsxportals/ai-workspace/src/pages/appShell/appShellPages/proxies/LLMProxyResourcesTab.tsxportals/ai-workspace/src/pages/appShell/appShellPages/serviceProvider/ProvidersList.tsxportals/ai-workspace/src/pages/appShell/appShellPages/serviceProvider/ServiceProviderDeploymentsCard.tsxportals/ai-workspace/src/pages/appShell/appShellPages/serviceProvider/ServiceProviderGuardrailsTab.tsxportals/ai-workspace/src/pages/appShell/appShellPages/serviceProvider/ServiceProviderModelsTab.tsxportals/ai-workspace/src/pages/appShell/appShellPages/serviceProvider/ServiceProviderOverview.tsx
… and AssociationsTable, enhancing user feedback with tooltips for create and delete actions. Update ViewGateway and ProxyOverview components to include permission checks for editing actions, ensuring consistent access control across the application.
$subject
Problem
A user holding
ap:llm_proxy:managefound the Create Proxy button disabled, while Add Provider stayed enabled for a role not meant to administer providers. Investigating both turned up four separate defects.1. Most action buttons had no permission check at all. The LLM-proxy create buttons, all application create/delete affordances, provider templates, MCP proxies, projects, gateway row actions, guardrail and model-catalog editors, and API-key actions rendered fully enabled regardless of scope. The disabled Create Proxy button was not a permission decision —
LLMProxyNew.tsxhad no scope logic at all, and the button was blocked by unmet form state (an unfulfilled provider API key) with no explanation shown.2. Two buttons that looked gated were not.
ProvidersListandLLMProxiesListcomputeddisabledfrom a quota flag hardcoded tofalse, so the scope check only controlled rendering. Whenever those buttons appeared they were clickable.3. Frontend scope matching disagreed with the API.
checkPermissionderived a parent:managefrom only the first two segments of the requested scope, soap:llm_provider:api_key:managefailed to satisfyap:llm_provider:api_key:read— a holder of the sub-resource grant was wrongly denied in the UI.4. Disabled link-buttons did not look disabled. A
Buttonrendered ascomponent={RouterLink}is an<a>, not a<button>. The click is blocked, but the anchor does not pick up the theme's disabled colouring, so the control appeared fully enabled while doing nothing — reading as a broken button rather than a permission boundary.ProvidersListandLLMProxiesListalready hand-rolled an opacity override for exactly this reason.Changes
Scope matching (
src/auth/permissions.ts)checkPermissionnow resolves in the same order the API authorizes a request — exact match → own-level:*wildcard → own-level:manage→ each broader ancestor's:manage. The ancestor rule is correct because sub-resource operations list the parent explicitly in their ownsecurityblock:ap:llm_provider:manageappears in the accepted set of every/llm-providers/{id}/api-keysand/deploymentsoperation.Scopes ending
:all:manageare excluded from derivation.ap:api_key:all:manageis an ownership override — it widens whose rows are reachable, not which actions — soap:api_key:managemust not confer it, matchingcanManageAPIKeyinplatform-api/internal/serviceand the one operation (/me/api-keys) whose accepted list omits a parent:manage.Two shared constants were added:
NO_PERMISSION_TOOLTIP, which deliberately does not name the missing scope so the tooltip cannot be used to probe the authorization model, andDISABLED_ACTION_SX, which supplies the disabled styling anchors don't inherit.UI gating
Create, update and delete affordances across LLM proxies, providers, applications, MCP proxies, projects, provider templates, gateways, guardrails, the model catalog and API keys are now
disabledwith an explanatory tooltip, rather than hidden or silently inert. Create pages for LLM proxies, applications and MCP proxies gained route-level guards mirroring the existing one inServiceProviderNew.tsx.Where a tab already had a gateway-managed read-only gate (guardrails, models), the permission was folded into that existing flag rather than added as a parallel mechanism, so one definition change covers every control it already guarded — and the tooltip switches to the permission message when that is the cause rather than mislabelling it "gateway-managed".
The Create Proxy submit button now names its actual blocker ("Select an LLM provider", "This provider requires an API key…") instead of disabling itself with no signal — the behaviour that made a form-state problem look like a permissions problem.
Detail-page header actions are gated alongside their list-table equivalents.
ViewGatewayandLLMProxyOvervieweach carried an Edit control that the corresponding row action inGatewaysTable/ProxiesListalready gated, so the same operation was reachable or not depending on which screen you opened it from.LLMProxyOverview's sticky Save is gated onap:llm_proxy:updateas well, withhandleSaveChangesreturning beforeupdateProxy— the button is not the only entry point to it.Where two tooltips could fire on one control, the permission message takes precedence. The resource-level Add buttons on both guardrails tabs were wrapped in a
DisabledActionTooltipthat fell through to its "gateway-managed" default, so a permission block was mislabelled; the provider models tab nested aDisabledActionTooltipinside aTooltip, letting the missing-permission and single-provider-limit messages render together. Both now resolve to one title, permission first.Reads that would 403 are no longer attempted
Gating write actions is not sufficient on its own: a surface whose read is denied fails just as visibly. The gateway Policies tab loaded the gateway manifest and the org's custom policies on mount, so a caller without
ap:gateway:manifest:readandap:gateway_custom_policy:readgot a 403 and a "Failed to load gateway policies" error that looked like an outage rather than a permission boundary.GatewayPoliciesProvidernow skips the request entirely when either scope is missing, and the tab renders a permission notice instead. The per-row Sync action is separately gated onap:gateway_custom_policy:create, since viewing the manifest and syncing a policy into the organization are different grants.Role-to-scope mapping (committed separately)
With ancestor matching in place, sub-resource scopes listed alongside a parent
:managebecame redundant. 25 such entries were removed across the five roles, and the convention block now documents that a resource:managecovers its sub-resources and that:all:manageis neither implied by nor implies the plain:manage. The Helm chart's inlinedap_adminis now identical to the canonical role'sap:scopes.Documentation links
Nine AI Workspace doc links pointed at the retired
wso2.com/bijira/docs/base (one was onapi-platformbut missing the/next/segment). All now point athttps://wso2.com/api-platform/docs/next/ai-workspace/…, verified against the live site — the section exists and its structure matches ours one-for-one.Behaviour changes
Every role's effective access is unchanged except
ap_publisher, which was deliberately narrowed:ap_admin,ap_operator,ap_subscriber,ap_viewerap_publisherap:llm_provider:manage→ap:llm_provider:read;ap:llm_provider:deployment:manageremovedPublishers can no longer create, edit, delete or deploy LLM providers — provider administration is now an admin task. They retain
ap:llm_proxy:manage(which covers proxy API keys and deployments), so pointing a proxy at an existing provider and deploying it still works.Three consequences to be aware of:
ap:llm_provider:readdoes not reach the sub-resource, because those operations accept the parent:manage, never the parent:read. Addap:llm_provider:deployment:readif read-only visibility is wanted.ap:llm_provider:api_key:createis admin-only). The Generate API Key button shows disabled with a tooltip; the manual key field on the proxy form remains available.ap:gateway:read) can list gateways, open one and see live status — the row click and both read endpoints are unaffected — while Edit, Delete and Reconfigure are disabled, and the Policies tab shows a permission notice instead of erroring.This is a live authorization narrowing — any
ap_publishercurrently managing LLM providers will receive403s after a restart, which the mapping file requires to take effect.Validation
tsc --noEmitreports zero errors in every touched file andnpm run buildsucceeds; the remaining ~30 errors are pre-existing on this branch (missingJSXnamespace, absent@typesforjs-yaml/swagger-ui-react, and unrelated context type errors). No lint run —portals/ai-workspacehas noeslint.config.js.scopeSatisfieslooping over each operation's declared accepted scopes, for all 5 roles × 114 operations: 0 mismatches — no false enables, no false disables.NO_PERMISSION_TOOLTIPsite was checked for a paired disabled control, and separately for the subtler case where the tooltip condition and thedisabledexpression use different flags. Zero unpaired sites; a brace-aware JSX scan confirms no gated link-button is left without disabled styling. Review follow-up caught the inverse of that case — a control correctlydisabledbut carrying a tooltip that named the wrong reason — which the scan did not look for; those are fixed above.ModelPill's remove icon takesremoveDisabled={isSaving || isReadOnlyProvider}and re-checks the flag insideonRemove, so the missingap:llm_provider:updatecheck is a tooltip gap, not a reachable path.openapi.yamlaccept the parent:manage, confirming the ancestor rule models the API correctly.Notable fixes found while verifying
ap:gateway:token:create. A holder of:read+:createwithout:deletewould have minted a new token while revoking nothing, leaving stale tokens live. It now requires all three;ap:gateway:token:manageandap:gateway:managesatisfy them via the ancestor rule.ServiceProviderModelsTabhadisReadOnlyProviderhardcodedfalse, deliberately, because the model catalog is not part of the runtime artifact. That reasoning holds for read-only artifacts but not for permissions — editing it still writes to the provider — so it is now driven byap:llm_provider:update, with the original comment preserved and extended.AssociationsTabmapped and unmapped keys throughPOST/DELETE /applications/{id}/api-keys— the same two endpointsAPIKeyTabgates onap:application:api_key:create/:delete— but the file had nohasPermissioncall at all, so the association scopes were the only thing standing in front of them. The two grants are distinct: associating a provider with an application is not the same as minting a key against it. Both handlers now return before touching drawer state or the API, the permission is folded into the existingselectionBlockedMessage(which already gates key selection inside both association drawers), and the key payload is dropped from the add-association flows so associating still works without the key grant.ap:gateway:read-only role can actually reach, rather than by testing the button gating alone.Not covered
No unit tests for the changed matching logic. The portal has only Cypress e2e configured; adding a unit runner is a dependency decision under
js-dependency-management.md. This logic is security-relevant and should get tests — worth resolving before or shortly after merge. The spec-driven equivalence checks above were run manually and are not committed as regression tests.Eight files still lack scope checks, all deliberately: sub-components of an already-gated form (
PolicyParameterEditor/*,GuardrailsSection,TemplateVersionDialog,ProviderTemplateFormFields,PolicyMapper), a display-only tab (LLMProxyOverviewTab), and pre-auth onboarding (OrgProvisioningPage, which runs before a user has scopes).AssociationsTabwas on this list as "dialog confirm buttons whose trigger is gated" — that turned out to be wrong for its API-key actions, see above.dp:scope redundancy not addressed. Three entries look redundant by the mapping file's documented convention (dp:key_manager:readunderdp:key_manager:manage;dp:application_key:revokeunderdp:application_key:manage).platform-apionly shape-checksdp:scopes — the API Portal enforces them against a spec not verified here, andrevokebeing separately named suggests it may be an override like:all:manage. Removing them needs the portal's accepted-scope lists checked first.Four scopes are referenced but declared nowhere —
ap:gateway:artifact:readandap:organization:subscription:read(in both OIDC scope-request strings) andap:rest_api:api_key:read,ap:rest_api:publication:read(inpermissions.ts). No role grants them and no UI check uses them, but they were left in place: the mapping file notes that scopes may also be declared by compiled-in plugins, exactly as thewebsub/webbrokerentries are.Five non-docs
wso2.com/bijira/*links remain (terms-of-use,privacy-policyinappShellMain.tsxandBasicAuthLoginPage.tsx). These are legal pages, not documentation, and the api-platform equivalents were not verified.Reviewer notes
/ai-workspacepath-prefix commits. Confirm the intended PR scope before opening.ap_publishernarrowing, since it changes production authorization.