Skip to content

UN-3586 [FEAT] Allow platform API key rotation via API#2133

Open
Deepak-Kesavan wants to merge 4 commits into
mainfrom
UN-3586-platform-key-self-rotate
Open

UN-3586 [FEAT] Allow platform API key rotation via API#2133
Deepak-Kesavan wants to merge 4 commits into
mainfrom
UN-3586-platform-key-self-rotate

Conversation

@Deepak-Kesavan

@Deepak-Kesavan Deepak-Kesavan commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

What

Allow a platform API key (bearer token) to call the rotate endpoint, so credentials can be rotated via the API for automation. Session org-admins keep rotate (unchanged).

Why

UN-3586 (RLDatix on-prem blocker) asks for "rotate credentials through the API" for operational automation. Confirmed empirically on staging (globe.unstract.com, main):

GET  …/api/deployment/                     → 200   (valid key, bearer auth + reads work)
POST …/platform-api/keys/<id>/rotate/      → 403   "Only organization admins can manage platform API keys."

Rotation via a logged-in admin session already works (the UI's rotate button), but rotation with a platform API key was blocked — the rotate endpoint's IsOrganizationAdmin gate rejects service accounts, and a platform API key authenticates as a service account. So the token/automation path did not exist. This PR opens it.

How

  • New CanRotatePlatformApiKey permission on the rotate action:
    • Session callers → must be org admins (unchanged behavior).
    • Platform API key callers → must present a full_access key (the admin-equivalent tier, which already permits DELETE). read/read_write keys cannot rotate.
  • Why full_access (not any bearer key): rotate returns the new key value, so letting a lower-tier read_write key rotate a full_access key would let it read that key's new secret and escalate read_write → full_access. Requiring full_access closes that path — a full_access caller rotating any key gains no privilege (it's already the top tier), matching what a session admin can do.
  • Org scoping unchanged and still enforced: the auth middleware validates the URL org_id against the key's immutable org (403 on mismatch), and the viewset queryset is org-scoped — a key can only rotate keys in its own organization. Cross-org is impossible.
  • rotate already returns the new key once via PlatformApiKeyDetailSerializer — no serializer change.

Note: an earlier revision restricted key callers to self-only rotation; that wasn't a ticket requirement and was dropped in favor of the full_access-tier gate above (a full_access key may rotate any key in its org, matching a session admin) — which also closes a privilege-escalation path flagged in review.

Can this PR break any existing features? If yes, please list possible items. If no, please explain why. (PS: Admins do not merge the PR without this section filled)

No.

  • Session-admin rotation is unchanged — the permission was only widened, additively, to also admit platform-API-key callers to an existing endpoint.
  • Cross-org isolation is unchanged (auth middleware + org-scoped querysets; independently verified).
  • No model, serializer, or migration change.

Database Migrations

None.

Env Config

None.

Notes on Testing

  • Staging (globe.unstract.com, main) baseline: bearer rotate403 (blocked); same key reads deployments → 200. Confirms the gap.
  • Dev (deepak-unstract-dev, snapshot.1782909801): verified live against the new pod — a full_access key rotating a different key via bearer → 200 + new key; a read_write key attempting rotate → 403 (tested 3×, all denied — the privilege-escalation path is closed); session admin rotate → 200. Cross-org remains blocked (auth middleware + org-scoped queryset).

Related Issues or PRs

Checklist

I have read and understood the Contribution Guidelines.

Operational automation (RLDatix) needs credential rotation through the API,
but the rotate endpoint was gated by IsOrganizationAdmin, which rejects
service accounts — so a platform API key could not rotate itself.

Add CanRotatePlatformApiKey for the rotate action: session callers still must
be org admins (may rotate any key in the org), while a platform API key
(bearer) may rotate ONLY its own key (pk == request.platform_api_key.id).
Cross-org access remains impossible (auth middleware + org-scoped queryset);
this only relaxes the intra-org gate to self-rotation. read keys still can't
POST (middleware), so only read_write/full_access keys reach rotate.

rotate returns the new key once via PlatformApiKeyDetailSerializer. No model
or migration change.
@coderabbitai

coderabbitai Bot commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

Walkthrough

This PR adds dedicated authorization for platform API key rotation and applies it only to the rotate action on PlatformApiKeyViewSet. Rotation is allowed for authenticated org-admin sessions or full_access platform API keys; other actions keep the existing permission path.

Changes

Platform API key rotation access control

Layer / File(s) Summary
Rotation permission and viewset hook
backend/platform_api/permissions.py, backend/platform_api/views.py
Adds CanRotatePlatformApiKey with org-admin and full-access key checks, then uses it only for the rotate action while leaving other actions on the default permission flow.

Estimated code review effort: 2 (Simple) | ~10 minutes

🚥 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 clearly states the main change: enabling platform API key rotation via the API.
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.
Description check ✅ Passed The PR description follows the required template and fills the critical sections with clear details and testing notes.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch UN-3586-platform-key-self-rotate

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.

@greptile-apps

