feat(auth): cut MxGateway API keys over to ZB.MOM.WW.Auth.ApiKeys 0.1.2; keep constraint enforcement+gRPC+CLI on top (Task 1.3)

This commit is contained in:
Joseph Doherty
2026-06-02 02:08:38 -04:00
parent f4dc11bae4
commit 05009d7370
49 changed files with 515 additions and 1642 deletions
@@ -1,3 +1,4 @@
using ZB.MOM.WW.Auth.Abstractions.ApiKeys;
using ZB.MOM.WW.MxGateway.Contracts.Proto.Galaxy;
using ZB.MOM.WW.MxGateway.Contracts.Proto;
using ZB.MOM.WW.MxGateway.Server.Dashboard;
@@ -6,6 +7,10 @@ using ZB.MOM.WW.MxGateway.Server.Security.Authentication;
using ZB.MOM.WW.MxGateway.Server.Security.Authorization;
using ZB.MOM.WW.MxGateway.Server.Sessions;
// ConstraintEnforcer enforces against the gateway's constraint-bearing identity; the shared library
// also defines an ApiKeyIdentity, so disambiguate to the gateway type.
using ApiKeyIdentity = ZB.MOM.WW.MxGateway.Server.Security.Authentication.ApiKeyIdentity;
namespace ZB.MOM.WW.MxGateway.Tests.Security.Authorization;
public sealed class ConstraintEnforcerTests
@@ -250,9 +255,9 @@ public sealed class ConstraintEnforcerTests
}
/// <inheritdoc />
public Task<IReadOnlyList<ApiKeyAuditRecord>> ListRecentAsync(int count, CancellationToken cancellationToken)
public Task<IReadOnlyList<ApiKeyAuditEntry>> ListRecentAsync(int limit, CancellationToken ct)
{
return Task.FromResult<IReadOnlyList<ApiKeyAuditRecord>>([]);
return Task.FromResult<IReadOnlyList<ApiKeyAuditEntry>>([]);
}
}
}
@@ -2,6 +2,7 @@ using System.Runtime.CompilerServices;
using Grpc.Core;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using ZB.MOM.WW.Auth.Abstractions.ApiKeys;
using ZB.MOM.WW.MxGateway.Contracts;
using ZB.MOM.WW.MxGateway.Contracts.Proto;
using ZB.MOM.WW.MxGateway.Server.Configuration;
@@ -12,6 +13,11 @@ using ZB.MOM.WW.MxGateway.Server.Security.Authorization;
using ZB.MOM.WW.MxGateway.Server.Sessions;
using ZB.MOM.WW.MxGateway.Tests.TestSupport;
// The handler exposes the gateway's constraint-bearing identity; alias the shared library identity
// (returned by the verifier) so the two can be referenced unambiguously.
using ApiKeyIdentity = ZB.MOM.WW.MxGateway.Server.Security.Authentication.ApiKeyIdentity;
using LibApiKeyIdentity = ZB.MOM.WW.Auth.Abstractions.ApiKeys.ApiKeyIdentity;
namespace ZB.MOM.WW.MxGateway.Tests.Security.Authorization;
public sealed class GatewayGrpcAuthorizationInterceptorTests
@@ -21,8 +27,7 @@ public sealed class GatewayGrpcAuthorizationInterceptorTests
public async Task UnaryServerHandler_MissingApiKey_ReturnsUnauthenticated()
{
GatewayGrpcAuthorizationInterceptor interceptor = CreateInterceptor(
new FakeApiKeyVerifier(ApiKeyVerificationResult.Fail(
ApiKeyVerificationFailure.MissingOrMalformedCredentials)),
new FakeApiKeyVerifier(Failure(ApiKeyFailure.MissingOrMalformed)),
new GatewayRequestIdentityAccessor());
RpcException exception = await Assert.ThrowsAsync<RpcException>(
@@ -40,7 +45,7 @@ public sealed class GatewayGrpcAuthorizationInterceptorTests
public async Task UnaryServerHandler_InvalidApiKey_DoesNotExposeRawCredentialInStatus()
{
GatewayGrpcAuthorizationInterceptor interceptor = CreateInterceptor(
new FakeApiKeyVerifier(ApiKeyVerificationResult.Fail(ApiKeyVerificationFailure.SecretMismatch)),
new FakeApiKeyVerifier(Failure(ApiKeyFailure.SecretMismatch)),
new GatewayRequestIdentityAccessor());
RpcException exception = await Assert.ThrowsAsync<RpcException>(
@@ -146,8 +151,7 @@ public sealed class GatewayGrpcAuthorizationInterceptorTests
public async Task UnaryServerHandler_AuthenticationDisabled_SkipsApiKeyVerification()
{
GatewayRequestIdentityAccessor identityAccessor = new();
FakeApiKeyVerifier verifier = new(ApiKeyVerificationResult.Fail(
ApiKeyVerificationFailure.MissingOrMalformedCredentials));
FakeApiKeyVerifier verifier = new(Failure(ApiKeyFailure.MissingOrMalformed));
GatewayGrpcAuthorizationInterceptor interceptor = CreateInterceptor(
verifier,
identityAccessor,
@@ -374,13 +378,21 @@ public sealed class GatewayGrpcAuthorizationInterceptorTests
}));
}
private static ApiKeyVerificationResult SuccessWithScopes(params string[] scopes)
private static ApiKeyVerification SuccessWithScopes(params string[] scopes)
{
return ApiKeyVerificationResult.Success(new ApiKeyIdentity(
KeyId: "operator01",
KeyPrefix: "mxgw_operator01",
DisplayName: "Operator Key",
Scopes: new HashSet<string>(scopes, StringComparer.Ordinal)));
return new ApiKeyVerification(
Succeeded: true,
Identity: new LibApiKeyIdentity(
KeyId: "operator01",
DisplayName: "Operator Key",
Scopes: new HashSet<string>(scopes, StringComparer.Ordinal),
Constraints: null),
Failure: null);
}
private static ApiKeyVerification Failure(ApiKeyFailure failure)
{
return new ApiKeyVerification(Succeeded: false, Identity: null, Failure: failure);
}
private static TestServerCallContext ContextWithAuthorization(string authorizationHeader)
@@ -495,7 +507,7 @@ public sealed class GatewayGrpcAuthorizationInterceptorTests
}
}
private sealed class FakeApiKeyVerifier(ApiKeyVerificationResult result) : IApiKeyVerifier
private sealed class FakeApiKeyVerifier(ApiKeyVerification result) : IApiKeyVerifier
{
/// <summary>Gets whether the verifier was called.</summary>
public bool WasCalled { get; private set; }
@@ -505,11 +517,11 @@ public sealed class GatewayGrpcAuthorizationInterceptorTests
/// <summary>Verifies the authorization header against stored result.</summary>
/// <param name="authorizationHeader">The authorization header to verify.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <param name="ct">Cancellation token.</param>
/// <returns>Configured verification result.</returns>
public Task<ApiKeyVerificationResult> VerifyAsync(
string? authorizationHeader,
CancellationToken cancellationToken)
public Task<ApiKeyVerification> VerifyAsync(
string authorizationHeader,
CancellationToken ct)
{
WasCalled = true;
LastAuthorizationHeader = authorizationHeader;