Skip to content

Add ap:api_key:all:manage scope for cross-user API key administration - #2911

Merged
renuka-fernando merged 11 commits into
wso2:mainfrom
Thushani-Jayasekera:fix-api-keu
Jul 29, 2026
Merged

Add ap:api_key:all:manage scope for cross-user API key administration#2911
renuka-fernando merged 11 commits into
wso2:mainfrom
Thushani-Jayasekera:fix-api-keu

Conversation

@Thushani-Jayasekera

@Thushani-Jayasekera Thushani-Jayasekera commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Summary

API key CRUD is creator-scoped: only the user recorded in api_keys.created_by may update or
revoke a key. There is currently no way for an administrator to manage a key created by someone
else — a problem when a user leaves the organization, or when an operator needs to rotate a
compromised key on their behalf.

This PR introduces ap:api_key:all:manage, an organization-wide scope that lets its holder
perform API key CRUD on any user's keys, alongside the existing creator-based access. Every other
caller keeps the creator-scoped default.

It also removes the gateway-controller's ownership re-check on the platform-api event path, which
cannot work for admin-initiated changes: an apikey.updated/apikey.revoked event carries only
the acting user's id, never their scopes, so the gateway has no way to tell an org key admin apart
from an unauthorized third party and rejects every admin-initiated rotation.

Scope model

ap:api_key:all:manage grants two things:

  1. Route access — it is accepted on every API-key operation's security list, so a dedicated
    key-admin persona can call them without also holding ap:rest_api:manage,
    ap:llm_provider:manage, etc.
  2. Ownership override — it lifts the created_by restriction inside the service layer.

Both are required; neither alone is sufficient. Reaching the endpoint (GO-AUTH-007) and being
allowed to touch another user's row are separate decisions, and the service-layer check is the
authoritative one (GO-AUTH-015).

The override widens which rows within the caller's own organization are reachable — never
which organization. Every query still sources organization_uuid from token claims (GO-AUTH-005).

Granted to platform-admin only. platform-operator, platform-developer, and platform-viewer
deliberately do not receive it.

Surfaces covered

Surface Behavior with the scope
PUT/DELETE /rest-apis/{id}/api-keys/{keyId} Update / revoke any user's key
PUT/DELETE /websub-apis, /webbroker-apis .../api-keys/{keyId} Same (shared APIKeyService)
GET/DELETE /llm-providers/{id}/api-keys List and delete any user's key
GET/DELETE /llm-proxies/{id}/api-keys List and delete any user's key
POST /applications/{id}/api-keys Bind any user's key to an application
GET /me/api-keys Returns every user's keys in the org

Implementation

One shared predicate. The ownership rule was previously written out separately in each service,
with three subtly different forms. It is now a single function in internal/service/apikey.go
that every path calls, so it cannot drift between artifact kinds:

func canManageAPIKey(createdBy, callerUserID string, keyAdmin bool) bool {
    if keyAdmin {
        return true
    }
    return callerUserID != "" && createdBy == callerUserID
}

Explicit parameters, not implicit context. Handlers resolve the scope once from the request's
verified claims via middleware.HasEffectiveScope (the pattern already used in
organization.go), then pass an explicit keyAdmin bool down. An authorization input travels as
a visible argument that a future refactor cannot silently drop.

The repository race guard is unaffected. Update/Revoke still re-verify created_by in
their WHERE clause, and admin paths pass the key's stored creator, so the delete/recreate
protection holds for admin-initiated writes too.

Gateway side. APIKeyUpdateParams and APIKeyRevocationParams gained an
AuthorizedUpstream bool, set to true only by UpdateExternalAPIKeyFromEvent /
RevokeExternalAPIKeyFromEvent. The check is skipped there, not deleted — the gateway's own REST
API path still enforces it in full, since the flag defaults to false.

Behavior changes worth reviewer attention

1. A latent break in the webhook path is fixed.
internal/webhook/handlers_apikey.go calls update/revoke with userID: "". The fail-closed
userId == "" check added in 6598519 made both of those webhook events return 403 on every
delivery. They now pass keyAdmin: true, justified by the envelope's HMAC signature (verified in
ReceiveEvent before dispatch) rather than by key ownership — the payload carries no user
identity at all. Please confirm this reading matches intent.

2. The LLM services' implicit-admin bypass is removed.
They previously treated an empty caller id as admin (if userID != "" && key.CreatedBy != userID),
which fails open for any path that forgets to populate the actor. The shared predicate fails
closed instead. All callers go through resolveActorErr, which errors on an unresolvable actor,
so no live path is affected.

3. /me/api-keys widening is an explicit flag, not an empty string.
ListAPIKeysByUser takes allUsers bool and errors if given an empty username without it,
rather than silently dropping the creator filter and returning the whole org's keys. The SQL is
unchanged in shape: the creator predicate is one of two code-controlled literals and the username
is always bound through a ? placeholder (GO-AUTH-008).

