diff --git a/pkg/audit/workflow_auditor.go b/pkg/audit/workflow_auditor.go index 5a12ae1331..e87a3ce94c 100644 --- a/pkg/audit/workflow_auditor.go +++ b/pkg/audit/workflow_auditor.go @@ -60,17 +60,7 @@ func (w *WorkflowAuditor) LogWorkflowStarted( return } - source := w.extractSource(ctx) - subjects := w.extractSubjects(ctx) - - event := NewAuditEvent( - EventTypeWorkflowStarted, - source, - OutcomeSuccess, - subjects, - w.component, - ) - w.attachDelegation(ctx, event) + event := w.newEvent(ctx, EventTypeWorkflowStarted, OutcomeSuccess) target := map[string]string{ TargetKeyWorkflowID: workflowID, @@ -112,17 +102,7 @@ func (w *WorkflowAuditor) LogWorkflowCompleted( return } - source := w.extractSource(ctx) - subjects := w.extractSubjects(ctx) - - event := NewAuditEvent( - EventTypeWorkflowCompleted, - source, - OutcomeSuccess, - subjects, - w.component, - ) - w.attachDelegation(ctx, event) + event := w.newEvent(ctx, EventTypeWorkflowCompleted, OutcomeSuccess) target := map[string]string{ TargetKeyWorkflowID: workflowID, @@ -165,17 +145,7 @@ func (w *WorkflowAuditor) LogWorkflowFailed( return } - source := w.extractSource(ctx) - subjects := w.extractSubjects(ctx) - - event := NewAuditEvent( - EventTypeWorkflowFailed, - source, - OutcomeFailure, - subjects, - w.component, - ) - w.attachDelegation(ctx, event) + event := w.newEvent(ctx, EventTypeWorkflowFailed, OutcomeFailure) target := map[string]string{ TargetKeyWorkflowID: workflowID, @@ -205,17 +175,7 @@ func (w *WorkflowAuditor) LogWorkflowTimedOut( return } - source := w.extractSource(ctx) - subjects := w.extractSubjects(ctx) - - event := NewAuditEvent( - EventTypeWorkflowTimedOut, - source, - OutcomeFailure, - subjects, - w.component, - ) - w.attachDelegation(ctx, event) + event := w.newEvent(ctx, EventTypeWorkflowTimedOut, OutcomeFailure) target := map[string]string{ TargetKeyWorkflowID: workflowID, @@ -245,17 +205,7 @@ func (w *WorkflowAuditor) LogStepStarted( return } - source := w.extractSource(ctx) - subjects := w.extractSubjects(ctx) - - event := NewAuditEvent( - EventTypeWorkflowStepStarted, - source, - OutcomeSuccess, - subjects, - w.component, - ) - w.attachDelegation(ctx, event) + event := w.newEvent(ctx, EventTypeWorkflowStepStarted, OutcomeSuccess) target := map[string]string{ TargetKeyWorkflowID: workflowID, @@ -283,17 +233,7 @@ func (w *WorkflowAuditor) LogStepCompleted( return } - source := w.extractSource(ctx) - subjects := w.extractSubjects(ctx) - - event := NewAuditEvent( - EventTypeWorkflowStepCompleted, - source, - OutcomeSuccess, - subjects, - w.component, - ) - w.attachDelegation(ctx, event) + event := w.newEvent(ctx, EventTypeWorkflowStepCompleted, OutcomeSuccess) target := map[string]string{ TargetKeyWorkflowID: workflowID, @@ -323,17 +263,7 @@ func (w *WorkflowAuditor) LogStepFailed( return } - source := w.extractSource(ctx) - subjects := w.extractSubjects(ctx) - - event := NewAuditEvent( - EventTypeWorkflowStepFailed, - source, - OutcomeFailure, - subjects, - w.component, - ) - w.attachDelegation(ctx, event) + event := w.newEvent(ctx, EventTypeWorkflowStepFailed, OutcomeFailure) target := map[string]string{ TargetKeyWorkflowID: workflowID, @@ -361,17 +291,7 @@ func (w *WorkflowAuditor) LogStepSkipped( return } - source := w.extractSource(ctx) - subjects := w.extractSubjects(ctx) - - event := NewAuditEvent( - EventTypeWorkflowStepSkipped, - source, - OutcomeSuccess, - subjects, - w.component, - ) - w.attachDelegation(ctx, event) + event := w.newEvent(ctx, EventTypeWorkflowStepSkipped, OutcomeSuccess) target := map[string]string{ TargetKeyWorkflowID: workflowID, @@ -390,6 +310,22 @@ func (w *WorkflowAuditor) LogStepSkipped( event.LogTo(ctx, w.auditLogger, LevelAudit) } +// newEvent creates an audit event of the given type and outcome with the +// source, subjects, and RFC 8693 delegation chain extracted from the context. +// Every Log* method MUST build its event through this helper so that the +// delegation chain cannot be forgotten at any individual call site. +func (w *WorkflowAuditor) newEvent(ctx context.Context, eventType, outcome string) *AuditEvent { + event := NewAuditEvent( + eventType, + w.extractSource(ctx), + outcome, + w.extractSubjects(ctx), + w.component, + ) + w.attachDelegation(ctx, event) + return event +} + // extractSource extracts source information from context. // For workflows, source is always local since they're internal orchestration. func (*WorkflowAuditor) extractSource(_ context.Context) EventSource { diff --git a/pkg/audit/workflow_auditor_test.go b/pkg/audit/workflow_auditor_test.go index 6eddce71aa..4b8f49dd2c 100644 --- a/pkg/audit/workflow_auditor_test.go +++ b/pkg/audit/workflow_auditor_test.go @@ -617,86 +617,18 @@ func TestWorkflowAuditor_DelegationChain(t *testing.T) { }, } - // attachDelegation is hand-repeated in every Log* method, so each call site is pinned individually. - logMethods := []struct { - name string - logFunc func(a *WorkflowAuditor, ctx context.Context) - }{ - { - name: "LogWorkflowStarted", - logFunc: func(a *WorkflowAuditor, ctx context.Context) { - a.LogWorkflowStarted(ctx, "wf-1", "wf", nil, time.Second) - }, - }, - { - name: "LogWorkflowCompleted", - logFunc: func(a *WorkflowAuditor, ctx context.Context) { - a.LogWorkflowCompleted(ctx, "wf-1", "wf", time.Second, 1, nil) - }, - }, - { - name: "LogWorkflowFailed", - logFunc: func(a *WorkflowAuditor, ctx context.Context) { - a.LogWorkflowFailed(ctx, "wf-1", "wf", time.Second, 1, errors.New("failed")) - }, - }, - { - name: "LogWorkflowTimedOut", - logFunc: func(a *WorkflowAuditor, ctx context.Context) { - a.LogWorkflowTimedOut(ctx, "wf-1", "wf", time.Second, 1) - }, - }, - { - name: "LogStepStarted", - logFunc: func(a *WorkflowAuditor, ctx context.Context) { - a.LogStepStarted(ctx, "wf-1", "step-1", "tool", "some-tool") - }, - }, - { - name: "LogStepCompleted", - logFunc: func(a *WorkflowAuditor, ctx context.Context) { - a.LogStepCompleted(ctx, "wf-1", "step-1", time.Second, 0) - }, - }, - { - name: "LogStepFailed", - logFunc: func(a *WorkflowAuditor, ctx context.Context) { - a.LogStepFailed(ctx, "wf-1", "step-1", time.Second, 0, errors.New("failed")) - }, - }, - { - name: "LogStepSkipped", - logFunc: func(a *WorkflowAuditor, ctx context.Context) { - a.LogStepSkipped(ctx, "wf-1", "step-1", "condition") - }, - }, - } - - for _, tt := range logMethods { - t.Run(tt.name, func(t *testing.T) { - t.Parallel() - auditor, writer := createTestAuditor(t, DefaultConfig()) - - ctx := auth.WithIdentity(context.Background(), delegatedIdentity) - tt.logFunc(auditor, ctx) + // Every Log* method builds its event through newEvent, so testing the + // helper covers the delegation attachment for all of them by construction. + t.Run("newEvent attaches delegation chain", func(t *testing.T) { + t.Parallel() + auditor, _ := createTestAuditor(t, DefaultConfig()) - require.NotEmpty(t, writer.logs, "expected log entry") - entry := parseLogEntry(t, writer.getLastLog()) + ctx := auth.WithIdentity(context.Background(), delegatedIdentity) + event := auditor.newEvent(ctx, EventTypeWorkflowStarted, OutcomeSuccess) - chain, ok := entry["delegation"].(map[string]any) - require.True(t, ok, "delegation should be present in the log output") - assert.Equal(t, false, chain["truncated"]) - hops, ok := chain["chain"].([]any) - require.True(t, ok) - require.Len(t, hops, 2) - first, ok := hops[0].(map[string]any) - require.True(t, ok) - assert.Equal(t, "agent-1", first["sub"]) - second, ok := hops[1].(map[string]any) - require.True(t, ok) - assert.Equal(t, "agent-2", second["sub"]) - }) - } + require.NotNil(t, event.DelegationChain, "delegation chain should be attached") + assert.Equal(t, delegatedChain, event.DelegationChain) + }) t.Run("no identity omits delegation chain", func(t *testing.T) { t.Parallel()