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 tosecrets_manager_namespace_enrollments(small table, no index needed). - When
secrets_manager_paid_experienceis enabled for the namespace, unenrolling soft-disables the enrollment (setsdisabled_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 clearsdisabled_atand re-evaluates the free beta cohort. SecretsManagement::Availabilitydenies access for a paid-experience top-level group that explicitly opted out.- No GraphQL schema change: the
namespaceSecretsManagerEnrollmentresolver now only resolves enabled enrollment records, so an opted-out group resolves tonull— 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 thesecretsManagerEntitlementfield on GroupType now allows users withread_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_namespacesservice 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_experiencefeature 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:
GroupPolicypreventsread_secrets_managerwhen 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, andcreate_secrets_manager_api_jwt, which gates authenticating to OpenBao), so GraphQL queries and mutations fail authorization.ProjectPolicyhas the equivalent block for project-level secrets. - CI jobs:
Ci::BuildRunnerPresentercalls 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
enabledscope, which treats a record withdisabled_atset 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
- Run GDK with SaaS simulation:
export GITLAB_SIMULATE_SAAS=1 - In rails console:
Feature.enable(:secrets_manager_paid_experience)andFeature.enable(:group_secrets_manager) - Visit a top-level group's Secrets Manager settings, confirm the Secrets Manager pages are reachable
- Turn the Secrets Manager toggle off, reload — the group's secrets pages now return 404
- 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.