Enforce composite identity for API-started workflows
What does this MR do and why?
API-triggered workflow starts currently fall back to the organization-level Duo workflow service account when neither a flow mapping nor the caller's composite identity resolves an account. This allows unattended execution to proceed without enforcing the flow's intended composite identity.
This change adds a default-disabled rollout flag that:
- removes the organization-level service account fallback for API-triggered workflow starts;
- requires the selected service account to enforce composite identity;
- preserves explicitly mapped flow accounts over caller composite identities;
- applies the same validation to AI Catalog consumer execution; and
- leaves workflow creation without
start_workflowunchanged.
The flag remains disabled by default while CEF depends on the legacy fallback.
How to set up and validate locally
Retested on 2026-09-09 with local GDK / GitLab 19.4.0 at commit
393a6759168e6274efce1cc0a5c317ff823180d7.
Setup
- Use a project with Duo and remote flows enabled. The test used
gitlab-duo/test(project ID1000000). - Enable
enforce_composite_identity_for_api_started_workflowsfor the project's root namespace. - Configure an AI Catalog flow consumer with a mapped service account. The
test used the Developer consumer (ID
13) and service account ID74. - Use a caller that can read the project but cannot execute the catalog item.
The test caller had
execute_ai_catalog_item=false. - Send the following request, replacing
<caller-token>with that caller's token:
curl --request POST \
--header "PRIVATE-TOKEN: <caller-token>" \
--header "Content-Type: application/json" \
--data '{"project_id":1000000,"ai_catalog_item_consumer_id":13,"start_workflow":true,"goal":"Execute catalog flow"}' \
"https://gdk.test:3443/api/v4/ai/duo_workflows/workflows"Manual results
Repeat the same request with the mapped account's
composite_identity_enforced value set to each value:
| Mapped account setting | Expected | Actual | Result |
|---|---|---|---|
false |
Catalog permission error without starting anything | HTTP 400: 400 Bad request - ["You have insufficient permissions"] (correlation ID 01M235S5WKHYY6XVZ841XTKV0D) |
PASS |
true |
The same response, without revealing the setting | HTTP 400 with the identical message (correlation ID 01M235WN7KQRD95Y0YD1AA8AJJ) |
PASS |
A Rails-console read before and after both requests showed no side effects:
workflows=63, workflow/workload links=23, pipelines=24
latest workflow ID=83, latest workload ID=35, latest pipeline ID=604All counts and latest IDs were unchanged. This confirms the authorization ordering leak is fixed at the tested commit.
Focused automated checks
bundle exec rspec ee/spec/requests/api/ai/duo_workflows/workflows_spec.rb \
--example 'when the user cannot execute the catalog item'
# 2 examples, 0 failures (seed 44157)
bundle exec rspec ee/spec/requests/api/ai/duo_workflows/agent_workflows_spec.rb \
--example 'when the user cannot execute the catalog item'
# 2 examples, 0 failures (seed 22036)
bundle exec rspec ee/spec/requests/api/ai/duo_workflows/workflows_spec.rb \
--example 'executes the AI catalog flow with correct service account from project consumer' \
--example 'when the consumer service account does not enforce composite identity' \
--example 'allows creating the flow without starting it' \
--example 'when composite identity does not resolve a service account' \
--example 'when the composite identity actor is a service account' \
--example 'when consumer project is nil and enforcement is enabled'
# 11 examples, 0 failures (seed 27034)The focused checks cover both affected API entry points, mapped-account success, invalid-account rejection, create-only behavior, legacy fallback, composite-identity selection, and the consumer-without-project path.
Screenshots and screencasts
References
Closes #601901