greptile-apps Bot commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds the ability for full_access platform API keys to call the rotate endpoint, enabling automated credential rotation without a logged-in session. Session org-admins retain their existing ability to rotate any key unchanged.

  • A new CanRotatePlatformApiKey permission class gates the rotate action: session callers must be org admins; bearer callers must present a full_access key (blocking the read_writefull_access privilege-escalation path where a lower-tier key could read a higher-tier key's new secret from the rotate response).
  • get_permissions() in the viewset overrides the default IsOrganizationAdmin gate for the rotate action only; all other key-management actions (list, retrieve, create, update, destroy) remain session-admin-only.
  • Org isolation is doubly enforced: the auth middleware rejects cross-org bearer tokens at the HTTP level, and get_queryset() scopes the object lookup to the caller's org.

Confidence Score: 5/5

The change additively widens rotate access for bearer tokens while leaving all other key-management actions unchanged; org isolation and active-key checks are independently enforced at the middleware layer.

The permission logic is correct: only full_access bearer keys can reach rotate (blocking the read_write→full_access privilege-escalation path), session admins retain their existing gate unchanged, and cross-org rotation is doubly blocked by the middleware org-match check and the org-scoped queryset. No model, serializer, or migration changes are included.

No files require special attention.

Important Files Changed

Filename Overview
backend/platform_api/permissions.py Adds CanRotatePlatformApiKey permission; correctly gates on full_access tier for bearer callers and delegates to IsOrganizationAdmin for session callers. Logic is sound and well-documented.
backend/platform_api/views.py Adds get_permissions() override to swap in CanRotatePlatformApiKey for the rotate action only; all other standard ModelViewSet actions remain behind IsOrganizationAdmin.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant Client as Automation Client
    participant MW as CustomAuthMiddleware
    participant VP as PlatformApiKeyViewSet
    participant CP as CanRotatePlatformApiKey
    participant DB as Database

    Client->>MW: "POST /platform-api/keys/{pk}/rotate/ with Bearer full_access_key"
    MW->>DB: "Lookup key is_active=True"
    DB-->>MW: "key full_access org=X"
    MW->>MW: Validate org match in URL
    MW->>MW: "permission.allows POST = True"
    MW->>MW: Set request.user and request.platform_api_key
    MW->>VP: dispatch
    VP->>VP: "action == rotate override permissions"
    VP->>CP: CanRotatePlatformApiKey.has_permission
    CP->>CP: "platform_key.permission == FULL_ACCESS True"
    CP-->>VP: allowed
    VP->>DB: get_queryset filtered by org fetch by pk
    DB-->>VP: target key instance
    VP->>DB: "instance.key = uuid4 save"
    VP-->>Client: 200 with new plaintext key
    Note over Client,DB: read_write key CanRotatePlatformApiKey 403
    Note over Client,DB: cross-org key MW org check 403
    Note over Client,DB: session non-admin IsOrganizationAdmin 403
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant Client as Automation Client
    participant MW as CustomAuthMiddleware
    participant VP as PlatformApiKeyViewSet
    participant CP as CanRotatePlatformApiKey
    participant DB as Database

    Client->>MW: "POST /platform-api/keys/{pk}/rotate/ with Bearer full_access_key"
    MW->>DB: "Lookup key is_active=True"
    DB-->>MW: "key full_access org=X"
    MW->>MW: Validate org match in URL
    MW->>MW: "permission.allows POST = True"
    MW->>MW: Set request.user and request.platform_api_key
    MW->>VP: dispatch
    VP->>VP: "action == rotate override permissions"
    VP->>CP: CanRotatePlatformApiKey.has_permission
    CP->>CP: "platform_key.permission == FULL_ACCESS True"
    CP-->>VP: allowed
    VP->>DB: get_queryset filtered by org fetch by pk
    DB-->>VP: target key instance
    VP->>DB: "instance.key = uuid4 save"
    VP-->>Client: 200 with new plaintext key
    Note over Client,DB: read_write key CanRotatePlatformApiKey 403
    Note over Client,DB: cross-org key MW org check 403
    Note over Client,DB: session non-admin IsOrganizationAdmin 403
Loading

Reviews (4): Last reviewed commit: "UN-3586 [FIX] Require full_access key to..." | Re-trigger Greptile

Comment thread backend/platform_api/permissions.py
…iew)

Relocate the 'key may rotate only its own key' constraint from has_permission
(view-level, via view.kwargs[pk]) to has_object_permission, the idiomatic DRF
location for per-object access control — it receives the already-fetched obj.
has_permission stays as the coarse session-vs-key gate. Behavior is unchanged
(self-rotate 200, cross-key 403): this permission is only used by the rotate
detail action, which calls get_object() and so always triggers the
object-level check. (Greptile)
Empirically confirmed on staging that rotating a platform API key via the API
(bearer token) is blocked (403 'Only organization admins...') — the automation
path UN-3586 asks for. Enable it: a platform API key caller may rotate, same
as a session org admin. Drop the earlier self-only restriction (not a ticket
requirement) and the now-unneeded has_object_permission. Org scoping (auth
middleware + org-scoped queryset) still confines a key to its own org; read
keys still can't POST (middleware). rotate already returns the new key.
@Deepak-Kesavan Deepak-Kesavan changed the title UN-3586 [FEAT] Allow platform API key self-rotation via API UN-3586 [FEAT] Allow platform API key rotation via API Jul 1, 2026

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

