Repoint the server-rendered sign-in/fallback HTML (DashboardEndpointRouteBuilderExtensions) from /css/theme.css to the kit's _content/ZB.MOM.WW.Theme/css/{theme,layout}.css, mirroring ThemeHead, since that static page cannot use the Razor component.
Introduce IAuditActorAccessor seam + HttpAuditActorAccessor impl (reads ZbClaimTypes.Username
from IHttpContextAccessor; falls back to Identity.Name / ZbClaimTypes.Name; null when
unauthenticated). Register in DI via DashboardServiceCollectionExtensions.
Wire DashboardApiKeyManagementService: WriteDashboardAuditAsync now accepts the ClaimsPrincipal
user already in scope at each call site; ResolveOperatorActor extracts ZbClaimTypes.Username
(preferred) or Identity.Name. All four dashboard-* events now emit Actor = LDAP operator
username and Target = managed keyId, fixing the semantic gap where both fields held the keyId.
ConstraintEnforcer (gRPC / API-key actor) and CanonicalForwardingApiKeyAuditStore (CLI /
"system"/"cli" fallback) are unchanged.
Tests: DashboardApiKeyManagementServiceTests updated — CreateAuthorizedUser adds ZbClaimTypes.Username
("alice"), all dashboard-* audit assertions updated to Actor = "alice" / Target = "operator01";
new CreateAsync_AuthorizedUser_CanonicalAuditEventHasOperatorAsActorAndKeyIdAsTarget verifies the
canonical AuditEvent directly. New HttpAuditActorAccessorTests (4 cases: username claim, Identity.Name
fallback, unauthenticated → null, no context → null). ConstraintEnforcer tests still assert API-key/anonymous actor.
Standardize the dashboard role VALUE on the canonical six: Admin→Administrator
(Viewer unchanged). Pure value rename via DashboardRoles.Admin constant +
appsettings GroupToRole; the GatewayOptionsValidator allowed-set/message track
the constant so they now require 'Administrator' or 'Viewer'. Enforcement is
unchanged — Administrator authorizes exactly what Admin did.
Dashboard roles are derived at login from LDAP groups via GroupToRole and are
never persisted to the SQLite auth store, so no DB migration/seed change.
UNTOUCHED: the separate gRPC API-key scope GatewayScopes.Admin = "admin"
(lowercase) and every "admin" scope literal — a distinct data-plane system.
- DashboardAuthenticator.CreatePrincipal: emit ZbClaimTypes.Username ("zb:username") with
the login username, ZbClaimTypes.DisplayName ("zb:displayname") with the display name,
ZbClaimTypes.Name (== ClaimTypes.Name) for Identity.Name resolution, ZbClaimTypes.Role
(== ClaimTypes.Role) for IsInRole/[Authorize]. Keep ClaimTypes.NameIdentifier for back-compat
read-sites; keep mxgateway:ldap_group unchanged (MxGateway-specific, no ZbClaimType for groups).
ClaimsIdentity built with nameType=ZbClaimTypes.Name, roleType=ZbClaimTypes.Role.
- DashboardServiceCollectionExtensions.AddGatewayDashboard: route cookie hardening through
ZbCookieDefaults.Apply(requireHttps:true, idleTimeout:8h); set cookie name/path/redirects
after Apply; PostConfigure still overrides SecurePolicy per RequireHttpsCookie setting.
- DashboardAuthenticatorTests: add AuthenticateAsync_Success_EmitsCanonicalZbClaims asserting
zb:username, zb:displayname, ZbClaimTypes.Role per role, Identity.Name, and ldap_group preserved.
Defect 1: ValidateLdap used AddIfNotPositive for Port, accepting any value
> 0 including 70000. Replaced with builder.Port() from the shared
ZB.MOM.WW.Configuration library, which enforces the 1-65535 TCP range and
emits "MxGateway:Ldap:Port must be between 1 and 65535 (was {value})".
Defect 2: AddIfInvalidPath only caught ArgumentException, NotSupportedException,
and PathTooLongException from Path.GetFullPath. On macOS/Linux a path containing
an embedded null throws IOException, which escaped the catch block and caused
Validate() to throw instead of returning a failure. Added catch (IOException).
Tests: added Validate_Fails_WhenLdapPortIsZero, Validate_Fails_WhenLdapPortExceedsMaximum,
and Validate_Succeeds_WhenLdapEnabledWithValidPort to cover the new range boundary.
Best-effort resolve the host FQDN via Dns.GetHostEntry and add it as a
DNS SAN when it differs (OrdinalIgnoreCase) from the short machine name
and "localhost". SocketException / ArgumentException are caught and
silently skipped so cert generation remains robust when DNS is absent.
Wrap the WriteAllBytes/Move/HardenPermissions sequence in a try/catch so
that any failure best-effort deletes the hardened .tmp file (which may
already hold PFX/private-key bytes) before rethrowing. Add a test that
induces a persist failure by pointing SelfSignedCertPath inside a
regular file and asserts no .tmp is left on disk.
Drop KeyEncipherment from the self-signed cert's key-usage extension — it
is semantically wrong for ECDSA (RSA key-transport only); DigitalSignature
alone is correct for TLS 1.3 / ECDHE server certs. CertificateRequest is
unchanged (not IDisposable in .NET 10). Test now also asserts MachineName,
127.0.0.1 and IPv6 loopback are present in the SAN extension.
Adds missing <summary>/<param> XML docs across 99 server, worker, and test
files so CommentChecker reports zero issues (TreatWarningsAsErrors needs the
analyzer clean). Bundles in WIP dashboard work: NavSection extraction,
MainLayout/site.css/js styling alignment, and DashboardOptions/Auth tweaks.
The Worker + Worker.Tests projects pull in the Windows-only ArchestrA.MxAccess
COM stack and can't be built off Windows. Add a sibling .slnx that lists only
the cross-platform projects (Contracts, Server, IntegrationTests, Tests) so
non-Windows hosts can restore + build the rest of the solution with:
dotnet build src/ZB.MOM.WW.MxGateway.NonWindows.slnx
The canonical solution on Windows remains ZB.MOM.WW.MxGateway.slnx.