chore(audit): add the management audit seam with actor capture

Why

The remote audit stream has no actor anywhere, and Phase 6's management surface needs audit payloads that carry one. This MR lands the three payloads (repository writes with the previous and new URL on a URL change, upstream association changes, and the connection test with a passed/failed/unavailable outcome), the Actor field on the existing credential event, the structured-log rendering, and the managementapi AuditSink seam with the actor resolver, which is the seam the later handler steps consume. No management handler emits yet: consumers arrive with the connection-test, settings-PATCH, create-enablement, URL-change, and association steps. Real actor population arrives when the bootstrap identity stub is replaced. Until then the field renders present-and-empty.

Plan: 2026-08-13-s17-phase6-virtual-remote-repositories.md (Step 3). Spec: S17-rest-management-api.md.

What (non-obvious only)

  • internal/managementapi/handler.go is a fifth file beyond the plan step's list. The Deps.Audit field is the receiving end of wire_management's "pass the sink". Nothing reads it yet, and the requireDeps guard lands with the first handler step that does.
  • RecordCredentialWrite's signature is unchanged: existing store emitters keep compiling with an empty actor, and the per-format store steps thread the real one when their inputs gain it.
  • RepositoryWriteAction, UpstreamAssociationAction, and ConnectionTestOutcome are exported discriminators because the spec names their values. Each is consumed by its payload.
  • The URL pair renders when either side is set, exposing a contract-violating half-set pair rather than hiding it (mutation-pinned test).
  • docs/dev/observability.md is updated because this diff falsified its audit-stream section (the no-actor claim, the credential field list, and the record catalog).
  • The boot suppression breadcrumb now names the whole remote audit trail, keeping the greppable "audit trail suppressed" stem.

Diff size

1216 reviewable LOC, past the 500 guardrail. Justification: the payloads, renderer, seam, and actor resolver are one contract, and 775 of the 1216 are the tests that pin it. By file group: tests 775 (sink rendering 587, actor resolver 186, wiring-test comment 2), production 423 (payload types 140, renderer 157, seam plus resolver 101, Deps field 11, breadcrumb 11, wiring 3), docs 18.

Test plan

gofmt, build, and vet clean. golangci-lint 2.12 (plain and --build-tags=integration, with --max-same-issues=0 --max-issues-per-linter=0): 0 new findings. Full unit sweep green. Integration suites for internal/remote, internal/managementapi, and cmd/artifact-registry green. Sink rendering tests cover every enumerated action/outcome value as a positive hit, the URL half-set case, the userinfo masking and unparseable-value arms, and per-method empty actors. The actor resolver has 7 cases through the real auth middleware chain, including empty-actor and fail-closed invalid-origin arms. No e2e scenario affected: the catalogs are protocol-client driven and this is management-surface plumbing with no served behavior change.

Spec coverage

# Criterion (slice) Tests
AC-79 (remote CRUD) Create, update, and delete of a remote repository each emit one audit event naming the actor TestRemoteAuditLogSink_RepositoryWritePayload (all three actions as positive hits; audit_actor asserted per case, including present-but-empty)
AC-79 (url change) The update event for a url change records the previous and new URL TestRemoteAuditLogSink_RepositoryWritePayload/update_with_url_change (both keys present, together, with the event's values); /create, /update_without_url_change, /delete_with_empty_actor (both keys absent, not merely empty); half-set arm pins either-field rendering
AC-79 (credential event) Every credential write emits the credential event Pre-existing TestRemoteAuditLogSink_EmitsStructuredPayload and _DropsAboveItsLevel kept whole; actor attribute pinned by _EmitsStructuredPayload (empty renders) and _RendersCredentialActor (non-empty renders). Store-side emission with a real actor is the store steps' to wire.
AC-79 (associations) Each association mutation (associate, reposition, dissociate) emits one naming the actor TestRemoteAuditLogSink_UpstreamAssociationPayload (all three actions; virtual and upstream repository ids, position, actor)
AC-85 (audit slice) The connection test emits its audit event naming the actor on every request TestRemoteAuditLogSink_ConnectionTestPayload (all three outcomes as positive hits); the endpoint's emission lands with the connection-test handler step
AC-87 (audit slice) The audit entry records outcome unavailable when the probe ran but the health write failed TestRemoteAuditLogSink_ConnectionTestPayload/unavailable; remote.ConnectionTestUnavailable's doc pins it as audit-entry-only, never a stored health status
Error case: connection test, health write failed Audit entry still names the actor, outcome unavailable TestRemoteAuditLogSink_ConnectionTestPayload/unavailable; the 500 envelope is the handler step's
Security: credential values never in logs No audit payload carries a credential field, and the renderer masks a userinfo component in URL values (fixed redacted marker, invalid-url for a value that fails URL parsing) with tests asserting the secret bytes never reach the rendered line
Security: SSRF attribution (url + association changes audited with actor, url event records previous/new) TestRemoteAuditLogSink_RepositoryWritePayload (URL arms, actor), TestRemoteAuditLogSink_UpstreamAssociationPayload (actor on all three mutations)
Step acceptance: each payload renders as one structured log line with actor, namespace, and repository attributes the three per-payload tests (single-JSON-line parse, level Info, per-payload attribute set); shared-level ridership: TestRemoteAuditLogSink_DropsAboveItsLevel (info emits, warn/error drop, per method)
Emitter helper incl. empty-actor case TestAuditActor: no holder, holder without identity, anonymous, ids-under-OriginUnspecified → ""; organization:/gitlab_federated: renderings; out-of-range Origin → fail-closed invalid_origin:

Related to #314

Edited by Hayley Swimelar

Merge request reports

Loading
Loading