Known limitation

On the webhook path, keyAdmin: true bypasses ownership for whatever key occupies the target
(artifact, handle)
— not only keys that channel provisioned. Since api_keys uniqueness is
artifact + name, a Developer Portal event could rotate or revoke a key an interactive user created
via the REST API under a colliding handle. This is documented in the code rather than left
implicit. If it is unacceptable, the tighter fix is to assert created_by == "" (system-owned) on
the webhook path instead of passing a blanket flag — happy to switch.

Documentation

Added GO-AUTH-019: Ownership-Override Scopes Must Be Named, Service-Layer Enforced, and Never
Silently Assumed
to .claude/rules/authentication_authorization.md. This pattern deliberately
cuts against the creator-scoped default and looks like a tenant-isolation hole at a glance, so it
needs a written rationale. The rule covers why an empty caller id must never mean "admin", why the
downstream check is gated rather than deleted, and why the override must not widen past the
caller's own organization. Purely additive — no existing rule renumbered or edited.

Testing

  • New: internal/service/apikey_authz_test.goTestCanManageAPIKey, 7 cases pinning the
    ownership rule including the fail-closed empty-caller behavior. Passes.
  • go vet clean across both platform-api and gateway-controller, test files included.
  • Existing internal/service/application_test.go call sites updated for the new parameter.

⚠️ Not run: the full test suite. The dev machine ran out of disk space, so
internal/repository and plugins/eventgateway/handler could not be linked. Those two packages
are unverified — CI should be treated as the source of truth for them.

Configuration / upgrade notes

The scope is appended to every shipped admin scope string (config.toml,
config-template.toml, default_config.go, Helm values.yaml, and both e2e configs). Operators
using IDP role mode get it automatically via platform-admin in roles.yaml. Operators using
scope mode with a hand-maintained scope list must add ap:api_key:all:manage to their admin
client for cross-user key management to work; without it, behavior is unchanged (creator-scoped).

portals/ai-workspace/configs/config-template.toml was deliberately not updated — it requests
neither ap:api_key:read nor this scope today, and defaulting a user-facing portal to an
org-wide admin grant seemed wrong. Easy to add if you disagree.

…forbidden access in Update and Revoke methods. Introduce new error codes for API key not found and forbidden access in the error catalog.
@coderabbitai

coderabbitai Bot commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

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

API key management now supports an explicit organization-wide manage scope, centralized creator authorization, trusted event origins, expiry validation, cataloged lookup errors, updated repository filtering, and removal of built-in file-auth admin credentials.

Changes

API key authorization

