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::DenialTelemetryemitter: fires thesecrets_manager_access_deniedinternal event (Snowplow + Service Ping) and a structuredGitlab::AppJsonLoggerline withnamespace_id,denial_reason,entitlement_state,surface, andmode. No PII:namespace_idis the only namespace identifier. - Event properties:
label= denial reason (fromEntitlement#write_action_denial_reason),property= surface, plus customstateandmode.modeis hardcoded toenforce— 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:
EnforcesWriteEntitlementGraphQL 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;gracenever appears here since it permits direct reads) EntitlementGateservice concern (service_gate)
- Deliberately not wired into the group/project policy
conditionblocks. DeclarativePolicy conditions are pure predicates — memoized, re-run for rule scoring/.debug, and evaluated on speculativecan?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
SafeRequestStorekeyed by[namespace_id, reason]— only the first (most specific) layer emits. - Service Ping metrics:
counts.count_total_secrets_manager_access_deniedandredis_hll_counters.count_distinct_namespace_id_from_secrets_manager_access_denied. - Everything is gated by the new
secrets_manager_denial_telemetryfeature flag (default off; denials themselves only occur whensecrets_manager_paid_experienceis enabled).
No changelog: all changes are behind a default-off feature flag.
References
- https://gitlab.com/gitlab-org/gitlab/-/work_items/602364 (D4)
- Parent epic: https://gitlab.com/groups/gitlab-org/-/work_items/21755
How to set up and validate locally
- Enable both flags for a group:
Feature.enable(:secrets_manager_paid_experience, group)andFeature.enable(:secrets_manager_denial_telemetry, group). - Stub/force
SecretsManagement::Entitlement.forto return a:blockedstate (e.g.blocked_reason: :trial_expired) for that group's root namespace. - Attempt a gated write (e.g. the
projectSecretCreateGraphQL mutation) and observe:- the
secrets_manager_access_deniedinternal event (seelog/snowplow_micro.logor the internal events monitorbin/rails runner scripts/internal_events/monitor.rb secrets_manager_access_denied) - the structured line in
log/application_json.logwithdenial_reason,surface,mode.
- the
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