f40409a688
Tracking IDs are review bookkeeping that rots in shipped source; the rationale they tagged is kept. Comment-only change (CommentChecker 458 -> 16 false positives).
117 lines
5.0 KiB
C#
117 lines
5.0 KiB
C#
using Grpc.Core;
|
|
using Microsoft.Extensions.Logging.Abstractions;
|
|
using Microsoft.Extensions.Options;
|
|
using ZB.MOM.WW.MxGateway.Server.Configuration;
|
|
using ZB.MOM.WW.MxGateway.Server.Security.Authentication;
|
|
using ZB.MOM.WW.MxGateway.Server.Security.Authorization;
|
|
using ZB.MOM.WW.MxGateway.Tests.TestSupport;
|
|
|
|
namespace ZB.MOM.WW.MxGateway.Tests.Security.Authorization;
|
|
|
|
/// <summary>
|
|
/// Unit tests for <see cref="GatewayBrowseScopeProvider"/>, the host seam that feeds the
|
|
/// calling API key's <c>BrowseSubtrees</c> constraint into the shared library's Galaxy
|
|
/// browse RPCs via <c>IGalaxyBrowseScopeProvider</c>.
|
|
/// </summary>
|
|
public sealed class GatewayBrowseScopeProviderTests
|
|
{
|
|
/// <summary>
|
|
/// With an ambient identity whose effective constraints carry a browse subtree,
|
|
/// the provider returns exactly that subtree list to the library.
|
|
/// </summary>
|
|
[Fact]
|
|
public void ResolveBrowseSubtrees_WithCurrentIdentity_ReturnsConstraintBrowseSubtrees()
|
|
{
|
|
StubIdentityAccessor accessor = new(CreateIdentity(["AreaA"]));
|
|
GatewayBrowseScopeProvider provider = CreateProvider(accessor, AuthenticationMode.ApiKey);
|
|
|
|
IReadOnlyList<string>? result = provider.ResolveBrowseSubtrees(new TestServerCallContext());
|
|
|
|
Assert.Equal(["AreaA"], result);
|
|
}
|
|
|
|
/// <summary>
|
|
/// An authenticated key with no BrowseSubtrees constraint is legitimately unscoped: the
|
|
/// empty list is what the library reads as "no scoping". This is the state that must
|
|
/// stay distinguishable from an absent identity, so it is asserted alongside its
|
|
/// denying twin below rather than left implicit.
|
|
/// </summary>
|
|
[Fact]
|
|
public void ResolveBrowseSubtrees_WithUnconstrainedIdentity_ReturnsEmptyList()
|
|
{
|
|
StubIdentityAccessor accessor = new(CreateIdentity([]));
|
|
GatewayBrowseScopeProvider provider = CreateProvider(accessor, AuthenticationMode.ApiKey);
|
|
|
|
IReadOnlyList<string>? result = provider.ResolveBrowseSubtrees(new TestServerCallContext());
|
|
|
|
Assert.NotNull(result);
|
|
Assert.Empty(result);
|
|
}
|
|
|
|
/// <summary>
|
|
/// With API-key authentication enabled, an absent ambient identity means the
|
|
/// interceptor-ordering invariant is broken — the request's browse scope cannot be
|
|
/// established. The provider must DENY rather than resolve to the full hierarchy, because
|
|
/// a silently-unscoped browse is an invisible authorization bypass. The status code is
|
|
/// pinned: PermissionDenied is what the gRPC surface returns for an authorization failure
|
|
/// the caller cannot fix by re-authenticating.
|
|
/// </summary>
|
|
[Fact]
|
|
public void ResolveBrowseSubtrees_WithNoCurrentIdentityAndApiKeyAuth_ThrowsPermissionDenied()
|
|
{
|
|
StubIdentityAccessor accessor = new(current: null);
|
|
GatewayBrowseScopeProvider provider = CreateProvider(accessor, AuthenticationMode.ApiKey);
|
|
|
|
RpcException exception = Assert.Throws<RpcException>(
|
|
() => provider.ResolveBrowseSubtrees(new TestServerCallContext()));
|
|
|
|
Assert.Equal(StatusCode.PermissionDenied, exception.StatusCode);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Under <c>Authentication:Mode=Disabled</c> nothing authenticates, so an absent identity
|
|
/// is the expected steady state rather than a broken invariant: there is no key and no
|
|
/// constraint to scope by. The provider keeps returning unscoped, so disabling
|
|
/// authentication does not turn every Galaxy browse into a PermissionDenied.
|
|
/// </summary>
|
|
[Fact]
|
|
public void ResolveBrowseSubtrees_WithNoCurrentIdentityAndAuthDisabled_ReturnsEmptyList()
|
|
{
|
|
StubIdentityAccessor accessor = new(current: null);
|
|
GatewayBrowseScopeProvider provider = CreateProvider(accessor, AuthenticationMode.Disabled);
|
|
|
|
IReadOnlyList<string>? result = provider.ResolveBrowseSubtrees(new TestServerCallContext());
|
|
|
|
Assert.NotNull(result);
|
|
Assert.Empty(result);
|
|
}
|
|
|
|
private static GatewayBrowseScopeProvider CreateProvider(
|
|
StubIdentityAccessor accessor,
|
|
AuthenticationMode mode) =>
|
|
new(
|
|
accessor,
|
|
Options.Create(new GatewayOptions
|
|
{
|
|
Authentication = new Server.Configuration.AuthenticationOptions { Mode = mode },
|
|
}),
|
|
NullLogger<GatewayBrowseScopeProvider>.Instance);
|
|
|
|
private static ApiKeyIdentity CreateIdentity(IReadOnlyList<string> browseSubtrees) =>
|
|
new(
|
|
KeyId: "operator01",
|
|
KeyPrefix: "mxgw_operator01",
|
|
DisplayName: "Operator",
|
|
Scopes: new HashSet<string>(StringComparer.Ordinal),
|
|
Constraints: ApiKeyConstraints.Empty with { BrowseSubtrees = browseSubtrees });
|
|
|
|
private sealed class StubIdentityAccessor(ApiKeyIdentity? current) : IGatewayRequestIdentityAccessor
|
|
{
|
|
/// <inheritdoc />
|
|
public ApiKeyIdentity? Current { get; } = current;
|
|
|
|
/// <inheritdoc />
|
|
public IDisposable Push(ApiKeyIdentity identity) => throw new NotSupportedException();
|
|
}
|
|
}
|