🧹 Nitpick comments (1)
backend/platform_api/permissions.py (1)

26-40: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Docstring conflates "own organization" with "own key."

The docstring says platform API key callers are confined to their own org via middleware/queryset, but the PR objectives claim callers may rotate only their "own key." Given the missing object-level check flagged above, the actual enforced behavior (org-wide, not key-specific) doesn't match the PR's documented intent. Once the object-permission gap is fixed, update this docstring to accurately describe the enforced 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 `@backend/platform_api/permissions.py` around lines 26 - 40, Update the
CanRotatePlatformApiKey docstring to match the actual permission scope enforced
by the rotate flow. The current wording in the class docstring suggests platform
API key callers are limited to their “own key,” but the behavior in this
permission and the org-scoped queryset/middleware is organization-wide unless an
object-level check is added elsewhere. Adjust the text in
CanRotatePlatformApiKey to describe the exact enforced scope and remove the “own
key”/“own organization” mismatch.
🤖 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.

Nitpick comments:
In `@backend/platform_api/permissions.py`:
- Around line 26-40: Update the CanRotatePlatformApiKey docstring to match the
actual permission scope enforced by the rotate flow. The current wording in the
class docstring suggests platform API key callers are limited to their “own
key,” but the behavior in this permission and the org-scoped queryset/middleware
is organization-wide unless an object-level check is added elsewhere. Adjust the
text in CanRotatePlatformApiKey to describe the exact enforced scope and remove
the “own key”/“own organization” mismatch.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 715dc80c-dd6b-453d-a5ad-d2c3c9cb2e78

📥 Commits

Reviewing files that changed from the base of the PR and between cb03445 and 8305fa4.

📒 Files selected for processing (1)
  • backend/platform_api/permissions.py

Comment thread backend/platform_api/permissions.py
…lation)

Greptile caught a real privilege escalation: after dropping self-only, a
read_write bearer key could rotate a full_access key and read its new secret
from the rotate response (rotate returns the new key), escalating read_write
-> full_access. Fix: key-based callers must present a full_access key to
rotate. read_write keys can no longer rotate; a full_access caller rotating
any key gains no privilege (already top tier) and matches what a session
admin can do. Session-admin rotate and org scoping unchanged.
@sonarqubecloud

sonarqubecloud Bot commented Jul 1, 2026

Copy link
Copy Markdown

@github-actions

github-actions Bot commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Unstract test results

Per-group results

Status Group Tier Passed Failed Errors Skipped Duration (s)
unit-connectors unit 64 12 0 3 16.6
unit-core unit 0 0 4 0 1.2
unit-platform-service unit 9 0 1 0 1.3
unit-rig unit 53 0 0 0 3.3
unit-runner unit 11 0 0 0 3.1
unit-sdk1 unit 381 0 0 0 18.2
unit-tool-registry unit 0 0 1 0 1.2
unit-workers unit 0 0 0 0 17.7
TOTAL 518 12 6 3 62.8

Critical paths

⚠️ Critical paths not yet covered

  • auth-login — User can log in and obtain a session cookie. (entry: POST /api/v1/auth/login; declared coverage: no groups declared)
  • adapter-register-llm — Register and validate an LLM adapter. (entry: POST /api/v1/adapter/; declared coverage: no groups declared)
  • workflow-create-execute — Create a workflow, configure source+destination, execute, poll, fetch result. (entry: POST /api/v1/workflow/{id}/execute/; declared coverage: e2e-workflow)
  • api-deployment-run — Deploy a workflow as an API, POST a document, receive structured JSON. (entry: POST /deployment/api/{org}/{name}/; declared coverage: e2e-api-deployment)
  • prompt-studio-fetch-response — Prompt Studio: create project, add prompt, run single-pass, get response. (entry: POST /api/v1/prompt-studio/prompt-studio-tool/{id}/fetch_response/; declared coverage: e2e-prompt-studio)
  • pipeline-etl-execute — Run an ETL pipeline from source connector to destination. (entry: POST /api/v1/pipeline/{id}/execute/; declared coverage: no groups declared)
  • usage-token-tracking — Per-execution token usage is recorded and retrievable. (entry: GET /api/v1/usage/get_token_usage/; declared coverage: no groups declared)
  • workflow-execution-fan-out — Multi-file workflow execution fans out to file-processing workers and rejoins. (entry: internal: backend → rabbitmq → workers/file_processing; declared coverage: no groups declared)
  • callback-result-delivery — Async results are posted back via the callback worker. (entry: internal: workers/callback → backend /internal endpoints; declared coverage: no groups declared)
✅ Covered critical paths
  • tool-sandbox-exec — covered by unit-runner

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant