Skip to content

UN-3853 [FIX] Attribute platform-key-created resources to the API key's creator - #2274

Open
kirtimanmishrazipstack wants to merge 2 commits into
mainfrom
UN-3853-co-ownership-resources-with-platform-key
Open

UN-3853 [FIX] Attribute platform-key-created resources to the API key's creator#2274
kirtimanmishrazipstack wants to merge 2 commits into
mainfrom
UN-3853-co-ownership-resources-with-platform-key

Conversation

@kirtimanmishrazipstack

@kirtimanmishrazipstack kirtimanmishrazipstack commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

What

  • A resource created through a platform API key — a Prompt Studio project, workflow, ETL pipeline, API deployment, connector or adapter — now belongs to the person who created that API key, instead of belonging to nobody.
  • In the rare case where that person has since been deleted from Unstract, the "Owned By" column reads Platform key rather than showing a made-up email address.

Why

  • When a script drives Unstract with a platform API key, Unstract signs in as an invisible robot account belonging to that key, and everything the script created was recorded as owned by the robot.
  • Unstract deliberately hides robots wherever a person's name belongs. So those resources had no human owner at all: they did not appear in anyone's list, and only an org admin could open, share or delete them. The "Owned By" column had nothing real to show — it used to print Unknown, and since UN-3770 [MISC] Make list pagination consistent across shared resource endpoints #2208 prints the robot's synthetic @platform.internal address with an avatar, which reads as a colleague who does not exist.
  • Reported by Chandrasekharan M: a project named "PlatformKey Share Test" showing UN / Unknown in Owned By.
  • Today the damage is limited because only org admins can mint platform keys, and admins can manage everything anyway. But if that admin is demoted or leaves the org, the resource is left with nobody attached to it.

How

  • New helper owner_user_for(user) in backend/platform_api/services.py: if the acting user is a platform key's service account, return the key's created_by; otherwise return the user unchanged. It short-circuits before any query for a normal session, so the UI path is unaffected.
  • This is not a new rule. delete_api_user_for_key in the same module already hands everything a key created over to platform_api_key.created_by when the key is deleted. The change applies that same successor at creation time instead of only at deletion time.
  • The helper is routed into all seven places an OWNER membership row is granted: Prompt Studio create + import, workflow, pipeline, API deployment, connector, adapter.
  • The service account no longer receives a membership row and needs none — every permission class and every for_user() manager already short-circuits on is_service_account.
  • created_by is untouched everywhere. It is an audit field, and "the robot did it" is the truthful answer.
  • renderOwner in ResourceTable.jsx recognises the reserved platform.internal domain and renders Platform key instead of dressing a machine identity up as a person. This is what makes a data migration unnecessary — rows created before this change still read correctly.

Can this PR break any existing features. If yes, please list possible items. If no, please explain why.

  • No. For a normal signed-in session owner_user_for() returns the user unchanged before touching the database, so every UI-driven create behaves exactly as before — bit for bit.
  • The only behaviour that changes is the platform-key path, which today produces an ownerless resource. The service account keeps full access to everything in its org: IsOwner, IsOwnerOrSharedUser, IsParentWorkflowOwner and friends all return True for a service account before ownership is consulted, and for_user() returns the unfiltered queryset for one.
  • delete_api_user_for_key still works. Its membership transfer simply finds nothing left to move, and its created_by/modified_by re-pointing is unchanged and still required.
  • Existing co-owner tests build membership rows directly rather than going through the create endpoints, so none of them exercise the changed line.

Database Migrations

  • None. Resources created before this change keep pointing at a robot, but they read correctly in the UI thanks to the Platform key label, and they are exactly as reachable as they are today (org admins). If we later decide to repair them, the shipped _transfer_membership_rows(key.api_user, key.created_by) does it from a shell — no new code, and no irreversible migration running against production for a handful of rows.

Env Config

  • None.

Relevant Docs

  • None.

Related Issues or PRs

Dependencies Versions

  • None.

Notes on Testing

# Acceptance criterion How to verify Verdict
1 A resource created via a platform API key never renders Unknown in Owned By Create a Prompt Studio project with POST /api/v1/unstract/{org}/prompt-studio/ using a Bearer platform key, then open the Prompt Studio list Met — the column shows the key creator's real email
2 It is reachable by a human owner Sign in as the key's creator (non-admin is the interesting case) and confirm the project appears in the list and can be shared and deleted Met — the creator holds a real OWNER row
3 The fix covers every resource type, not just Prompt Studio Repeat via the workflow, pipeline, API deployment, connector and adapter create endpoints with the same key Met — all seven grant sites route through the helper
4 A genuinely ownerless resource is labelled, not faked Null the key's created_by, then reload the list Met — the cell reads Platform key
5 The UI path is unchanged Create each resource type normally through the UI Met — helper returns early for non-service-account users
6 Existing platform-key resources still behave Load the list as an org admin with a pre-change row present Met — reads Platform key, same access as before

No automated tests added, per the repo convention of not adding tests unless asked. Happy to add coverage for owner_user_for if reviewers want it.

Screenshots

Checklist

I have read and understood the Contribution Guidelines.

🤖 Generated with Claude Code

https://claude.ai/code/session_01JHnDZZWGhsevUdwgMyR2ai

…reator

A platform API key authenticates as a service account, and every resource
create path granted the OWNER membership row to that machine identity. Service
accounts are filtered out of every owner surface (HasMembersMixin), so such a
resource ended up with no human owner: invisible to its creator in list views,
manageable only through the org-admin fallback, and rendered in "Owned By" as a
synthetic @platform.internal address dressed up as a colleague.

Record the key's creator as owner instead — the same successor
delete_api_user_for_key already hands ownership to when a key is deleted, now
applied at creation rather than only at deletion. The service account loses
nothing: permission classes and for_user() short-circuit on is_service_account.

