Respect Secrets Manager toggle under the paid experience

What does this MR do and why?

Backend part of the fix; the frontend part is in !251060 (merged).

On GitLab.com, when the secrets_manager_paid_experience feature flag is enabled, every top-level group is implicitly granted Secrets Manager availability before enrolling, so the trial call-to-action stays reachable. Turning the "Secrets Manager" toggle off in group settings deleted the enrollment record, but the availability check ignored enrollment for these groups — so the toggle had no effect and the Secrets Manager pages stayed accessible.

The fix makes enrollment tri-state: no record means the group never enrolled, an active record means it is enrolled, and a record with disabled_at set means it explicitly opted out. A paid-experience top-level group that opted out is now denied access, so its secrets routes return 404.

  • Adds a nullable disabled_at (timestamptz) column to secrets_manager_namespace_enrollments (small table, no index needed).
  • When secrets_manager_paid_experience is enabled for the namespace, unenrolling soft-disables the enrollment (sets disabled_at, keeping the record and stored secrets); with the flag off, unenrolling destroys the record exactly as before this MR, so the free beta cohort's behavior is unchanged. Enrolling again clears disabled_at and re-evaluates the free beta cohort.
  • SecretsManagement::Availability denies access for a paid-experience top-level group that explicitly opted out.
  • No GraphQL schema change: the namespaceSecretsManagerEnrollment resolver now only resolves enabled enrollment records, so an opted-out group resolves to null — indistinguishable from a never-enrolled group at the API level.
  • Owners can still re-enable after opting out: an explicit opt-out denies read_secrets_manager, but the secretsManagerEntitlement field on GroupType now allows users with read_secrets_manager_enrollment (group owners), so the settings page can still show the entitlement and offer the toggle to re-enroll. Without this, opting out would lock the group out of the settings UI permanently.
  • The count_enrolled_namespaces service ping metric counts only enabled enrollments.
  • The frontend part — computing enrollment from the resolved record and gating the enrollment toggle on a selected trial or paid add-on — moved to a follow-up MR: !251060 (merged)
  • Everything is behind the secrets_manager_paid_experience feature flag (default off); with the flag off, behavior is unchanged.

How the opt-out is enforced

All enforcement flows through a single choke point — SecretsManagement::Availability.for_group? / for_project? — so every consumer inherits the opt-out denial:

  • Web UI, GraphQL, and API: GroupPolicy prevents read_secrets_manager when availability is false, so group secrets pages return 404. The same rule prevents all secrets abilities (read_secret, create_secret, delete_secret, provision_secrets_manager, and create_secrets_manager_api_jwt, which gates authenticating to OpenBao), so GraphQL queries and mutations fail authorization. ProjectPolicy has the equivalent block for project-level secrets.
  • CI jobs: Ci::BuildRunnerPresenter calls the same availability check before including the secrets manager server config and credentials in the runner payload, so jobs in an opted-out namespace never receive access to those secrets. In-flight jobs that already got credentials can finish (job JWTs are short-lived).
  • Subgroups and projects under an opted-out root group are denied too: their paths resolve enrollment through the new enabled scope, which treats a record with disabled_at set as not enrolled.

Deploy safety and frontend compatibility

This backend MR deploys before the frontend MR, and the feature flag may already be enabled in production at that point. Because the API shape is unchanged and an opted-out enrollment resolves to null, the current frontend — which computes the toggle state as Boolean(enrollment) — stays consistent with the actual access denial in every combination: old or new frontend, flag on or off. No coordination between the two deployments is needed.

References

Closes https://gitlab.com/gitlab-org/gitlab/-/work_items/618054

Frontend follow-up MR: !251060 (merged)

Screenshots or screen recordings

The change is behavioral, so there is little to show visually: the Secrets Manager toggle now takes effect, and the group's secrets routes return 404 after opting out.

Before After

How to set up and validate locally

  1. Run GDK with SaaS simulation: export GITLAB_SIMULATE_SAAS=1
  2. In rails console: Feature.enable(:secrets_manager_paid_experience) and Feature.enable(:group_secrets_manager)
  3. Visit a top-level group's Secrets Manager settings, confirm the Secrets Manager pages are reachable
  4. Turn the Secrets Manager toggle off, reload — the group's secrets pages now return 404
  5. Turn the toggle back on — access is restored

MR acceptance checklist

Evaluate this MR against the MR acceptance checklist. It helps you analyze changes to reduce risks in quality, performance, reliability, security, and maintainability.

Edited by Dmytro Biryukov

Merge request reports

Loading
Loading