b211911f98
v2-ci / unit-tests (push) Failing after 13m22s
Akka.TestKit.Xunit2 -> Akka.TestKit.Xunit 1.5.62 (xunit.v3 TestKit; needs xunit.v3 3.2.2, set via VersionOverride on the four projects). Removes the xunit 2.9.3 pin. IAsyncLifetime returns ValueTask, SqlServerFactAttribute gains the source-info ctor, and xUnit1051 call sites pass TestContext.Current.CancellationToken. Test counts unchanged. Claude-Session: https://claude.ai/code/session_01GT67AEHd2Acv1YPwGfXEoV
151 lines
6.4 KiB
C#
151 lines
6.4 KiB
C#
using Shouldly;
|
|
using Xunit;
|
|
using ZB.MOM.WW.OtOpcUa.Configuration.Entities;
|
|
using ZB.MOM.WW.OtOpcUa.Configuration.Enums;
|
|
using ZB.MOM.WW.OtOpcUa.Core.Authorization;
|
|
using ZB.MOM.WW.OtOpcUa.Host.OpcUa;
|
|
|
|
namespace ZB.MOM.WW.OtOpcUa.Host.Tests.OpcUa;
|
|
|
|
/// <summary>
|
|
/// The key source for a driver-only node (#520 stage 4) — the one with no ConfigDb, which until this
|
|
/// stage denied every UserName token.
|
|
/// </summary>
|
|
public sealed class ArtifactApiKeyStoreTests
|
|
{
|
|
private static readonly DateTime Now = new(2026, 7, 31, 12, 0, 0, DateTimeKind.Utc);
|
|
|
|
/// <summary>
|
|
/// No test here exercises a narrower-than-Cluster scope, so the resolved scope paths are empty.
|
|
/// </summary>
|
|
private static readonly IReadOnlyDictionary<AclScopeRef, IReadOnlyList<NodeAclPath>> NoScopePaths =
|
|
new Dictionary<AclScopeRef, IReadOnlyList<NodeAclPath>>();
|
|
|
|
[Fact]
|
|
public async Task A_node_that_has_not_been_deployed_to_authenticates_nothing()
|
|
{
|
|
var sut = new ArtifactApiKeyStore(new DataPlaneAuthorityState());
|
|
|
|
(await sut.FindAsync("abc", Now, TestContext.Current.CancellationToken)).ShouldBeNull();
|
|
}
|
|
|
|
[Fact]
|
|
public async Task A_deployed_key_resolves_with_its_principals_cluster_grants()
|
|
{
|
|
var state = new DataPlaneAuthorityState();
|
|
state.Apply("cluster-a", "rev-1",
|
|
[Key("abc", "scadabridge-dcl")],
|
|
[Acl("acl-1", "scadabridge-dcl", NodeAclScopeKind.Cluster, NodePermissions.WriteOperate)], NoScopePaths);
|
|
|
|
var found = await new ArtifactApiKeyStore(state).FindAsync("abc", Now, TestContext.Current.CancellationToken);
|
|
|
|
found.ShouldNotBeNull();
|
|
found!.Key.Principal.ShouldBe("scadabridge-dcl");
|
|
found.Grants.ShouldHaveSingleItem().PermissionFlags.ShouldBe(NodePermissions.WriteOperate);
|
|
}
|
|
|
|
/// <summary>
|
|
/// The FULL grant set is returned, narrow scopes included (#520 stage 5). It used to narrow to
|
|
/// <c>Cluster</c> here, which was redundant — <see cref="ApiKeyInterimRoleMapper"/> applies exactly
|
|
/// that filter itself — and became a trap once scope was enforced, because a caller reading
|
|
/// <c>ApiKeyLookup.Grants</c> as "this principal's grants" got a silently truncated set.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Cluster confinement still happens, just not here: <c>ParseDataPlaneAuthority</c> already kept
|
|
/// only this node's cluster's rows, which is where this store differs from
|
|
/// <c>ConfigDbApiKeyStore</c> (that one reads the fleet-wide database and confines it itself; see
|
|
/// <c>ConfigDbApiKeyStoreTests</c>).
|
|
/// </remarks>
|
|
[Fact]
|
|
public async Task Grants_narrower_than_cluster_are_returned_too()
|
|
{
|
|
var state = new DataPlaneAuthorityState();
|
|
state.Apply("cluster-a", "rev-1",
|
|
[Key("abc", "p")],
|
|
[
|
|
Acl("acl-cluster", "p", NodeAclScopeKind.Cluster, NodePermissions.Read),
|
|
Acl("acl-equipment", "p", NodeAclScopeKind.Equipment, NodePermissions.WriteOperate),
|
|
], NoScopePaths);
|
|
|
|
var found = await new ArtifactApiKeyStore(state).FindAsync("abc", Now, TestContext.Current.CancellationToken);
|
|
|
|
found!.Grants.Select(g => g.NodeAclId).ShouldBe(["acl-cluster", "acl-equipment"], ignoreOrder: true);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Widening the store did not widen the ROLES. The narrow grant reaches the mapper and still
|
|
/// confers nothing, so a rule authored as "WriteOperate on one equipment" cannot become
|
|
/// server-wide write authority on a node with ACL enforcement switched off.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// This assertion is the reason the widening is safe, and it lives here rather than only in the
|
|
/// mapper's own tests because the store is what changed: a test of the mapper alone would have
|
|
/// stayed green no matter what the store returned.
|
|
/// </remarks>
|
|
[Fact]
|
|
public async Task Widening_the_store_does_not_widen_the_interim_roles()
|
|
{
|
|
var state = new DataPlaneAuthorityState();
|
|
state.Apply("cluster-a", "rev-1",
|
|
[Key("abc", "p")],
|
|
[Acl("acl-equipment", "p", NodeAclScopeKind.Equipment, NodePermissions.WriteOperate)],
|
|
NoScopePaths);
|
|
|
|
var found = await new ArtifactApiKeyStore(state).FindAsync("abc", Now, TestContext.Current.CancellationToken);
|
|
|
|
found!.Grants.ShouldHaveSingleItem();
|
|
ApiKeyInterimRoleMapper.Map(found.Grants, "p").ShouldBeEmpty();
|
|
}
|
|
|
|
[Fact]
|
|
public async Task A_revoked_key_is_denied_once_the_revoking_artifact_arrives()
|
|
{
|
|
// The whole revocation story for a driver-only node: it takes effect on the next deployment, and
|
|
// this is that deployment landing.
|
|
var state = new DataPlaneAuthorityState();
|
|
var sut = new ArtifactApiKeyStore(state);
|
|
|
|
state.Apply("cluster-a", "rev-1", [Key("abc", "p")], [], NoScopePaths);
|
|
(await sut.FindAsync("abc", Now, TestContext.Current.CancellationToken)).ShouldNotBeNull();
|
|
|
|
var revoked = Key("abc", "p");
|
|
revoked.Enabled = false;
|
|
state.Apply("cluster-a", "rev-2", [revoked], [], NoScopePaths);
|
|
|
|
(await sut.FindAsync("abc", Now, TestContext.Current.CancellationToken)).ShouldBeNull();
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Anonymous_grants_resolve_without_a_key()
|
|
{
|
|
// The anonymous principal has grants but obviously no credential, so FindGrantsAsync is its path.
|
|
var state = new DataPlaneAuthorityState();
|
|
state.Apply("cluster-a", "rev-1", [],
|
|
[Acl("acl-anon", OpcUaPrincipals.Anonymous, NodeAclScopeKind.Cluster, NodePermissions.Read)],
|
|
NoScopePaths);
|
|
|
|
var grants = await new ArtifactApiKeyStore(state).FindGrantsAsync(OpcUaPrincipals.Anonymous, TestContext.Current.CancellationToken);
|
|
|
|
grants.ShouldHaveSingleItem().PermissionFlags.ShouldBe(NodePermissions.Read);
|
|
}
|
|
|
|
private static ApiKey Key(string id, string principal) => new()
|
|
{
|
|
ApiKeyId = id,
|
|
Name = id,
|
|
SecretHash = "hash",
|
|
Principal = principal,
|
|
Enabled = true,
|
|
CreatedUtc = Now.AddDays(-1),
|
|
};
|
|
|
|
private static NodeAcl Acl(string id, string principal, NodeAclScopeKind scope, NodePermissions flags) => new()
|
|
{
|
|
NodeAclId = id,
|
|
ClusterId = "cluster-a",
|
|
Principal = principal,
|
|
ScopeKind = scope,
|
|
PermissionFlags = flags,
|
|
};
|
|
}
|