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)
endUsers 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
- Feature flag:
dap_require_identity_verification(gitlab_com_derisk, default off) - Rollout work item: #599755 (closed)
- Per the FF lifecycle, convert to an application setting once the flag is cleaned up.
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_reasonfield is the structured signal a follow-up MR can read to surface/-/identity_verificationredirects 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
- Issue: #595952
- Original proposal: #592580
- Parent epic: #21243
- SM track epic: #21713
- FF rollout: #599755 (closed)
- Background analysis:
sm-dap-trial-abuse-analysis.md(in repo)
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_workflowwith 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