Files
scadaproj/components/auth/GAPS.md
T

9.9 KiB
Raw Blame History

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.2.0 PUBLISHED 2026-08-10 — LDAP backup-DC sticky failover (plan docs/plans/2026-08-10-ldap-backup-dc-failover.md). New capability in Auth.Ldap: an additive LdapOptions.FallbackServers list makes LdapAuthService walk primary + fallback endpoints, failing over on system-side faults only (connect/TLS, service-account bind, search) while a user-side answer (Success/UserNotFound/AmbiguousUser/BadCredentials/GroupLookupFailed) is final and never retried elsewhere. Sticky endpoint preference (updates on any authoritative answer), boot validation of entries, optional ILogger observability. Empty default = failover off, single-endpoint behaviour byte-unchanged. No Abstractions contract change beyond the new option; no LdapAuthFailure enum change. Motivation: ZB production directory is NAM AD with two DCs. 4 pkgs on the feed at 0.2.0, restore-verified. App adoption is a follow-up — all four apps still pin 0.1.5; adopting means bumping the pin to 0.2.0 and authoring FallbackServers (the backup DC + LDAPS trust for the ZB 1024-bit certs), deployment-time work tracked in ldap_setup.md.

0.1.5 PUBLISHED 2026-07-18 — security fix. Auth.ApiKeys 0.1.4 and earlier ship a high-severity transitive vuln: Microsoft.Data.Sqlite 10.0.7 pulls SQLitePCLRaw.lib.e_sqlite3 2.1.11 (GHSA-2m69-gcr7-jv3q). 0.1.5 pins the patched 2.1.12 (same approach as ZB.MOM.WW.LocalDb); the .nuspec declares it directly, so consumers get the fix on bump without pinning themselves. No API change — 0.1.5 is 0.1.4 plus the pin. All three consumers bumped to 0.1.5 the same day (mxaccessgw e107019, ScadaBridge 50d79ed1, HistorianGateway df211ee — all pushed). Exposure was verified per repo rather than assumed: mxaccessgw and ScadaBridge genuinely resolved the vulnerable 2.1.11 and now resolve 2.1.12; HistorianGateway was never exposed (already resolved 3.50.3 via another dependency), so its bump is consistency-only. ScadaBridge's 0.1.3→0.1.5 jump crosses the 0.1.4 ExpiresUtc change, but that was additive — no source changes were needed and its suites match baseline exactly (Security 181, InboundAPI 269). OtOpcUa stayed at 0.1.1 at the time and was never exposed (it takes no ApiKeys/SQLite dependency) — it then bumped 0.1.1→0.1.5 on 2026-07-20 for family alignment (1adaa2fc, PR #484, on origin/master).

⚠️ Caveat — RESOLVED 2026-07-19: HistorianGateway's bump could NOT be test-verified at the time — a pre-existing, unrelated AngleSharp NU1902 error blocked its test project under TreatWarningsAsErrors (ScadaBridge's CentralUI.Tests had the same break; both confirmed present on clean trees beforehand). The HG blocker was fixed by HG commit 6bc005d ("fix(tests): pin AngleSharp 1.5.2 — clear GHSA-pgww-w46g-26qg NU1902 build break"), after which the suite runs (854 pass; see the scadaproj CLAUDE.md index Secrets narrative).

Current state (re-verified 2026-08-07 against each repo's Directory.Packages.props): feed carries 0.1.0–0.1.5; all four consumers pin 0.1.5 — mxaccessgw e107019, ScadaBridge 50d79ed1, HistorianGateway df211ee (all 2026-07-18), OtOpcUa 1adaa2fc (2026-07-20). (The previous snapshot here — OtOpcUa 0.1.1 / ScadaBridge 0.1.3 / MxAccessGateway 0.1.4 / HistorianGateway 0.1.4 — predated the same-day bumps recorded in the banner above and contradicted it.) HistorianGateway is a fourth consumer this file predates, taking all four packages. 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-auth branch — 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). Shared Auth.Ldap + Auth.ApiKeys (ScadaBridge inbound re-architected to keyId/Bearer), IGroupRoleMapper<TRole>, Transport-enum config, canonical ZbClaimTypes/ZbCookieDefaults, unified dev base DN dc=zb,dc=local, and the canonical-six roles (with ScadaBridge's accepted auditor/admin SoD collapse). Consumer pins: OtOpcUa 0.1.1, MxGateway 0.1.2, ScadaBridge 0.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.

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.

⛔ 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 small IConstraintPolicy (shared-contract Q3).
  • Shared JWT/refresh helper or not (#7).