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_resolverread_project_secrets_manager— project_secrets_permissions_resolver, permissions/secret_permissions_resolverread_project_secrets_manager_status— project_secrets_manager_resolverread_secret— group_secrets_resolver, group_secret_resolver, group_secrets_manager_resolver, group_secrets_count_resolver, group_secrets/list_needing_rotation_resolverread_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))
endCoverage (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 returnsblocked. e2e can't be stubbed; needs a real entitlement (seed self-managed secrets-managerAddOnPurchase→:offline_paid, or run as.comwith CDot). Rollout follow-up; not required here since e2e runs flag-off today.
Related
read_secrets_manageris 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.