Layer / File(s) Summary
Authorization contracts and scope definitions
platform-api/internal/apperror/*, platform-api/internal/constants/*, platform-api/resources/*, platform-api/plugins/eventgateway/openapi.yaml, .claude/rules/*
Adds the ap:api_key:all:manage scope, API-key not-found/forbidden errors, OpenAPI security requirements, role permissions, and authorization guidance.
Service ownership and expiry enforcement
platform-api/internal/service/*
Centralizes creator-or-admin checks, validates future expirations, applies authorization to REST, LLM, proxy, and application API-key flows, and updates revoke audit ordering.
Repository ownership filtering
platform-api/internal/repository/*, platform-api/internal/integration/*
Adds explicit all-user listing behavior, rejects empty-user widening, and updates repository call sites for creator-aware revocation.
Request authorization wiring
platform-api/internal/handler/*, platform-api/internal/server/server.go, platform-api/plugins/eventgateway/*
Passes validation mode into handlers, derives manage-all authorization from effective scopes, and forwards it to service operations.
Trusted events and file-auth configuration
gateway/gateway-controller/*, platform-api/internal/webhook/*, platform-api/config/*, kubernetes/helm/*, tests/*, platform-api/README.md
Marks validated event requests as trusted, skips downstream ownership rechecks for those events, adds admin scopes to configurations, and requires externally supplied file-auth credentials.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant APIKeyHandler
  participant APIKeyService
  participant APIKeyRepo
  Client->>APIKeyHandler: API key operation with effective scope
  APIKeyHandler->>APIKeyService: pass keyAdmin and trustedOrigin
  APIKeyService->>APIKeyRepo: load key and enforce ownership
  APIKeyRepo-->>APIKeyService: key record or lookup error
  APIKeyService-->>APIKeyHandler: result or catalog error
Loading

Possibly related PRs

Suggested reviewers: krishanx92, renuka-fernando, lasanthas, anugayan, malinthaprasan

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: adding organization-wide API key management scope.
Description check ✅ Passed The description is detailed and covers purpose, goals, implementation, tests, docs, and rollout notes well enough for review.
Docstring Coverage ✅ Passed Docstring coverage is 82.76% which is sufficient. The required threshold is 80.00%.
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.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🤖 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 `@platform-api/internal/service/apikey.go`:
- Around line 565-567: The lookup-failure logs in the API-key update flow must
not expose raw resource identifiers. Update the Error calls around
existingKeyErr and the corresponding failure at the second referenced location
to omit, redact, or replace apiHandle and keyName with an approved keyed-hash
correlation value, while preserving the existing error handling and returned
errors.
- Around line 520-521: Update the creator authorization checks in the API-key
update and revoke flows to reject empty userId values before comparing
ownership; require a non-empty identity that matches the key creator. Apply the
same fail-closed logic to both updateKey and revokeKey, preserving creator-only
access and avoiding implicit authorization for system callers.
- Around line 564-573: Update APIKeyRepo.Update and APIKeyRepo.Revoke to include
the expected creator/owner in each SQL WHERE clause alongside artifact_uuid and
handle, preventing delete/recreate races from mutating another key. Check the
affected-row count and return the existing “api key not found” error when no
rows are affected; preserve normal success behavior otherwise.
- Around line 564-570: Update UpdateAPIKey and RevokeAPIKey to resolve apiId,
then immediately load the API key and enforce ownership before gateway lookup,
broadcasting, hashing, or other gateway operations. Preserve request-derived
input validation at that point, and translate sql.ErrNoRows from
GetByArtifactAndName into RESTAPIKeyNotFound while retaining existing error
handling for other repository failures.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: c4343b45-b16a-41a3-93d4-38a830fc0cc5

📥 Commits

Reviewing files that changed from the base of the PR and between 8e2b470 and 6598519.

📒 Files selected for processing (3)
  • platform-api/internal/apperror/catalog.go
  • platform-api/internal/apperror/codes.go
  • platform-api/internal/service/apikey.go

Comment thread platform-api/internal/service/apikey.go Outdated
Comment thread platform-api/internal/service/apikey.go Outdated
Comment thread platform-api/internal/service/apikey.go Outdated
Comment thread platform-api/internal/service/apikey.go Outdated
coderabbitai[bot]
coderabbitai Bot previously approved these changes Jul 27, 2026
@Thushani-Jayasekera Thushani-Jayasekera changed the title Enhance API key management: add error handling for key not found and forbidden access in Update and Revoke methods Add ap:api_key:all:manage scope for cross-user API key administration Jul 28, 2026
…ith the `ap:api_key:all:manage` scope to manage any API key

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 4

Caution

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

⚠️ Outside diff range comments (4)
platform-api/resources/openapi.yaml (1)

4407-4418: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Document the organization-wide /me/api-keys response.

Callers with ap:api_key:all:manage are intended to receive every user’s keys in the organization, but the summary and description still say “current user” and “caller.” Update the contract to describe the conditional organization-wide behavior.

🤖 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 `@platform-api/resources/openapi.yaml` around lines 4407 - 4418, Update the
/me/api-keys GET contract identified by operationId listUserAPIKeys so its
summary and description document that ap:api_key:all:manage callers receive all
users’ API keys in the organization, while retaining the current-user behavior
for callers without that scope.
platform-api/internal/service/llm_apikey.go (1)

155-168: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Make API key deletion authorization atomic.

GetByArtifactAndName(...) authorizes delete access, but APIKeyRepo.Delete(...) only deletes api_keys WHERE artifact_uuid = ? AND handle = ?. Under the UNIQUE(artifact_uuid, handle) index, an authorized request can revoke/delete a replacement key if this key is removed and recreated before the delete. Use a conditional delete keyed by the original row identity/version/creator, or re-check the authorizing key ownership inside the deletion transaction.

🤖 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 `@platform-api/internal/service/llm_apikey.go` around lines 155 - 168, The
deletion flow around GetByArtifactAndName and apiKeyRepo.Delete must bind
authorization to the originally fetched key so a replacement cannot be deleted
between lookup and deletion. Update the repository/API to perform a conditional,
transactional delete using the original key’s stable identity, version, or
creator fields, and ensure the service treats a failed condition as no longer
authorized/not found.
platform-api/internal/service/llm_proxy_apikey.go (1)

133-137: 🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift

Re-verify the creator in the delete statement.

Authorization protects the fetched key, but apiKeyRepo.Delete(proxy.UUID, keyName) can delete a same-name replacement created after that check. Extend Delete with the expected CreatedBy and constrain its WHERE clause accordingly.

🤖 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 `@platform-api/internal/service/llm_proxy_apikey.go` around lines 133 - 137,
Extend apiKeyRepo.Delete to accept the authorized key’s expected CreatedBy
value, pass existingKey.CreatedBy from the delete flow after canManageAPIKey
succeeds, and add CreatedBy to the delete statement’s WHERE constraints so a
same-name replacement cannot be removed.

Source: Coding guidelines

platform-api/internal/service/application.go (1)

783-795: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Fail closed for missing binding identities.

When keyAdmin is false, Lines 790-792 still return success for an empty requester, allowing an unidentified caller to bind another user’s key. Use canManageAPIKey (after trimming if needed) and return ApplicationAPIKeyForbidden on any non-admin missing/mismatched identity.

As per coding guidelines, creator-scoped resource access must fail closed for missing identities; cross-user access requires an explicit administrative scope.

🤖 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 `@platform-api/internal/service/application.go` around lines 783 - 795, Update
the non-admin identity check in the API-key authorization flow around
canManageAPIKey to fail closed: after trimming identities as needed, return
ApplicationAPIKeyForbidden whenever the creator or requester is missing or they
do not match. Preserve the keyAdmin bypass, and use canManageAPIKey rather than
allowing empty identities to return success.

Source: Coding guidelines

🧹 Nitpick comments (2)
platform-api/internal/service/apikey_user.go (1)

50-65: 🚀 Performance & Scalability | 🔵 Trivial | 🏗️ Heavy lift

Organization-wide listing should not paginate only after full materialization. Both admin paths now widen filtering to all keys but still load and identity-resolve the complete result set before slicing the requested page.

  • platform-api/internal/service/apikey_user.go#L50-L65: add repository-level pagination and total counting for organization-wide /me/api-keys listings.
  • platform-api/internal/service/llm_apikey.go#L83-L97: apply repository-level filtering, pagination, and counting for provider listings.
🤖 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 `@platform-api/internal/service/apikey_user.go` around lines 50 - 65, Update
APIKeyUserService.ListAPIKeysByUser in
platform-api/internal/service/apikey_user.go (lines 50-65) to use
repository-level filtering, pagination, and total counting for organization-wide
listings, avoiding full result materialization and identity resolution before
slicing. Apply the same repository-level filtering, pagination, and counting
change to the provider listing flow in
platform-api/internal/service/llm_apikey.go (lines 83-97), preserving the
requested page and total-count response behavior.
platform-api/internal/repository/apikey_test.go (1)

141-141: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Add mismatch coverage for creator re-verification.

Add update and revoke cases with a different createdBy, asserting an error and that the original row remains unchanged. The current test covers only the matching-owner path.

🤖 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 `@platform-api/internal/repository/apikey_test.go` at line 141, Add update and
revoke test cases in the existing repository tests around repo.Revoke, using a
createdBy value different from the artifact’s original creator. Assert both
operations return an error and verify the original API key row remains unchanged
after each attempt.

Source: Coding guidelines

🤖 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 @.claude/rules/authentication_authorization.md:
- Around line 1502-1515: Update RevokeAPIKey to use the existing handle
parameter consistently wherever the repository calls currently pass the
undefined artifactID, including GetByArtifactAndName and Revoke. Keep the
function signature and authorization behavior unchanged.

In `@platform-api/config/config.toml`:
- Line 39: Remove the functional default admin credential before granting
ap:api_key:all:manage: in platform-api/config/config.toml at lines 39-39,
eliminate the default username/password-hash fallback; in
platform-api/config/default_config.go at lines 96-96, remove the hard-coded
default user/hash and make startup fail closed when no operator-provided
bootstrap user exists, or generate and persist a random bootstrap credential.

In `@platform-api/internal/webhook/handlers_apikey.go`:
- Around line 229-237: Replace the empty actor plus keyAdmin=true authorization
in the update path at platform-api/internal/webhook/handlers_apikey.go:229-237
with an explicit authorizedUpstream context propagated through the shared
API-key service, restricted to the verified webhook path. Apply the same
explicit trusted-upstream authorization mechanism to the revoke path at
platform-api/internal/webhook/handlers_apikey.go:252-254, preserving fail-closed
creator checks for ordinary requests.

In `@platform-api/plugins/eventgateway/handler/webbroker_apikey.go`:
- Line 193: Map RESTAPIKeyForbidden to the catalog 403 response before the
generic error handling in the UpdateAPIKey error branches at
platform-api/plugins/eventgateway/handler/webbroker_apikey.go lines 193-193 and
240-240, and platform-api/plugins/eventgateway/handler/websub_apikey.go lines
193-193 and 241-241; preserve the existing generic handling for other errors.

---

Outside diff comments:
In `@platform-api/internal/service/application.go`:
- Around line 783-795: Update the non-admin identity check in the API-key
authorization flow around canManageAPIKey to fail closed: after trimming
identities as needed, return ApplicationAPIKeyForbidden whenever the creator or
requester is missing or they do not match. Preserve the keyAdmin bypass, and use
canManageAPIKey rather than allowing empty identities to return success.

In `@platform-api/internal/service/llm_apikey.go`:
- Around line 155-168: The deletion flow around GetByArtifactAndName and
apiKeyRepo.Delete must bind authorization to the originally fetched key so a
replacement cannot be deleted between lookup and deletion. Update the
repository/API to perform a conditional, transactional delete using the original
key’s stable identity, version, or creator fields, and ensure the service treats
a failed condition as no longer authorized/not found.

In `@platform-api/internal/service/llm_proxy_apikey.go`:
- Around line 133-137: Extend apiKeyRepo.Delete to accept the authorized key’s
expected CreatedBy value, pass existingKey.CreatedBy from the delete flow after
canManageAPIKey succeeds, and add CreatedBy to the delete statement’s WHERE
constraints so a same-name replacement cannot be removed.

In `@platform-api/resources/openapi.yaml`:
- Around line 4407-4418: Update the /me/api-keys GET contract identified by
operationId listUserAPIKeys so its summary and description document that
ap:api_key:all:manage callers receive all users’ API keys in the organization,
while retaining the current-user behavior for callers without that scope.

---

Nitpick comments:
In `@platform-api/internal/repository/apikey_test.go`:
- Line 141: Add update and revoke test cases in the existing repository tests
around repo.Revoke, using a createdBy value different from the artifact’s
original creator. Assert both operations return an error and verify the original
API key row remains unchanged after each attempt.

In `@platform-api/internal/service/apikey_user.go`:
- Around line 50-65: Update APIKeyUserService.ListAPIKeysByUser in
platform-api/internal/service/apikey_user.go (lines 50-65) to use
repository-level filtering, pagination, and total counting for organization-wide
listings, avoiding full result materialization and identity resolution before
slicing. Apply the same repository-level filtering, pagination, and counting
change to the provider listing flow in
platform-api/internal/service/llm_apikey.go (lines 83-97), preserving the
requested page and total-count response behavior.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: b71bbe6e-3906-4ea4-a41b-43cb8d37e757

📥 Commits

Reviewing files that changed from the base of the PR and between 6598519 and e5c22fc.

📒 Files selected for processing (36)
  • .claude/rules/authentication_authorization.md
  • gateway/gateway-controller/pkg/utils/api_key.go
  • kubernetes/helm/platform-api-helm-chart/values.yaml
  • platform-api/config/config-template.toml
  • platform-api/config/config.toml
  • platform-api/config/default_config.go
  • platform-api/internal/constants/constants.go
  • platform-api/internal/handler/api_key.go
  • platform-api/internal/handler/apikey_user.go
  • platform-api/internal/handler/application.go
  • platform-api/internal/handler/llm_apikey.go
  • platform-api/internal/handler/llm_proxy_apikey.go
  • platform-api/internal/integration/audit_identity_it_test.go
  • platform-api/internal/integration/crud_test.go
  • platform-api/internal/repository/apikey.go
  • platform-api/internal/repository/apikey_test.go
  • platform-api/internal/repository/interfaces.go
  • platform-api/internal/server/server.go
  • platform-api/internal/service/apikey.go
  • platform-api/internal/service/apikey_authz_test.go
  • platform-api/internal/service/apikey_user.go
  • platform-api/internal/service/application.go
  • platform-api/internal/service/application_test.go
  • platform-api/internal/service/llm_apikey.go
  • platform-api/internal/service/llm_proxy_apikey.go
  • platform-api/internal/webhook/handlers_apikey.go
  • platform-api/internal/webhook/receiver.go
  • platform-api/plugins/eventgateway/handler/webbroker_apikey.go
  • platform-api/plugins/eventgateway/handler/websub_apikey.go
  • platform-api/plugins/eventgateway/openapi.yaml
  • platform-api/plugins/eventgateway/plugin.go
  • platform-api/resources/openapi.yaml
  • platform-api/resources/roles.yaml
  • tests/ai-workspace-cli-e2e/platform-api-config.toml
  • tests/integration-e2e/platform-api-config.toml
  • tests/integration-e2e/suite_test.go

Comment thread .claude/rules/authentication_authorization.md Outdated
Comment thread platform-api/config/config.toml
Comment thread platform-api/internal/webhook/handlers_apikey.go Outdated
Comment thread platform-api/plugins/eventgateway/handler/webbroker_apikey.go Outdated
@Thushani-Jayasekera

Copy link
Copy Markdown
Contributor Author

@CodeRabbit review

@coderabbitai

coderabbitai Bot commented Jul 28, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 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 `@platform-api/config/config_test.go`:
- Around line 510-513: Replace the placeholder APIP_CP_ADMIN_PASSWORD_HASH value
in the successful LoadConfig fixture with a known-valid bcrypt digest, while
keeping the existing generated-admin username and successful configuration flow
unchanged.
- Around line 504-505: Update the test setup around the credential environment
variables and the related t.Setenv calls so original values are captured before
any os.Unsetenv mutation, then restored after the test using t.Setenv or
equivalent cleanup. Ensure APIP_CP_ADMIN_USERNAME and
APIP_CP_ADMIN_PASSWORD_HASH retain their pre-test values, including when they
were initially set.

In `@platform-api/README.md`:
- Around line 31-32: Update the README instructions around the htpasswd command
to remove the password positional argument and batch mode, and direct users to
generate the hash interactively so the admin password is not exposed in shell
history, process listings, or CI logs.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: d051dcc4-c818-4cb8-9a98-3d8fe2e53bbb

📥 Commits

Reviewing files that changed from the base of the PR and between e5c22fc and d7073df.

📒 Files selected for processing (20)
  • .claude/rules/authentication_authorization.md
  • kubernetes/helm/platform-api-helm-chart/values.yaml
  • platform-api/README.md
  • platform-api/config/config-template.toml
  • platform-api/config/config.toml
  • platform-api/config/config_test.go
  • platform-api/config/default_config.go
  • platform-api/internal/repository/apikey.go
  • platform-api/internal/repository/apikey_test.go
  • platform-api/internal/repository/interfaces.go
  • platform-api/internal/server/server.go
  • platform-api/internal/service/application.go
  • platform-api/internal/service/llm_apikey.go
  • platform-api/internal/service/llm_proxy_apikey.go
  • platform-api/internal/webhook/receiver.go
  • platform-api/plugins/eventgateway/handler/webbroker_apikey.go
  • platform-api/plugins/eventgateway/handler/websub_apikey.go
  • platform-api/resources/openapi.yaml
  • tests/integration-e2e/platform-api-config.toml
  • tests/integration-e2e/suite_test.go
🚧 Files skipped from review as they are similar to previous changes (12)
  • platform-api/config/config-template.toml
  • kubernetes/helm/platform-api-helm-chart/values.yaml
  • tests/integration-e2e/platform-api-config.toml
  • .claude/rules/authentication_authorization.md
  • platform-api/plugins/eventgateway/handler/webbroker_apikey.go
  • platform-api/internal/service/llm_proxy_apikey.go
  • platform-api/plugins/eventgateway/handler/websub_apikey.go
  • platform-api/internal/server/server.go
  • platform-api/internal/webhook/receiver.go
  • tests/integration-e2e/suite_test.go
  • platform-api/internal/service/llm_apikey.go
  • platform-api/resources/openapi.yaml

Comment thread platform-api/config/config_test.go Outdated
Comment thread platform-api/config/config_test.go
Comment thread platform-api/README.md Outdated
…ameter to Update and Revoke methods, ensuring proper authorization checks for directly-authenticated callers.
… password generation and ensure environment variables are restored in tests.
coderabbitai[bot]
coderabbitai Bot previously approved these changes Jul 29, 2026
Comment thread platform-api/internal/apperror/codes.go Outdated
Comment on lines +99 to +100
CodeRESTAPIKeyNotFound = "REST_API_KEY_NOT_FOUND"
CodeRESTAPIKeyForbidden = "REST_API_KEY_FORBIDDEN"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
CodeRESTAPIKeyNotFound = "REST_API_KEY_NOT_FOUND"
CodeRESTAPIKeyForbidden = "REST_API_KEY_FORBIDDEN"
CodeRESTAPIKeyNotFound = "REST_API_API_KEY_NOT_FOUND"
CodeRESTAPIKeyForbidden = "REST_API_API_KEY_FORBIDDEN"

// before it was broadcast, so the gateway must not re-run its own creator check.
// Set only on the platform-api event path (see RevokeExternalAPIKeyFromEvent); it is
// always false for requests arriving on the gateway's own REST API.
AuthorizedUpstream bool

@malinthaprasan malinthaprasan Jul 29, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

What does "upstream" mean? Do you mean AuthorizedOrigin? (origin of the call) (or TrustedOrigin - bit shorter)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

yes, a flag to identify if the request is from a trusted / pre validated origin. Will update the parameter name to TrustedOrigin as AuthorizedUpstream gives incorrect meaning.

response, err := h.apiKeyUserService.ListAPIKeysByUser(r.Context(), orgID, callerUserID, types, limit, offset)
// Holding constants.ScopeAPIKeyAllManage widens this listing from the caller's own keys
// to every user's keys in the organization.
keyAdmin := middleware.HasEffectiveScope(r, h.authzMode, constants.ScopeAPIKeyAllManage)

@malinthaprasan malinthaprasan Jul 29, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This name is bit confusing. "key" is used to indicate a "field". So this can be misread as a generic admin check and can be reused in unwanted places. Shall we be explicit here? Fix other places too.
hasApiKeyAdmin := ..

Comment thread platform-api/internal/service/apikey.go Outdated
Comment on lines +626 to +629
// CreatedBy is required so the repository can re-verify the creator in its WHERE
// clause, guarding against a delete/recreate race between the check above and this
// write (see APIKeyRepo.Update).
CreatedBy: existingKey.CreatedBy,

@malinthaprasan malinthaprasan Jul 29, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Are we always passing the existing Key's CreatedBy? Does the created by check underneath the repository method ever fail in practice?
If not - its better to just drop this as this is confusing. You've already done the authz check earlier.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Ah yeah, before reaching L629, we have this check

	if !trustedOrigin && !canManageAPIKey(existingKey.CreatedBy, userId, keyAdmin) {
		return apperror.RESTAPIAPIKeyForbidden.New()
	}

created by was passed here as an additional guard when selecting the API Key record to update. But since the validation is already done before reaching the db update state we can remove.


## GO-AUTH-019: Ownership-Override Scopes Must Be Named and Service-Layer Enforced — High

Most resource CRUD here is *creator-scoped*: a caller may act only on rows they created (`api_keys.created_by`). Where cross-user administration is genuinely needed, express it as one explicitly named org-wide scope (`ap:api_key:all:manage`) resolved from verified claims at the handler, passed down as an explicit argument, and enforced by a single shared predicate at the service layer that every entry point funnels through (GO-AUTH-015). Never approximate it with an implicit signal — an empty caller id, or "this call came from an internal path so it must be trusted". An empty identity treated as elevated fails *open* for any path that merely forgot to populate the actor. The override widens which rows within the caller's own organization are reachable, never which organization (GO-AUTH-005).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

"one explicitly named org-wide scope" - This should not mean that we should use one scope for everything. Scopes are always specific to resources. Nobody should ever create a scope called "ap:admin"


## GO-AUTH-019: Ownership-Override Scopes Must Be Named and Service-Layer Enforced — High

Most resource CRUD here is *creator-scoped*: a caller may act only on rows they created (`api_keys.created_by`). Where cross-user administration is genuinely needed, express it as one explicitly named org-wide scope (`ap:api_key:all:manage`) resolved from verified claims at the handler, passed down as an explicit argument, and enforced by a single shared predicate at the service layer that every entry point funnels through (GO-AUTH-015). Never approximate it with an implicit signal — an empty caller id, or "this call came from an internal path so it must be trusted". An empty identity treated as elevated fails *open* for any path that merely forgot to populate the actor. The override widens which rows within the caller's own organization are reachable, never which organization (GO-AUTH-005).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Most resource CRUD here is creator-scoped -
What does "here" mean? Under this topic or platform? Shall we be specific?
In APIP, most CRUD resources are NOT creator-scoped

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Updated Rule

GO-AUTH-019: Ownership-Override Scopes Must Be Named and Service-Layer Enforced — High

Most resources in this repo are organization-scoped: any caller in the org with the right resource scope may act on them, and GO-AUTH-005 alone governs access. A few are additionally creator-scoped — a caller may act only on rows they created — API keys (api_keys.created_by, enforced by canManageAPIKey in platform-api/internal/service) being the canonical case. This rule applies to those creator-scoped resources; note that a created_by/CreatedBy column is usually just an audit field and does not by itself make a resource creator-scoped. Where cross-user administration of a creator-scoped resource is genuinely needed, express it as an explicitly named override scope for that one resource type (ap:api_key:all:manage overrides creator-scoping on API keys and nothing else) — resolved from verified claims at the handler, passed down as an explicit argument, and enforced by a single shared predicate at the service layer that every entry point funnels through (GO-AUTH-015). Never approximate it with an implicit signal — an empty caller id, or "this call came from an internal path so it must be trusted". An empty identity treated as elevated fails open for any path that merely forgot to populate the actor. The override widens which rows within the caller's own organization are reachable, never which organization (GO-AUTH-005).

"One named scope" means one per resource type, not one for the whole platform. Never define a catch-all override like ap:admin or ap:all:manage that a handler for any resource can accept: a scope is always bound to the resource it governs, so granting API-key administration cannot silently confer subscription or application administration. A caller who legitimately administers several resource types holds several scopes. Wildcards are not an exception to this, :* is own-level only — it covers the actions directly at its level and never descends into sub-resources, matches a prefix, or acts transitively (ap:gateway:* satisfies ap:gateway:create but not ap:gateway:token:create). scopeSatisfies in platform-api/internal/middleware/authorization.go implements exactly that expansion. Wildcards live on the grant side only: a handler or service-layer predicate always names the concrete scope it requires and never matches on a wildcard itself.

A scope decision does not survive a broadcast: an event carries the acting user's id, not their scopes, so a downstream created_by re-check rejects every legitimate admin-initiated change. Gate that check behind an explicit "trusted origin" flag (the call came from a pre-validated origin) set only by the event path — deleting it outright would also disable it for that component's own directly-authenticated API.

…stedOrigin`, enhancing clarity in authorization checks for event-driven operations.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

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 (2)
platform-api/internal/service/apikey.go (1)

163-185: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Guard expiresIn arithmetic against overflow.

expiresIn.Duration is multiplied by unit constants without checking the product. A large request value can wrap to a different duration; a wrapped future value then passes validateExpiryInFuture, producing an incorrect expiration. Reject out-of-range values before multiplication and test the boundaries.

🤖 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 `@platform-api/internal/service/apikey.go` around lines 163 - 185, Guard the
expiration calculation in the API key creation flow around expiresIn and
validateExpiryInFuture against duration overflow before multiplying by the
unit-specific factor. Reject values whose converted duration would exceed the
supported time.Duration range, including each unit’s boundary cases, while
preserving the existing unsupported-unit validation and future-expiry checks.
platform-api/internal/repository/apikey.go (1)

60-73: 🔒 Security & Privacy | 🟠 Major

Bind mutations to the authorized key record.

Update, Revoke, and Delete match only (artifact_uuid, handle) after a separate ownership lookup. If the key is deleted and recreated between those operations, the authorized request can mutate or delete the replacement key owned by another user. Pass an immutable key UUID/owner into each mutation predicate, or perform authorization and mutation atomically, and map zero affected rows to the catalog not-found error.

This is the same ownership-predicate/TOCTOU issue reported previously; the current SQL still lacks the safeguard. As per coding guidelines, authorization invariants must fail closed across the shared service/data path.

Also applies to: 89-96, 199-203

🤖 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 `@platform-api/internal/repository/apikey.go` around lines 60 - 73, Bind the
Update, Revoke, and Delete mutations in APIKeyRepo to the immutable authorized
key UUID and owner, not only artifact_uuid and handle, so replacements cannot be
modified after the ownership lookup. Extend each SQL predicate and arguments
with those authorization fields, and map zero affected rows to the catalog
not-found error while preserving the existing mutation behavior for authorized
records.

Source: Coding guidelines

🤖 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 `@platform-api/internal/repository/apikey.go`:
- Around line 60-73: Bind the Update, Revoke, and Delete mutations in APIKeyRepo
to the immutable authorized key UUID and owner, not only artifact_uuid and
handle, so replacements cannot be modified after the ownership lookup. Extend
each SQL predicate and arguments with those authorization fields, and map zero
affected rows to the catalog not-found error while preserving the existing
mutation behavior for authorized records.

In `@platform-api/internal/service/apikey.go`:
- Around line 163-185: Guard the expiration calculation in the API key creation
flow around expiresIn and validateExpiryInFuture against duration overflow
before multiplying by the unit-specific factor. Reject values whose converted
duration would exceed the supported time.Duration range, including each unit’s
boundary cases, while preserving the existing unsupported-unit validation and
future-expiry checks.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 1a22472f-f4a8-404c-9c69-3fbbc1179657

📥 Commits

Reviewing files that changed from the base of the PR and between 32b33bd and b74b921.

📒 Files selected for processing (13)
  • .claude/rules/authentication_authorization.md
  • gateway/gateway-controller/pkg/utils/api_key.go
  • platform-api/internal/apperror/catalog.go
  • platform-api/internal/apperror/codes.go
  • platform-api/internal/repository/apikey.go
  • platform-api/internal/repository/interfaces.go
  • platform-api/internal/service/apikey.go
  • platform-api/internal/service/llm_apikey.go
  • platform-api/internal/service/llm_proxy_apikey.go
  • platform-api/internal/webhook/handlers_apikey.go
  • platform-api/internal/webhook/receiver.go
  • platform-api/plugins/eventgateway/handler/webbroker_apikey.go
  • platform-api/plugins/eventgateway/handler/websub_apikey.go
🚧 Files skipped from review as they are similar to previous changes (7)
  • platform-api/internal/service/llm_proxy_apikey.go
  • platform-api/plugins/eventgateway/handler/webbroker_apikey.go
  • platform-api/internal/webhook/receiver.go
  • .claude/rules/authentication_authorization.md
  • platform-api/internal/webhook/handlers_apikey.go
  • platform-api/internal/service/llm_apikey.go
  • platform-api/plugins/eventgateway/handler/websub_apikey.go

coderabbitai[bot]
coderabbitai Bot previously approved these changes Jul 29, 2026
… rules, emphasizing the distinction between organization-scoped and creator-scoped resources, and reinforcing the need for explicit override scopes per resource type.
…eter, streamlining the revoke function calls in integration tests.
@renuka-fernando
renuka-fernando merged commit 787fc6d into wso2:main Jul 29, 2026
15 checks passed
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.

3 participants