Add a create-only permission action for Secrets Manager permissions
Why are we doing this work
Iteration 2 of &22561 grants default roles "create + read secrets metadata". Today write is a single action that fans out to both the OpenBao create and update capabilities, so there is no way to grant create without update.
Per #621682 (closed), the Developer default role was deferred pending an estimate for this:
Can we add a new "Create" permission in 19.4? Answer: Team will check how complex would it be to do this in 19.4. Ideally "Write" should be both
create secret + updating existing secretswhile "Create" is justcreating new secrets. In the meantime we will only add theMaintainerdefault permission.
This unblocks #623438 (closed) (Developer). Note that #623437 (closed) (Maintainer) is not blocked by it, since Maintainer gets create + update, which is the existing write action.
What
Add a create action that maps only to the OpenBao create capability, leaving write as create + update.
Two things make this cheaper than it looks:
- OpenBao already separates them. In KV v2,
createapplies when no version exists andupdatewhen it does. So create-only genuinely means "can add new secrets but not change existing ones", and OpenBao does the real enforcement. - The read side already exposes them separately.
Types::PermissionTypes::SecretsManagement::{Group,Project}SecretsManageralready exposecreate_secretsfromeffective_capabilities['create']andupdate_secretsfromeffective_capabilities['update'], viacapabilities_self. Nothing to change there.
Implementation plan
In ee/app/models/secrets_management/base_secrets_permission.rb:
- Add
create: 'create'toACTIONS. It flows intoVALID_ACTIONSautomatically. #management_capabilities: add awhen 'create'branch that adds onlycreatetodataandmetadata. Keepwriteas%w[create update]. Sets are used, so grantingwriteandcreatetogether stays idempotent.#set_actions_from_capabilitiesis the risky part. It currently emitswriteonly when bothcreateandupdateare present, so a create-only policy reads back with no write action at all and would render as read-only. Permissions are stored in the OpenBao policies themselves and rebuilt on read (seeSecretsManagement::SecretsPermissions::ListServiceHelpers), so this method is how the UI sees a grant. Make the mapping precise: both capabilities givewrite, create only givescreate. Decide explicitly whether update-only should be representable. If not, document that rather than letting it silently disappear.- Fix the two stale comments about the
readalias while you are in this file. See the section below. - Check whether the
actionsargument on the permissions mutations is a GraphQL enum or a validated string list. If it is an enum, add the value while the mutations still carryexperiment:, so ahead of #596011, to avoid owing a formal deprecation cycle. - Update
doc/user/permissions.md, both the group and project Secrets Manager tables.
No migration is needed. No existing records use create, and existing create plus update policies keep mapping to write unchanged.
validate_actions already requires one of read, read_metadata or read_value, so "create + read secrets metadata" is already a valid combination with no validation change.
create + write in the same payload
write already covers create, so a payload containing both is redundant rather than invalid. Drop create when write is present instead of rejecting the request. The frontend can plausibly send both, for example if the "Create" checkbox stays checked when "Write" is ticked, and a validation error there would be a confusing dead end for something with an obvious intended meaning.
Capability generation already tolerates this, because #management_capabilities uses sets. The visible part is readback: #set_actions_from_capabilities sees create + update and emits write alone, so the stored actions and the actions the UI reads back would otherwise disagree. Normalizing on input keeps the round-trip stable.
Frontend counterpart
We do want a "Create" checkbox in the permissions modal, so owners can grant create-only themselves. That is frontend work and out of scope here, but it depends on this issue, because the action has to exist on the backend before the UI can offer it. Confirm whether it lands in #623352 (closed) or a separate frontend issue.
Note the default role grants in #623437 (closed) and #623438 (closed) are set server side, so they do not wait on the checkbox.
The read alias, and why it does not block this
The comments in base_secrets_permission.rb say the frontend is migrating from read to read_metadata and point at #602726 (closed). That is misleading. #602726 (closed) is closed as complete and its read_value work did ship, but the rename did not:
- The frontend still sends
read. See theactionsobject insecrets_manager_permissions_modal.vue, where the key isreadeven though the label reads "Read metadata". - The backend still emits
readon readback in#set_actions_from_capabilities. - The
readinput alias is still inACTIONS.
So for this issue:
- Do not remove the
readalias here. It would break the permissions modal. - Do fix the comments, both the one above
ACTIONS[:read]and the one above#set_actions_from_capabilities, so they no longer imply a migration is in flight. Point them at #623488 instead.
The rename itself is tracked in #623488. It needs frontend and backend in one change, it is not scheduled for 19.4, and it blocks nothing. It touches the same method as this issue, so expect a rebase but no real conflict.
Acceptance criteria
-
createis accepted as an action and grants only thecreatecapability on the data and metadata paths -
writestill grants create + update, with no behaviour change for existing grants - A payload with both
createandwriteis accepted, withcreatedropped, and behaves identically towritealone - A create-only grant round-trips correctly. Readback returns
create, not read-only - A create-only grant resolves to
createSecrets: trueandupdateSecrets: false - Updating an existing secret is denied for a create-only grant
- Spec matrix covers group and project, across each principal type
- Stale
readalias comments corrected and pointed at #623488 - Docs updated