Phase 0 command-exact (publish + feed-map); Phases 1-3 decomposed into bite-sized cutover tasks with files-to-edit contracts, classification, parallelizability, and per-phase explore/elaborate gates. Co-located .tasks.json mirrors native tasks #7-#31.
22 KiB
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
Fidelity note: Phase 0 tasks are command-exact and executable as written. Phase 1–3 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)
#!/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
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)
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)
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 underdohertj2-gitea) - Modify:
~/Desktop/OtOpcUa/Directory.Packages.props(addPackageVersionentries)
Step 1: create branch feat/adopt-zb-auth in OtOpcUa.
Step 2: under the dohertj2-gitea packageSource, add:
<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(inlineVersion=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.mdcomponents/auth/spec/SPEC.md,components/auth/spec/CANONICAL-ROLES.md,components/auth/shared-contract/ZB.MOM.WW.Auth.mdZB.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 / A1–A2, 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 eachfeat/adopt-zb-authto the repo's local default branch (no push).
PHASE 2 — Audit adoption (audit GAPS #1–#3, #5, #6)
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; bridgeNodeId/CorrelationIdvalue-types at construction) - Modify:
src/Server/ZB.MOM.WW.OtOpcUa.ControlPlane/Audit/AuditWriterActor.cs(implementIAuditWriter; 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 nullableOutcome) - 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 setNodeIdnotClusterId— 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→ adoptZB.MOM.WW.Audit.IAuditRedactor(outright rename;DefaultAuditPayloadFilter/SafeDefaultAuditPayloadFilterimplement it unchanged) - Modify: all references across
AuditLog/Site,AuditLog/Central, wiring,Commons - Adopt canonical
AuditOutcomeenum; confirmIAuditWritersignature 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; OtOpcUaOutcomemigration applies; ScadaBridge rename clean. Merge eachfeat/adopt-zb-auditlocally (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.2–3.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 principalend-to-end across all 3 repos; all suites green; everything on local default branches (no push). Updatecomponents/auth/GAPS.mdandcomponents/audit/GAPS.mdto mark the adopted items done, and refresh the relevantCLAUDE.mdstatus 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.1and 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.