Files
scadaproj/components/auth/current-state/scadabridge/CURRENT-STATE.md
T

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.csAuthenticateAsync(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 section ScadaBridge:Security, flat keys): LdapServer, LdapPort, LdapTransport (enum Ldaps/StartTls/None), AllowInsecureLdap, LdapSearchBase (dc=scadabridge,dc=local), LdapServiceAccountDn, LdapServiceAccountPassword, LdapUserIdAttribute (uid; AD→sAMAccountName), LdapDisplayNameAttribute, LdapGroupAttribute (memberOf), LdapConnectionTimeoutMs, plus JwtSigningKey, JwtExpiryMinutes (15), IdleTimeoutMinutes (30), JwtRefreshThresholdMinutes (5), RequireHttpsCookie.
  • SecurityOptionsValidator.cs — startup fail-fast if LdapServer/LdapSearchBase empty; JwtSigningKey ≥ 32 bytes.
  • Dev LDAP: GLAuth in infra/glauth/config.toml, base dc=scadabridge,dc=local, groups SCADA-Admins/Designers/Deploy-All/Deploy-SiteA, users incl. multi-role / admin (pw password).

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 (LdapGroupNameRole), repo ConfigurationDatabase/Repositories/SecurityRepository.cs, EF map Configurations/SecurityConfiguration.cs. Security/RoleMapper.csMapGroupsToRolesAsync returns roles + permitted site IDs + isSystemWideDeployment (union semantics: any unscoped Deployment mapping ⇒ system-wide).
  • Site-scoping: Commons/Entities/Security/SiteScopeRule.cs (LdapGroupMappingIdSiteId). 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.jsonappsettings.{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) + LdapAuthResultZB.MOM.WW.Auth.Ldap. ScadaBridge's escaping/timeout/fail-closed hygiene is strong — fold it into the shared impl. Reconcile config: flat ScadaBridge:Security:Ldap* + LdapTransport enum vs the canonical schema in ../../spec/SPEC.md (canonical uses the transport enum, which ScadaBridge already has — adopt ScadaBridge's LdapTransport shape, rename keys to canonical).
  • InboundAPI/ApiKeyValidator.csZB.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). JwtTokenService is 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; implement IGroupRoleMapper<CanonicalRole> over the DB mapping and expand canonical→native. Per ../../spec/CANONICAL-ROLES.md: AuditReadOnly→Viewer, Audit→Administrator (SoD collapse); no Operator/Engineer.
  • ManagementActor role/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.