438 lines
31 KiB
Markdown
438 lines
31 KiB
Markdown
# Cluster 04 — Auth
|
||
|
||
Auditor: Claude Code (claude-sonnet-4-6)
|
||
Date: 2026-06-03
|
||
Docs audited: docs/Authentication.md, docs/Authorization.md, glauth.md
|
||
Code verified against: src/ZB.MOM.WW.MxGateway.Server/Security/** and Dashboard/**
|
||
|
||
---
|
||
|
||
DOC / Authentication.md / LINES 253–271
|
||
CLAIM / `AuthStoreServiceCollectionExtensions.AddSqliteAuthStore` wires services via direct `AddSingleton` calls for `IApiKeyParser`, `IApiKeySecretHasher`, `IApiKeyVerifier`, `IApiKeyStore`/`SqliteApiKeyStore`, `IApiKeyAdminStore`/`SqliteApiKeyAdminStore`, `IApiKeyAuditStore`/`SqliteApiKeyAuditStore`, `AuthSqliteConnectionFactory`, `IAuthStoreMigrator`/`SqliteAuthStoreMigrator`, `AuthStoreMigrationHostedService`.
|
||
CLAIM_TYPE / behavior-rule
|
||
VERDICT / stale
|
||
EVIDENCE / src/ZB.MOM.WW.MxGateway.Server/Security/Authentication/AuthStoreServiceCollectionExtensions.cs:67 — the shared library `ZB.MOM.WW.Auth.ApiKeys` is registered via `services.AddZbApiKeyAuth(effectiveConfig, AuthenticationSectionPath)`, which owns all of those types. The local method no longer registers them individually. The doc code block is a fabricated snapshot of pre-migration code that no longer matches any method in the codebase.
|
||
CODE_AREA / auth.apikeys
|
||
SEVERITY / high
|
||
PROPOSED_FIX / Replace the Registration section code block with the actual method body from AuthStoreServiceCollectionExtensions.cs (calls AddZbApiKeyAuth, then registers CanonicalForwardingApiKeyAuditStore, SqliteCanonicalAuditStore, IAuditWriter, ApiKeyAdminCommands, ApiKeyAdminCliRunner). Remove the statement that AddSqliteAuthStore "registers the migration hosted service" — the hosted service is registered by AddZbApiKeyAuth, not by local code.
|
||
|
||
---
|
||
|
||
DOC / Authentication.md / LINES 53–68
|
||
CLAIM / `ApiKeySecretHasher` (registered behind `IApiKeySecretHasher`) hashes secrets with `HMACSHA256` keyed by a server-side pepper. The pepper is resolved by `IConfiguration` lookup against `PepperSecretName`. `ApiKeyPepperUnavailableException` is thrown when the pepper is missing.
|
||
CLAIM_TYPE / behavior-rule
|
||
VERDICT / stale
|
||
EVIDENCE / src/ZB.MOM.WW.MxGateway.Server/Security/Authentication/AuthStoreServiceCollectionExtensions.cs:5–8 — these types (`ApiKeySecretHasher`, `IApiKeySecretHasher`, `ApiKeyPepperUnavailableException`) now live in the shared package `ZB.MOM.WW.Auth.ApiKeys` (PackageReference in .csproj line 11). The behavior is correct but the doc presents them as if they are local gateway types. The interceptor's return type is `ApiKeyVerification` not `ApiKeyVerificationResult` (AuthStoreServiceCollectionExtensions.cs context; GatewayGrpcAuthorizationInterceptor.cs:69).
|
||
CODE_AREA / auth.apikeys
|
||
SEVERITY / medium
|
||
PROPOSED_FIX / Clarify that `ApiKeySecretHasher`, `IApiKeySecretHasher`, and `ApiKeyPepperUnavailableException` are provided by the `ZB.MOM.WW.Auth.ApiKeys` shared library, not gateway-local types. Correct `ApiKeyVerificationResult` → `ApiKeyVerification` (the type returned by `IApiKeyVerifier.VerifyAsync` in the interceptor).
|
||
|
||
---
|
||
|
||
DOC / Authentication.md / LINES 72–98
|
||
CLAIM / `ApiKeyVerifier` (`IApiKeyVerifier`) step 5: "Compare hashes with `CryptographicOperations.FixedTimeEquals`." Step 6: "Record a `LastUsedUtc` timestamp via `MarkKeyUsedAsync` and return an `ApiKeyIdentity`." Code block shows `ApiKeyVerificationResult.Fail(ApiKeyVerificationFailure.SecretMismatch)` and `ApiKeyVerificationResult.Success(new ApiKeyIdentity(...))`.
|
||
CLAIM_TYPE / behavior-rule
|
||
VERDICT / stale
|
||
EVIDENCE / src/ZB.MOM.WW.MxGateway.Server/Security/Authorization/GatewayGrpcAuthorizationInterceptor.cs:69 — the interceptor receives `ApiKeyVerification verification`, not `ApiKeyVerificationResult`. These types are from the shared package `ZB.MOM.WW.Auth.ApiKeys` which was migrated to. The types, method signatures, and return types shown in the code block may have been renamed or restructured during the migration to the shared library; the gateway no longer owns or contains these implementations.
|
||
CODE_AREA / auth.apikeys
|
||
SEVERITY / medium
|
||
PROPOSED_FIX / Update type names to match the shared library (`ApiKeyVerification` instead of `ApiKeyVerificationResult`). Add note that `ApiKeyVerifier` is from `ZB.MOM.WW.Auth.ApiKeys`. Verify failure enum values against the shared library.
|
||
|
||
---
|
||
|
||
DOC / Authentication.md / LINES 108–122
|
||
CLAIM / "`AuthSqliteConnectionFactory` reads `GatewayOptions.Authentication.SqlitePath`"
|
||
CLAIM_TYPE / term
|
||
VERDICT / stale
|
||
EVIDENCE / src/ZB.MOM.WW.MxGateway.Server/Security/Authentication/AuthStoreServiceCollectionExtensions.cs:67 — `AuthSqliteConnectionFactory` is now registered by `AddZbApiKeyAuth` from the shared package. The doc implies it is a local type that reads the gateway's `GatewayOptions`, but it is actually from `ZB.MOM.WW.Auth.ApiKeys` and reads `ApiKeyOptions.SqlitePath` (bound from `MxGateway:Authentication` section). The behavior is equivalent but the doc is misleading about the type ownership.
|
||
CODE_AREA / auth.apikeys
|
||
SEVERITY / low
|
||
PROPOSED_FIX / Note that `AuthSqliteConnectionFactory` is from `ZB.MOM.WW.Auth.ApiKeys` and reads `ApiKeyOptions.SqlitePath` (bound via `MxGateway:Authentication:SqlitePath`).
|
||
|
||
---
|
||
|
||
DOC / Authentication.md / LINES 126–133
|
||
CLAIM / "`SqliteAuthSchema` declares table names and the current schema version as constants. Three tables are involved: `api_keys`, `api_key_audit`, `schema_version`."
|
||
CLAIM_TYPE / behavior-rule
|
||
VERDICT / stale
|
||
EVIDENCE / src/ZB.MOM.WW.MxGateway.Server/Security/Authentication/AuthStoreServiceCollectionExtensions.cs:69–74 — a new `audit_event` table now exists in the same SQLite file, written by `SqliteCanonicalAuditStore`. The `api_key_audit` table is left in place but nothing writes to it once the `CanonicalForwardingApiKeyAuditStore` adapter overrides the library's audit store. The doc says only three tables; there are now at minimum four.
|
||
CODE_AREA / auth.apikeys
|
||
SEVERITY / medium
|
||
PROPOSED_FIX / Add `audit_event` as a fourth table (from `SqliteCanonicalAuditStore`). Note that `api_key_audit` is retained by the schema but is no longer written to at runtime (the `CanonicalForwardingApiKeyAuditStore` adapter redirects all writes to `audit_event` via `IAuditWriter`).
|
||
|
||
---
|
||
|
||
DOC / Authentication.md / LINES 134–153
|
||
CLAIM / "`SqliteApiKeyStore` (`IApiKeyStore`) handles the two reads needed at request time: `FindByKeyIdAsync` and `FindActiveByKeyIdAsync`. `MarkKeyUsedAsync` updates `last_used_utc` only for non-revoked rows." Shows `ApiKeyRecordReader.Read` code block with column-ordinal reader.
|
||
CLAIM_TYPE / behavior-rule
|
||
VERDICT / stale
|
||
EVIDENCE / src/ZB.MOM.WW.MxGateway.Server/Security/Authentication/AuthStoreServiceCollectionExtensions.cs:67 — `SqliteApiKeyStore` is in the shared package `ZB.MOM.WW.Auth.ApiKeys`. The code block shown is from the package, not local gateway code. If the package's internal implementation has changed, the doc may be inaccurate. The doc presents this as if it is local gateway source.
|
||
CODE_AREA / auth.apikeys
|
||
SEVERITY / low
|
||
PROPOSED_FIX / Clarify that `SqliteApiKeyStore`, `ApiKeyRecord`, and `ApiKeyRecordReader` are in the shared `ZB.MOM.WW.Auth.ApiKeys` package and are not directly modifiable in this repository. Remove or label the code block as "from shared library."
|
||
|
||
---
|
||
|
||
DOC / Authentication.md / LINES 156–164
|
||
CLAIM / "`SqliteApiKeyAdminStore` (`IApiKeyAdminStore`) implements administrative mutations: `CreateAsync`, `RevokeAsync`, `RotateAsync`, `DeleteAsync`."
|
||
CLAIM_TYPE / term
|
||
VERDICT / stale
|
||
EVIDENCE / src/ZB.MOM.WW.MxGateway.Server/Security/Authentication/AuthStoreServiceCollectionExtensions.cs:67 — `SqliteApiKeyAdminStore` is in `ZB.MOM.WW.Auth.ApiKeys`. The gateway now wraps admin operations through `ApiKeyAdminCommands` (from the same package), not by injecting `IApiKeyAdminStore` directly in the CLI runner. `DashboardSnapshotService` and `DashboardApiKeyManagementService` do consume `IApiKeyAdminStore` directly, which is fine.
|
||
CODE_AREA / auth.apikeys
|
||
SEVERITY / low
|
||
PROPOSED_FIX / Note that `SqliteApiKeyAdminStore` is from the shared library. Note that the gateway CLI runner delegates through `ApiKeyAdminCommands` (shared library), not by calling `IApiKeyAdminStore` directly.
|
||
|
||
---
|
||
|
||
DOC / Authentication.md / LINES 165–183
|
||
CLAIM / "`SqliteAuthStoreMigrator` executes the migration inside a single transaction so a partial failure leaves the database untouched, refuses to start when the on-disk schema version is newer than the binary supports, and idempotently creates the v1 schema." "Operators who manage schema out-of-band can disable the hosted run and use the admin CLI's `init-db` command instead."
|
||
CLAIM_TYPE / behavior-rule
|
||
VERDICT / stale
|
||
EVIDENCE / src/ZB.MOM.WW.MxGateway.Server/Security/Authentication/AuthStoreServiceCollectionExtensions.cs:104 — `SqliteAuthStoreMigrator` is from `ZB.MOM.WW.Auth.ApiKeys` (resolved via `sp.GetRequiredService<SqliteAuthStoreMigrator>()`). The description of its behavior is likely still accurate but is presented as locally-owned code. `AuthStoreMigrationHostedService` is also from the shared package (registered by `AddZbApiKeyAuth`). The code block shown at lines 171–179 is from the package.
|
||
CODE_AREA / auth.apikeys
|
||
SEVERITY / low
|
||
PROPOSED_FIX / Clarify that `SqliteAuthStoreMigrator`, `IAuthStoreMigrator`, and `AuthStoreMigrationHostedService` are from the shared library.
|
||
|
||
---
|
||
|
||
DOC / Authentication.md / LINES 187–208
|
||
CLAIM / CLI subcommand table lists: `init-db`, `create-key`, `list-keys`, `revoke-key`, `rotate-key`. CLI example uses `mxgateway apikey create-key --key-id ops.alice --display-name "Alice (ops)" --scopes read,write`.
|
||
CLAIM_TYPE / command
|
||
VERDICT / wrong
|
||
EVIDENCE / src/ZB.MOM.WW.MxGateway.Server/Security/Authorization/GatewayScopes.cs:5–13 — `GatewayScopes.All` contains `session:open`, `session:close`, `invoke:read`, `invoke:write`, `invoke:secure`, `events:read`, `metadata:read`, `admin`. The values `read` and `write` are not in the scope catalog. `ApiKeyAdminCommandLineParser.ValidateScopes` at line 170–177 would reject `--scopes read,write` as unknown scopes.
|
||
CODE_AREA / auth.scopes
|
||
SEVERITY / high
|
||
PROPOSED_FIX / Replace `--scopes read,write` with valid scope strings, e.g. `--scopes invoke:read,invoke:write`. Update all CLI examples in Authentication.md to use canonical scope strings from `GatewayScopes.All`.
|
||
|
||
---
|
||
|
||
DOC / Authentication.md / LINES 229–248
|
||
CLAIM / "`ApiKeyScopeSerializer.Serialize` writes a JSON array sorted with `StringComparer.Ordinal`." Code block shown.
|
||
CLAIM_TYPE / behavior-rule
|
||
VERDICT / stale
|
||
EVIDENCE / src/ZB.MOM.WW.MxGateway.Server/Security/Authentication/AuthStoreServiceCollectionExtensions.cs:5 — `ApiKeyScopeSerializer` is from the shared `ZB.MOM.WW.Auth.ApiKeys` package. The behavior described is likely correct but is presented as local gateway code.
|
||
CODE_AREA / auth.apikeys
|
||
SEVERITY / low
|
||
PROPOSED_FIX / Note that `ApiKeyScopeSerializer` is in the shared `ZB.MOM.WW.Auth.ApiKeys` library.
|
||
|
||
---
|
||
|
||
DOC / Authorization.md / LINES 107–113
|
||
CLAIM / Scope resolver code block includes `TestConnectionRequest or GetLastDeployTimeRequest or DiscoverHierarchyRequest or WatchDeployEventsRequest => GatewayScopes.MetadataRead`.
|
||
CLAIM_TYPE / rpc/proto
|
||
VERDICT / stale
|
||
EVIDENCE / src/ZB.MOM.WW.MxGateway.Server/Security/Authorization/GatewayGrpcScopeResolver.cs:23–28 — the actual resolver also includes `BrowseChildrenRequest => GatewayScopes.MetadataRead` in the same arm. `BrowseChildrenRequest` was added (per docs/plans/2026-05-28-lazy-browse-implementation.md) but the code block in Authorization.md was not updated.
|
||
CODE_AREA / auth.scopes
|
||
SEVERITY / high
|
||
PROPOSED_FIX / Add `BrowseChildrenRequest` to the `MetadataRead` arm of the scope resolver code block. Update the scope catalog table at line 212 to include `GalaxyRepository.BrowseChildren` in the `MetadataRead` row.
|
||
|
||
---
|
||
|
||
DOC / Authorization.md / LINE 212
|
||
CLAIM / Scope catalog table row: `MetadataRead` / `metadata:read` / "`MxCommandKind.ArchestraUserToId`, `MxCommandKind.GetSessionState`, `MxCommandKind.GetWorkerInfo`, `GalaxyRepository.TestConnection`, `GalaxyRepository.GetLastDeployTime`, `GalaxyRepository.DiscoverHierarchy`, `GalaxyRepository.WatchDeployEvents`".
|
||
CLAIM_TYPE / rpc/proto
|
||
VERDICT / stale
|
||
EVIDENCE / src/ZB.MOM.WW.MxGateway.Server/Security/Authorization/GatewayGrpcScopeResolver.cs:27 — `BrowseChildrenRequest` is also mapped to `metadata:read` but is absent from the table.
|
||
CODE_AREA / auth.scopes
|
||
SEVERITY / high
|
||
PROPOSED_FIX / Add `GalaxyRepository.BrowseChildren` to the `MetadataRead` row of the scope catalog table.
|
||
|
||
---
|
||
|
||
DOC / Authorization.md / LINES 260–270
|
||
CLAIM / Registration code block for `AddGatewayGrpcAuthorization` shows three `AddSingleton` calls: `GatewayGrpcScopeResolver`, `IGatewayRequestIdentityAccessor`/`GatewayRequestIdentityAccessor`, `GatewayGrpcAuthorizationInterceptor`, then `AddGrpc`.
|
||
CLAIM_TYPE / behavior-rule
|
||
VERDICT / stale
|
||
EVIDENCE / src/ZB.MOM.WW.MxGateway.Server/Security/Authorization/GrpcAuthorizationServiceCollectionExtensions.cs:18–31 — the actual method also registers `IConstraintEnforcer`/`ConstraintEnforcer` as a singleton (line 20) and configures `GrpcServiceOptions` with `MaxReceiveMessageSize`/`MaxSendMessageSize` from `MxGateway:Protocol`. The doc code block omits both.
|
||
CODE_AREA / auth.scopes
|
||
SEVERITY / medium
|
||
PROPOSED_FIX / Update the Registration code block to include `services.AddSingleton<IConstraintEnforcer, ConstraintEnforcer>()` and the `AddOptions<GrpcServiceOptions>` configuration block for message size limits.
|
||
|
||
---
|
||
|
||
DOC / Authorization.md / LINE 273
|
||
CLAIM / "none of the three classes hold per-request state on instance fields"
|
||
CLAIM_TYPE / behavior-rule
|
||
VERDICT / stale
|
||
EVIDENCE / src/ZB.MOM.WW.MxGateway.Server/Security/Authorization/GrpcAuthorizationServiceCollectionExtensions.cs:20 — there are now four singleton classes registered by `AddGatewayGrpcAuthorization` (`GatewayGrpcScopeResolver`, `GatewayRequestIdentityAccessor`, `GatewayGrpcAuthorizationInterceptor`, `ConstraintEnforcer`), not three.
|
||
CODE_AREA / auth.scopes
|
||
SEVERITY / low
|
||
PROPOSED_FIX / Update "three classes" to "four classes."
|
||
|
||
---
|
||
|
||
DOC / glauth.md / LINES 63–66
|
||
CLAIM / "`LdapOptions.RequiredGroup` defaults to `GwAdmin`, so the dashboard login and `DashboardLdapLiveTests` require `admin` to be a member of a `GwAdmin` group."
|
||
CLAIM_TYPE / config-key
|
||
VERDICT / wrong
|
||
EVIDENCE / src/ZB.MOM.WW.MxGateway.Server/Configuration/LdapOptions.cs — no `RequiredGroup` field exists on the gateway's `LdapOptions`. The gateway enforces group membership via `MxGateway:Dashboard:GroupToRole` (a dictionary mapping LDAP group names to dashboard roles) in `DashboardOptions`. Authorization succeeds if the user's LDAP groups map to at least one role — there is no `RequiredGroup` concept in the current architecture.
|
||
CODE_AREA / auth.ldap
|
||
SEVERITY / high
|
||
PROPOSED_FIX / Remove the sentence "`LdapOptions.RequiredGroup` defaults to `GwAdmin`." Replace with: the dashboard enforces that at least one of the user's LDAP groups appears in `MxGateway:Dashboard:GroupToRole` (e.g. `GwAdmin: Administrator`); a login with no matching group is rejected. `DashboardLdapLiveTests` seeds the role map with `GwAdmin -> Administrator`.
|
||
|
||
---
|
||
|
||
DOC / glauth.md / LINES 181–182
|
||
CLAIM / "the authenticator strips to `GwAdmin` and matches against `RequiredGroup`"
|
||
CLAIM_TYPE / behavior-rule
|
||
VERDICT / wrong
|
||
EVIDENCE / src/ZB.MOM.WW.MxGateway.Server/Dashboard/DashboardGroupRoleMapping.cs:35–48 — the shared `ILdapAuthService` strips the leading RDN value from each group DN, and the gateway's `DashboardGroupRoleMapper` looks up the short name in `GroupToRole`. There is no `RequiredGroup` property or concept anywhere in the codebase.
|
||
CODE_AREA / auth.ldap
|
||
SEVERITY / high
|
||
PROPOSED_FIX / Replace "matches against `RequiredGroup`" with "looks up the short RDN name (e.g. `GwAdmin`) in `MxGateway:Dashboard:GroupToRole`."
|
||
|
||
---
|
||
|
||
DOC / glauth.md / LINES 113–136
|
||
CLAIM / "Suggested mxgw configuration shape" YAML block uses config keys `useTls`, `allowInsecureLdap`, `userNameAttribute`.
|
||
CLAIM_TYPE / config-key
|
||
VERDICT / wrong
|
||
EVIDENCE / src/ZB.MOM.WW.MxGateway.Server/Configuration/LdapOptions.cs:49,52,64 — the current config keys (as bound by the shared `LdapOptions` and the gateway's shadow `LdapOptions`) are `Transport` (an enum: `None`/`Ldaps`/`StartTls`), `AllowInsecure` (bool), `UserNameAttribute` (string, default `"cn"` not `"uid"`). The YAML block uses stale camelCase key names from a pre-migration configuration shape.
|
||
CODE_AREA / auth.ldap
|
||
SEVERITY / high
|
||
PROPOSED_FIX / Update the YAML config example to use `Transport: None` (or `Ldaps`/`StartTls`) instead of `useTls: false`, `AllowInsecure: true` instead of `allowInsecureLdap: true`, `UserNameAttribute: "cn"` (gateway default; note GLAuth populates `cn` not `uid` per the gateway default). Rename the section header from `ldap:` to `MxGateway: Ldap:` to match the actual config path.
|
||
|
||
---
|
||
|
||
DOC / glauth.md / LINE 128
|
||
CLAIM / `userNameAttribute: "uid" # GLAuth populates this; AD uses sAMAccountName`
|
||
CLAIM_TYPE / config-key
|
||
VERDICT / wrong
|
||
EVIDENCE / src/ZB.MOM.WW.MxGateway.Server/Configuration/LdapOptions.cs:64 — the gateway `LdapOptions` default for `UserNameAttribute` is `"cn"`, not `"uid"`. GLAuth does populate both `uid` and `cn`, but the gateway ships `"cn"` as default.
|
||
CODE_AREA / auth.ldap
|
||
SEVERITY / medium
|
||
PROPOSED_FIX / Change example to `UserNameAttribute: "cn"` with a note that the gateway default is `cn`; to use `uid` instead set `MxGateway:Ldap:UserNameAttribute: uid`.
|
||
|
||
---
|
||
|
||
DOC / glauth.md / LINES 261–269
|
||
CLAIM / AD migration cheat-sheet uses field names `UseTls` and `AllowInsecureLdap`.
|
||
CLAIM_TYPE / config-key
|
||
VERDICT / wrong
|
||
EVIDENCE / src/ZB.MOM.WW.MxGateway.Server/Configuration/LdapOptions.cs:49,52 — these fields were renamed: `UseTls` → `Transport` (enum), `AllowInsecureLdap` → `AllowInsecure`.
|
||
CODE_AREA / auth.ldap
|
||
SEVERITY / high
|
||
PROPOSED_FIX / Update the AD migration table: rename `UseTls` row to `Transport` (GLAuth dev value: `None`, AD value: `Ldaps`); rename `AllowInsecureLdap` row to `AllowInsecure` (GLAuth dev: `true`, AD: `false`).
|
||
|
||
---
|
||
|
||
DOC / CLAUDE.md / LINE 119
|
||
CLAIM / "maps the user's LDAP groups to `Admin` or `Viewer` via `MxGateway:Dashboard:GroupToRole`, then issues an HTTP-only secure `__Host-MxGatewayDashboard` cookie"
|
||
CLAIM_TYPE / term
|
||
VERDICT / wrong
|
||
EVIDENCE / src/ZB.MOM.WW.MxGateway.Server/Dashboard/DashboardAuthenticationDefaults.cs:38 — the cookie name constant is `CookieName = "MxGatewayDashboard"` (no `__Host-` prefix). `__Host-` is a browser security prefix that requires `Path=/`, no `Domain`, and `Secure` — the code sets `Path = "/"` and `SecurePolicy = Always` by default, satisfying the requirements, but the actual cookie name in the constant and in `ZbCookieDefaults.Apply` is `MxGatewayDashboard`, not `__Host-MxGatewayDashboard`. Additionally, `Admin` should be `Administrator` (the renamed role value per `DashboardRoles.Admin = "Administrator"`).
|
||
CODE_AREA / auth.cookie
|
||
SEVERITY / high
|
||
PROPOSED_FIX / Change `__Host-MxGatewayDashboard` to `MxGatewayDashboard` in CLAUDE.md. Change `Admin` to `Administrator`.
|
||
|
||
---
|
||
|
||
DOC / CLAUDE.md / LINE 119
|
||
CLAIM / "maps the user's LDAP groups to `Admin` or `Viewer`"
|
||
CLAIM_TYPE / term
|
||
VERDICT / wrong
|
||
EVIDENCE / src/ZB.MOM.WW.MxGateway.Server/Dashboard/DashboardRoles.cs:14 — `DashboardRoles.Admin = "Administrator"` (not `"Admin"`). The role value was renamed in Task 1.7. CLAUDE.md was not updated.
|
||
CODE_AREA / auth.roles
|
||
SEVERITY / high
|
||
PROPOSED_FIX / Change `Admin` to `Administrator` in the CLAUDE.md authentication paragraph.
|
||
|
||
---
|
||
|
||
DOC / CLAUDE.md / LINE 35
|
||
CLAIM / `dotnet run --project src/MxGateway.Server/MxGateway.Server.csproj -- apikey create --display-name "dev" --scopes session,invoke,event,metadata,admin`
|
||
CLAIM_TYPE / command
|
||
VERDICT / wrong
|
||
EVIDENCE / src/ZB.MOM.WW.MxGateway.Server/Security/Authorization/GatewayScopes.cs:5–13 — canonical scopes are `session:open`, `session:close`, `invoke:read`, `invoke:write`, `invoke:secure`, `events:read`, `metadata:read`, `admin`. The shorthand values `session`, `invoke`, `event`, `metadata` are not recognized and would be rejected by `ApiKeyAdminCommandLineParser.ValidateScopes` as unknown scopes. Also, the subcommand is `create-key` not `create`.
|
||
CODE_AREA / auth.scopes
|
||
SEVERITY / high
|
||
PROPOSED_FIX / Replace the example with a valid invocation, e.g.: `dotnet run --project src/MxGateway.Server/MxGateway.Server.csproj -- apikey create-key --key-id dev --display-name "dev" --scopes session:open,session:close,invoke:read,invoke:write,events:read,metadata:read,admin`
|
||
|
||
---
|
||
|
||
DOC / CLAUDE.md / LINE 117
|
||
CLAIM / "Keys are stored hashed (with a peppered SHA) in a gateway-owned SQLite DB (default `C:\ProgramData\MxGateway\gateway-auth.db`). Scopes (`session`, `invoke`, `event`, `metadata`, `admin`) gate specific RPCs"
|
||
CLAIM_TYPE / config-key
|
||
VERDICT / wrong
|
||
EVIDENCE / src/ZB.MOM.WW.MxGateway.Server/Configuration/AuthenticationOptions.cs:9 — SQLite path default is correct. However, scope names `session`, `invoke`, `event`, `metadata` are not the canonical scope strings. Actual scopes are `session:open`, `session:close`, `invoke:read`, `invoke:write`, `invoke:secure`, `events:read`, `metadata:read`, `admin`.
|
||
CODE_AREA / auth.scopes
|
||
SEVERITY / high
|
||
PROPOSED_FIX / Replace the scope shorthand list with the full canonical scope strings from `GatewayScopes.All`. The SQLite path is accurate and should be kept.
|
||
|
||
---
|
||
|
||
DOC / glauth.md / LINES 70–74
|
||
CLAIM / "> **Dashboard role value (Task 1.7):** the LDAP `GwAdmin` group now maps to the canonical dashboard role **`Administrator`** (was `Admin`); `GwReader` maps to `Viewer`."
|
||
CLAIM_TYPE / term
|
||
VERDICT / accurate
|
||
EVIDENCE / src/ZB.MOM.WW.MxGateway.Server/Dashboard/DashboardRoles.cs:14 — `DashboardRoles.Admin = "Administrator"`, `DashboardRoles.Viewer = "Viewer"`. src/ZB.MOM.WW.MxGateway.Server/appsettings.json:63–64 confirms `"GwAdmin": "Administrator"`, `"GwReader": "Viewer"`.
|
||
CODE_AREA / auth.roles
|
||
SEVERITY / low
|
||
PROPOSED_FIX / flag only
|
||
|
||
---
|
||
|
||
DOC / glauth.md / LINES 21–26
|
||
CLAIM / Connection details: Protocol LDAP, Host `localhost`, Port `3893`, Base DN `dc=zb,dc=local`, Bind DN format `cn={username},dc=zb,dc=local`, Group OU `ou=<groupname>,ou=groups,dc=zb,dc=local`.
|
||
CLAIM_TYPE / config-key
|
||
VERDICT / accurate
|
||
EVIDENCE / src/ZB.MOM.WW.MxGateway.Server/Configuration/LdapOptions.cs:36,39,55,58 — defaults: `Server=localhost`, `Port=3893`, `SearchBase=dc=zb,dc=local`, `ServiceAccountDn=cn=serviceaccount,dc=zb,dc=local`.
|
||
CODE_AREA / auth.ldap
|
||
SEVERITY / low
|
||
PROPOSED_FIX / flag only
|
||
|
||
---
|
||
|
||
DOC / docs/Authentication.md / LINES 1–30
|
||
CLAIM / Token format `mxgw_<keyId>_<secret>`, prefix `mxgw_`, parser is `ApiKeyParser` behind `IApiKeyParser`.
|
||
CLAIM_TYPE / term
|
||
VERDICT / accurate
|
||
EVIDENCE / src/ZB.MOM.WW.MxGateway.Server/Security/Authentication/AuthStoreServiceCollectionExtensions.cs:30,33 — `TokenPrefix = "mxgw"`, `PepperSecretName = "MxGateway:ApiKeyPepper"`. The token format claim is accurate; `IApiKeyParser`/`ApiKeyParser` are from the shared package but the behavior description matches.
|
||
CODE_AREA / auth.apikeys
|
||
SEVERITY / low
|
||
PROPOSED_FIX / flag only
|
||
|
||
---
|
||
|
||
DOC / docs/Authentication.md / LINE 110
|
||
CLAIM / "`AuthSqliteConnectionFactory` reads `GatewayOptions.Authentication.SqlitePath`"
|
||
CLAIM_TYPE / config-key
|
||
VERDICT / accurate
|
||
EVIDENCE / src/ZB.MOM.WW.MxGateway.Server/Configuration/AuthenticationOptions.cs:9 — `SqlitePath` default is `C:\ProgramData\MxGateway\gateway-auth.db`. The factory reads from `ApiKeyOptions.SqlitePath` which is bound from `MxGateway:Authentication:SqlitePath`, so the effective config key path matches `GatewayOptions.Authentication.SqlitePath`.
|
||
CODE_AREA / auth.apikeys
|
||
SEVERITY / low
|
||
PROPOSED_FIX / flag only
|
||
|
||
---
|
||
|
||
DOC / docs/Authentication.md / LINES 189–208
|
||
CLAIM / CLI subcommands: `init-db`, `create-key`, `list-keys`, `revoke-key`, `rotate-key`.
|
||
CLAIM_TYPE / command
|
||
VERDICT / accurate
|
||
EVIDENCE / src/ZB.MOM.WW.MxGateway.Server/Security/Authentication/ApiKeyAdminCommandKind.cs — enum has `InitDb`, `CreateKey`, `ListKeys`, `RevokeKey`, `RotateKey`. ApiKeyAdminCommandLineParser.cs maps these to exactly those string values.
|
||
CODE_AREA / auth.apikeys
|
||
SEVERITY / low
|
||
PROPOSED_FIX / flag only
|
||
|
||
---
|
||
|
||
DOC / docs/Authentication.md / LINES 220–225
|
||
CLAIM / "Every destructive dashboard action is gated by a confirmation dialog and emits its own audit event (`dashboard-create-key`, `dashboard-rotate-key`, `dashboard-revoke-key`, `dashboard-delete-key`)."
|
||
CLAIM_TYPE / behavior-rule
|
||
VERDICT / accurate
|
||
EVIDENCE / src/ZB.MOM.WW.MxGateway.Server/Dashboard/DashboardApiKeyManagementService.cs:69,201 — audit event strings `dashboard-create-key` and `dashboard-delete-key` confirmed in code.
|
||
CODE_AREA / auth.apikeys
|
||
SEVERITY / low
|
||
PROPOSED_FIX / flag only
|
||
|
||
---
|
||
|
||
DOC / docs/Authorization.md / LINES 94–116
|
||
CLAIM / Scope resolver switches on request type; `_ => GatewayScopes.Admin` fallback for unrecognized types.
|
||
CLAIM_TYPE / behavior-rule
|
||
VERDICT / accurate
|
||
EVIDENCE / src/ZB.MOM.WW.MxGateway.Server/Security/Authorization/GatewayGrpcScopeResolver.cs:13–29 — the pattern and fallback match exactly.
|
||
CODE_AREA / auth.scopes
|
||
SEVERITY / low
|
||
PROPOSED_FIX / flag only
|
||
|
||
---
|
||
|
||
DOC / docs/Authorization.md / LINE 85
|
||
CLAIM / "If `GatewayOptions.Authentication.Mode` is `AuthenticationMode.Disabled`, the helper returns `null` immediately. No identity is pushed onto the accessor and the continuation runs without scope enforcement. This matches the `AuthenticationMode` enum, which only defines `ApiKey` and `Disabled`."
|
||
CLAIM_TYPE / behavior-rule
|
||
VERDICT / accurate
|
||
EVIDENCE / src/ZB.MOM.WW.MxGateway.Server/Security/Authorization/GatewayGrpcAuthorizationInterceptor.cs:59 — confirmed.
|
||
CODE_AREA / auth.apikeys
|
||
SEVERITY / low
|
||
PROPOSED_FIX / flag only
|
||
|
||
---
|
||
|
||
DOC / docs/Authorization.md / LINE 215
|
||
CLAIM / "The `Admin` constant is also referenced by `DashboardAuthenticator` and `DashboardAuthorizationHandler` so that the dashboard and the gRPC layer agree on what 'admin' means."
|
||
CLAIM_TYPE / behavior-rule
|
||
VERDICT / stale
|
||
EVIDENCE / src/ZB.MOM.WW.MxGateway.Server/Dashboard/DashboardAuthenticator.cs — `DashboardAuthenticator` does not reference `GatewayScopes.Admin`. The `admin` gRPC scope and the `Administrator` dashboard role are separate concepts. The dashboard authorization policy uses `DashboardRoles.Admin = "Administrator"`, not `GatewayScopes.Admin = "admin"`. These are distinct and do not share a constant.
|
||
CODE_AREA / auth.roles
|
||
SEVERITY / medium
|
||
PROPOSED_FIX / Correct or remove the claim that `GatewayScopes.Admin` is referenced by `DashboardAuthenticator`. The dashboard and gRPC "admin" are deliberately separate concepts — the dashboard role is `Administrator` (a role claim value on the ClaimsPrincipal), while the gRPC scope is the literal string `"admin"` (a scope string on ApiKeyIdentity).
|
||
|
||
---
|
||
|
||
DOC / docs/Authorization.md / LINE 116
|
||
CLAIM / "`AcknowledgeAlarm` is treated as a write — it mutates alarm state, mirroring `MxCommandKind.Write*` — and `StreamAlarms` shares the alarm/event surface with `StreamEvents` and `MxCommandKind.DrainEvents`, so it carries `events:read`. Both alarm RPCs are session-less."
|
||
CLAIM_TYPE / behavior-rule
|
||
VERDICT / accurate
|
||
EVIDENCE / src/ZB.MOM.WW.MxGateway.Server/Security/Authorization/GatewayGrpcScopeResolver.cs:21,22 — `AcknowledgeAlarmRequest => GatewayScopes.InvokeWrite`, `StreamAlarmsRequest => GatewayScopes.EventsRead`. Confirmed.
|
||
CODE_AREA / auth.scopes
|
||
SEVERITY / low
|
||
PROPOSED_FIX / flag only
|
||
|
||
---
|
||
|
||
DOC / docs/Authorization.md / LINES 205–215
|
||
CLAIM / Scope catalog table — all scope strings and their `Required For` mappings.
|
||
CLAIM_TYPE / rpc/proto
|
||
VERDICT / stale
|
||
EVIDENCE / GatewayGrpcScopeResolver.cs:27 — `BrowseChildrenRequest` is missing from the `MetadataRead` row (already captured above). All other rows are accurate.
|
||
CODE_AREA / auth.scopes
|
||
SEVERITY / high
|
||
PROPOSED_FIX / (Same as finding above — add `GalaxyRepository.BrowseChildren` to `MetadataRead` row.)
|
||
|
||
---
|
||
|
||
## GAP FINDINGS (auth behavior in code but undocumented)
|
||
|
||
DOC / (none — gap)
|
||
CLAIM / `DashboardAuthenticationDefaults.CookieName` is the default cookie name `"MxGatewayDashboard"`, but `DashboardOptions.CookieName` allows a per-deployment override via `MxGateway:Dashboard:CookieName`. Auth docs do not mention this override.
|
||
CLAIM_TYPE / config-key
|
||
VERDICT / gap
|
||
EVIDENCE / src/ZB.MOM.WW.MxGateway.Server/Dashboard/DashboardServiceCollectionExtensions.cs:91–97, src/ZB.MOM.WW.MxGateway.Server/Configuration/DashboardOptions.cs:33.
|
||
CODE_AREA / auth.cookie
|
||
SEVERITY / medium
|
||
PROPOSED_FIX / Add documentation of `MxGateway:Dashboard:CookieName` override and when to use it (multiple gateway instances sharing a hostname).
|
||
|
||
---
|
||
|
||
DOC / (none — gap)
|
||
CLAIM / The dashboard cookie idle timeout is 8 hours (set by `ZbCookieDefaults.Apply` with `idleTimeout: TimeSpan.FromHours(8)`). The hub bearer token expires in 30 minutes (`HubTokenService.TokenLifetime = TimeSpan.FromMinutes(30)`). Neither timeout is documented in Authentication.md.
|
||
CLAIM_TYPE / behavior-rule
|
||
VERDICT / gap
|
||
EVIDENCE / src/ZB.MOM.WW.MxGateway.Server/Dashboard/DashboardServiceCollectionExtensions.cs:66, src/ZB.MOM.WW.MxGateway.Server/Dashboard/HubTokenService.cs:29.
|
||
CODE_AREA / auth.hub
|
||
SEVERITY / medium
|
||
PROPOSED_FIX / Add a section in Authentication.md (or GatewayDashboardDesign.md) documenting the 8-hour dashboard cookie idle timeout and the 30-minute hub bearer token lifetime.
|
||
|
||
---
|
||
|
||
DOC / (none — gap)
|
||
CLAIM / The `CanonicalForwardingApiKeyAuditStore` overrides the shared library's `IApiKeyAuditStore`. As a result, the `api_key_audit` table in the SQLite DB is written by the shared library's migration but is NOT written to at runtime — all audit records go to `audit_event` via `IAuditWriter`. This is operationally important for anyone reading the DB directly but is not documented.
|
||
CLAIM_TYPE / behavior-rule
|
||
VERDICT / gap
|
||
EVIDENCE / src/ZB.MOM.WW.MxGateway.Server/Security/Authentication/AuthStoreServiceCollectionExtensions.cs:85–94, src/ZB.MOM.WW.MxGateway.Server/Security/Audit/CanonicalForwardingApiKeyAuditStore.cs.
|
||
CODE_AREA / auth.apikeys
|
||
SEVERITY / medium
|
||
PROPOSED_FIX / Document in Authentication.md that `api_key_audit` exists in the schema but is unused at runtime; all audit events flow to `audit_event` via `IAuditWriter`/`SqliteCanonicalAuditStore`.
|
||
|
||
---
|
||
|
||
DOC / (none — gap)
|
||
CLAIM / `DashboardOptions.RequireHttpsCookie` (default `true`) controls whether the dashboard cookie uses `SecurePolicy.Always` or `SameAsRequest`. Setting it `false` is required for plain-HTTP dev deployments. This config key is not mentioned in auth docs.
|
||
CLAIM_TYPE / config-key
|
||
VERDICT / gap
|
||
EVIDENCE / src/ZB.MOM.WW.MxGateway.Server/Configuration/DashboardOptions.cs:22, src/ZB.MOM.WW.MxGateway.Server/Dashboard/DashboardServiceCollectionExtensions.cs:87.
|
||
CODE_AREA / auth.cookie
|
||
SEVERITY / low
|
||
PROPOSED_FIX / Reference `MxGateway:Dashboard:RequireHttpsCookie` in the auth cookie documentation.
|
||
|
||
---
|
||
|
||
DOC / (none — gap)
|
||
CLAIM / `ZbClaimTypes` and `ZbCookieDefaults` (from `ZB.MOM.WW.Auth.AspNetCore` package) are now used for claim and cookie setup. Authentication.md does not mention the shared library claim types (`zb:username`, `zb:displayname`) or that cookie hardening defaults come from `ZbCookieDefaults.Apply`.
|
||
CLAIM_TYPE / behavior-rule
|
||
VERDICT / gap
|
||
EVIDENCE / src/ZB.MOM.WW.MxGateway.Server/Dashboard/DashboardAuthenticator.cs:111–115, src/ZB.MOM.WW.MxGateway.Server/Dashboard/DashboardServiceCollectionExtensions.cs:66.
|
||
CODE_AREA / auth.cookie
|
||
SEVERITY / low
|
||
PROPOSED_FIX / Add a brief note in dashboard auth documentation about `ZbClaimTypes` (`zb:username`, `zb:displayname`, `zb:name`, `zb:role`) and `ZbCookieDefaults.Apply` providing cookie security defaults.
|