Skip to content

UN-3057 [FIX] Repair Prompt Studio projects left ownerless by the clone path - #2239

Open
praveen-formido wants to merge 3 commits into
mainfrom
fix/clone-owner-membership
Open

praveen-formido wants to merge 3 commits into
mainfrom
fix/clone-owner-membership

Conversation

@praveen-formido

@praveen-formido praveen-formido commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

What

Adds a data migration repairing Prompt Studio projects that were left with no owner by the clone path, plus the reusable helper behind it.

Paired with Zipstack/unstract-cloud#1721, which fixes the clone helper itself. This PR only repairs rows already written; that one stops new breakage.

Why

Since UN-2202 (3653b418c), created_by is audit-only: _is_resource_owner takes the membership branch for any resource exposing membershipsCustomTool does (models.py:195) — and never falls back to created_by.

The clone helper never created the OWNER ResourceMembership row, so every project cloned after 0009_absorb_shared_users ran is ownerless. That backfill seeded OWNER rows from created_by for everything existing at the time, which is exactly why the regression window starts there.

The symptom users hit: the clone is visible (it copies the parent's shared_to_org) and profiles can still be created (that route uses the looser IsOwnerOrSharedUserOrSharedToOrg), but IsParentToolOwner denies DELETE/PUT/PATCH — "unable to delete the LLM profile of a cloned project" returned 403.

How

  • repair_ownerless_owner_rows() grants created_by an OWNER row on resources with zero OWNER rows. A creator deliberately replaced by a co-owner is not resurrected; null creator / null organization are skipped. Idempotent.
  • It iterates _base_manager, not objects — several resources' default manager is org-scoped by UserContext, which is unset during a migration and would silently filter every row out and repair nothing. Same guard tenant_account_v2.signals already documents.
  • Migration 0011 applies it to CustomTool and reverses to a no-op.

Testing

Three regression tests pin repair / leave-alone / skip.

Verified red-green rather than assumed: with the helper body stubbed to return 0, the behaviour test fails and the two guard tests still pass (they assert the repair must not act).

integration-backend: 4 passed (this PR's tests + the cloud clone test)
unit-backend:        842 passed, 1 skipped
integration-backend: 837 passed, 29 skipped

One pre-existing local failure unrelated to this change: plugins/notification/tests/test_sharing_notification.py errors with ModuleNotFoundError: No module named 'sendgrid' (chain: test → sharing_notification → email_service → sendgrid; the package is absent from the local venv).

Reviewer notes

Two adjacent defects in the same clone path were found but deliberately not fixed here, to keep this scoped to the reported bug — worth separate tickets:

  1. Cloned ProfileManager.created_by still points at the original project's owner, so validate_profile_manager_owner_access evaluates adapter access against the wrong user.
  2. The clone inherits shared_to_org=True from the parent, so a clone of a shared project is silently org-wide.

🤖 Generated with Claude Code

…ne path

Since UN-2202, `_is_resource_owner` consults only ResourceMembership OWNER
rows for resources that expose `memberships` (CustomTool does) and no longer
falls back to `created_by`. The Prompt Studio clone path never created that
row, so every project cloned after 0009_absorb_shared_users ran has no owner
at all: still visible (the clone copies the parent's `shared_to_org`) and
profiles can still be created, but `IsParentToolOwner` denies every mutation
on them — deleting an LLM profile returned 403.

The clone helper itself is fixed in unstract-cloud; that stops new breakage
but cannot help rows already written. This adds the repair:

- `repair_ownerless_owner_rows()` grants `created_by` an OWNER row on
  resources that have zero OWNER rows. Only ownerless resources are touched,
  so a creator deliberately replaced by a co-owner is not resurrected, and a
  null creator or null organization is skipped. Idempotent.
- It iterates `_base_manager`: several resources' default manager is
  org-scoped by `UserContext`, which is unset during a migration and would
  silently filter every row out. Same guard `tenant_account_v2.signals` uses.
- Migration 0011 applies it to CustomTool; reverses to a no-op.

Regression tests pin all three branches (repair, leave-alone, skip). Verified
red-green: with the helper body stubbed to a no-op the behaviour test fails
and the two guard tests still pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@sonarqubecloud

Copy link
Copy Markdown

@greptile-apps

greptile-apps Bot commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

via Greptile

RetriggerConfidence Score: 5/5

The PR appears safe to merge.

Summary

The PR adds an idempotent migration helper that restores authoritative OWNER memberships for ownerless Prompt Studio projects.

  • Repairs eligible CustomTool rows using their audit creator and organization.
  • Promotes an existing creator VIEWER membership rather than leaving it unchanged.
  • Leaves already-owned resources and resources without a usable creator or organization untouched.
  • Adds regression coverage for repair, promotion, preservation, and null-creator behavior.

Diagram

%%{init: {'theme': 'neutral'}}%%
flowchart TD
  Tool[CustomTool] --> HasOwner{Has OWNER membership?}
  HasOwner -- Yes --> Leave[Leave unchanged]
  HasOwner -- No --> Eligible{Creator and organization present?}
  Eligible -- No --> Skip[Skip resource]
  Eligible -- Yes --> Existing{Creator membership exists?}
  Existing -- Yes --> Promote[Update role to OWNER and organization]
  Existing -- No --> Create[Create OWNER membership]
Loading

Reviews (2) · Last reviewed commit: "UN-3057 [FIX] Promote an existing non-ow..."

Comment thread backend/tenant_account_v2/migrations/_membership_backfill.py Outdated
praveen-formido and others added 2 commits September 15, 2026 13:01
…he ownerless backfill

Membership is unique per (user, resource), so get_or_create returned a creator's existing VIEWER row unchanged and left the project ownerless. update_or_create promotes it; only projects with zero OWNER rows reach this path. The repaired count now includes promoted rows.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@sonarqubecloud

Copy link
Copy Markdown

@github-actions

Copy link
Copy Markdown
Contributor

Unstract test results

Per-group results

Status Group Tier Passed Failed Errors Skipped Duration (s)
frontend unit 0 1 0 0 0.0
unit-backend unit 1276 0 0 1 44.0
unit-connectors unit 63 0 0 0 9.8
unit-core unit 115 0 0 0 2.0
unit-platform-service unit 15 0 0 0 2.6
unit-rig unit 120 0 0 0 4.6
unit-runner unit 5 0 0 0 2.9
unit-sdk1 unit 563 0 0 0 28.4
unit-workers unit 1425 0 0 1 124.7
TOTAL 3582 1 0 2 219.1

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, but not exercised in this build
  • auth-login — User can log in and obtain a session cookie. (covered by e2e-login; no result reported in this build)
  • adapter-register-llm — Register and validate an LLM adapter. (covered by integration-backend; no result reported in this build)
  • workflow-author — Create a workflow; its source+destination endpoints materialise and are configurable. (covered by integration-backend; no result reported in this build)
  • co-owner-manage — Add/remove co-owners of a shared resource; enforce the last-owner guard. (covered by integration-backend, e2e-coowners; no result reported in this build)
  • workflow-create-execute — Create a workflow, configure source+destination, execute, poll, fetch result. (covered by e2e-workflow; no result reported in this build)
  • api-deployment-provision — Deploying a workflow as an API mints a usable key and a resolvable endpoint. (covered by integration-backend; no result reported in this build)
  • api-deployment-auth — Unauthenticated or mis-scoped API-deployment calls are rejected before dispatch. (covered by integration-backend; no result reported in this build)
  • api-deployment-run — Deploy a workflow as an API, POST a document, receive structured JSON. (covered by e2e-api-deployment; no result reported in this build)
  • mcp-server-auth — Unauthenticated or mis-scoped hosted-MCP calls are rejected before any tool runs. (covered by integration-backend; no result reported in this build)
  • mcp-platform-auth — The org-scoped MCP endpoint stays behind the platform-API-key middleware; unauthenticated or mis-scoped calls reach no tool. (covered by integration-backend; no result reported in this build)
  • platform-key-whoami — A platform API key resolves its own organisation over the org-less whoami endpoint; the org comes from the key row, not the URL. (covered by integration-backend; no result reported in this build)
  • prompt-studio-author — Create a Prompt Studio project and add a prompt to it. (covered by integration-backend; no result reported in this build)
  • prompt-studio-fetch-response — Prompt Studio: create project, add prompt, run a prompt, get response. (covered by e2e-prompt-studio; no result reported in this build)
  • connector-register-test — Connector credentials are validated against the live system and stored encrypted. (covered by integration-backend; no result reported in this build)
  • pipeline-etl-execute — Run an ETL pipeline from source connector to destination. (covered by e2e-etl; no result reported in this build)
  • usage-aggregate-read — Per-run token usage aggregates correctly and stays scoped to its organization. (covered by integration-backend; no result reported in this build)
  • usage-token-tracking — Per-execution token usage is recorded and retrievable. (covered by e2e-api-deployment; no result reported in this build)
  • callback-result-delivery — Async results are posted back via the callback worker. (covered by e2e-api-deployment; no result reported in this build)

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