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_date is 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. Within end_date + GRACE_DAYS (inclusive) it returns Entitlement(state: :blocked, blocked_reason: :grace) (read-only access); once the window has elapsed it returns Entitlement(state: :blocked, blocked_reason: :subscription_grace_period_expired) (full lockout).
  • No end date on record (the namespace has no gitlab_subscription row, or it has no end date): if /trials reports state: :expired, this is an expired trial and the resolver returns Entitlement(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_expired producible for the first time — it previously existed in Entitlement::BLOCKED_REASONS with 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: accept no_billable_source_error in BLOCKED_REASONS
  • ee/lib/secrets_management/entitlement.rb: derive GRACE_DAYS from GitlabSubscription::SUBSCRIPTION_GRACE_PERIOD (previously dead code hardcoded to 30)
  • ee/lib/secrets_management/entitlement/resolver.rb: map no_billable_source_error locally from the subscription end date (grace window), or, with no end date on record, to :trial_expired for expired trials / fail-closed otherwise
  • ee/spec/lib/gitlab/subscription_portal/secrets_manager_consumer_resolve_response_spec.rb: new dedicated spec for the response object, covering the new no_billable_source_error reason
  • Specs for the resolver and entitlement changes

References

How to set up and validate locally

  1. Enable the secrets_manager_paid_experience feature flag for a top-level group in the Rails console.
  2. Stub or point the CustomersDot base URL at an instance that returns 402 with the no_billable_source_error block reason for /api/v1/consumers/resolve.
  3. With the /trials stub reporting state: :expired and the group having no gitlab_subscription (or no end date), check that SecretsManagement::Entitlement.for(group) returns blocked/trial_expired.
  4. Set the group's gitlab_subscription.end_date to 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.
  5. 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
Edited by Dmytro Biryukov

Merge request reports

Loading
Loading