Closes out the security-fix rollout: mxaccessgw, ScadaBridge and HistorianGateway are all on Auth 0.1.5 and pushed. Exposure was checked per repo rather than assumed -- only mxaccessgw and ScadaBridge actually resolved the vulnerable 2.1.11; HistorianGateway already resolved 3.50.3, and OtOpcUa takes no ApiKeys/SQLite dependency at all. Also records two things worth not rediscovering: HistorianGateway's bump is unverified by tests, and both it and ScadaBridge have a pre-existing AngleSharp NU1902 error failing full-solution builds under TreatWarningsAsErrors. Claude-Session: https://claude.ai/code/session_01BL2Vu1ESDQ9SCN4gVKkdts
8.2 KiB
Auth — gaps & adoption backlog
Divergence of each project from spec/SPEC.md, and the ordered backlog to
reach the shared ZB.MOM.WW.Auth library. Status legend: ⛔ gap · 🟡 partial · ✅ matches.
0.1.5PUBLISHED 2026-07-18 — security fix.Auth.ApiKeys0.1.4and earlier ship a high-severity transitive vuln:Microsoft.Data.Sqlite 10.0.7pullsSQLitePCLRaw.lib.e_sqlite3 2.1.11(GHSA-2m69-gcr7-jv3q).0.1.5pins the patched2.1.12(same approach asZB.MOM.WW.LocalDb); the.nuspecdeclares it directly, so consumers get the fix on bump without pinning themselves. No API change —0.1.5is0.1.4plus the pin. All three consumers bumped to0.1.5the same day (mxaccessgwe107019, ScadaBridge50d79ed1, HistorianGatewaydf211ee— all pushed). Exposure was verified per repo rather than assumed: mxaccessgw and ScadaBridge genuinely resolved the vulnerable2.1.11and now resolve2.1.12; HistorianGateway was never exposed (already resolved3.50.3via another dependency), so its bump is consistency-only. ScadaBridge's0.1.3→0.1.5jump crosses the0.1.4ExpiresUtcchange, but that was additive — no source changes were needed and its suites match baseline exactly (Security 181, InboundAPI 269). OtOpcUa stays at0.1.1and was never exposed: it takes noApiKeys/SQLite dependency.⚠️ Caveat: HistorianGateway's bump could NOT be test-verified — a pre-existing, unrelated
AngleSharpNU1902 error blocks its test project underTreatWarningsAsErrors(ScadaBridge'sCentralUI.Testshas the same break). Both confirmed present on clean trees beforehand.Current state (verified 2026-07-18): feed carries
0.1.0–0.1.5; consumer pins are OtOpcUa0.1.1, ScadaBridge0.1.3, MxAccessGateway0.1.4, HistorianGateway0.1.4(a fourth consumer this file predates, taking all four packages). OtOpcUa is three minors behind — it does not get the0.1.4API-key-expiry enforcement. Library suite: 215 passing, 1 skipped, green 2026-07-18.✅ ADOPTED 2026-06-02 (local-only). The full backlog (#1–#8) was implemented across all three apps on each repo's
feat/adopt-zb-authbranch — committed + spec/code-reviewed, then merged to each repo's local default (main/master) and PUSHED to origin (gitea) on 2026-06-03 (in sync;feat/*kept locally). SharedAuth.Ldap+Auth.ApiKeys(ScadaBridge inbound re-architected to keyId/Bearer),IGroupRoleMapper<TRole>,Transport-enum config, canonicalZbClaimTypes/ZbCookieDefaults, unified dev base DNdc=zb,dc=local, and the canonical-six roles (with ScadaBridge's accepted auditor/admin SoD collapse). Consumer pins: OtOpcUa0.1.1, MxGateway0.1.2, ScadaBridge0.1.3. Detail:docs/plans/2026-06-02-auth-audit-normalization*.md. The ⛔/🟡 cells below describe the PRE-adoption divergence (kept for history).
Divergence vs spec
§1 LDAP config schema
| Spec key | OtOpcUa | MxAccessGateway | ScadaBridge |
|---|---|---|---|
| Section nesting | 🟡 Authentication:Ldap (nested) |
✅ MxGateway:Ldap (nested) |
⛔ flat ScadaBridge:Security:Ldap* |
Transport enum |
⛔ UseTls bool |
⛔ UseTls bool |
✅ LdapTransport enum |
AllowInsecure |
🟡 AllowInsecureLdap |
🟡 AllowInsecureLdap |
🟡 AllowInsecureLdap (rename) |
UserNameAttribute |
✅ UserNameAttribute |
✅ UserNameAttribute |
⛔ LdapUserIdAttribute |
GroupAttribute |
✅ memberOf |
✅ memberOf |
🟡 LdapGroupAttribute (rename) |
dev SearchBase |
dc=lmxopcua,dc=local |
dc=lmxopcua,dc=local |
dc=scadabridge,dc=local |
→ Gap A1: adopt the Transport enum in OtOpcUa + gateway (replace UseTls).
→ Gap A2: ScadaBridge: nest keys + rename LdapUserIdAttribute→UserNameAttribute, LdapGroupAttribute→GroupAttribute.
→ Gap A3: unify the dev base DN (dc=lmxopcua vs dc=scadabridge) — pick one shared GLAuth base.
§2 bind-then-search
All three do bind-then-search; ScadaBridge has the most complete hygiene (RFC-4514 + filter escaping, per-op timeout, fail-closed on group lookup, username trim, service-account-bind distinction). 🟡 OtOpcUa/gateway: confirm each has filter escaping + fail-closed-on-group-lookup parity. → Gap B1: make ScadaBridge's hygiene the shared baseline; backfill any missing checks.
§3 group→role mapping
⛔ Mechanism split: OtOpcUa + gateway map in config (GroupToRole); ScadaBridge maps in
the database (LdapGroupMapping). → Gap C1: IGroupRoleMapper<CanonicalRole> must support both
backings; ship a config-backed and a DB/delegate-backed mapper.
⛔ Role vocabulary now standardized to the canonical six (spec/CANONICAL-ROLES.md);
native enforcement stays per-project. → Gap C2: implement the canonical → native expansion in each
project. ⚠ Removing Auditor collapses ScadaBridge AuditReadOnly→Viewer and Audit→Administrator,
losing its auditor/admin separation-of-duties (accepted). OtOpcUa lacks a first-class Deployer
(publish ⊂ FleetAdmin); ScadaBridge has no Operator/Engineer; mxaccessgw no Designer/Deployer —
each project assigns only the applicable subset.
§4 API-key contract
| OtOpcUa | MxAccessGateway | ScadaBridge | |
|---|---|---|---|
| Has API keys | n/a (OPC UA transport security) | ✅ mxgw_…, SQLite, scopes + constraints |
🟡 X-API-Key, per-method approval (Inbound API only) |
| Peppered HMAC-SHA256 | — | ✅ | ✅ |
| Constant-time compare | — | ✅ | ✅ |
Token format <prefix>_<id>_<secret> |
— | ✅ | ⛔ raw X-API-Key (no keyId/prefix structure) |
| Audit log | — | ✅ append-only | 🟡 (verify) |
→ Gap D1: extract mxaccessgw's pipeline as ZB.MOM.WW.Auth.ApiKeys.
→ Gap D2: ScadaBridge Inbound API adopts it; reconcile token format and model "per-method approval" as the opaque constraint policy.
§5 cookie / claim conventions
⛔ Cookie names differ (MxGatewayDashboard vs ZB.MOM.WW.ScadaBridge.Auth vs OtOpcUa control-plane cookie); claim-type conventions differ. → Gap E1: define canonical claim types + cookie defaults in ZB.MOM.WW.Auth.AspNetCore; each app keeps its own cookie name but shares attributes/claims.
§6 dev / secrets
✅ All never-log-secrets and pepper-external. 🟡 escape-hatch flag names vary. Covered by A3 (base DN) + E.
Adoption backlog (ordered)
| # | Item | Projects | Priority | Effort | Risk | Notes |
|---|---|---|---|---|---|---|
| 1 | Extract ZB.MOM.WW.Auth.Ldap from ScadaBridge's hardened impl |
all 3 | High | M | Med | security-sensitive; needs strong tests before cutover |
| 2 | Extract ZB.MOM.WW.Auth.ApiKeys from mxaccessgw Model A |
gw, SB | High | M | Med | gateway adopts first (it's the source), then SB |
| 3 | IGroupRoleMapper<TRole> seam + config & DB mappers (Gap C1) |
all 3 | High | S | Low | unblocks per-project role retention |
| 4 | Config migration to §1 schema (Gaps A1–A2) | all 3 | Med | S | Low | mechanical; do with #1 |
| 5 | ZB.MOM.WW.Auth.AspNetCore claims/cookie conventions (Gap E1) |
all 3 (UIs) | Med | S | Low | incl. OtOpcUa Blazor Admin UI control-plane |
| 6 | Unify dev GLAuth base DN (Gap A3) | all 3 | Low | S | Low | dev-only; touches fixtures/infra |
| 7 | Decide shared JWT/refresh helper vs per-project | SB (+?) | Low | S | Low | only if a 2nd project wants the same |
| 8 | Adopt canonical roles: canonical → native mapping per project (Gap C2) |
all 3 | Med | M | Med | governance (assign canonical role per LDAP group org-wide) + each project's expansion; SB audit roles collapse |
Sequencing: #3 first (cheap, unblocks), then #1 and #2 in parallel (independent libraries), then #4–#5 alongside cutover, then #6–#7 as cleanup. Each extraction lands behind tests in the source project before any consumer migrates. This stays consistent with the repos' loose coupling: adoption is opt-in per project, one consumer version-bump at a time.
Decisions still open
- Shared dev base DN value (A3).
- Whether constraints stay opaque
object?or get a smallIConstraintPolicy(shared-contract Q3). - Shared JWT/refresh helper or not (#7).