using Grpc.Core; using Microsoft.Extensions.Options; using ZB.MOM.WW.GalaxyRepository.Grpc; using ZB.MOM.WW.MxGateway.Server.Configuration; using ZB.MOM.WW.MxGateway.Server.Security.Authentication; namespace ZB.MOM.WW.MxGateway.Server.Security.Authorization; /// Scopes Galaxy browse results to the calling API key's BrowseSubtrees constraint. /// /// Distinguishes two states the earlier code collapsed into one: "authenticated, no /// BrowseSubtrees constraint" (legitimately unscoped — the common case) and "no ambient identity /// at all" (the interceptor-ordering invariant below is broken). The second used to resolve to the /// full hierarchy silently, which made a scoping bypass invisible; it now denies loudly. /// /// Accessor for the ambient per-call API-key identity. /// Gateway options supplying MxGateway:Authentication:Mode. /// Logger used to name the broken invariant when identity is absent. public sealed class GatewayBrowseScopeProvider( IGatewayRequestIdentityAccessor identityAccessor, IOptions options, ILogger logger) : IGalaxyBrowseScopeProvider { /// Resolves the Galaxy browse subtrees the calling API key is scoped to, from the ambient request identity. /// The gRPC server call context for the in-flight browse request. /// The caller's configured browse subtrees, or /empty when the caller is unscoped (full hierarchy). /// /// when API-key authentication is enabled but no /// ambient identity is present — the invariant described below has been broken and the /// request's scope cannot be established. /// public IReadOnlyList? ResolveBrowseSubtrees(ServerCallContext context) { // Invariant: the caller identity is the ambient one pushed by // GatewayGrpcAuthorizationInterceptor earlier in this same call. That interceptor is // registered globally and therefore runs before the library's galaxy service, so // Current is populated for any authenticated galaxy RPC. Do not reorder the interceptor // without revisiting this. ApiKeyIdentity? identity = identityAccessor.Current; if (identity is not null) { return identity.EffectiveConstraints.BrowseSubtrees; } // Mode=Disabled is the one configuration in which identity-absent is expected rather than // broken: nothing authenticates, so no key and no constraints exist to scope by, and the // deployment has already accepted an unauthenticated gRPC surface. Empty BrowseSubtrees is // what the library reads as "no scoping". if (options.Value.Authentication.Mode == AuthenticationMode.Disabled) { return ApiKeyConstraints.Empty.BrowseSubtrees; } // Authentication is on, so an identity MUST have been pushed before the galaxy service ran. // Failing open here would hand a browse-scoped key the full hierarchy with nothing logged // and nothing failing — a constraint-narrowing control's absence is invisible in normal // operation. Deny instead, and name the invariant so the misconfiguration is diagnosable // from one log line rather than from a scoping audit nobody thought to run. logger.LogWarning( "Galaxy browse scope could not be resolved for {Method}: no ambient API-key identity " + "while Authentication:Mode is {Mode}. GatewayGrpcAuthorizationInterceptor must run " + "before the Galaxy service on every call; the request is denied rather than served " + "unscoped.", context.Method, options.Value.Authentication.Mode); throw new RpcException(new Status( StatusCode.PermissionDenied, "Browse scope could not be established for this request.")); } }