Record human author on AI audit events under composite identity
What does this MR do and why?
AI audit events (ai_audit_events) ingested from Duo Workflow Service lose the composite identity. The ingest endpoint authenticates with the flow's service-account OAuth token, and auth_finders resolves current_user for such tokens to the scoped human user (Gitlab::Auth::AuthFinders#resolve_composite_identity_user). So the events were attributed to the human, with no trace of the service account that actually authenticated and nothing linking the two - the same defect !240418 (merged) fixes for standard audit events.
This applies the same convention to the AI ingest path:
- Resolve the author with
Gitlab::Auth::Identity.resolve_composite_identity_actor, exactly likeGitlab::Audit::Auditordoes, soauthor_idpoints at the service account, the true authenticating actor. - Record the triggering human as flat
human_author_id/human_author_name/human_author_usernamekeys in the eventdetailsJSON, the same keys the Auditor writes, so streaming consumers see one consistent shape. The keys are merged intodetailslast, so a relayed payload cannot spoof them. - Outside composite identity (plain service account, human's own token,
permission_check-context links) behavior is unchanged:author_idstays oncurrent_userand no human keys are written.
No code dependency on !240418 (merged). The human_author_* key names, however, deliberately mirror the details convention !240418 (merged) introduces (still in review) - if that MR renames the keys, this path must follow. The CompositeIdentityAuthor wrapper is not needed here: it feeds the persisted author_name column, which ai_audit_events doesn't have (author_name derives from author_id at serialization time).
References
- Closes #604323
- #599098 - service account attribution on standard audit events (same convention)
- !240418 (merged) - introduces the
human_author_*details convention this MR reuses
Screenshots or screen recordings
| Before | After |
|---|---|
|
Event in the list is attributed to a human Author name - attributed to a human, no human author details shown |
Event in the list is attributed to a service account Author name - attributed to the service account, additional human author details are shown |
Note, no frontend code changes are in this MR, what we see is the consequence of placing different values to existing attributes.
There might be a potential minor UX issue with the change because the user is no longer able to click onto human name to see their profile, and requires manual search, I suggest to iterate outside of the scope of the current MR - Relevant thread
How to set up and validate locally
The steps exercise the real request path: a composite service-account OAuth token sent to the ingest endpoint over HTTP, so the auth resolution in auth_finders runs for real instead of being simulated in a console. Verified on GDK, each step below was run as written.
-
In a rails console, prepare a composite service account, a workflow and a token. This mirrors what the product does when starting a flow:
admin = User.find_by!(username: 'root') org = Organizations::Organization.default_organization Ai::DuoWorkflows::OnboardingService.new(current_user: admin, organization: org).execute sa = User.find(Ai::Setting.instance.duo_workflow_service_account_user_id) project = Project.find_by_full_path('flightjs/Flight') # any project works project.project_setting.update!(duo_features_enabled: true, ai_audit_events_storage_enabled: true) project.add_developer(sa) workflow = Ai::DuoWorkflows::Workflow.create!( user: admin, project: project, goal: 'verify attribution', workflow_definition: 'software_development', environment: :web, service_account: sa) token = Ai::DuoWorkflows::CreateCompositeOauthAccessTokenService.new( current_user: admin, organization: org, service_account: sa, container: project ).execute.payload[:oauth_access_token] puts token.plaintext_token, workflow.idWithout
duo_features_enabledand the SA membership the endpoint returns 403 (read_duo_workflowrequires the ability for both the human and the SA). Withoutai_audit_events_storage_enabledthe worker silently drops the events. -
Optional sanity check, and the premise of this MR in one request: the API resolves this token to the human, not to the service account that owns it:
curl -s -H "Authorization: Bearer <TOKEN>" http://gdk.test:3000/api/v4/user # => {"id":1,"username":"root",...} -
Send an event to the ingest endpoint:
curl -X POST -H "Authorization: Bearer <TOKEN>" -H "Content-Type: application/json" \ -d '{"events":[{"id":"'$(uuidgen | tr 'A-Z' 'a-z')'","type":"ai_llm_input_sent","source":"duo-workflow-service","time":"'$(date -u +%FT%TZ)'","data":{"check":"composite"}}]}' \ http://gdk.test:3000/api/v4/ai/duo_workflows/workflows/<WORKFLOW_ID>/audit_events -
Check the stored event (sidekiq picks the job up with some delay on GDK, retry if empty):
event = AuditEvents::AiAuditEvent.for_workflow(workflow.id).recent_first.first event.author_id # => sa.id (the service account) event.details.with_indifferent_access.slice(:human_author_id, :human_author_name, :human_author_username) # => {"human_author_id"=>1, "human_author_name"=>"Administrator", "human_author_username"=>"root"} -
Negative path, no composite identity: mint a plain token owned by the human (the legacy Duo Workflow token path) and repeat the ingest request with it:
token = Ai::DuoWorkflows::CreateOauthAccessTokenService.new( current_user: admin, organization: org ).execute.payload[:oauth_access_token]The stored event keeps
author_idon the human and its details contain only the payload keys plusip_address, nohuman_author_*keys. -
Optional, to see the events in the UI: enable the page flag and sync the session artifact (console-created workflows never populate
duo_workflow_session_artifacts):Feature.enable(:agent_artifacts_page) Ai::DuoWorkflows::SessionArtifact.sync_from_workflow!(workflow)Then open
<project>/-/security/agent_artifactsdirectly (the page has no menu entry yet), click the session and open an event: the author is the service account and the Details section lists the human author fields.
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.



