plan(phase1): Task 1.0 exploration findings + elaborated Auth cutover
Per-app cutover steps mapped to the library surface; flags 5 findings that change the plan (OtOpcUa section is Security:Ldap not Authentication:Ldap; singleton 'bug' already mitigated; ScadaBridge inbound API keys are a re-architecture not a reformat; OtOpcUa config+DB mapping + DevStubMode + 2nd LDAP consumer; MxGateway ApiKeys is the low-risk donor path).
This commit is contained in:
@@ -0,0 +1,99 @@
|
||||
# Phase 1 (Auth adoption) — elaborated steps + Task 1.0 findings
|
||||
|
||||
Companion to `2026-06-02-auth-audit-normalization.md`. Produced by the Task 1.0 read-only
|
||||
exploration gate (4 parallel explorers: library surface + 3 repos). All paths verified
|
||||
2026-06-02 against source.
|
||||
|
||||
## Cutover target — `ZB.MOM.WW.Auth` public surface
|
||||
|
||||
| Package | Consumer entry points |
|
||||
|---|---|
|
||||
| `.Abstractions` | `ILdapAuthService`, `LdapOptions` (`Transport: LdapTransport{Ldaps,StartTls,None}`, `AllowInsecure`, `UserNameAttribute`, `GroupAttribute`, `ServiceAccountDn/Password`, `SearchBase`, `ConnectionTimeoutMs`, `ServerCertificateValidationCallback`), `LdapAuthResult(Succeeded,Username,DisplayName,Groups,Failure)`, `LdapAuthFailure`, `CanonicalRole{Viewer,Operator,Engineer,Designer,Deployer,Administrator}`, `IGroupRoleMapper<TRole>` (**no default impl — consumer writes it**) → `GroupRoleMapping<TRole>(Roles, Scope:object?)`, plus API-key abstractions (`IApiKeyVerifier`, `ApiKeyVerification`, `ApiKeyIdentity`, `IApiKeyStore`/`IApiKeyAdminStore`/`IApiKeyAuditStore`, `ApiKeyOptions{TokenPrefix,PepperSecretName,SqlitePath,RunMigrationsOnStartup}`) |
|
||||
| `.Ldap` | `LdapAuthService(LdapOptions)` : `ILdapAuthService`. Bind-then-search, fail-closed, never throws. `LdapOptionsValidator` (TLS-or-AllowInsecure) auto-registered. |
|
||||
| `.ApiKeys` | `ApiKeyVerifier(ApiKeyOptions, IApiKeyStore, IApiKeyPepperProvider, TimeProvider?)`, `ApiKeyParser.TryParse` (`<prefix>_<keyId>_<secret>`), `ApiKeySecretGenerator.NewSecret()`, default SQLite stores, `ConfigurationApiKeyPepperProvider`. **Extracted from MxGateway — near-1:1 with its pipeline.** |
|
||||
| `.AspNetCore` | `ZbClaimTypes{Name,Role,DisplayName,Username,ScopeId}`, `ZbCookieDefaults.Apply(opts, requireHttps, idleTimeout)`, DI: `AddZbLdapAuth(services, config, sectionPath)`, `AddZbApiKeyAuth(services, config, sectionPath)`. |
|
||||
|
||||
## Per-app current state (verified) and elaborated cutover
|
||||
|
||||
### OtOpcUa — packages: Abstractions + Ldap + AspNetCore (no ApiKeys)
|
||||
|
||||
Current LDAP: `src/Server/ZB.MOM.WW.OtOpcUa.Security/Ldap/LdapAuthService.cs` (impl), `ILdapAuthService.cs`,
|
||||
`LdapOptions.cs` (**section `Security:Ldap`**, `UseTls` bool, `Enabled`, `DevStubMode`, embedded `GroupToRole` dict),
|
||||
`LdapAuthResult.cs` (already carries `Roles`). Role mapping is **config + DB**: `RoleMapper.Map` (config
|
||||
`GroupToRole`) + `RoleMapper.Merge` with DB `LdapGroupRoleMappingService`/`LdapGroupRoleMapping` (system-wide rows).
|
||||
Native roles `AdminRole{ConfigViewer,ConfigEditor,FleetAdmin}` (control-plane only; data-plane is a separate
|
||||
`NodePermissions` bitmask). DI: two `TryAddSingleton<ILdapAuthService,LdapAuthService>` sites
|
||||
(`Security/ServiceCollectionExtensions.cs:42` + `Host/Program.cs:106`). Cookie `ZB.MOM.WW.OtOpcUa.Auth`,
|
||||
single Cookie scheme (JWT inside cookie). **Second LDAP consumer:** OPC UA data-plane
|
||||
`LdapOpcUaUserAuthenticator` + `OpcUaApplicationHost.HandleImpersonation` call the LDAP service too.
|
||||
|
||||
- **1.1 mapper:** implement `IGroupRoleMapper<AdminRole>` (or `<string>`) wrapping `RoleMapper.Map` + DB `Merge`.
|
||||
- **1.2 Ldap:** replace `LdapAuthService` with `Auth.Ldap`; restructure flow to `ILdapAuthService → Groups → IGroupRoleMapper → roles → claims`; **preserve `DevStubMode` app-side** (library has no stub); wire BOTH consumers (login endpoint + OPC UA impersonation).
|
||||
- **1.4 config:** `UseTls`→`Transport` enum (section already `Security:Ldap` — see Finding #1).
|
||||
- **1.5 cookie/claims:** use `ZbClaimTypes` + `ZbCookieDefaults.Apply`; keep cookie name.
|
||||
- **1.7 roles:** `ConfigViewer→Viewer`, `ConfigEditor→Designer`, `FleetAdmin→Administrator(+Deployer; publish⊂FleetAdmin)`. Data-plane `NodePermissions` unaffected.
|
||||
|
||||
### MxAccessGateway — packages: all 4 (ApiKeys **source**, cuts over first)
|
||||
|
||||
Current API keys (`src/ZB.MOM.WW.MxGateway.Server/Security/Authentication/`): `ApiKeyParser` (`mxgw_<id>_<secret>`),
|
||||
`ApiKeySecretHasher` (HMAC-SHA256 + pepper `MxGateway:ApiKeyPepper`), `ApiKeySecretGenerator`, `ApiKeyVerifier`
|
||||
(`FixedTimeEquals`), SQLite stores, `ConstraintEnforcer` + rich `ApiKeyConstraints`, gRPC
|
||||
`GatewayGrpcAuthorizationInterceptor` + `GatewayScopes`. DI `AddSqliteAuthStore()`. → **near-1:1 with `Auth.ApiKeys`.**
|
||||
LDAP: `Dashboard/DashboardAuthenticator.cs` (`MxGateway:Ldap`, `UseTls`), `GroupToRole` under `MxGateway:Dashboard`,
|
||||
roles `Admin`/`Viewer`, cookie `MxGatewayDashboard`.
|
||||
|
||||
- **1.1 mapper:** `IGroupRoleMapper<string>` wrapping `DashboardAuthenticator.MapGroupsToRoles`.
|
||||
- **1.2 Ldap:** replace `DashboardAuthenticator`'s LDAP internals with `Auth.Ldap` (keep dashboard claims/principal build).
|
||||
- **1.3 ApiKeys:** delete the local parser/hasher/generator/verifier/stores; re-point to `Auth.ApiKeys`; **keep** `ConstraintEnforcer` + gRPC interceptor + scopes on top (constraints carried as the opaque blob). Lowest-risk ApiKeys cutover (it's the donor).
|
||||
- **1.4 config:** `UseTls`→`Transport`.
|
||||
- **1.5/1.7:** `ZbClaimTypes`/cookie defaults; `Viewer→Viewer`, `Admin→Administrator`.
|
||||
|
||||
### ScadaBridge — packages: all 4 (Ldap **source**; ApiKeys consumer)
|
||||
|
||||
Current LDAP (`src/ZB.MOM.WW.ScadaBridge.Security/LdapAuthService.cs`): the hardened reference (RFC-4514 DN escape,
|
||||
filter escape, per-op timeout, fail-closed group lookup, username trim, service-account-bind distinction). Config is
|
||||
**flat** `ScadaBridge:Security:Ldap*` in `SecurityOptions.cs` with **`LdapTransport` enum already** (`Ldaps/StartTls/None`),
|
||||
`AllowInsecureLdap`, `LdapUserIdAttribute`, `LdapGroupAttribute`, validated by `SecurityOptionsValidator : OptionsValidatorBase`.
|
||||
Role mapping **DB-backed** with **site-scoping**: `RoleMapper.MapGroupsToRolesAsync` → `RoleMappingResult(Roles, PermittedSiteIds, IsSystemWideDeployment)` over `LdapGroupMapping` + `SiteScopeRule` (SQL Server). Roles
|
||||
`Admin/Design/Deployment/Audit/AuditReadOnly`; SoD via `OperationalAudit{Admin,Audit,AuditReadOnly}` + `AuditExport{Admin,Audit}`.
|
||||
Cookie `ZB.MOM.WW.ScadaBridge.Auth`; JWT-in-cookie via `JwtTokenService`.
|
||||
**Inbound API keys** (`InboundAPI/ApiKeyValidator.cs`): **raw `X-API-Key`**, **deterministic** HMAC (`ApiKeyHasher`, no per-row salt, by-value lookup), `ApiKey{Name,KeyHash,IsEnabled}` in **SQL Server**, **per-method approval** via `ApiMethod.ApprovedApiKeyIds` — **architecturally different from the library's keyId/scope/SQLite model.**
|
||||
|
||||
- **1.1 mapper:** `IGroupRoleMapper<string>` wrapping `RoleMapper.MapGroupsToRolesAsync`, carrying `PermittedSiteIds`/`IsSystemWideDeployment` in `GroupRoleMapping.Scope`.
|
||||
- **1.2 Ldap:** ScadaBridge is the donor — confirm `Auth.Ldap` behaviour-matches, then re-point `LdapAuthService` usages to the library type. Lowest-risk Ldap cutover.
|
||||
- **1.3 ApiKeys:** **see Finding #3 — bigger than a token reformat; needs a scope decision.**
|
||||
- **1.4 config:** nest flat `Security:Ldap*` under a sub-section + rename `LdapUserIdAttribute→UserNameAttribute`, `LdapGroupAttribute→GroupAttribute`, `LdapTransport→Transport` (+ `SecurityOptionsValidator` + appsettings). Enum already matches.
|
||||
- **1.7 roles:** `Admin→Administrator`, `Design→Designer`, `Deployment→Deployer`, `Audit→Administrator` (collapse), `AuditReadOnly→Viewer` (collapse) — removes the `OperationalAudit`/`AuditExport` SoD (accepted).
|
||||
|
||||
## Key findings that change the plan
|
||||
|
||||
1. **OtOpcUa LDAP section is `Security:Ldap`, not `Authentication:Ldap`.** Both `components/auth/GAPS.md §1`
|
||||
and the auth current-state doc are wrong; the code (and the prior fix in memory) use `Security:Ldap`.
|
||||
→ Task 1.4 for OtOpcUa is only `UseTls`→`Transport`, not a section move.
|
||||
2. **OtOpcUa "double-singleton bug" is already mitigated.** Both registration sites use `TryAddSingleton`
|
||||
(dedupes); the `Enabled` flag is an intentional fail-closed master switch. → Not a blocking fix; verify and
|
||||
keep `Enabled`. Removes a risk the plan flagged.
|
||||
3. **ScadaBridge inbound API keys are a re-architecture, not a token reformat.** The library's ApiKeys model
|
||||
(`<prefix>_<keyId>_<secret>` Bearer, keyId lookup + constant-time compare, SQLite store, scopes + opaque
|
||||
constraints) is fundamentally different from ScadaBridge's (raw `X-API-Key`, deterministic by-value HMAC
|
||||
lookup, SQL Server `ApiKey{Name,KeyHash}`, per-method approval list). Wholesale adoption means re-architecting
|
||||
inbound-API auth AND resolving a SQLite-vs-SQL-Server storage mismatch. **Needs a scope decision (Decision A).**
|
||||
4. **OtOpcUa role mapping is config + DB**, not just config (`RoleMapper.Map` baseline + DB `Merge`). The
|
||||
`IGroupRoleMapper` impl must combine both. OtOpcUa also has `DevStubMode` (no library equivalent — keep app-side)
|
||||
and a **second LDAP consumer** (OPC UA data-plane impersonation) that must be re-wired too.
|
||||
5. **MxGateway ApiKeys cutover is the donor path — lowest risk** (delete locals, re-point to library; keep
|
||||
`ConstraintEnforcer`/gRPC/scopes on top). Confirms the GAPS sequencing (gateway first).
|
||||
|
||||
## Open decisions (check back with user)
|
||||
|
||||
- **Decision A — ScadaBridge inbound API keys depth:**
|
||||
(a) Full adopt `Auth.ApiKeys` (re-architect inbound auth: Bearer token format, keyId/scope model, move keys to the
|
||||
library SQLite store or implement `IApiKeyStore` over its SQL Server tables) — faithful but large.
|
||||
(b) Keep ScadaBridge's inbound-API auth as-is, adopt only the shared **token format + pepper/hashing convention**
|
||||
(smallest behaviour-visible change; the GAPS D2 "reconcile token format" reading).
|
||||
(c) Implement the library's `IApiKeyStore`/`IApiKeyVerifier` over ScadaBridge's existing SQL Server tables +
|
||||
per-method-approval policy (middle path: shared verifier seam, keep storage + approval model).
|
||||
- **Decision B — canonical role mappings:** confirm the per-app tables above (esp. OtOpcUa `ConfigEditor→Designer`,
|
||||
`FleetAdmin→Administrator+Deployer`; ScadaBridge `Audit`/`AuditReadOnly` collapse).
|
||||
- **Decision C — `DevStubMode`** (OtOpcUa) and MxGateway loopback/`AllowAnonymousLocalhost` bypass: keep app-side
|
||||
(library has no equivalent) — confirm we preserve these dev escape hatches unchanged.
|
||||
Reference in New Issue
Block a user