Where no human can be named (the key's creator has since been deleted), the
resource stays deliberately ownerless and the table labels it "Platform key"
rather than naming a machine.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JHnDZZWGhsevUdwgMyR2ai
@greptile-apps

greptile-apps Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR attributes resources created through platform API keys to the human who created the key while retaining the service account in audit fields.

  • Adds a shared backend resolver and applies it at resource OWNER-membership creation sites.
  • Exposes owner email data for pipeline and API deployment lists.
  • Centralizes table and card owner rendering, including the ownerless “Platform key” label.

Confidence Score: 5/5

The PR appears safe to merge because no blocking failure remains within the eligible follow-up scope.

No blocking failure remains.

Important Files Changed

Filename Overview
backend/platform_api/services.py Adds the central service-account-to-key-creator ownership resolver and shared reserved email-domain constant.
backend/api_v2/api_deployment_views.py Grants API deployment ownership to the resolved platform-key creator.
backend/pipeline_v2/views.py Grants pipeline ownership to the resolved platform-key creator during creation.
backend/prompt_studio/prompt_studio_core_v2/views.py Attributes newly created Prompt Studio resources to the platform-key creator.
backend/workflow_manager/workflow_v2/views.py Attributes new workflow OWNER memberships to the resolved human owner.
frontend/src/components/widgets/owner-display.js Centralizes owner selection, “Me” labeling, co-owner counts, and platform-key fallback rendering.
frontend/src/components/widgets/resource-table/ResourceTable.jsx Reuses the shared owner-display resolver for resource-table rows.
frontend/src/components/widgets/card-grid-view/CardFieldComponents.jsx Aligns card owner rendering with the shared table behavior.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart LR
  K[Platform API key request] --> S[Service-account identity]
  S --> R[owner_user_for]
  R -->|Creator exists| H[Key creator]
  R -->|Creator deleted| S
  H --> M[OWNER membership]
  S --> A[Audit created_by]
  M --> D[Human owner display and access]
  A --> P[Platform key label when no human owner exists]
Loading

Reviews (2): Last reviewed commit: "UN-3853 [FIX] Name the real owner on the..." | Re-trigger Greptile

…s too

The ticket asks for Owned By to be correct on every resource type. The API
deployment and ETL pipeline card views were still wrong: OwnerFieldRow read
created_by_email only, and their serializers never exposed owner_emails — so
those cards named the audit creator, which on a platform-key create is the
service account. The backend fix alone could not reach them.

Expose owner_emails on both serializers (their querysets already prefetch
memberships__user, so it costs no extra query), and move the owner-label rule
into one resolveOwnerDisplay helper shared by the table and the cards. The two
had already drifted on both the source field and the "Me" rule — the card said
"Me" to any owner, which is the co-owner bug the table's comment warns about.
Cards now match the table: "Me" tracks the displayed owner.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JHnDZZWGhsevUdwgMyR2ai
@github-actions

github-actions Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Frontend Lint Report (Biome)

All checks passed! No linting or formatting issues found.

@sonarqubecloud

sonarqubecloud Bot commented Sep 2, 2026

Copy link
Copy Markdown

@github-actions

github-actions Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Unstract test results

Per-group results

Status Group Tier Passed Failed Errors Skipped Duration (s)
e2e-api-deployment e2e 3 0 0 0 16.1
e2e-coowners e2e 1 0 0 0 1.1
e2e-etl e2e 1 0 0 0 7.9
e2e-login e2e 2 0 0 0 1.0
e2e-prompt-studio e2e 1 0 0 0 4.1
e2e-smoke e2e 2 0 0 0 1.0
e2e-workflow e2e 1 0 0 0 20.1
frontend unit 0 1 0 0 0.0
integration-backend integration 310 0 0 26 44.9
integration-connectors integration 1 0 0 7 7.9
integration-workers integration 157 0 0 1 50.1
ui e2e 0 1 0 0 0.0
unit-backend unit 1158 0 0 1 29.9
unit-connectors unit 63 0 0 0 10.4
unit-core unit 33 0 0 0 0.9
unit-platform-service unit 15 0 0 0 1.8
unit-rig unit 120 0 0 0 3.6
unit-runner unit 5 0 0 0 3.3
unit-sdk1 unit 563 0 0 0 28.3
unit-workers unit 1397 0 0 1 119.6
TOTAL 3833 2 0 36 352.0

Critical paths

⚠️ Critical paths not yet covered

  • workflow-execution-fan-out — Multi-file workflow execution fans out to file-processing workers and rejoins. (declared coverage: no groups declared)
✅ Covered critical paths
  • auth-login — covered by e2e-login
  • adapter-register-llm — covered by integration-backend
  • workflow-author — covered by integration-backend
  • co-owner-manage — covered by integration-backend, e2e-coowners
  • workflow-create-execute — covered by e2e-workflow
  • api-deployment-provision — covered by integration-backend
  • api-deployment-auth — covered by integration-backend
  • api-deployment-run — covered by e2e-api-deployment
  • mcp-server-auth — covered by integration-backend
  • mcp-platform-auth — covered by integration-backend
  • prompt-studio-author — covered by integration-backend
  • prompt-studio-fetch-response — covered by e2e-prompt-studio
  • connector-register-test — covered by integration-backend
  • pipeline-etl-execute — covered by e2e-etl
  • usage-aggregate-read — covered by integration-backend
  • usage-token-tracking — covered by e2e-api-deployment
  • callback-result-delivery — covered by e2e-api-deployment

@kirtimanmishrazipstack

Copy link
Copy Markdown
Contributor Author

Merge this before the cloud PR

Zipstack/unstract-cloud#1753 imports owner_user_for from platform_api.services, and cloud CI pins the OSS tree to refs/heads/main with no override — so its test (integration) and e2e stay red until this merges. Once this lands, a re-run there goes green on its own.

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