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 secrets while "Create" is just creating new secrets. In the meantime we will only add the Maintainer default 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, create applies when no version exists and update when 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}SecretsManager already expose create_secrets from effective_capabilities['create'] and update_secrets from effective_capabilities['update'], via capabilities_self. Nothing to change there.

Implementation plan

In ee/app/models/secrets_management/base_secrets_permission.rb:

  1. Add create: 'create' to ACTIONS. It flows into VALID_ACTIONS automatically.
  2. #management_capabilities: add a when 'create' branch that adds only create to data and metadata. Keep write as %w[create update]. Sets are used, so granting write and create together stays idempotent.
  3. #set_actions_from_capabilities is the risky part. It currently emits write only when both create and update are 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 (see SecretsManagement::SecretsPermissions::ListServiceHelpers), so this method is how the UI sees a grant. Make the mapping precise: both capabilities give write, create only gives create. Decide explicitly whether update-only should be representable. If not, document that rather than letting it silently disappear.
  4. Fix the two stale comments about the read alias while you are in this file. See the section below.
  5. Check whether the actions argument 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 carry experiment:, so ahead of #596011, to avoid owing a formal deprecation cycle.
  6. 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 the actions object in secrets_manager_permissions_modal.vue, where the key is read even though the label reads "Read metadata".
  • The backend still emits read on readback in #set_actions_from_capabilities.
  • The read input alias is still in ACTIONS.

So for this issue:

  • Do not remove the read alias 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

  • create is accepted as an action and grants only the create capability on the data and metadata paths
  • write still grants create + update, with no behaviour change for existing grants
  • A payload with both create and write is accepted, with create dropped, and behaves identically to write alone
  • A create-only grant round-trips correctly. Readback returns create, not read-only
  • A create-only grant resolves to createSecrets: true and updateSecrets: false
  • Updating an existing secret is denied for a create-only grant
  • Spec matrix covers group and project, across each principal type
  • Stale read alias comments corrected and pointed at #623488
  • Docs updated
Edited by Erick Bajao