Recognize the self-managed Secrets Manager beta cohort

What does this MR do and why?

Self-managed (SM) instances that joined the Secrets Manager open beta lose UI access to their secrets, and cannot create or edit secrets, as soon as secrets_manager_paid_experience is enabled instance-wide. Equivalent GitLab.com beta groups keep full access and see a transition banner.

Cause: SM enrollment (SecretsManagement::InstanceEnrollment) is a plain boolean application setting with no beta marker, unlike SecretsManagement::NamespaceEnrollment on GitLab.com, which has a beta column (see !249180 (merged)). Because of this, SecretsManagement::Entitlement::Resolver#beta_enrolled? was always false on SM, beta_window_eligible was always false, and the group/project policies (which queried the namespace enrollment directly) hid subgroup and project secrets.

This MR:

  • Adds a secrets_manager_instance_beta_enrolled boolean column on application_settings (migration 20260908193413, default false, not null), plus a regular data migration 20260908193414 (BackfillApplicationSettingsSecretsManagerInstanceBetaEnrolled) that backfills it with UPDATE application_settings SET secrets_manager_instance_beta_enrolled = secrets_manager_instance_enrolled. This is correct because secrets_manager_paid_experience has never defaulted to on for self-managed, so every instance enrolled today enrolled during the free beta. The backfill ships in 19.4, the same release in which secrets_manager_paid_experience is planned to default on for self-managed (https://gitlab.com/gitlab-org/gitlab/-/work_items/616379, Phase 3a); the beta program end flag follows in 19.5 (Phase 3b). It is a regular migration as an explicit exception: the marker must exist before flag-on code serves requests, including on zero-downtime upgrades that defer post-deployment migrations.
  • SecretsManagement::InstanceEnrollmentService#enroll writes the marker as !Feature.enabled?(:secrets_manager_paid_experience, :instance) at enrollment time, re-evaluated on every enroll, mirroring NamespaceEnrollmentService#beta_enrollment? on GitLab.com. InstanceEnrollment.beta_enrolled? now reads beta? && Gitlab::CurrentSettings.secrets_manager_instance_beta_enrolled, with no queries against secrets manager tables.
  • Makes Entitlement::Resolver#beta_enrolled? branch on Gitlab::Saas.feature_available?(:gitlab_com_subscriptions): SaaS keeps reading NamespaceEnrollment.beta_enrolled?, self-managed reads the new InstanceEnrollment.beta_enrolled?. The beta window still only opens in trial_eligible/ineligible and still closes when end_secrets_manager_beta_program is enabled, same as on SaaS.
  • Updates GroupPolicy/ProjectPolicy to read beta_window_eligible from the resolved entitlement instead of a private helper that queried NamespaceEnrollment directly; the helpers are removed. On SaaS this is equivalent because the condition only fires in trial_eligible, where beta_window_eligible equals the old enrollment lookup.
  • Adds a nullable betaWindowEligible GraphQL field on EntitlementType (experiment, milestone 19.4) so the frontend can read one realm-aware signal.

Notes for reviewers:

  • The marker is instance-wide, not per group, because self-managed entitlement is instance-wide: CustomersDot is asked per instance and the resolver caches one entitlement per install, so a per-group answer would leak between groups through that cache. Instance-wide also covers projects in personal namespaces.
  • Why a stored marker instead of deriving it from existing secrets manager rows (the first iteration of this MR): a derived answer moves when CustomersDot fails or returns ineligible, when the instance unenrolls and re-enrolls, or when the last manager is deleted. A value written at enrollment time does not. See the review discussion at !254012 (comment 3803023229).
  • The migrations were going to be a separate MR; folded into this one by agreement with the reviewers. The backfill is a regular migration, agreed in !254012 (comment 3806869696) as an exception to the rule that data migrations are post-deployment: with the flag flip in the same release, a zero-downtime upgrade with SKIP_POST_DEPLOYMENT_MIGRATIONS would otherwise serve flag-on code before the marker exists and treat beta instances as new customers until the deferred migrations run. It is a single-row update (0.7 ms, plan below) that depends only on the preceding add_column, and the old code never reads the column, so it carries none of the compatibility risk post-deployment migrations guard against.
  • The setting is deliberately absent from the Admin Area form, the REST settings API, and GraphQL; only InstanceEnrollmentService and the backfill write it.
  • Out of scope: offline (air-gapped) and trial licenses take resolver paths that skip the beta attributes and are unchanged. The self-managed beta cutoff date and its communication are a product decision tracked separately.

Database

application_settings has exactly one row, so every statement below touches a single row. Plans are EXPLAIN (ANALYZE, BUFFERS) from a local GDK database; the table has the same single-row shape on every installation, so a Database Lab plan would not differ in kind.

Migration output

$ bin/rails db:migrate:down:main VERSION=20260908193414
main: == 20260908193414 BackfillApplicationSettingsSecretsManagerInstanceBetaEnrolled: reverting 
main: == 20260908193414 BackfillApplicationSettingsSecretsManagerInstanceBetaEnrolled: reverted (0.0108s) 
$ bin/rails db:migrate:down:main VERSION=20260908193413
main: == 20260908193413 AddSecretsManagerInstanceBetaEnrolledToApplicationSettings: reverting 
main: -- remove_column(:application_settings, :secrets_manager_instance_beta_enrolled, {:if_exists=>true})
main:    -> 0.2354s
main: == 20260908193413 AddSecretsManagerInstanceBetaEnrolledToApplicationSettings: reverted (0.2441s) 
$ bin/rails db:migrate
main: == 20260908193413 AddSecretsManagerInstanceBetaEnrolledToApplicationSettings: migrating 
main: -- add_column(:application_settings, :secrets_manager_instance_beta_enrolled, :boolean, {:default=>false, :null=>false, :if_not_exists=>true})
main:    -> 0.2652s
main: == 20260908193413 AddSecretsManagerInstanceBetaEnrolledToApplicationSettings: migrated (0.2782s) 
main: == 20260908193414 BackfillApplicationSettingsSecretsManagerInstanceBetaEnrolled: migrating 
main: == 20260908193414 BackfillApplicationSettingsSecretsManagerInstanceBetaEnrolled: migrated (0.0136s)

Backfill (regular migration 20260908193414, update_all)

UPDATE application_settings SET secrets_manager_instance_beta_enrolled = secrets_manager_instance_enrolled
Update on application_settings  (cost=0.00..4.01 rows=0 width=0) (actual time=0.355..0.355 rows=0 loops=1)
  Buffers: shared hit=170 dirtied=1
  ->  Seq Scan on application_settings  (cost=0.00..4.01 rows=1 width=7) (actual time=0.013..0.013 rows=1 loops=1)
        Buffers: shared hit=4
Planning:
  Buffers: shared hit=153
Planning Time: 0.316 ms
Execution Time: 0.671 ms

Enrollment service (InstanceEnrollmentService#enroll, changed query)

Gitlab::CurrentSettings.update! now writes two columns instead of one. Exact statement emitted by ActiveRecord:

UPDATE "application_settings" SET "updated_at" = '2026-09-08 20:39:36.961037', "secrets_manager_instance_enrolled" = FALSE, "secrets_manager_instance_beta_enrolled" = FALSE WHERE "application_settings"."id" = 1
Update on application_settings  (cost=0.12..2.15 rows=0 width=0) (actual time=0.216..0.216 rows=0 loops=1)
  Buffers: shared hit=76
  ->  Index Scan using application_settings_pkey on application_settings  (cost=0.12..2.15 rows=1 width=16) (actual time=0.009..0.009 rows=1 loops=1)
        Index Cond: (id = 1)
        Buffers: shared hit=5
Planning:
  Buffers: shared hit=7
Planning Time: 0.040 ms
Execution Time: 0.223 ms

Removed queries

The first iteration of this MR added SELECT 1 AS one FROM "group_secrets_managers" LIMIT 1 and the same for project_secrets_managers in InstanceEnrollment.beta_enrolled?. Both are gone; the method now reads the cached application settings row and issues no query.

Rollback

20260908193413 drops the column; the backfill's down is a no-op because the preceding migration's down already removes the column. Output above shows both down steps and the re-run.

References

Screenshots or screen recordings

This is a backend-only change with no UI impact.

How to set up and validate locally

  1. Run GDK as self-managed (no SaaS simulation) with an online cloud license that includes native_secrets_management, with CustomersDot reachable (staging) or the CustomersDot client stubbed to return trial_eligible and blocked: false.

  2. In Admin area > Settings > General, enable Secrets Manager instance enrollment. Create a top-level group and provision a group secrets manager (Settings > General > Permissions and group features > Secrets manager toggle).

  3. Run bin/rails db:migrate. For an instance enrolled before this change, Gitlab::CurrentSettings.secrets_manager_instance_beta_enrolled returns true (backfilled).

  4. In a Rails console, enable the flag and check eligibility:

    Feature.enable(:secrets_manager_paid_experience)
    SecretsManagement::Entitlement.for(group).beta_window_eligible # => true
    SecretsManagement::Entitlement.for(group).permits_writes? # => true
  5. End the beta program and re-check:

    Feature.enable(:end_secrets_manager_beta_program)
    SecretsManagement::Entitlement.for(group).permits_writes? # => false
    SecretsManagement::Entitlement.for(group).permits_direct_read? # => false
    SecretsManagement::Entitlement.for(group).permits_read? # => true
  6. Query GraphQL: group(fullPath:) { secretsManagerEntitlement { state betaWindowEligible betaProgramEnded } } and check betaWindowEligible: true.

  7. Counter check: in the Rails console, run Gitlab::CurrentSettings.update!(secrets_manager_instance_beta_enrolled: false) and confirm SecretsManagement::Entitlement.for(group).beta_window_eligible returns false; restore it with Gitlab::CurrentSettings.update!(secrets_manager_instance_beta_enrolled: true).

  8. Counter check: unenroll and re-enroll the instance in Admin area > Settings > General while secrets_manager_paid_experience is enabled, and confirm both the marker and beta_window_eligible are false.

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