6.6 KiB
Auth — current state: ScadaBridge
Repo: ~/Desktop/ScadaBridge. Stack: .NET 10, Akka.NET; solution ZB.MOM.WW.ScadaBridge.slnx.
Auth code centers on the ZB.MOM.WW.ScadaBridge.Security project. All paths relative to repo root.
Verified 2026-06-01.
LDAP-centric identity with database-driven role mapping and site-scoped deployment; multiple authenticated surfaces (Blazor UI, CLI, Management API, Inbound API).
1. Authentication
src/ZB.MOM.WW.ScadaBridge.Security/:
LdapAuthService.cs—AuthenticateAsync(username,password)→LdapAuthResult. Direct LDAP bind (no Kerberos/NTLM). RFC 4514 DN escaping + LDAP filter escaping (injection-safe), username trim-normalization, per-operation socket timeout, distinct exception for service-account-bind failure vs bad user creds, fails login if the group lookup fails (won't admit with zero roles).SecurityOptions.cs(config sectionScadaBridge:Security, flat keys):LdapServer,LdapPort,LdapTransport(enumLdaps/StartTls/None),AllowInsecureLdap,LdapSearchBase(dc=scadabridge,dc=local),LdapServiceAccountDn,LdapServiceAccountPassword,LdapUserIdAttribute(uid; AD→sAMAccountName),LdapDisplayNameAttribute,LdapGroupAttribute(memberOf),LdapConnectionTimeoutMs, plusJwtSigningKey,JwtExpiryMinutes(15),IdleTimeoutMinutes(30),JwtRefreshThresholdMinutes(5),RequireHttpsCookie.SecurityOptionsValidator.cs— startup fail-fast ifLdapServer/LdapSearchBaseempty;JwtSigningKey≥ 32 bytes.- Dev LDAP: GLAuth in
infra/glauth/config.toml, basedc=scadabridge,dc=local, groupsSCADA-Admins/Designers/Deploy-All/Deploy-SiteA, users incl.multi-role/admin(pwpassword).
2. Authorization (roles + site scope — stays bespoke)
- Roles (
Security/Roles.cs):Admin,Design,Deployment,Audit,AuditReadOnly. - Group→role is DB-driven (not config): entity
Commons/Entities/Security/LdapGroupMapping.cs(LdapGroupName→Role), repoConfigurationDatabase/Repositories/SecurityRepository.cs, EF mapConfigurations/SecurityConfiguration.cs.Security/RoleMapper.cs→MapGroupsToRolesAsyncreturns roles + permitted site IDs +isSystemWideDeployment(union semantics: any unscoped Deployment mapping ⇒ system-wide). - Site-scoping:
Commons/Entities/Security/SiteScopeRule.cs(LdapGroupMappingId→SiteId). No scope rules on a Deployment mapping ⇒ all sites. - ASP.NET policies (
Security/AuthorizationPolicies.cs):RequireAdmin/RequireDesign/RequireDeployment/OperationalAudit/AuditExport.
3. Authenticated surfaces
| Surface | Entry | Mechanism | Role check | Site scope |
|---|---|---|---|---|
| Central UI (Blazor Server) | /auth/login form |
LDAP → cookie | [Authorize(Policy=…)], AuthorizeView |
CentralUI/Auth/SiteScopeService.cs |
| CLI | --username/--password |
HTTP Basic → Management API → LDAP | at Management API | at Management API |
| Management API | POST /management |
HTTP Basic → LDAP | ManagementActor.GetRequiredRole then Roles.Contains |
ManagementActor.EnforceSiteScope |
| Inbound API | X-API-Key header |
API key hash lookup (not LDAP) | per-method approval in DB | n/a |
| Central↔Site | Akka ClusterClient + gRPC | none (cluster membership is the trust boundary; TLS in prod) | — | — |
Key files: CentralUI/Auth/AuthEndpoints.cs (/auth/login,/auth/token,/auth/logout,/auth/ping), CentralUI/Auth/CookieAuthenticationStateProvider.cs, ManagementService/ManagementEndpoints.cs + ManagementActor.cs, CLI/ManagementHttpClient.cs, InboundAPI/ApiKeyValidator.cs.
4. Session / identity model
Cookie auth (Security/ServiceCollectionExtensions.cs): cookie name ZB.MOM.WW.ScadaBridge.Auth, HttpOnly, SameSite=Strict, Secure conditional, sliding IdleTimeoutMinutes (30). Claims carry name/display/username/roles/site-ids. Separate JWT (Security/JwtTokenService.cs, HMAC-SHA256, issuer/aud scadabridge-central, 15-min expiry, refresh at 5-min threshold, idle-timeout enforced) used for programmatic/CLI via /auth/token. Shared JwtSigningKey across central nodes ⇒ no sticky sessions. Note: the cookie is the ASP.NET session token; JWT is not embedded in it.
Inbound API keys (InboundAPI/ApiKeyValidator.cs): X-API-Key, peppered HMAC-SHA256, constant-time compare, per-method approval, indistinguishable 403 for "no method"/"not approved".
5. Secrets & config
ldap_login.txt / sql_login.txt (real creds, git-ignored). Docker config in docker/central-node-*/appsettings.Central.json. Config hierarchy: appsettings.json → appsettings.{Central|Site}.json → env → CLI. Spec: docs/requirements/Component-Security.md.
6. Notable limits / TODOs
No Kerberos/NTLM; no dynamic role refresh (only on token expiry/LDAP re-query); no forced session revocation; AddSecurityActors() is a placeholder; no MFA; no password-reset (AD-managed).
Adoption plan → ZB.MOM.WW.Auth
Replace with the shared library:
LdapAuthService+SecurityOptions(LDAP portion) +LdapAuthResult→ZB.MOM.WW.Auth.Ldap. ScadaBridge's escaping/timeout/fail-closed hygiene is strong — fold it into the shared impl. Reconcile config: flatScadaBridge:Security:Ldap*+LdapTransportenum vs the canonical schema in../../spec/SPEC.md(canonical uses the transport enum, which ScadaBridge already has — adopt ScadaBridge'sLdapTransportshape, rename keys to canonical).InboundAPI/ApiKeyValidator.cs→ZB.MOM.WW.Auth.ApiKeys(same peppered-HMAC contract mxaccessgw extracts). Map "per-method approval" onto the contract's opaque constraint/policy hook.- Cookie/JWT wiring →
ZB.MOM.WW.Auth.AspNetCore(claims + cookie conventions).JwtTokenServiceis a candidate for a shared token helper if mxaccessgw/OtOpcUa want the same refresh model — otherwise keep bespoke.
Keep bespoke:
- Native role set (
Admin/Design/Deployment/Audit/AuditReadOnly) + site-scoping (SiteScopeRule, union semantics) stay as the enforcement layer; implementIGroupRoleMapper<CanonicalRole>over the DB mapping and expand canonical→native. Per../../spec/CANONICAL-ROLES.md:AuditReadOnly→Viewer,Audit→Administrator (SoD collapse); noOperator/Engineer. ManagementActorrole/scope enforcement, Akka cluster trust model.
Note the group→role mechanism divergence: ScadaBridge maps groups→roles in the database; OtOpcUa and mxaccessgw map in config. The shared seam (IGroupRoleMapper) must allow either backing store — see ../../GAPS.md.