[feat] Managed vault secrets: platform-owned connections users cannot edit or delete - #6165
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. 🗂️ Base branches to auto review (1)
Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Repository YAML (base), Organization UI (inherited) Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
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 |
641b4e1 to
26a019f
Compare
The row is Agenta's from the moment it exists: `managed_by="starter-credits-bridge"` on the create refuses user deletes and re-credentialing, and `write_only=True` keeps the proxy virtual key out of every user-facing read. Setting both on CREATE (rather than tightening afterwards) leaves no window in which the seeded connection is readable or removable. Every write this component makes to its own row now passes `allow_managed=True`, the in-process owner's key past the managed guard: the grant-record upsert and the finalize that writes the minted key. There is no delete path in the bridge, so nothing else needs the flag. Also documents that the mint-policy cache key is deliberately global, unlike the per-org feature-flag cache above it: the policy is one program-wide payload, identical for every organization. Stacked on managed-secrets (#6165) and write-only-secrets-api (#6164), which provide the two flags.
The row is Agenta's from the moment it exists: `managed_by="starter-credits-bridge"` on the create refuses user deletes and re-credentialing, and `write_only=True` keeps the proxy virtual key out of every user-facing read. Setting both on CREATE (rather than tightening afterwards) leaves no window in which the seeded connection is readable or removable. Every write this component makes to its own row now passes `allow_managed=True`, the in-process owner's key past the managed guard: the grant-record upsert and the finalize that writes the minted key. There is no delete path in the bridge, so nothing else needs the flag. Also documents that the mint-policy cache key is deliberately global, unlike the per-org feature-flag cache above it: the policy is one program-wide payload, identical for every organization. Stacked on managed-secrets (#6165) and write-only-secrets-api (#6164), which provide the two flags.
mmabrouk
left a comment
There was a problem hiding this comment.
Reviewer guide for this PR. It ships inert - nothing is managed until a component sets a marker, no row carries one today, and there is no migration - so the review is really about whether the guard is in the right place and fails in the right direction. The inline notes cover the allowlist of what a user may still change, the pin-and-refuse mechanics, the default-denies parameter, and the one place where an omitted field could silently un-manage a row.
5224149 to
784e94e
Compare
The row is Agenta's from the moment it exists: `managed_by="starter-credits-bridge"` on the create refuses user deletes and re-credentialing, and `write_only=True` keeps the proxy virtual key out of every user-facing read. Setting both on CREATE (rather than tightening afterwards) leaves no window in which the seeded connection is readable or removable. Every write this component makes to its own row now passes `allow_managed=True`, the in-process owner's key past the managed guard: the grant-record upsert and the finalize that writes the minted key. There is no delete path in the bridge, so nothing else needs the flag. Also documents that the mint-policy cache key is deliberately global, unlike the per-org feature-flag cache above it: the policy is one program-wide payload, identical for every organization. Stacked on managed-secrets (#6165) and write-only-secrets-api (#6164), which provide the two flags.
8bbd078 to
3ef279c
Compare
784e94e to
aa04102
Compare
The row is Agenta's from the moment it exists: `managed_by="starter-credits-bridge"` on the create refuses user deletes and re-credentialing, and `write_only=True` keeps the proxy virtual key out of every user-facing read. Setting both on CREATE (rather than tightening afterwards) leaves no window in which the seeded connection is readable or removable. Every write this component makes to its own row now passes `allow_managed=True`, the in-process owner's key past the managed guard: the grant-record upsert and the finalize that writes the minted key. There is no delete path in the bridge, so nothing else needs the flag. Also documents that the mint-policy cache key is deliberately global, unlike the per-org feature-flag cache above it: the policy is one program-wide payload, identical for every organization. Stacked on managed-secrets (#6165) and write-only-secrets-api (#6164), which provide the two flags.
3ef279c to
f763fb1
Compare
aa04102 to
3ca898d
Compare
The row is Agenta's from the moment it exists: `managed_by="starter-credits-bridge"` on the create refuses user deletes and re-credentialing, and `write_only=True` keeps the proxy virtual key out of every user-facing read. Setting both on CREATE (rather than tightening afterwards) leaves no window in which the seeded connection is readable or removable. Every write this component makes to its own row now passes `allow_managed=True`, the in-process owner's key past the managed guard: the grant-record upsert and the finalize that writes the minted key. There is no delete path in the bridge, so nothing else needs the flag. Also documents that the mint-policy cache key is deliberately global, unlike the per-org feature-flag cache above it: the policy is one program-wide payload, identical for every organization. Stacked on managed-secrets (#6165) and write-only-secrets-api (#6164), which provide the two flags.
The row is Agenta's from the moment it exists: `managed_by="starter-credits-bridge"` on the create refuses user deletes and re-credentialing, and `write_only=True` keeps the proxy virtual key out of every user-facing read. Setting both on CREATE (rather than tightening afterwards) leaves no window in which the seeded connection is readable or removable. Every write this component makes to its own row now passes `allow_managed=True`, the in-process owner's key past the managed guard: the grant-record upsert and the finalize that writes the minted key. There is no delete path in the bridge, so nothing else needs the flag. Also documents that the mint-policy cache key is deliberately global, unlike the per-org feature-flag cache above it: the policy is one program-wide payload, identical for every organization. Stacked on managed-secrets (#6165) and write-only-secrets-api (#6164), which provide the two flags.
521f728 to
ed50b36
Compare
The row is Agenta's from the moment it exists: `managed_by="starter-credits-bridge"` on the create refuses user deletes and re-credentialing, and `write_only=True` keeps the proxy virtual key out of every user-facing read. Setting both on CREATE (rather than tightening afterwards) leaves no window in which the seeded connection is readable or removable. Every write this component makes to its own row now passes `allow_managed=True`, the in-process owner's key past the managed guard: the grant-record upsert and the finalize that writes the minted key. There is no delete path in the bridge, so nothing else needs the flag. Also documents that the mint-policy cache key is deliberately global, unlike the per-org feature-flag cache above it: the policy is one program-wide payload, identical for every organization. Stacked on managed-secrets (#6165) and write-only-secrets-api (#6164), which provide the two flags.
The row is Agenta's from the moment it exists: `managed_by="starter-credits-bridge"` on the create refuses user deletes and re-credentialing, and `write_only=True` keeps the proxy virtual key out of every user-facing read. Setting both on CREATE (rather than tightening afterwards) leaves no window in which the seeded connection is readable or removable. Every write this component makes to its own row now passes `allow_managed=True`, the in-process owner's key past the managed guard: the grant-record upsert and the finalize that writes the minted key. There is no delete path in the bridge, so nothing else needs the flag. Also documents that the mint-policy cache key is deliberately global, unlike the per-org feature-flag cache above it: the policy is one program-wide payload, identical for every organization. Stacked on managed-secrets (#6165) and write-only-secrets-api (#6164), which provide the two flags.
Railway Preview Environment
Updated at 2026-08-23T20:46:24.026Z |
7ce4c55 to
eb72639
Compare
3cd3c5a to
d0717cd
Compare
The row is Agenta's from the moment it exists: `managed_by="starter-credits-bridge"` on the create refuses user deletes and re-credentialing, and `write_only=True` keeps the proxy virtual key out of every user-facing read. Setting both on CREATE (rather than tightening afterwards) leaves no window in which the seeded connection is readable or removable. Every write this component makes to its own row now passes `allow_managed=True`, the in-process owner's key past the managed guard: the grant-record upsert and the finalize that writes the minted key. There is no delete path in the bridge, so nothing else needs the flag. Also documents that the mint-policy cache key is deliberately global, unlike the per-org feature-flag cache above it: the policy is one program-wide payload, identical for every organization. Stacked on managed-secrets (#6165) and write-only-secrets-api (#6164), which provide the two flags.
eb72639 to
48d8948
Compare
d0717cd to
f134b5b
Compare
The row is Agenta's from the moment it exists: `managed_by="starter-credits-bridge"` on the create refuses user deletes and re-credentialing, and `write_only=True` keeps the proxy virtual key out of every user-facing read. Setting both on CREATE (rather than tightening afterwards) leaves no window in which the seeded connection is readable or removable. Every write this component makes to its own row now passes `allow_managed=True`, the in-process owner's key past the managed guard: the grant-record upsert and the finalize that writes the minted key. There is no delete path in the bridge, so nothing else needs the flag. Also documents that the mint-policy cache key is deliberately global, unlike the per-org feature-flag cache above it: the policy is one program-wide payload, identical for every organization. Stacked on managed-secrets (#6165) and write-only-secrets-api (#6164), which provide the two flags.
f134b5b to
e12188b
Compare
e12188b to
362860c
Compare
362860c to
6c70e7b
Compare
mmabrouk
left a comment
There was a problem hiding this comment.
Review round 3 (final heads)
Head 6c70e7b3c9, base write-only-secrets-api at d1e4f8832f.
The lane content is unchanged since round 2 (362860c6d1). The only difference is a line offset in dao.py from the rebase onto the new base. So this round is a re-verification plus a look at what the new base changes underneath.
Tests run read-only: pytest oss/tests/pytest/unit/secrets/ oss/tests/pytest/unit/vault/ -q → 119 passed.
Verified addressed
The seven reviewer-guide comments on this PR all describe an earlier design: a free-form managed_by string, an allowlist of still-editable fields, pin-and-refuse with rewrite-back, and an allow_managed parameter on update and delete. None of that is in the code now. GitHub marks all seven threads outdated, correctly.
What replaced it is simpler and I prefer it. A managed row is read-only, full stop.
management.policyis the only public half.PublicSecretManagementDTOcarriespolicyand nothing else (api/oss/src/core/secrets/managed.py:20-23), andproject_secret_responsebuilds it by hand rather than dumping the internal model (api/oss/src/core/secrets/redaction.py:97-99). The manager identity cannot reach a caller.- Internal ownership is typed, not free-form.
SecretManageris an enum (managed.py:5-7), not a string. - No
managed_bymarker remains anywhere in the API or SDK. The only hits are two negative assertions in the tests. - Update and delete both refuse with 409, and both refuse under the DAO row lock.
_resolve_updateis passed intoSecretsDAO.update, which selects.with_for_update()before calling it (api/oss/src/dbs/postgres/secrets/dao.py:124-160)._authorize_deleteis passed intoSecretsDAO.delete, which now also selects.with_for_update()before authorizing (dao.py:181-198). Adding the lock to the delete path is the right call. - A client cannot claim the marker.
CreateSecretDTOandUpdateSecretDTOnow setextra="forbid", so a body carryingmanagementis rejected with 422 before any handler runs. The parametrized test covers bothmanagementand the oldmanaged_byspelling (test_managed_routes.py:98). - No migration needed. The marker rides inside the encrypted
dataJSON besidewrite_only, andmap_secrets_dto_to_dbe_updatereads the stored value back before the wholesale data rewrite so an update that omits it cannot un-manage the row (mappings.py:83-99).
Nit
Forward compatibility on read: an unknown manager makes the row unreadable. mappings.py:107 pops the stored management dict and hands it to SecretResponseDTO, where SecretManagementDTO has extra="forbid" and a closed manager enum. If a later version writes a second manager value, or adds a field, every build that predates it raises a ValidationError on read of that row, not only on edit. That includes a rollback and a mixed-version deploy. Today there is exactly one manager and no such row exists, so nothing is broken. It is worth deciding now whether reads should tolerate an unknown manager, because the cost of getting it wrong is a row nobody can read rather than a row nobody can edit.
Dead branch in _create_secret. api/oss/src/core/secrets/services.py:327-339 branches on management is None and calls secrets_dao.create twice with the only difference being whether the management kwarg is passed. The DAO parameter already defaults to None (dao.py:54), so both arms do the same thing. One call passing management=management reads better.
Not a defect, but worth stating in the description
Managed rows are not hidden on the wire. There is no query flag and no server-side filter. GET /secrets/ returns managed rows like any other, with management.policy set. Hiding them from Settings and keeping them in model selection is entirely a client decision, made in AIProvidersPage.tsx and connectionPicker.ts. Those files are not in this PR. That split is a reasonable design, since the runtime and the picker want the row and only the edit surfaces do not. It does mean the Settings page shows a row users cannot edit until the frontend lane lands. Worth naming in the PR body so the ordering is explicit.
Verdict
Safe to merge in order. The guard is in the right place, it fails closed, and it is enforced under the row lock on both mutating paths.
d1e4f88 to
15ab532
Compare
6c70e7b to
22dcd97
Compare
22dcd97 to
d896cd0
Compare
Context
Agenta provisions some Vault connections and owns their lifecycle, but the earlier model exposed a free-form
managed_bycomponent string to clients and used a universalallow_managedbypass. That mixed internal identity, user mutation policy, value visibility, and frontend presentation.Changes
This PR introduces a typed managed-secret model without a database migration.
Internal storage:
{"management": {"manager": "starter-credits-bridge", "policy": "manager_only"}}Public response:
{"management": {"policy": "manager_only"}}The manager identity stays internal. Public create and update DTOs cannot claim or clear management. Trusted components create a managed row through
create_managed_secretwith a typedSecretManagerandSecretManagementPolicy.General update and delete operations have no boolean bypass. They load and lock the current row, invoke a service-owned resolver or authorizer, and reject a manager-only row with HTTP 409 in the same transaction. The DAO owns transaction mechanics but does not interpret policy.
Management and
write_onlyremain independent. Existing rows have nomanagementobject and stay unmanaged. The object lives in the existing encrypted JSON payload, so this needs no schema migration.Tests / notes
What to QA
management.policybut never the internal manager.Depends on #6164 through the
write-only-secrets-apibase.