UN-3739 [FIX] Prompt Studio owner-access check validates requester, not just profile creator - #2183
Conversation
…access check Prompt Studio's validate_profile_manager_owner_access() only checked the profile creator's adapter access — the requesting user was never passed in. Org admins were denied on any project whose profile creator lost adapter access (share revoked, member offboarded, or the profile was created by a service account via the platform API), with an error that blamed the requester. - Plumb request_user_id into the check from all call sites (including the single-pass and summarize paths, which previously dropped user_id) - Pass when the requester is an org admin (implicit access) - Pass when the profile creator is a service account (platform-API / org-migration projects hold no adapter shares by design) - Keep the revocation guard: non-admin requesters are still denied when the creator lacks adapter access (delegated use unchanged) - Error message now names the profile creator who lacks access (and flags offboarded creators) instead of misattributing to "You"; also fixes the message accidentally being raised as a 1-tuple Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…e-path owner Devil's-advocate review of the previous commit found it defeated itself: every Prompt Studio run/index view passes user_id=tool.created_by.user_id (the project creator, who owns the document directory), so forwarding user_id as the requester re-validated the creator, not the person clicking Index. - Add an explicit request_user_id parameter through all four view entry points (index, fetch, bulk fetch, single pass) and their internal chains; views now pass request.user.user_id - Keep user_id untouched everywhere — it addresses the project creator's file storage path, not an identity to authorize - Plumbing regression test now pins request_user_id != user_id so the two identities can't be conflated again Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Note Reviews pausedIt 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 Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (2)
🚧 Files skipped from review as they are similar to previous changes (2)
Summary by CodeRabbit
WalkthroughThe profile access guard now evaluates requester-aware bypasses and detailed adapter denials. Request identity is propagated through IDE views, payload builders, prompt execution, response fetching, and summarization, with tests covering forwarding and authorization behavior. ChangesProfile access validation
Estimated code review effort: 4 (Complex) | ~45 minutes Suggested reviewers: Sequence Diagram(s)sequenceDiagram
participant Request
participant views
participant PromptStudioHelper
participant OrganizationMemberService
participant has_group_access
Request->>views: provide request.user
views->>PromptStudioHelper: build payload with request_user
PromptStudioHelper->>OrganizationMemberService: check requester and creator status
PromptStudioHelper->>has_group_access: evaluate adapter access
has_group_access-->>PromptStudioHelper: return access results
PromptStudioHelper-->>Request: continue execution or raise PermissionError
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 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 |
|
| Filename | Overview |
|---|---|
| backend/prompt_studio/prompt_studio_core_v2/prompt_studio_helper.py | Core logic change: validate_profile_manager_owner_access gains a request_user parameter with correct evaluation order. request_user threaded through all four builders as required keyword-only. Error messages fixed from tuple to plain string. |
| backend/prompt_studio/prompt_studio_core_v2/views.py | All four view entry points now pass request_user=request.user to their respective builder. Clean and complete. |
| backend/prompt_studio/prompt_studio_core_v2/tests/test_validate_profile_manager_owner_access.py | New test suite with full case matrix covering all bypass paths, access disjuncts, PII-free message contract, and builder-signature invariants. |
| backend/prompt_studio/prompt_studio_core_v2/tests/test_build_index_payload.py | Adds TestOwnerAccessPlumbing to verify the sentinel request_user reaches validate_profile_manager_owner_access, pinning that user_id and request_user are never conflated. |
Flowchart
%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[validate_profile_manager_owner_access] --> B{request_user is org admin?}
B -- Yes --> PASS1[Return bypass]
B -- No --> C{owner is None?}
C -- Yes --> PASS2[Return bypass]
C -- No --> D{owner is service account?}
D -- Yes --> PASS3[Return bypass]
D -- No --> E{owner is org admin?}
E -- Yes --> PASS4[Return bypass]
E -- No --> F[Check all 4 adapters]
F --> G{All accessible by owner?}
G -- Yes --> PASS5[Return ok]
G -- No --> H{request_user IS owner?}
H -- Yes --> ERR1[PermissionError: you lost access]
H -- No --> I{owner still a member?}
I -- No --> ERR2[PermissionError: former member]
I -- Yes --> ERR3[PermissionError: creator lacks access]
%%{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"}}}%%
flowchart TD
A[validate_profile_manager_owner_access] --> B{request_user is org admin?}
B -- Yes --> PASS1[Return bypass]
B -- No --> C{owner is None?}
C -- Yes --> PASS2[Return bypass]
C -- No --> D{owner is service account?}
D -- Yes --> PASS3[Return bypass]
D -- No --> E{owner is org admin?}
E -- Yes --> PASS4[Return bypass]
E -- No --> F[Check all 4 adapters]
F --> G{All accessible by owner?}
G -- Yes --> PASS5[Return ok]
G -- No --> H{request_user IS owner?}
H -- Yes --> ERR1[PermissionError: you lost access]
H -- No --> I{owner still a member?}
I -- No --> ERR2[PermissionError: former member]
I -- Yes --> ERR3[PermissionError: creator lacks access]
Reviews (5): Last reviewed commit: "UN-3739 [FIX] Address review round 3: cr..." | Re-trigger Greptile
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 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 `@backend/prompt_studio/prompt_studio_core_v2/prompt_studio_helper.py`:
- Around line 264-290: Remove owner.email from the user-facing profile_ref used
by the PermissionError and replace it with a non-PII identifier, preferably the
available display name or a generic project-creator label. Preserve the existing
email-bearing server-side log in the surrounding permission-check flow, and keep
all denial branches’ guidance unchanged.
🪄 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: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: f4249cce-0f6b-44ef-a8b5-a86d144e03c7
📒 Files selected for processing (4)
backend/prompt_studio/prompt_studio_core_v2/prompt_studio_helper.pybackend/prompt_studio/prompt_studio_core_v2/tests/test_build_index_payload.pybackend/prompt_studio/prompt_studio_core_v2/tests/test_validate_profile_manager_owner_access.pybackend/prompt_studio/prompt_studio_core_v2/views.py
…uest_user_id - Denial messages no longer include the profile creator's email — after the requester-admin bypass, the only audience for these errors is non-admin collaborators, so remediation now points at an org admin and the creator's identity stays in server logs only (CodeRabbit) - Add missing str | None annotations on the two private single-pass helpers (Greptile) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
🧹 Nitpick comments (1)
backend/prompt_studio/prompt_studio_core_v2/prompt_studio_helper.py (1)
268-269: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueRemove unnecessary
fprefix from the string literal.The second part of the string literal does not contain any formatting expressions, so the
fprefix can be safely removed.♻️ Proposed fix
profile_ref = ( f"This project's LLM profile '{profile_manager.profile_name}' was" - f" created by another user" + " created by another user" )🤖 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/prompt_studio/prompt_studio_core_v2/prompt_studio_helper.py` around lines 268 - 269, Remove the unnecessary f-string prefix from the second literal in the profile ownership message while preserving the existing concatenated message and formatting in the first literal.
🤖 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/prompt_studio/prompt_studio_core_v2/prompt_studio_helper.py`:
- Around line 268-269: Remove the unnecessary f-string prefix from the second
literal in the profile ownership message while preserving the existing
concatenated message and formatting in the first literal.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 5da1b8d3-72ba-412d-b3d5-98d13941fb50
📒 Files selected for processing (2)
backend/prompt_studio/prompt_studio_core_v2/prompt_studio_helper.pybackend/prompt_studio/prompt_studio_core_v2/tests/test_validate_profile_manager_owner_access.py
ritwik-g
left a comment
There was a problem hiding this comment.
Reviewed against fcf48ba5. I ran an independent RCA of the underlying bug first (call chain, permission model, created_by provenance, git history of the check), then an adversarial verification pass over this diff.
Verdict: the fix is correct and lands at the right layer. Both real gaps are closed — the admin requester bypass (prompt_studio_helper.py:227) and the service-account creator bypass (:234). The SA bypass is load-bearing rather than belt-and-braces: is_user_organization_admin returns False for service accounts (organization_member_service.py:29-30), so the creator-admin bypass could never have fired for this customer's profile.
Particular credit for carrying the requester as a separate request_user_id instead of reusing user_id. user_id is tool.created_by.user_id — the file-path owner — at all four entry points (views.py:451/530/683/789), so the ticket's own "Proposed fix" wording ("call sites already have user_id") would have re-validated the creator and no-oped on the exact reported scenario. test_build_index_payload.py:212 pinning request_user_id != user_id is the right guard against that regressing.
The tests are genuinely behavioral, not theatre — I checked by installing main's pre-fix body behind the new signature: 5 of 10 fail, including both bypass tests and all three message tests. The 1-tuple error_msg defect is real (DRF renders .detail as a JSON array; confirmed by executing against the pinned DRF 3.17.1), and isinstance(exc.detail, str) is the only assertion that catches it — str(exc) on a list-detail still contains the email substring. Good catch.
Comments below: one P2 worth closing pre-merge (a test gap that makes future breakage silent), one sign-off ask, and three follow-ups. Nothing blocking beyond the P2.
Nit: "49/49 backend tests pass" in the description doesn't match a reproducible scope (I get 15 for the subtree, 137 for the backend unit tier).
…ng contract - Pass the requester as a User object (request_user) instead of a user_id string: deletes the re-resolution helper (3 queries → 1), stops keying an authz decision on the non-unique User.user_id CharField, and uses is_user_organization_admin directly so the service-account exclusion stays in one place - Make request_user keyword-only with NO default on the four view-facing builders — a dropped plumb is now a loud TypeError instead of a silent revert to pre-fix behavior; signature test pins it - Add forwarding tests for the three previously-untested builders (parametrized, sentinel-abort pattern) - Pin the three non-ownership access disjuncts (org-share, user-share, group-share) that a sabotage-check showed were untested - Document the created_by-None bypass honestly in the docstring; the platform-API label-set gap it interacts with is filed as UN-3750 Sonar: dedup forwarding tests (new-code duplication gate) and keep a single throwing invocation inside pytest.raises blocks. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
🧹 Nitpick comments (1)
backend/prompt_studio/prompt_studio_core_v2/tests/test_build_index_payload.py (1)
207-215: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winAlso pin requester forwarding into
_build_summarize_params.This test only verifies the default-profile validator call. Dropping
request_userfrom the summary-profile path would still pass, causing admin requesters to be denied when a distinct summarization profile is used.Proposed assertion
def test_owner_access_receives_requesting_user(self) -> None: validate_owner_mock = MagicMock(return_value=None) + summarize_mock = MagicMock(return_value=(None, "", MagicMock())) _dispatch_build( check_return=True, read_return="extracted text", validate_owner_mock=validate_owner_mock, + build_summarize_mock=summarize_mock, ) _args, kwargs = validate_owner_mock.call_args assert kwargs.get("request_user") is _REQUEST_USER + assert summarize_mock.call_args.kwargs["request_user"] is _REQUEST_USER🤖 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/prompt_studio/prompt_studio_core_v2/tests/test_build_index_payload.py` around lines 207 - 215, The test test_owner_access_receives_requesting_user currently verifies only the default-profile validator; extend coverage to the _build_summarize_params summary-profile path and assert that the requesting user is forwarded there as well. Ensure the test exercises a distinct summarization profile and checks the validator’s request_user argument remains _REQUEST_USER.
🤖 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/prompt_studio/prompt_studio_core_v2/tests/test_build_index_payload.py`:
- Around line 207-215: The test test_owner_access_receives_requesting_user
currently verifies only the default-profile validator; extend coverage to the
_build_summarize_params summary-profile path and assert that the requesting user
is forwarded there as well. Ensure the test exercises a distinct summarization
profile and checks the validator’s request_user argument remains _REQUEST_USER.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: e837b26d-6ce9-4e60-af8d-e188043730f1
📒 Files selected for processing (4)
backend/prompt_studio/prompt_studio_core_v2/prompt_studio_helper.pybackend/prompt_studio/prompt_studio_core_v2/tests/test_build_index_payload.pybackend/prompt_studio/prompt_studio_core_v2/tests/test_validate_profile_manager_owner_access.pybackend/prompt_studio/prompt_studio_core_v2/views.py
🚧 Files skipped from review as they are similar to previous changes (1)
- backend/prompt_studio/prompt_studio_core_v2/views.py
UN-2202 (#1797) made adapter created_by audit-only and replaced shared_users with owner/viewer ResourceMembership roles. The guard now resolves the profile creator's adapter access via main's _adapter_accessible_by bridge; the branch-local _user_has_adapter_access helper (which read the dropped shared_users M2M) is deleted, and the disjunct tests are rewired to owner/viewer/org/group. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
ritwik-g
left a comment
There was a problem hiding this comment.
Approving — the fix is correct, and I re-verified it still holds after the UN-2202 merge.
Verified at 4c8bbe13 by execution rather than reading. All four builders are genuinely keyword-only with no default (inspect.signature → KEYWORD_ONLY, default=empty), and every bypass is load-bearing under sabotage: deleting the admin-requester branch, the service-account branch, or the None-creator branch each fails exactly one targeted test. The rewired disjunct tests reproduce the original signature under the new model — reducing _adapter_accessible_by to owner-only fails exactly 3, and exactly the right 3. _adapter_accessible_by is byte-identical to main's (adopted, not reinvented), and no coverage was lost in the rewire: 15 test defs on both sides, the only delta a rename and a forced mock rewire. Backend unit tier green.
Worth calling out two judgement calls that went the right way: declining the Greptile suggestion (it would have dropped the service-account exclusion from is_user_organization_admin and duplicated admin-role resolution outside its one home), and catching during implementation that user_id is the file-path owner rather than plumbing it through blind — that would have no-oped on the exact reported bug.
One thing to fix before merge — the description, not the code
The "Can this PR break any existing features" section still says:
"All new parameters are trailing and optional (
Nonedefault): callers that don't pass a requester (worker/legacy paths) get exactly the pre-fix validation behavior."
That was true at fcf48ba5 and was falsified by 153e4dcc — the very commit that hardened the builders. It wants rewriting rather than deleting, because the two-tier truth is the useful part:
- the four view-facing builders take
request_useras required keyword-only — a dropped plumb is aTypeError; - internal/worker paths remain
None-defaulted and get pre-fix behavior.
As written it's the sentence a future contributor reads before re-adding = None and silently reverting UN-3739 — and the service-account sign-off currently sits underneath it, which undercuts the sign-off too.
Two smaller ones while you're in there:
- 3×
request_user_idin What / How / Notes on Testing — it's aUserobject now. That's the exact confusion your own commit removed from the code; leaving it in the body invites its return. - "49/49 backend tests pass (
prompt_studio/,adapter_processor_v2/)" — that scope collects 59 at HEAD. Also worth naming the three classes that are the review response (TestCreatorAccessDisjuncts,TestBuilderSignatures,TestBuilderForwarding), since they're the interesting part of the test plan.
Best follow-up if you want one (not a gate)
The plumb type is unpinned. Changing any view to request_user=request.user.user_id passes the entire suite green — but a str is truthy and lacks .is_authenticated, so is_user_organization_admin short-circuits to False, the admin bypass silently dies, and UN-3739 is back with no failing test and no log line. It's the same shape as the hole you just closed, one layer up, and it's invited by user_id=tool.created_by.user_id sitting on the adjacent line. One thin view-level test closes it.
Related, same family: request_user is still None-defaulted on 8 functions including the guard itself — only the 4 builders are hardened. Latent today (every forward is correct), but it's the remediation surface if you ever want to make the contract uniform.
|
Thanks for the approval and the triage framing — verified all three before acting: Dead code (mine): confirmed, filed as UN-3756. Re-ran the caller sweep on both repos: cloud's UN-1258 (mine): confirmed Jira-only. It exists — "BUG_REG (0.60.1): shared LLM selected in profile manager → permission error", May 2024, Closed — but there's no corresponding commit in this repo's history, so it's cited in UN-3739 as same-failure-family context, not code precedent. I've annotated the ticket's Related section accordingly. Agentic-table (Hari's call): one data point to help the triage — it's not just the view branch skipping 🤖 Generated with Claude Code |
jaseemjaskp
left a comment
There was a problem hiding this comment.
Review summary — approving with nits
Reviewed with six agents (code review, silent-failure, type design, test coverage, comments, simplification). No blocking findings; the authz core is sound and I could not break it:
- The admin/service-account bypasses grant nothing new. Both match the platform's existing adapter model —
AdapterInstanceModelManager.for_useralready returns the unfiltered queryset for org admins and grants service accounts every non-frictionless adapter. Not a privilege escalation. - Every failure mode fails closed.
is_user_organization_adminswallowing exceptions →Falseonly ever removes a bypass. Under a real DB outage_adapter_accessible_byissues uncaught queries, so you get a loud 500, not a bogus 403.MultipleObjectsReturnedis ruled out by the(organization, user)unique constraint. - No breaking change. All four builders are called only from
views.py, and all four passrequest_user. - Real bug fixed in passing: the old
error_msg = (f"...",)trailing comma made the DRF detail a tuple; now a plain string, and pinned. - The tests are strong. Mutation testing killed 11/15 mutants, including every bypass removal.
_adapter()settingshared_to_org = Falseand_user()settingis_service_accountexplicitly are load-bearing — a bareMagicMockis auto-truthy and would have made every denial test pass vacuously. Correctly handled.
The findings worth your time, in order:
profile_reftells users their own profile "was created by another user" (helper.py:273) — reachable via the most common revocation shape, and a regression against this PR's own goal._build_summarize_paramsis the one live hop that silently reverts (helper.py:339) — and mutation testing shows nothing catches it.- The summarize-profile validator call is entirely uncovered (test_build_index_payload.py) — same bug class as UN-3739, for summarize-enabled projects.
- The new suite's
Case Ncitations are systematically wrong (test_validate_...py:117) — two are a straight swap, three cite cases that don't exist.
The rest are genuine nits. Deliberately not raising: the getattr(owner, "is_service_account", False) idiom (matches the house convention at 8+ sites incl. organization_member_service.py:29; getattr(request_user, "user_id", None) is outright correct since request_user is legitimately None), and any value-object/NewType wrapper for the two identities — str vs User plus keyword-only already covers the residual conflation risk. The explicit-parameter plumb is the right call over a StateStore thread-local; the wider diff buys you an identity flow that can't be silently lost again.
Follow-up ticket material, not this PR: PromptStudioHelper.index_document / prompt_responder and their chain have zero callers — the request_user additions there are inert. Deleting the chain would also let you drop the = None defaults. Renaming user_id → file_owner_user_id on the four builders would make the invariant self-evident rather than test-enforced.
🤖 Generated with Claude Code
…arize hop contract - Denial message no longer claims "created by another user" when the requester IS the creator (share revoked on their own profile) — they are addressed directly, still PII-free - _build_summarize_params: request_user is now required keyword-only — it guards a live second validator call, so a dropped plumb must TypeError, not silently revert (added to the signature test; direct forwarding test added for the hop) - Denial logging collapsed to one record carrying profile, creator, denied adapter ids, and requester; ERROR_MSG constant deleted - Adapter names deduped in messages (adapters of different types can share a name); single-vs-multi message branches pinned by tests - Docstring no longer claims nonexistent "worker paths"; test case citations renumbered to the module docstring's contract; stale access-model comment reduced to a pointer; redundant None guard dropped (callee handles None) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Unstract test resultsPer-group results
Critical paths
|



What
validate_profile_manager_owner_access()now receives the requesting user (request_user, theUserobject) and passes immediately when the requester is an org admin.Why
An org admin indexing a Prompt Studio project could get
Permission Error: You do not have access to <adapter>. The check validated only the profile creator's adapter access — the requester was never passed in — so admins were denied on any project whose profile creator lost adapter access (share revoked, creator offboarded — offboarding auto-wipes share rows — or profile created by a service account, which holds no shares by design). The UN-3479 admin bypass (#1993) couldn't help: it was applied to the creator, the only identity the function received. This contradicts the admin model everywhere else (admins see all adapters and are hidden from share dialogs since UN-3318), and the only workaround was sharing adapters org-wide.How
validate_profile_manager_owner_access(profile_manager, request_user=None)— evaluation order: requester-is-admin → creator None → creator service-account → creator admin → creator has all-4 adapter access → else deny.request_user: Userparameter (the object views already hold — no re-resolution from a non-uniqueuser_idstring) through all four view entry points (build_index_payload,build_fetch_response_payload,build_bulk_fetch_response_payload,build_single_pass_payload) and their internal chains; views passrequest.user.request_useris keyword-only with no default — dropping the plumb is aTypeError, not a silent revert to pre-fix behavior; a signature test pins this, and each builder has a forwarding test.user_idparameter, which is the file-path owner (tool.created_by.user_id) and must not change — a regression test pins that the two identities are never conflated.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)
request_useris required keyword-only with no default — omitting it is aTypeError, so a dropped or refactored-away plumb fails loudly instead of silently reverting UN-3739 (do not re-add= Nonethere; a signature test pins this). On the internal/worker-path helpers (validate_profile_manager_owner_access,_build_summarize_params,index_document,prompt_responder, and the single-prompt/single-pass chain) it staysNone-defaulted, so callers without a request context get exactly the pre-fix validation behavior. The revocation guard is kept — a non-admin requester is still denied when the profile creator lacks adapter access — and delegated use (shared-project users piggybacking on the creator's adapter access) is unchanged and covered by tests.created_by=SET_NULLon key deletion is pre-existing and now tracked as UN-3750.Database Migrations
Env Config
Relevant Docs
Related Issues or PRs
_BUSINESS_APP_LABELSomitsprompt_profile_manager_v2, so API-key deletion NULLs profile creatorsDependencies Versions
Notes on Testing
test_validate_profile_manager_owner_access.pycovers the full case matrix: admin requester passes on lapsed-creator profiles, non-admin requester still blocked, service-account creator passes, admin creator passes,created_by=Nonepasses, delegated use passes, plus message contract (names creator + adapter, flags former members, plain-string detail).test_build_index_payload.py::TestOwnerAccessPlumbingpins that the validator receives therequest_userobject, distinct from the file-pathuser_id.prompt_studio/,adapter_processor_v2/); ruff 0.3.4 check + format clean.🤖 Generated with Claude Code