Add telemetry for Secrets Manager entitlement-driven denials

What does this MR do and why?

Emits structured telemetry whenever Secrets Manager access is denied due to entitlement state (D4 of the GSM paid-experience rollout). This gives product visibility into which denial reasons fire in production and gives support a grep-able trail when customers report access issues, before enforcement is rolled out more broadly.

  • New SecretsManagement::Entitlement::DenialTelemetry emitter: fires the secrets_manager_access_denied internal event (Snowplow + Service Ping) and a structured Gitlab::AppJsonLogger line with namespace_id, denial_reason, entitlement_state, surface, and mode. No PII: namespace_id is the only namespace identifier.
  • Event properties: label = denial reason (from Entitlement#write_action_denial_reason), property = surface, plus custom state and mode. mode is hardcoded to enforce — observe mode was deliberately skipped, the property keeps the schema forward-compatible.
  • Wired into the three action entry points where a real denied action is observable, each with the acting-user/surface context already in hand:
    • EnforcesWriteEntitlement GraphQL concern (graphql_mutation), emitted only after the entitlement-visibility ability check so unauthorized probes record nothing
    • CI runner payload gate in build_runner_presenter.rb (ci_runner_payload, no acting user; grace never appears here since it permits direct reads)
    • EntitlementGate service concern (service_gate)
  • Deliberately not wired into the group/project policy condition blocks. DeclarativePolicy conditions are pure predicates — memoized, re-run for rule scoring/.debug, and evaluated on speculative can? calls and GraphQL type-level authorization. Emitting from them would add synchronous Snowplow/Redis I/O to the authorization hot path and pollute the metric with passive read/visibility checks rather than real denied actions. The conditions remain side-effect-free predicates.
  • One denied request can trip several entry points, so the emitter dedupes per request via SafeRequestStore keyed by [namespace_id, reason] — only the first (most specific) layer emits.
  • Service Ping metrics: counts.count_total_secrets_manager_access_denied and redis_hll_counters.count_distinct_namespace_id_from_secrets_manager_access_denied.
  • Everything is gated by the new secrets_manager_denial_telemetry feature flag (default off; denials themselves only occur when secrets_manager_paid_experience is enabled).

No changelog: all changes are behind a default-off feature flag.

References

How to set up and validate locally

  1. Enable both flags for a group: Feature.enable(:secrets_manager_paid_experience, group) and Feature.enable(:secrets_manager_denial_telemetry, group).
  2. Stub/force SecretsManagement::Entitlement.for to return a :blocked state (e.g. blocked_reason: :trial_expired) for that group's root namespace.
  3. Attempt a gated write (e.g. the projectSecretCreate GraphQL mutation) and observe:
    • the secrets_manager_access_denied internal event (see log/snowplow_micro.log or the internal events monitor bin/rails runner scripts/internal_events/monitor.rb secrets_manager_access_denied)
    • the structured line in log/application_json.log with denial_reason, surface, mode.

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