Add entitlement-aware policy for Secrets Manager access

What does this MR do?

Makes GitLab Secrets Manager (GSM) abilities entitlement-aware at the policy layer (D1). The EE ProjectPolicy and GroupPolicy now consult SecretsManagement::Entitlement and prevent GSM abilities based on the namespace's entitlement state:

  • :ineligible / :blocked — deny access and writes
  • :trial_eligible — deny writes, allow access (so the "Start a trial" CTA page stays reachable)
  • :trial / :paid / :offline_paid — allow

Enforcement is gated on the existing secrets_manager_paid_experience feature flag (disabled by default), which also gates the resolver's online path.

This is the policy layer only. Entry-point guards and user-facing errors (D2), messaging (D3), and telemetry (D4) are tracked separately.

Entitlement → ability logic

Enforcement uses two umbrella tiers layered on top of the existing role checks:

  • Access (permits_access?) — may reach the GSM surface at all: render the secrets page and list/read secret data. The secrets page and GraphQL read resolvers authorize on the read abilities (read_project_secrets / read_secret), so denying access also denies reads.
  • Write (permits_writes?) — may mutate the manager or secrets: create/update/delete secrets, provision/deprovision, manage permissions.

Mapping per entitlement state:

State Access (incl. reads) Writes
:ineligible denied denied
:blocked denied denied
:trial_eligible allowed denied
:trial allowed allowed
:paid allowed allowed
:offline_paid allowed allowed

So :ineligible / :blocked deny reads and writes (no GSM surface — the page 404s). :trial_eligible is the read-only middle tier: the secrets page stays reachable so the "Start a trial" CTA is visible, but all writes are blocked until the namespace reaches :trial / :paid / :offline_paid.

All of this only takes effect when secrets_manager_paid_experience is enabled and GSM is otherwise available; with the flag off, behavior is unchanged.

Where this policy is enforced

These abilities are checked at the following entry points. This MR changes the policy decision behind them; the call sites are unchanged.

Controllers (page access)

  • read_project_secrets — ee/app/controllers/projects/secrets_controller.rb (authorize_view_secrets!)
  • read_secret — ee/app/controllers/groups/secrets_controller.rb (authorize_view_secrets!)

GraphQL resolvers (read / list secret data)

  • read_project_secrets — project_secrets_resolver, project_secret_resolver, project_secrets_count_resolver, project_secrets/list_needing_rotation_resolver
  • read_project_secrets_manager — project_secrets_permissions_resolver, permissions/secret_permissions_resolver
  • read_project_secrets_manager_status — project_secrets_manager_resolver
  • read_secret — group_secrets_resolver, group_secret_resolver, group_secrets_manager_resolver, group_secrets_count_resolver, group_secrets/list_needing_rotation_resolver
  • read_secrets_permission — group_secrets_permissions_resolver

GraphQL mutations (writes)

  • admin_project_secrets_manager — project_secrets_managers/{initialize,deprovision}, project_secrets_permissions/{update,delete}, permissions/{update,delete}
  • create_project_secrets / update_project_secrets / delete_project_secrets — project_secrets/{create,update,delete}
  • provision_secrets_manager / deprovision_secrets_manager — group_secrets_managers/{initialize,deprovision}
  • create_secret / update_secret / delete_secret — group_secrets/{create,update,delete}
  • update_secrets_permission / delete_secrets_permission — group_secrets_permissions/{update,delete}

Test impact & e2e

TL;DR: Flags are on by default in tests, so the entitlement-aware deny rules are already active across the GSM suite. Added a permissive :paid default at the enrollment chokepoint to keep SM-gated specs reachable. All GSM suites green. e2e is safe at the flag's default (off); flag-on e2e needs a real entitlement (follow-up).

Why the test-setup change was needed

With secrets_manager_paid_experience on by default in tests, the new deny rules fire everywhere. Without an entitlement, the resolver falls through to blocked/ineligible → all SM abilities denied. Verified: an untouched request spec went 10/14 → failing.

Fix: permissive default at enroll_instance_in_secrets_manager

Every SM-available spec funnels through this helper (directly or via provision_*):

def enroll_instance_in_secrets_manager
  stub_application_setting(secrets_manager_instance_enrolled: true)

  # Enrolled instance is assumed entitled; specs needing a specific
  # state re-stub `Entitlement.for` afterward and override this.
  allow(::SecretsManagement::Entitlement)
    .to receive(:for)
    .and_return(::SecretsManagement::Entitlement.new(state: :paid))
end
Coverage (all green)
Suite Result
GSM request + mutation 327, 0 failures
GSM lib + service 729, 0 failures
Project + Group GSM policy 2262, 0 failures

Policy specs are unaffected: they stub Availability.enabled_for_*? and Entitlement.for directly and never call the helper, so the :ineligible/:blocked deny assertions stay authoritative.

FF removal

When the flag is default-enabled and removed, the flag-off contexts get deleted and the (already covered) flag-on paths become baseline. The :paid stub stays → no re-break.

e2e (:orchestrated)
  • Flag default (off): no impact — deny rules inert, specs don't touch the flag.
  • Flag-on runs (nightly FF-enabled pipelines / post-default-enable): orchestrated GSM specs will break. QA only enrolls the instance — no entitlement (no AddOnPurchase, no CDot) → resolver returns blocked. e2e can't be stubbed; needs a real entitlement (seed self-managed secrets-manager AddOnPurchase:offline_paid, or run as .com with CDot). Rollout follow-up; not required here since e2e runs flag-off today.
  • read_secrets_manager is also checked by the granular-token authorization on group_secrets_managers/start_trial (the "Start a trial" mutation). It stays allowed in :trial_eligible, so the trial on-ramp is unaffected.

References

Author checklist

  • Behind secrets_manager_paid_experience (default off); no changelog required while the flag is disabled.
Edited by Dmytro Biryukov

Merge request reports

Loading
Loading