Files
scadaproj/docs/plans/2026-06-02-auth-audit-normalization.md
T

348 lines
22 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Auth + Audit Normalization Implementation Plan
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers-extended-cc:executing-plans to implement this plan task-by-task.
**Goal:** Publish `ZB.MOM.WW.Auth` (4 pkgs) + `ZB.MOM.WW.Audit` (1 pkg) to the Gitea feed and adopt both across OtOpcUa, MxAccessGateway, and ScadaBridge, ending with every audit emit site carrying the Auth-resolved principal as `AuditEvent.Actor`.
**Architecture:** Library-major waterfall — Phase 0 publish/feed-map → Phase 1 full Auth adoption (auth GAPS #1#8) → Phase 2 full Audit adoption (audit GAPS #1#3,#5,#6) → Phase 3 wire `Actor` from the principal. Behaviour-preserving cutover except two accepted changes (ScadaBridge token format, canonical-roles collapse). One feature branch per repo per library phase; local-only delivery (no `git push`).
**Tech Stack:** .NET 10, NuGet (Gitea feed + central package management), Akka.NET (OtOpcUa/ScadaBridge), EF Core + SQL Server (OtOpcUa) / SQLite (MxGateway, ScadaBridge site), Blazor admin UIs, gRPC (gateway), LDAP/GLAuth, peppered HMAC API keys, xUnit.
**Design doc:** [`2026-06-02-auth-audit-normalization-design.md`](2026-06-02-auth-audit-normalization-design.md)
**Fidelity note:** Phase 0 tasks are command-exact and executable as written. Phase 13 cutover tasks name exact files-to-edit and acceptance criteria but their per-step diffs are elaborated **just-in-time** by the per-phase "explore + elaborate" gate task (the implementer reads the named source first) — these repos' auth source has not been opened during planning, only the normalized `components/*/current-state/` docs. Audit (Phase 2) tasks cite the exact paths/lines those docs provide.
**Prerequisite the executor must supply:** Phase 0 push needs `GITEA_NUGET_KEY` (Gitea token with `package:write`). The agent cannot mint this — the user exports it, or runs the push step via `!`.
---
## PHASE 0 — Publish & feed-map (executable now)
Branch: work on `docs/auth-audit-normalization` (current) or a fresh `chore/publish-auth-audit`. The library packs happen in `scadaproj`; the feed-map edits happen in the three sibling repos (each on its own `feat/adopt-zb-auth` branch — created here, reused in Phase 1).
### Task 0.1: Add a push script for ZB.MOM.WW.Audit
**Classification:** trivial
**Estimated implement time:** ~2 min
**Parallelizable with:** none (blocks 0.3)
**Files:**
- Create: `ZB.MOM.WW.Audit/build/push.sh`
**Step 1: Create the script** (mirror `ZB.MOM.WW.Auth/build/push.sh`)
```bash
#!/usr/bin/env bash
# push.sh — pack and push the ZB.MOM.WW.Audit NuGet package to the Gitea feed.
#
# Required environment variables:
# GITEA_NUGET_SOURCE — full URL of the Gitea NuGet feed
# GITEA_NUGET_KEY — Gitea access token with package:write permission
set -euo pipefail
: "${GITEA_NUGET_SOURCE:?set GITEA_NUGET_SOURCE to your Gitea NuGet feed URL}"
: "${GITEA_NUGET_KEY:?set GITEA_NUGET_KEY to your Gitea access token}"
dotnet pack -c Release -o ./artifacts
dotnet nuget push "./artifacts/*.nupkg" \
--source "$GITEA_NUGET_SOURCE" \
--api-key "$GITEA_NUGET_KEY" \
--skip-duplicate
```
**Step 2:** `chmod +x ZB.MOM.WW.Audit/build/push.sh`
**Step 3: Commit**
```bash
git add ZB.MOM.WW.Audit/build/push.sh && git commit -m "build(audit): add Gitea push.sh"
```
### Task 0.2: Build + test both libraries green before publishing
**Classification:** small
**Estimated implement time:** ~4 min
**Parallelizable with:** 0.1
**Files:** none (verification only)
**Step 1:** `cd ZB.MOM.WW.Auth && dotnet test` — expect all 172 pass.
**Step 2:** `cd ZB.MOM.WW.Audit && dotnet test` — expect all 19 pass.
**Acceptance:** both suites green. If either fails, STOP — do not publish a red library.
### Task 0.3: Pack + push both libraries to the Gitea feed
**Classification:** standard
**Estimated implement time:** ~4 min (+ network)
**Parallelizable with:** none (blocked by 0.1, 0.2)
**Files:** none (publishes artifacts)
**Step 1: Export credentials** (user-supplied token)
```bash
export GITEA_NUGET_SOURCE="https://gitea.dohertylan.com/api/packages/dohertj2/nuget/index.json"
export GITEA_NUGET_KEY="<gitea token with package:write>"
```
**Step 2:** `cd ZB.MOM.WW.Auth && ./build/push.sh`
**Step 3:** `cd ZB.MOM.WW.Audit && ./build/push.sh`
**Step 4: Verify all 5 resolve (HTTP 200)**
```bash
for p in zb.mom.ww.auth.abstractions zb.mom.ww.auth.ldap zb.mom.ww.auth.apikeys \
zb.mom.ww.auth.aspnetcore zb.mom.ww.audit; do
printf '%s -> ' "$p"
curl -s -o /dev/null -w "%{http_code}\n" \
"https://gitea.dohertylan.com/api/packages/dohertj2/nuget/registration/$p/index.json"
done
```
**Acceptance:** all five print `200` (currently all `404`).
### Task 0.4: Feed-map + restore OtOpcUa
**Classification:** small
**Estimated implement time:** ~4 min
**Parallelizable with:** 0.5, 0.6 (different repos)
**Files:**
- Modify: `~/Desktop/OtOpcUa/NuGet.config` (add patterns under `dohertj2-gitea`)
- Modify: `~/Desktop/OtOpcUa/Directory.Packages.props` (add `PackageVersion` entries)
**Step 1:** create branch `feat/adopt-zb-auth` in OtOpcUa.
**Step 2:** under the `dohertj2-gitea` `packageSource`, add:
```xml
<package pattern="ZB.MOM.WW.Auth" />
<package pattern="ZB.MOM.WW.Auth.*" />
<package pattern="ZB.MOM.WW.Audit" />
```
**Step 3:** in `Directory.Packages.props` add (version 0.1.0): `ZB.MOM.WW.Auth.Abstractions`, `ZB.MOM.WW.Auth.Ldap`, `ZB.MOM.WW.Auth.AspNetCore`, `ZB.MOM.WW.Audit`. (No `ZB.MOM.WW.Auth.ApiKeys` — OtOpcUa uses OPC UA transport security.)
**Step 4:** `dotnet restore ZB.MOM.WW.OtOpcUa.slnx` — expect success, the new packages download from gitea.
**Step 5: Commit** `build: add ZB.MOM.WW.Auth/Audit feed mapping + version pins`.
**Acceptance:** restore succeeds; `obj/project.assets.json` lists the new packages from the gitea source.
### Task 0.5: Feed-map + restore MxAccessGateway
**Classification:** small
**Estimated implement time:** ~4 min
**Parallelizable with:** 0.4, 0.6
**Files:**
- Modify: `~/Desktop/MxAccessGateway/nuget.config`
- Modify: `~/Desktop/MxAccessGateway/src/ZB.MOM.WW.MxGateway.Server/ZB.MOM.WW.MxGateway.Server.csproj` (inline `Version=` style — no CPM)
**Step 1:** branch `feat/adopt-zb-auth` in MxAccessGateway.
**Step 2:** add the same three `<package pattern>` lines under `dohertj2-gitea`.
**Step 3:** `dotnet restore src/MxGateway.sln` (PackageReferences added in Phase 1; this step only proves the feed resolves — optionally add a throwaway reference and remove, or defer restore-proof to Phase 1's first add).
**Step 4: Commit** `build: add ZB.MOM.WW.Auth/Audit feed mapping`.
**Acceptance:** `nuget.config` maps the new patterns; restore of an added Auth package succeeds.
### Task 0.6: Feed-map + restore ScadaBridge
**Classification:** small
**Estimated implement time:** ~4 min
**Parallelizable with:** 0.4, 0.5
**Files:**
- Modify: `~/Desktop/ScadaBridge/nuget.config`
- Modify: `~/Desktop/ScadaBridge/Directory.Packages.props`
**Step 1:** branch `feat/adopt-zb-auth` in ScadaBridge.
**Step 2:** add the three `<package pattern>` lines under `dohertj2-gitea`.
**Step 3:** add `PackageVersion` entries @ 0.1.0 for all 4 Auth packages + `ZB.MOM.WW.Audit`.
**Step 4:** `dotnet restore ZB.MOM.WW.ScadaBridge.slnx`.
**Step 5: Commit** `build: add ZB.MOM.WW.Auth/Audit feed mapping + version pins`.
**Acceptance:** restore succeeds.
> **Phase 0 exit gate:** all 5 packages HTTP 200; all 3 repos restore green with the new feed mappings. Only then start Phase 1.
---
## PHASE 1 — Auth adoption (auth GAPS #1#8) *[HIGH-RISK PHASE]*
Order within the phase (per `components/auth/GAPS.md` sequencing): **#3 seam → #1 Ldap + #2 ApiKeys → #4 config + #5 claims/cookies → #6 base DN → #8 canonical roles.** Every cutover is gated by parity tests before merge.
### Task 1.0: Explore auth source + elaborate Phase 1 steps *(GATE — do first)*
**Classification:** standard
**Estimated implement time:** ~5 min (read-only)
**Parallelizable with:** none (blocks all 1.x)
**Files (read-only):**
- `components/auth/current-state/{otopcua,mxaccessgw,scadabridge}/CURRENT-STATE.md`
- `components/auth/spec/SPEC.md`, `components/auth/spec/CANONICAL-ROLES.md`, `components/auth/shared-contract/ZB.MOM.WW.Auth.md`
- `ZB.MOM.WW.Auth/src/**` (the public surface being adopted)
- Each repo's LDAP auth service, API-key pipeline, role mapper, and auth DI wiring (paths surfaced by the current-state docs).
**Action:** read the above; for each task below fill in the concrete diff, exact file paths, and the parity-test assertions. Append the elaborated steps to this plan section (or a `…-phase1.md` companion). **No code changes in this task.** This gate exists because the per-repo auth source was not opened during planning.
### Task 1.1: `IGroupRoleMapper<TRole>` seam — config + DB mappers (GAPS #3, all 3 repos)
**Classification:** standard
**Estimated implement time:** ~5 min/repo (split per repo if needed)
**Parallelizable with:** 1.2 within a repo only after the seam type is referenced
**Files:** per-repo role-mapping call sites (config-backed for OtOpcUa + MxGateway; DB-backed `LdapGroupMapping` for ScadaBridge) — exact paths from Task 1.0.
**Steps:** TDD — write a mapper test asserting current group→role outputs are preserved → wire the app to the library's `IGroupRoleMapper<TRole>` (config mapper for OtOpcUa/gw, DB/delegate mapper for SB) → green → commit. **Acceptance:** existing role-resolution behaviour byte-identical; #3 done (cheap, unblocks the rest).
### Task 1.2: Adopt `ZB.MOM.WW.Auth.Ldap` — cutover (GAPS #1, all 3 repos)
**Classification:** high-risk (security; LDAP)
**Estimated implement time:** split per repo (~5 min each)
**Parallelizable with:** 1.3 (different repos) — but within a repo, serial after 1.1
**Files:** each repo's LDAP authentication service + DI (ScadaBridge is the donor baseline; OtOpcUa/gw cut over to it). For OtOpcUa also fix the open `LdapAuthService` `Enabled`/double-singleton wiring (repo memory).
**Steps (per repo):** write parity tests reproducing current authn decisions (bind-then-search, fail-closed-on-group-lookup, RFC-4514 + filter escaping, username trim, service-account-bind distinction) → run red against the library path → replace bespoke LDAP with `Auth.Ldap` → green → commit. **Acceptance:** parity tests green; bespoke LDAP code removed/delegated; OtOpcUa singleton bug fixed.
### Task 1.3: Adopt `ZB.MOM.WW.Auth.ApiKeys` — cutover (GAPS #2; MxGateway then ScadaBridge)
**Classification:** high-risk (security; API keys)
**Estimated implement time:** ~5 min/repo
**Parallelizable with:** 1.2 (different files) — MxGateway first (source), then ScadaBridge
**Files:** MxGateway `Security/Authentication/` API-key verifier/store DI; ScadaBridge Inbound API `X-API-Key` path.
**Steps:** parity tests (peppered HMAC-SHA256, constant-time compare, scope/constraint enforcement) → cutover to `Auth.ApiKeys` → green → commit. **ScadaBridge behaviour change (accepted):** raw `X-API-Key` → structured `<prefix>_<id>_<secret>`; add an **interop check** that an inbound client using the new token format authenticates and the old format is rejected. **Acceptance:** parity + interop green; gateway is the proven source before SB cuts over.
### Task 1.4: Config schema migration (GAPS #4 / A1A2, all 3 repos)
**Classification:** standard
**Estimated implement time:** ~4 min/repo
**Parallelizable with:** bundled with 1.2 per the GAPS note ("mechanical; do with #1")
**Files:** OtOpcUa + MxGateway: `UseTls``Transport` enum binding + appsettings. ScadaBridge: flat `Security:Ldap*`→nested section; rename `LdapUserIdAttribute``UserNameAttribute`, `LdapGroupAttribute``GroupAttribute` (+ appsettings + any validators).
**Steps:** update options class + binding + appsettings + (ScadaBridge) `ConfigPreflight`/validator messages → run config-validation tests → commit. **Acceptance:** apps bind the new schema; no behaviour change beyond key names/enum.
### Task 1.5: `ZB.MOM.WW.Auth.AspNetCore` claims/cookie conventions (GAPS #5, all 3 UIs)
**Classification:** standard
**Estimated implement time:** ~4 min/repo
**Parallelizable with:** 1.4
**Files:** each UI's cookie/claims wiring (OtOpcUa Blazor Admin control-plane; MxGateway `MxGatewayDashboard`; ScadaBridge `ZB.MOM.WW.ScadaBridge.Auth`). Keep each cookie **name**; share canonical claim types + attributes.
**Steps:** adopt the shared claim-type constants + cookie attribute defaults → auth-flow test (login sets canonical claims) → commit. **Acceptance:** each app keeps its cookie name but emits canonical claims.
### Task 1.6: Unify dev GLAuth base DN (GAPS #6, all 3 + fixtures)
**Classification:** small (dev-only)
**Estimated implement time:** ~3 min
**Parallelizable with:** 1.5
**Files:** dev appsettings + LDAP/GLAuth fixtures/infra in each repo. Pick one shared base DN (open decision A3 — resolve in Task 1.0).
**Acceptance:** dev fixtures + all 3 apps share one base DN; dev login still works.
### Task 1.7: Canonical roles — `canonical → native` expansion (GAPS #8, all 3 repos)
**Classification:** high-risk (security policy)
**Estimated implement time:** ~5 min/repo
**Parallelizable with:** none (after 1.1)
**Files:** each repo's role-enforcement mapping. **ScadaBridge accepted collapse:** `AuditReadOnly`→Viewer, `Audit`→Administrator (auditor/admin SoD removed). OtOpcUa: publish ⊂ `FleetAdmin` (no first-class `Deployer`). MxGateway: assign applicable subset (no `Designer`/`Deployer`).
**Steps:** map each canonical role to native enforcement; test that each LDAP group still authorizes its expected actions; document the SoD change → commit. **Acceptance:** canonical six standardized org-wide; per-project native enforcement unchanged except the documented ScadaBridge collapse.
> **Phase 1 exit gate:** all 3 repos consume `ZB.MOM.WW.Auth.*` from the feed; bespoke LDAP/ApiKey/role code removed or delegated; existing auth tests + new parity tests green per repo; SB token-format interop check green. Merge each `feat/adopt-zb-auth` to the repo's local default branch (no push).
---
## PHASE 2 — Audit adoption (audit GAPS #1#3, #5, #6)
> ⚠️ **RE-SCOPED 2026-06-02 — the task specs below are SUPERSEDED.** The Task 2.0 gate (verified against
> live code) found these specs materially wrong: MxGateway's audit files moved into the shared library
> (Phase 1), OtOpcUa's structured audit path is dormant (zero emit sites), and the ScadaBridge
> "outright rename" is structurally impossible (its filter is typed to its own 24-field record, not the
> library's 9-field one). The user chose **DEEP adopt (canonical record)** + **pause for review**. The
> corrected, gate-grounded deep design is in
> [`2026-06-02-auth-audit-normalization-phase2-deep.md`](2026-06-02-auth-audit-normalization-phase2-deep.md)
> — **implementation is PAUSED pending user review of that doc (esp. the ScadaBridge audit-subsystem
> re-architecture cost).** The original specs below are kept for historical context only.
Branch `feat/adopt-zb-audit` per repo. Behaviour-preserving except the OtOpcUa `Outcome` column + `ClusterId` visibility fix. Concrete paths below come from `components/audit/current-state/*`.
### Task 2.0: Explore audit source + confirm elaboration *(GATE — light, paths already known)*
**Classification:** trivial
**Estimated implement time:** ~3 min (read-only)
**Parallelizable with:** none (blocks 2.x)
**Files (read-only):** the exact files cited in the tasks below (OtOpcUa `AuditWriterActor.cs`, `Commons/Messages/Audit/AuditEvent.cs`, `ConfigAuditLog.cs`, `OtOpcUaConfigDbContext.cs`, `ClusterAudit.razor`; MxGateway `IApiKeyAuditStore.cs`, `SqliteApiKeyAuditStore.cs`, `ApiKeyAuditEntry.cs`, `ConstraintEnforcer.cs`, the 3 producers; ScadaBridge `IAuditPayloadFilter.cs`, `IAuditWriter.cs`, `AuditEvent.cs`, the 4 enums). Confirm line refs still hold; adjust if drifted.
### Task 2.1: OtOpcUa — canonical record + `AuditWriterActor : IAuditWriter` + `Outcome` (GAPS #1)
**Classification:** high-risk (actor model + data contract)
**Estimated implement time:** split (record swap ~5 min; actor seam ~5 min; Outcome derivation ~5 min)
**Parallelizable with:** 2.3, 2.5 (different repos)
**Files:**
- Modify: `src/Core/ZB.MOM.WW.OtOpcUa.Commons/Messages/Audit/AuditEvent.cs` (replace with canonical record usage; bridge `NodeId`/`CorrelationId` value-types at construction)
- Modify: `src/Server/ZB.MOM.WW.OtOpcUa.ControlPlane/Audit/AuditWriterActor.cs` (implement `IAuditWriter`; map at `:75-84`)
- Modify: `tests/Server/ZB.MOM.WW.OtOpcUa.ControlPlane.Tests/AuditWriterActorTests.cs`
**Steps:** TDD — extend actor tests to assert `Outcome` derivation (`OpcUaAccessDenied`/`CrossClusterNamespaceAttempt`→Denied, config verbs→Success) and the canonical record mapping → red → swap record + implement seam + derive `Outcome` at emit sites → keep 500/5s batching + two-layer dedup → green → commit. **Acceptance:** existing tests + new `Outcome` tests green; transport/dedup unchanged.
### Task 2.2: OtOpcUa — `Outcome` column migration + `ClusterId` visibility fix (GAPS #1 storage, #5)
**Classification:** high-risk (EF migration + UI query)
**Estimated implement time:** ~5 min
**Parallelizable with:** none (after 2.1)
**Files:**
- Modify: `src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Entities/ConfigAuditLog.cs` (add nullable `Outcome`)
- Modify: `.../OtOpcUaConfigDbContext.cs` (mapping ~`:429-463`)
- Create: `Migrations/<ts>_AddConfigAuditLogOutcome.cs`
- Modify: `src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/Clusters/ClusterAudit.razor:78` (so structured actor rows — which set `NodeId` not `ClusterId` — are discoverable)
**Steps:** add column + migration → `dotnet ef migrations add` + apply on a test DB → adjust the query so structured rows appear under a cluster → commit. Leave the SP path bespoke (documented). **Acceptance:** migration applies forward; structured `AuditEvent` rows now visible in `ClusterAudit.razor`.
### Task 2.3: MxGateway — `IApiKeyAuditStore` → `IAuditWriter` adapter (GAPS #2, #6)
**Classification:** standard
**Estimated implement time:** ~5 min
**Parallelizable with:** 2.1, 2.5
**Files:**
- Modify: `src/ZB.MOM.WW.MxGateway.Server/Security/Authentication/``IApiKeyAuditStore.cs`, `SqliteApiKeyAuditStore.cs`, `ApiKeyAuditEntry.cs`, `AuthStoreServiceCollectionExtensions.cs:23`, and the 3 producers (`ApiKeyAdminCliRunner`, `DashboardApiKeyManagementService`, `ConstraintEnforcer.cs:117`)
- Test: gateway audit tests (`SqliteAuthStoreTests`, `ApiKeyAdminCliRunnerTests`)
**Steps:** map to canonical `AuditEvent` — generate `EventId`; `KeyId→Actor` with `"system"`/`"cli"` fallback; `EventType→Action`; `CreatedUtc→OccurredAtUtc`; `RemoteAddress→SourceNode`; `constraint-denied→Outcome.Denied` else `Success`; `Category="ApiKey"`; `Details→DetailsJson` **wrapped as a JSON object**; add `CorrelationId` capture + structured `Target` (#6). **Wrap `AppendAsync` so it never throws** (best-effort contract). Producers keep call sites; only the injected type changes. → tests green → commit. **Acceptance:** writes produce canonical events; writer never propagates; tests green.
### Task 2.5: ScadaBridge — rename `IAuditPayloadFilter`→`IAuditRedactor` + adopt `AuditOutcome` (GAPS #3)
**Classification:** high-risk (HIGH blast radius rename across site/central/wiring)
**Estimated implement time:** ~5 min (compiler-driven)
**Parallelizable with:** 2.1, 2.3
**Files:**
- Modify: `src/ZB.MOM.WW.ScadaBridge.AuditLog/Payload/IAuditPayloadFilter.cs` → adopt `ZB.MOM.WW.Audit.IAuditRedactor` (outright rename; `DefaultAuditPayloadFilter`/`SafeDefaultAuditPayloadFilter` implement it unchanged)
- Modify: all references across `AuditLog/Site`, `AuditLog/Central`, wiring, `Commons`
- Adopt canonical `AuditOutcome` enum; confirm `IAuditWriter` signature is byte-identical (keep the bespoke ~25-field record as storage shape — option (a))
**Steps:** outright rename (let the compiler enumerate sites) → adopt `AuditOutcome` and the `Status→Outcome` projection (`Delivered`→Success; `Failed`/`Parked`/`Discarded`→Failure; `InboundAuthFailure`→Denied) for cross-project reporting → build + full audit test suite green → commit. **Acceptance:** compiles clean; no transport/storage/CLI/UI behaviour change; enum + interface names canonical.
> **Phase 2 exit gate:** all 3 repos consume `ZB.MOM.WW.Audit`; seams/record/enum canonical; existing audit suites green; OtOpcUa `Outcome` migration applies; ScadaBridge rename clean. Merge each `feat/adopt-zb-audit` locally (no push).
---
## PHASE 3 — Wire `Actor` from the Auth principal (audit GAPS #4)
### Task 3.1: Introduce `IAuditActorAccessor` seam
**Classification:** standard
**Estimated implement time:** ~4 min
**Parallelizable with:** none (blocks 3.23.4)
**Files:** a small accessor per app (HTTP impl reads `HttpContext.User`; non-HTTP returns a threaded/fallback principal). Exact location decided in Task 1.0/3.1 from the now-adopted `Auth.AspNetCore` principal plumbing.
**Steps:** define the interface + an HTTP-backed impl + a fallback impl → unit test both → commit. **Acceptance:** accessor returns the Auth principal on authenticated paths, a fallback otherwise.
### Task 3.2 / 3.3 / 3.4: Wire emit sites — OtOpcUa / MxGateway / ScadaBridge
**Classification:** standard (each)
**Estimated implement time:** ~4 min each
**Parallelizable with:** each other (different repos), after 3.1
**Files:** each repo's audit emit sites (OtOpcUa config-write/authz emitters; MxGateway 3 producers — keep `"system"`/`"cli"` for keyless CLI; ScadaBridge `ManagementActor`/inbound boundary).
**Steps:** inject `IAuditActorAccessor`; set `AuditEvent.Actor = accessor.CurrentPrincipal` at each emit site → test `Actor == authenticated principal` on authenticated paths, fallback retained otherwise → commit. **Acceptance:** every authenticated emit carries the real Auth principal; keyless/system paths retain explicit fallbacks.
> **Program exit gate:** `Audit.Actor == Auth principal` end-to-end across all 3 repos; all suites green; everything on local default branches (no push). Update `components/auth/GAPS.md` and `components/audit/GAPS.md` to mark the adopted items done, and refresh the relevant `CLAUDE.md` status rows.
---
## Risk gates (cross-cutting)
- **Never publish a red library** (Task 0.2 gates 0.3). If a parity gap forces a lib fix, bump `0.1.0``0.1.1` and re-publish; don't edit a published version.
- **Phase 1 parity tests** must be green before any auth cutover merges — this is the security gate.
- **A green build in one repo does not prove interop.** The ScadaBridge token-format change (Task 1.3) is the one cross-boundary contract change and needs the explicit interop check.
- **Waterfall enforced by deps:** Phase 1 fully lands before Phase 2; Phase 3 after both.