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."));
}
}