Attribute audit events to service account with human author
What does this MR do and why?
When a service account acts via OAuth on behalf of a human (composite identity in the :authentication context), audit event records were attributed to the human (current_user after auth_finders resolution) instead of the service account, losing the human linkage entirely.
This implements the agreed hybrid of option 3 from #599098:
- Resolve the composite identity actor so
author_idpoints at the service account (the true authenticating actor) — satisfying "who did this?". - Wrap the SA in a new
Gitlab::Audit::CompositeIdentityAuthorsoauthor_namereads"<SA> on behalf of @human"for human-readable display. The wrapper delegates#idto the SA, keepingauthor_idcorrect and queryable. - Record the authorizing human as flat
human_-prefixed keys (human_author_id,human_author_name,human_author_username) inadditional_details, which is serialized into thedetailsJSONB and forwarded to streaming destinations. This gives consumers a queryable linkage without parsing the name string.
Applied to Gitlab::Audit::Auditor (active path, ~261 callers).
Deprecated AuditEventService is left untouched because it is no longer invoked in production code. There's already existing follow up issue to tackle the dead code removal outside of the scope of this MR.
author_name
In order to reflect correct string containing "on behalf of" the author_name attribute is now persisted on the AuditEvent.
Before this change the author_name would be resolved dynamically, so if an author changed their name after AuditEvent was recorded then the new author name would be shown. Whereas in this MR the author_name stays as a snapshot in time of tracking the event. Which seems how it meant to be for audit events. Even if an author changes name, they can be traced via author_id attribute.
Feature flag
The attribution change is gated behind composite_identity_audit_event_attribution (gitlab_com_derisk, disabled by default), checked before any of the new resolution code runs so disabling the flag stops the new path entirely. The flag actor is the audit scope's root namespace (:instance for user-scoped and instance-scoped events), so enabling per group covers the whole hierarchy. Rollout issue: #607074. No changelog entry while the flag defaults off; it moves to the flag removal MR.
Why not option 2 (new column)?
A first-class scoped_user_id column on AuditEvent was rejected: the AuditEvent tables are too large to justify the migration cost, making it an SRE/infra concern. The flat JSONB keys give the queryability benefit without a schema migration.
Screenshots or screen recordings
"on behalfo of @username" added
How to set up and validate locally
# rails console
RequestStore.begin! # identity links live in the request store; without this the link is a no-op
Feature.enable(:composite_identity_audit_event_attribution)
sa = User.find_by(username: '<service-account>') # must have composite_identity_enforced: true
me = User.find_by(username: '<your-username>')
group = me.owned_groups.first
Gitlab::Auth::Identity.link_from_scoped_user(sa, me, context: :authentication)
::Gitlab::Audit::Auditor.audit(
name: 'lock_duo_features_enabled_updated', # any YAML-defined, Group-scoped event type
author: me, # what auth_finders resolves current_user to in a real composite request
scope: group,
target: group,
message: 'Composite identity attribution check'
)
event = AuditEvents::GroupAuditEvent.order(id: :desc).first
# instances still writing the legacy table: AuditEvent.order(id: :desc).first
event.author_id # => sa.id
event.author_name # => "<SA name> on behalf of @<your-username>"
event.details.slice(:human_author_id, :human_author_name, :human_author_username)Then open the group's Secure > Audit events page: the Author column shows " on behalf of @".
Negative path: repeat with context: :permission_check on the link (a human acting while an SA is merely involved). The event stays attributed to the human and no human_author_* keys appear.
MR acceptance checklist
- Specs added for the new author and both audit paths (
:authenticationcontext). - Follow-up: confirm the
human_-prefixed key shape with the streaming/compliance side.
Refs: #599098
