Spike: central DAP identity verification gate

What this MR does

Adds a feature-flagged DAP Identity Verification gate inside Ai::UserAuthorizable#allowed_to_use. When :dap_require_identity_verification is enabled and a trial / free user on .com tries to use a Duo Agent Platform feature without identity verification, the call is denied with denial_reason: :identity_verification_required so a follow-up HTTP-layer MR can surface the IV redirect.

Implements #595952. Trial DAP credits have been actively abused at scale (~$1.3M model cost, March 23 – April 9, 2026 — sm-dap-trial-abuse-analysis.md). Self-managed is out of scope; the SM mitigation stack is the CDot kill switch + Marketo redirect + Omamori chain.

How it works

# ee/app/models/concerns/ai/user_authorizable.rb
def allowed_to_use(ai_feature, ...)
  # ... AmazonQ / namespace-access / unit_primitive resolution ...
  # ... add-on / Duo Core / credits checks (all short-circuit positively) ...

  # NEW: DAP IV gate runs after the paid-access chain
  dap_iv_response = check_dap_identity_verification(ai_feature, root_namespace)
  return dap_iv_response if dap_iv_response

  # ... free / tier-based access fallback ...
end

def check_dap_identity_verification(ai_feature, root_namespace)
  return unless THROUGH_NAMESPACE_ACCESS_FEATURE_MAP[ai_feature] == DAP_ACCESS_GROUP
  return unless Feature.enabled?(:dap_require_identity_verification, self)
  return if identity_verified?

  ns = governing_namespace(root_namespace)
  return unless ns
  return if ns.actual_plan&.paid_excluding_trials?

  denied_response(denial_reason: :identity_verification_required)
end

Users with a Duo add-on, Duo Core, or GitLab Credits short-circuit positively in the paid-access chain before the gate runs. Users on a paid plan tier without explicit add-ons are exempted by paid_excluding_trials?. The gate fires only when a trial / free user reaches the free-access fallback for a DAP feature.

Files

File Change
ee/app/models/concerns/ai/user_authorizable.rb Add DAP_ACCESS_GROUP constant, add :denial_reason to Response, add check_dap_identity_verification and wire it into allowed_to_use after the paid-access chain
config/feature_flags/gitlab_com_derisk/dap_require_identity_verification.yml New gitlab_com_derisk flag, default off
ee/spec/models/concerns/ai/user_authorizable_spec.rb New context 'DAP identity verification gate' inside the existing describe '#allowed_to_use', plus a file-level FF stub
ee/spec/features/gitlab_subscriptions/trials/creation_with_one_existing_namespace_flow_spec.rb Comment-only update (no stub needed now that the FF defaults to off)

Decision matrix (covered by specs)

ai_feature namespace plan identity_verified? FF expected
:duo_agent_platform trial no enabled denied, :identity_verification_required
:duo_agent_platform trial yes enabled proceeds
:duo_agent_platform ultimate (paid) no enabled proceeds (paid-plan exemption)
:duo_agent_platform trial no disabled proceeds (FF off)
:duo_chat trial no enabled proceeds (not a DAP feature)
:duo_agent_platform nil no enabled proceeds (fail-open on missing namespace)

Users with a paid Duo add-on / Duo Core / Credits purchase short-circuit positively in the chain before the gate evaluates, so the matrix above only describes users who would otherwise reach the free-access fallback.

Verification bar

The gate reuses the existing identity_verified? from IdentityVerifiable. That means the bar is the active-user IV flow plus Arkose risk-banding, not the stricter phone-or-CC bar from the original spike. For new low-risk users this allows email-only verification. If AppSec / @mcoons want phone/CC even for fresh low-risk accounts, that's a follow-up — flagged in the MR thread.

Rollout

Out of scope (tracked separately)

  • Self-managed trials — mitigated by Marketo redirect (!232188 (merged) + backports), CDot kill switch (#596877), Omamori per-instance block (customers-gitlab-com!15501). A CDot-side trial IV tracking issue will be filed alongside this MR.
  • HTTP-layer redirect wiring — the Response.denial_reason field is the structured signal a follow-up MR can read to surface /-/identity_verification redirects per call site. Doing this centrally would require redirect propagation through declarative policy code, which is the limitation @imand3r flagged in review.
  • Multi-group bypass (#595149 / #585342), service-account creation rate limits (#581887), JWT-cache TTL.

Refs

Test plan

  • bin/rspec ee/spec/models/concerns/ai/user_authorizable_spec.rb (the new 'DAP identity verification gate' context covers the matrix)
  • bin/rspec ee/spec/requests/api/graphql/mutations/ai/duo_workflows/create_spec.rb (regression — paid Duo Enterprise add-on still grants access without IV)
  • Manual: trial group + unverified user calls :duo_workflow with FF on → response.denial_reason == :identity_verification_required
  • Manual: same user completes phone verification → call succeeds
  • Manual: paid Ultimate user (no IV) → call succeeds (paid-plan exemption)
  • Manual: trial group + unverified user, FF off → call succeeds
Edited by Mark Mishaev

Merge request reports

Loading