Secrets Manager: Enforce granular permissions on the backend

Blocked by

  • !243263 (merged): a short OpenBao token TTL on the user_jwt CEL role. The userPermissions resolver mints a persisted token for capabilities-self and revokes it best-effort. Without a role TTL, a failed revoke leaves the token alive for OpenBao's ~32-day default, so the TTL must land first. Backfill of existing roles is tracked in #604545 (closed).

What this MR does

This is the backend part of enforcing granular Secrets Manager permissions. It does three things:

  1. Exposes the current user's permissions over GraphQL. New userPermissions field on ProjectSecretsManager and GroupSecretsManager with four booleans: readMetadata, createSecrets, updateSecrets, deleteSecrets. They are resolved from the user's real OpenBao effective capabilities, not their GitLab role.
  2. Blocks the page when the user has no read access. Projects::SecretsController and Groups::SecretsController return 404 when the user lacks the effective OpenBao read capability.
  3. Enforces granular-token (GPAT) authorization on the secrets manager query surface, via the read_secrets_manager assignable scope.

Implements #601817.

Why

Any Reporter or above can open the Secrets Manager page and see the action buttons even without an OpenBao grant, and creating a secret then fails on submit. The role check is too coarse. The real access rules live in OpenBao, so this makes the backend the single source of truth.

How it works

Resolving effective capabilities

EffectiveCapabilitiesService resolves the booleans from the user's real OpenBao capabilities:

  1. Log in as the user with a CEL login (ProjectUserJwt / GroupUserJwt) to mint a real token.
  2. Call sys/capabilities-self on the secrets data and metadata paths.
  3. Revoke the minted token (best-effort cleanup).

An explicit login is used rather than the usual inline auth, because inline auth mints a request-scoped token that OpenBao never writes to the token store, so sys/capabilities-self rejects it with "invalid token". OpenBao computes the union of the user's principal policies (user, member role, groups, role), deny precedence, and expiry, so no permission logic is duplicated in Ruby.

Fails closed: if OpenBao is unreachable, all permissions return false and the controller returns 404.

Path mapping: readMetadata from read/list on the metadata path, and createSecrets/updateSecrets/deleteSecrets from create/update/delete on the data path.

Granular-token authorization

The query surface is authorized for granular PATs via the read_secrets_manager assignable scope (shared with !240364 (merged), extended to the project and group boundaries), with directives on the ProjectSecretsManager/GroupSecretsManager types and the root projectSecretsManager/groupSecretsManager query fields.

Field naming

The read field is named readMetadata (not readSecrets) to align with the read_metadata vs read_value split being introduced for direct API access (!240364 (merged)). readValue is intentionally not exposed yet because it lives on a separate api_jwt mount. It is tracked in #602726 (closed).

Backend / frontend split

The frontend changes live in a separate MR: !240850. They are split because the Secrets Manager is already in production (Beta), so during a rolling deploy the backend must merge and deploy first. The GraphQL fields are additive, so this is safe to deploy on its own. The frontend MR must rename readSecrets to readMetadata.

Databases

None. No migrations, no schema or query changes.

Follow-ups (tracked separately)

  • readValue GraphQL exposure (#602726 (closed)).
  • Cache EffectiveCapabilitiesService results across requests to avoid the duplicate OpenBao round-trips between the controller's check_read_capability! and the GraphQL userPermissions resolver (#605467).
  • Consolidate the Secrets Manager authorization ability zoo (#605081).

How to validate locally

  1. Enable the Secrets Manager for a project or group.
  2. Sign in as a Reporter with no granular secrets permission.
  3. Open the Secrets Manager page. You should get a 404.
  4. Grant that user read access via the Secrets Manager permissions UI.
  5. Reload. The page now loads (200).

Tests

  • GraphQL request specs (project + group): each permission combination, grants via user/role/group principals, expired grants, guest, OpenBao unreachable, and the granular-token shared example.
  • Client spec: capabilities_self against real OpenBao, plus minted-token cleanup (revoke invoked + revoke failure tolerated).
  • Service unit specs: capability-to-boolean mapping, the data envelope, deny and partial combinations, fail-closed on every OpenBao error.
  • Controller specs: read present -> 200, read missing -> 404, OpenBao unreachable -> 404.

References

Edited by Erick Bajao

Merge request reports

Loading
Loading