fix(GWC-38): deny Galaxy browse when the ambient identity is absent under API-key auth
GatewayBrowseScopeProvider resolved `identityAccessor.Current?.EffectiveConstraints ?? ApiKeyConstraints.Empty`, so an absent ambient identity produced an empty BrowseSubtrees list — which the shared GalaxyRepository library reads as "no scoping". The control's fail state was therefore open: any future path reaching the library's galaxy service without GatewayGrpcAuthorizationInterceptor having pushed the identity would silently hand a browse-scoped key the full hierarchy, with nothing failing and nothing logged. A constraint-narrowing control's absence is invisible in normal operation, which is what made the honest "safe only because the global interceptor runs first" comment insufficient on its own. The provider now distinguishes the two states the old expression collapsed: - authenticated key with no BrowseSubtrees constraint -> empty list, unscoped (unchanged, and now asserted in its own test rather than implied); - no ambient identity with Authentication:Mode=ApiKey -> log a warning naming the broken ordering invariant and throw RpcException(PermissionDenied); - no ambient identity with Authentication:Mode=Disabled -> unscoped, because nothing authenticates there so no key and no constraints exist to scope by. The registration-order comment at GatewayApplication.cs now says the ordering is still required but no longer silently trusted. Tests: GatewayBrowseScopeProviderTests gains the PermissionDenied case and the Disabled-mode case alongside the unconstrained-identity one. GalaxyRepositoryHostWiringTests phase 1 now pushes an identity carrying no BrowseSubtrees instead of pushing none — that, not an absent identity, is production's unscoped case. Docs: docs/Authorization.md's constraint section states the two-state distinction and both fail behaviours under browse_subtrees.
This commit is contained in:
@@ -180,6 +180,21 @@ Supported constraints are:
|
||||
| `read_historized_only` | Read/subscription commands must target objects with historized attributes. |
|
||||
| `dashboard_tags` | Dashboard event-visibility tags. **Not a data-access constraint** — see below. |
|
||||
|
||||
`browse_subtrees` is applied by `GatewayBrowseScopeProvider`, the host seam the
|
||||
shared `ZB.MOM.WW.GalaxyRepository` package calls through `IGalaxyBrowseScopeProvider`;
|
||||
it reads the constraint off the ambient per-call identity that
|
||||
`GatewayGrpcAuthorizationInterceptor` pushed earlier in the same call. An empty
|
||||
`browse_subtrees` list is the library's "no scoping" signal, so the provider has to
|
||||
distinguish two states that look alike (GWC-38): an authenticated key with no browse
|
||||
constraint is legitimately unscoped and returns the empty list, whereas **no ambient
|
||||
identity at all** means the interceptor did not run before the Galaxy service and the
|
||||
request's scope cannot be established. In that second case, with
|
||||
`MxGateway:Authentication:Mode = ApiKey`, the provider logs a warning naming the broken
|
||||
ordering invariant and fails the call with `PermissionDenied` rather than serving the full
|
||||
hierarchy — a narrowing control's silent absence is invisible in normal operation, so it is
|
||||
made loud instead. Under `Mode = Disabled` an absent identity is the expected steady state
|
||||
(nothing authenticates, so no key and no constraints exist) and browse stays unscoped.
|
||||
|
||||
`dashboard_tags` is the one member of the blob that constrains nothing on the
|
||||
gRPC data path. No read, write, browse, or subscribe check consults it, and
|
||||
`HasReadConstraints` / `HasWriteConstraints` deliberately ignore it: adding a tag
|
||||
|
||||
Reference in New Issue
Block a user