Provision inherited foundational flows without user elevation
What does this MR do and why?
Projects that inherit foundational flows are synchronized during project creation and after import. That synchronization currently runs as the initiating user, so a project created by a Developer is rejected by Maintainer-level consumer and trigger checks even though the root group has already enabled the flow.
This MR implements a narrowly scoped inherited authorization context through the existing consumer and trigger services. It materializes only the configuration already persisted by the root group, without elevating the Developer or changing manual catalog permissions. A failure for one flow is still reported without failing project creation.
Scope and authorization contract
This mode is used only when GitLab creates a project or synchronizes one after an import. SyncFoundationalFlowsService constructs the authorization context, and the downstream consumer and trigger services reject any other value.
At each write boundary, the context revalidates:
- Project eligibility - group membership, foundational-flow settings, licensing, and catalog provisioning state
- Item eligibility - foundational status, root-group availability, organization, and the inherited allowlist
- Parent authority - the root-group consumer and its provisioned service account
- Trigger scope - the child consumer and computed trigger parameters
The write boundaries reload the relevant records so each write is authorized against the current hierarchy state, without introducing shared row locks or new persisted state. This will keep the path fail-closed while avoiding cross-project lock contention and data-model changes. Inherited mode isn't used to create or repair missing root-group state, and manual GraphQL, bulk catalog provisioning, group cascade, and trigger paths will continue to use their existing user authorization.
Change details
The existing consumer and trigger services still handle the writes and model validation as before. Synchronization now returns consistent results for each item, records automated changes as system actions while retaining the initiating user ID for context, and can be retried without creating duplicates. The policy and inherited path use the same catalog-provisioning check, and the Sidekiq arguments haven't changed, so jobs already in the queue can still run after deployment.
Validation evidence
I ran focused RSpec suites throughout the work, including a broader service suite that passed all 319 examples. I also reran the normal and inherited provisioning cases with Duo disabled but unlocked after the final test change, to make sure both paths still behave the same way.
Coverage includes:
- Successful provisioning - Developer project creation and import, normal Maintainer parity, and Duo features disabled but unlocked
- Rejection boundaries - licensing, invalid or stale allowlists, custom items, organization and root-group mismatches, missing parent state, project transfer, and generic bypass values
- Reliability and observability - partial failures, fresh-instance retries, trigger repair, deleted initiators, membership audits, bounded query growth, and absence of
FOR UPDATE
RuboCop completed with no complaints, and git diff --check passes.
How to set up and validate locally
- Run the focused specs:
mise exec -- bundle exec rspec \
ee/spec/models/ee/project_spec.rb \
ee/spec/services/ai/catalog/flows/inherited_project_authorization_spec.rb \
ee/spec/services/ai/catalog/flows/sync_foundational_flows_service_spec.rb \
ee/spec/services/ai/catalog/item_consumers/create_service_spec.rb \
ee/spec/services/ai/flow_triggers/create_service_spec.rb \
ee/spec/services/projects/create_service_spec.rb \
ee/spec/workers/ai/catalog/flows/sync_project_foundational_flows_worker_spec.rb- Run RuboCop against the changed Ruby files.
Screenshots or screen recordings
Not applicable. This MR has no UI changes.
Risks and follow-ups
- I didn't add a new feature flag because this restores the intended behavior already selected by the foundational-flow group setting, licensing checks, project setting lock, and inherited allowlist. Those controls continue to gate every write.
- Historical remediation is separate from this fix. Projects affected before this change still need the documented group setting re-toggle workaround because this MR does not add a backfill.
- The
create_ai_catalog_item_consumerevent still fires, but its optional triggers property is absent for inherited provisioning because trigger creation happens after the fact. I’d prefer to keep accurate restoration as a follow-up, since that will be a larger change... emitting the expected value before persistence could misreport a failed trigger, whereas deferring the event requires explicit success and retry semantics. - This introduces no migration, schema change, worker argument change, or shared row lock.
- This changes an authorization boundary, so I am requesting Application Security review.
References
Closes #606415
This implementation is based on https://gitlab.com/gitlab-org/gitlab/-/work_items/606415#note_3654315883
MR acceptance checklist
Evaluate this MR against the MR acceptance checklist.