fix(auth): authenticate client- and duplex-streaming gRPC calls
GatewayGrpcAuthorizationInterceptor only overrode the unary and server-streaming handlers; the base Interceptor passes client- and duplex-streaming calls through unauthenticated. Both now run the same API-key pipeline and fail closed to the admin scope, since there is no single request message to resolve a scope from.
This commit is contained in:
+49
-8
@@ -30,7 +30,10 @@ public sealed class GatewayGrpcAuthorizationInterceptor(
|
||||
ServerCallContext context,
|
||||
UnaryServerMethod<TRequest, TResponse> continuation)
|
||||
{
|
||||
ApiKeyIdentity? identity = await AuthenticateAndAuthorizeAsync(request, context).ConfigureAwait(false);
|
||||
ApiKeyIdentity? identity = await AuthenticateAndAuthorizeAsync(
|
||||
scopeResolver.ResolveRequiredScope(request),
|
||||
context)
|
||||
.ConfigureAwait(false);
|
||||
IDisposable? identityScope = identity is null ? null : identityAccessor.Push(identity);
|
||||
using (identityScope)
|
||||
{
|
||||
@@ -45,7 +48,10 @@ public sealed class GatewayGrpcAuthorizationInterceptor(
|
||||
ServerCallContext context,
|
||||
ServerStreamingServerMethod<TRequest, TResponse> continuation)
|
||||
{
|
||||
ApiKeyIdentity? identity = await AuthenticateAndAuthorizeAsync(request, context).ConfigureAwait(false);
|
||||
ApiKeyIdentity? identity = await AuthenticateAndAuthorizeAsync(
|
||||
scopeResolver.ResolveRequiredScope(request),
|
||||
context)
|
||||
.ConfigureAwait(false);
|
||||
IDisposable? identityScope = identity is null ? null : identityAccessor.Push(identity);
|
||||
using (identityScope)
|
||||
{
|
||||
@@ -53,15 +59,51 @@ public sealed class GatewayGrpcAuthorizationInterceptor(
|
||||
}
|
||||
}
|
||||
|
||||
// The base Interceptor passes client- and duplex-streaming calls straight to the handler, so
|
||||
// without these overrides any such RPC (none exist today, but a .proto edit could add one, and
|
||||
// the shared Galaxy Repository service is mapped through the same interceptor) would be
|
||||
// reachable with no API key at all. Both run the identical authentication path. There is no
|
||||
// single request message to derive a scope from before the handler reads the stream, so they
|
||||
// fail closed to the scope the resolver assigns an unrecognized request: admin.
|
||||
|
||||
/// <inheritdoc />
|
||||
public override async Task<TResponse> ClientStreamingServerHandler<TRequest, TResponse>(
|
||||
IAsyncStreamReader<TRequest> requestStream,
|
||||
ServerCallContext context,
|
||||
ClientStreamingServerMethod<TRequest, TResponse> continuation)
|
||||
{
|
||||
ApiKeyIdentity? identity = await AuthenticateAndAuthorizeAsync(GatewayScopes.Admin, context)
|
||||
.ConfigureAwait(false);
|
||||
IDisposable? identityScope = identity is null ? null : identityAccessor.Push(identity);
|
||||
using (identityScope)
|
||||
{
|
||||
return await continuation(requestStream, context).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override async Task DuplexStreamingServerHandler<TRequest, TResponse>(
|
||||
IAsyncStreamReader<TRequest> requestStream,
|
||||
IServerStreamWriter<TResponse> responseStream,
|
||||
ServerCallContext context,
|
||||
DuplexStreamingServerMethod<TRequest, TResponse> continuation)
|
||||
{
|
||||
ApiKeyIdentity? identity = await AuthenticateAndAuthorizeAsync(GatewayScopes.Admin, context)
|
||||
.ConfigureAwait(false);
|
||||
IDisposable? identityScope = identity is null ? null : identityAccessor.Push(identity);
|
||||
using (identityScope)
|
||||
{
|
||||
await continuation(requestStream, responseStream, context).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Authenticates the API key and authorizes the RPC call by required scope.</summary>
|
||||
/// <typeparam name="TRequest">Request message type.</typeparam>
|
||||
/// <param name="request">Request payload.</param>
|
||||
/// <param name="requiredScope">The scope the call requires, resolved by the caller.</param>
|
||||
/// <param name="context">RPC server call context.</param>
|
||||
/// <returns>Authenticated API key identity, or null if authentication is disabled.</returns>
|
||||
private async Task<ApiKeyIdentity?> AuthenticateAndAuthorizeAsync<TRequest>(
|
||||
TRequest request,
|
||||
private async Task<ApiKeyIdentity?> AuthenticateAndAuthorizeAsync(
|
||||
string requiredScope,
|
||||
ServerCallContext context)
|
||||
where TRequest : class
|
||||
{
|
||||
if (options.Value.Authentication.Mode == AuthenticationMode.Disabled)
|
||||
{
|
||||
@@ -116,7 +158,6 @@ public sealed class GatewayGrpcAuthorizationInterceptor(
|
||||
|
||||
ApiKeyIdentity identity = GatewayApiKeyIdentityMapper.ToGatewayIdentity(verification.Identity);
|
||||
|
||||
string requiredScope = scopeResolver.ResolveRequiredScope(request);
|
||||
if (!identity.Scopes.Contains(requiredScope))
|
||||
{
|
||||
throw new RpcException(new Status(
|
||||
|
||||
Reference in New Issue
Block a user