Compute Secrets Manager grace/expired from subscription end date
What does this MR do and why?
CustomersDot cannot compute the grace/expired distinction for the Secrets Manager entitlement, because it cannot unambiguously resolve which of potentially multiple orders is currently provisioned to a group. Following discussion on the CDot side, that computation moves to GitLab Rails. CDot's /api/v1/consumers/resolve endpoint now returns HTTP 402 with {"block_reason": "no_billable_source_error"} when there is no billable source (shipped in the CDot MR linked below, merged Aug 7).
This MR handles that response on the Rails side. When the resolve call returns no_billable_source_error, the entitlement resolver decides based purely on the subscription evidence we hold locally:
- A
gitlab_subscription.end_dateis on record (past or future — a future end date means a mid-term cancellation, where the term is paid through): the resolver computes the grace window from it. Withinend_date + GRACE_DAYS(inclusive) it returnsEntitlement(state: :blocked, blocked_reason: :grace)(read-only access); once the window has elapsed it returnsEntitlement(state: :blocked, blocked_reason: :subscription_grace_period_expired)(full lockout). - No end date on record (the namespace has no
gitlab_subscriptionrow, or it has no end date): if/trialsreportsstate: :expired, this is an expired trial and the resolver returnsEntitlement(state: :blocked, blocked_reason: :trial_expired)— a hard cutoff, per the intended trial semantics. Anything else fails closed to:subscription_grace_period_expired. This also makes:trial_expiredproducible for the first time — it previously existed inEntitlement::BLOCKED_REASONSwith no producer.
Note that on self-managed the grace window effectively never opens — the term end lives in License.current, and nothing populates gitlab_subscriptions there in practice — so a lapsed self-managed customer fails closed on day 0, where the same customer on gitlab.com gets 14 read-only days.
GRACE_DAYS was previously dead code with value 30; this MR revives it into real logic and derives it from the confirmed gitlab.com plan-downgrade grace period (GRACE_DAYS = ::GitlabSubscription::SUBSCRIPTION_GRACE_PERIOD.in_days.to_i, currently 14 days), so the two windows cannot drift apart. A 30-day window would be partially unreachable because the plan-tier availability gate hard-blocks once the namespace's plan actually downgrades. Note that no_billable_source_error is only accepted by the CDot client response allowlist — it is never surfaced to consumers. The resolver always maps it to :trial_expired, :grace, or :subscription_grace_period_expired, all of which were already fully plumbed downstream (policies, GraphQL enums, CI presenter).
Changes:
ee/lib/gitlab/subscription_portal/secrets_manager_consumer_resolve_response.rb: acceptno_billable_source_errorinBLOCKED_REASONSee/lib/secrets_management/entitlement.rb: deriveGRACE_DAYSfromGitlabSubscription::SUBSCRIPTION_GRACE_PERIOD(previously dead code hardcoded to 30)ee/lib/secrets_management/entitlement/resolver.rb: mapno_billable_source_errorlocally from the subscription end date (grace window), or, with no end date on record, to:trial_expiredfor expired trials / fail-closed otherwiseee/spec/lib/gitlab/subscription_portal/secrets_manager_consumer_resolve_response_spec.rb: new dedicated spec for the response object, covering the newno_billable_source_errorreason- Specs for the resolver and entitlement changes
References
- Work item: https://gitlab.com/gitlab-org/gitlab/-/work_items/607572
- CDot work item: https://gitlab.com/gitlab-org/customers-gitlab-com/-/work_items/17870
- CDot MR: https://gitlab.com/gitlab-org/customers-gitlab-com/-/merge_requests/16747
- E2E test plan (A4/B2 contract gap): https://gitlab.com/gitlab-org/gitlab/-/work_items/605626
How to set up and validate locally
- Enable the
secrets_manager_paid_experiencefeature flag for a top-level group in the Rails console. - Stub or point the CustomersDot base URL at an instance that returns 402 with the
no_billable_source_errorblock reason for/api/v1/consumers/resolve. - With the
/trialsstub reportingstate: :expiredand the group having nogitlab_subscription(or no end date), check thatSecretsManagement::Entitlement.for(group)returns blocked/trial_expired. - Set the group's
gitlab_subscription.end_dateto a date within the last 14 days and check that it returns blocked/grace. The same holds for a future end date, e.g. a subscription cancelled mid-term. - Set the end date to older than 14 days and check that it returns blocked/subscription_grace_period_expired.
Alternatively, run the specs:
bin/rspec ee/spec/lib/secrets_management/entitlement/resolver_spec.rb ee/spec/lib/secrets_management/entitlement_spec.rb ee/spec/lib/gitlab/subscription_portal/clients/rest_spec.rb ee/spec/lib/gitlab/subscription_portal/secrets_manager_consumer_resolve_response_spec.rb