using System.Net; using System.Net.Sockets; using Microsoft.Extensions.Logging.Abstractions; using NATS.Client.Core; using NATS.Server; using NATS.Server.Auth; namespace NATS.Server.Tests.Accounts; /// /// Tests for publish/subscribe permission enforcement, account-level limits, /// and per-user permission isolation. /// Reference: Go auth_test.go — TestUserClone* (permission structure tests) /// Reference: Go accounts_test.go — account limits (max connections, max subscriptions). /// public class PermissionTests { private static int GetFreePort() { using var sock = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); sock.Bind(new IPEndPoint(IPAddress.Loopback, 0)); return ((IPEndPoint)sock.LocalEndPoint!).Port; } private static async Task<(NatsServer server, int port, CancellationTokenSource cts)> StartServerAsync(NatsOptions options) { var port = GetFreePort(); options.Port = port; var server = new NatsServer(options, NullLoggerFactory.Instance); var cts = new CancellationTokenSource(); _ = server.StartAsync(cts.Token); await server.WaitForReadyAsync(); return (server, port, cts); } private static bool ExceptionChainContains(Exception ex, string substring) { Exception? current = ex; while (current != null) { if (current.Message.Contains(substring, StringComparison.OrdinalIgnoreCase)) return true; current = current.InnerException; } return false; } // Go: Permissions — publish allow list [Fact] public void Publish_allow_list_only() { var perms = ClientPermissions.Build(new Permissions { Publish = new SubjectPermission { Allow = ["foo.>", "bar.*"] }, }); perms.ShouldNotBeNull(); perms.IsPublishAllowed("foo.bar").ShouldBeTrue(); perms.IsPublishAllowed("foo.bar.baz").ShouldBeTrue(); perms.IsPublishAllowed("bar.one").ShouldBeTrue(); perms.IsPublishAllowed("baz.one").ShouldBeFalse(); } // Go: Permissions — publish deny list [Fact] public void Publish_deny_list_only() { var perms = ClientPermissions.Build(new Permissions { Publish = new SubjectPermission { Deny = ["secret.>"] }, }); perms.ShouldNotBeNull(); perms.IsPublishAllowed("foo.bar").ShouldBeTrue(); perms.IsPublishAllowed("secret.data").ShouldBeFalse(); perms.IsPublishAllowed("secret.nested.deep").ShouldBeFalse(); } // Go: Permissions — publish allow + deny combined [Fact] public void Publish_allow_and_deny_combined() { var perms = ClientPermissions.Build(new Permissions { Publish = new SubjectPermission { Allow = ["events.>"], Deny = ["events.internal.>"], }, }); perms.ShouldNotBeNull(); perms.IsPublishAllowed("events.public.data").ShouldBeTrue(); perms.IsPublishAllowed("events.internal.secret").ShouldBeFalse(); } // Go: Permissions — subscribe allow list [Fact] public void Subscribe_allow_list() { var perms = ClientPermissions.Build(new Permissions { Subscribe = new SubjectPermission { Allow = ["data.>"] }, }); perms.ShouldNotBeNull(); perms.IsSubscribeAllowed("data.updates").ShouldBeTrue(); perms.IsSubscribeAllowed("admin.logs").ShouldBeFalse(); } // Go: Permissions — subscribe deny list [Fact] public void Subscribe_deny_list() { var perms = ClientPermissions.Build(new Permissions { Subscribe = new SubjectPermission { Deny = ["admin.>"] }, }); perms.ShouldNotBeNull(); perms.IsSubscribeAllowed("data.updates").ShouldBeTrue(); perms.IsSubscribeAllowed("admin.logs").ShouldBeFalse(); } // Go: Permissions — null permissions allow everything [Fact] public void Null_permissions_allows_everything() { var perms = ClientPermissions.Build(null); perms.ShouldBeNull(); } // Go: Permissions — empty permissions allows everything [Fact] public void Empty_permissions_allows_everything() { var perms = ClientPermissions.Build(new Permissions()); perms.ShouldBeNull(); } // Go: Permissions — subscribe allow + deny combined [Fact] public void Subscribe_allow_and_deny_combined() { var perms = ClientPermissions.Build(new Permissions { Subscribe = new SubjectPermission { Allow = ["data.>"], Deny = ["data.secret.>"], }, }); perms.ShouldNotBeNull(); perms.IsSubscribeAllowed("data.public").ShouldBeTrue(); perms.IsSubscribeAllowed("data.secret.key").ShouldBeFalse(); } // Go: Permissions — separate publish and subscribe permissions [Fact] public void Separate_publish_and_subscribe_permissions() { var perms = ClientPermissions.Build(new Permissions { Publish = new SubjectPermission { Allow = ["pub.>"] }, Subscribe = new SubjectPermission { Allow = ["sub.>"] }, }); perms.ShouldNotBeNull(); perms.IsPublishAllowed("pub.data").ShouldBeTrue(); perms.IsPublishAllowed("sub.data").ShouldBeFalse(); perms.IsSubscribeAllowed("sub.data").ShouldBeTrue(); perms.IsSubscribeAllowed("pub.data").ShouldBeFalse(); } // Go: Account limits — max connections [Fact] public void Account_enforces_max_connections() { var acc = new Account("test") { MaxConnections = 2 }; acc.AddClient(1).ShouldBeTrue(); acc.AddClient(2).ShouldBeTrue(); acc.AddClient(3).ShouldBeFalse(); // exceeds limit acc.ClientCount.ShouldBe(2); } // Go: Account limits — unlimited connections [Fact] public void Account_unlimited_connections_when_zero() { var acc = new Account("test") { MaxConnections = 0 }; for (ulong i = 1; i <= 100; i++) acc.AddClient(i).ShouldBeTrue(); acc.ClientCount.ShouldBe(100); } // Go: Account limits — max subscriptions [Fact] public void Account_enforces_max_subscriptions() { var acc = new Account("test") { MaxSubscriptions = 2 }; acc.IncrementSubscriptions().ShouldBeTrue(); acc.IncrementSubscriptions().ShouldBeTrue(); acc.IncrementSubscriptions().ShouldBeFalse(); } // Go: Account limits — subscription decrement frees slot [Fact] public void Account_decrement_subscriptions_frees_slot() { var acc = new Account("test") { MaxSubscriptions = 1 }; acc.IncrementSubscriptions().ShouldBeTrue(); acc.DecrementSubscriptions(); acc.IncrementSubscriptions().ShouldBeTrue(); // slot freed } // Go: Account limits — max connections via integration [Fact] public void Account_remove_client_frees_slot() { var acc = new Account("test") { MaxConnections = 1 }; acc.AddClient(1).ShouldBeTrue(); acc.AddClient(2).ShouldBeFalse(); // full acc.RemoveClient(1); acc.AddClient(2).ShouldBeTrue(); // slot freed } // Go: Account limits — default permissions on account [Fact] public void Account_default_permissions() { var acc = new Account("test") { DefaultPermissions = new Permissions { Publish = new SubjectPermission { Allow = ["pub.>"] }, }, }; acc.DefaultPermissions.ShouldNotBeNull(); acc.DefaultPermissions.Publish!.Allow![0].ShouldBe("pub.>"); } // Go: Account stats tracking [Fact] public void Account_tracks_message_stats() { var acc = new Account("stats-test"); acc.InMsgs.ShouldBe(0L); acc.OutMsgs.ShouldBe(0L); acc.InBytes.ShouldBe(0L); acc.OutBytes.ShouldBe(0L); acc.IncrementInbound(5, 1024); acc.IncrementOutbound(3, 512); acc.InMsgs.ShouldBe(5L); acc.InBytes.ShouldBe(1024L); acc.OutMsgs.ShouldBe(3L); acc.OutBytes.ShouldBe(512L); } // Go: Account — user with publish permission can publish [Fact] public async Task User_with_publish_permission_can_publish_and_subscribe() { var (server, port, cts) = await StartServerAsync(new NatsOptions { Users = [ new User { Username = "limited", Password = "pass", Permissions = new Permissions { Publish = new SubjectPermission { Allow = ["allowed.>"] }, Subscribe = new SubjectPermission { Allow = [">"] }, }, }, ], }); try { await using var client = new NatsConnection(new NatsOpts { Url = $"nats://limited:pass@127.0.0.1:{port}", }); await client.ConnectAsync(); // Subscribe to allowed subjects await using var sub = await client.SubscribeCoreAsync("allowed.test"); await client.PingAsync(); // Publish to allowed subject await client.PublishAsync("allowed.test", "hello"); using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(3)); var msg = await sub.Msgs.ReadAsync(timeout.Token); msg.Data.ShouldBe("hello"); } finally { await cts.CancelAsync(); server.Dispose(); } } // Go: Account — user with publish deny [Fact] public async Task User_with_publish_deny_blocks_denied_subjects() { var (server, port, cts) = await StartServerAsync(new NatsOptions { Users = [ new User { Username = "limited", Password = "pass", Permissions = new Permissions { Publish = new SubjectPermission { Allow = [">"], Deny = ["secret.>"], }, Subscribe = new SubjectPermission { Allow = [">"] }, }, }, ], }); try { await using var client = new NatsConnection(new NatsOpts { Url = $"nats://limited:pass@127.0.0.1:{port}", }); await client.ConnectAsync(); // Subscribe to catch anything await using var sub = await client.SubscribeCoreAsync("secret.data"); await client.PingAsync(); // Publish to denied subject — server should silently drop await client.PublishAsync("secret.data", "shouldnt-arrive"); using var timeout = new CancellationTokenSource(TimeSpan.FromMilliseconds(500)); try { await sub.Msgs.ReadAsync(timeout.Token); throw new Exception("Should not have received message on denied subject"); } catch (OperationCanceledException) { // Expected — message was blocked by permissions } } finally { await cts.CancelAsync(); server.Dispose(); } } // Go: Account — user revocation [Fact] public void Account_user_revocation() { var acc = new Account("test"); acc.IsUserRevoked("user1", 100).ShouldBeFalse(); acc.RevokeUser("user1", 200); acc.IsUserRevoked("user1", 100).ShouldBeTrue(); // issued before revocation acc.IsUserRevoked("user1", 200).ShouldBeTrue(); // issued at revocation time acc.IsUserRevoked("user1", 300).ShouldBeFalse(); // issued after revocation } // Go: Account — wildcard user revocation [Fact] public void Account_wildcard_user_revocation() { var acc = new Account("test"); acc.RevokeUser("*", 500); acc.IsUserRevoked("anyuser", 400).ShouldBeTrue(); acc.IsUserRevoked("anyuser", 600).ShouldBeFalse(); } // Go: Account — JetStream stream reservation [Fact] public void Account_jetstream_stream_reservation() { var acc = new Account("test") { MaxJetStreamStreams = 2 }; acc.TryReserveStream().ShouldBeTrue(); acc.TryReserveStream().ShouldBeTrue(); acc.TryReserveStream().ShouldBeFalse(); // limit reached acc.JetStreamStreamCount.ShouldBe(2); acc.ReleaseStream(); acc.JetStreamStreamCount.ShouldBe(1); acc.TryReserveStream().ShouldBeTrue(); // slot freed } // Go: Account limits — permissions cache behavior [Fact] public void Permission_cache_returns_consistent_results() { var perms = ClientPermissions.Build(new Permissions { Publish = new SubjectPermission { Allow = ["foo.>"] }, }); perms.ShouldNotBeNull(); // First call populates cache perms.IsPublishAllowed("foo.bar").ShouldBeTrue(); // Second call uses cache — should return same result perms.IsPublishAllowed("foo.bar").ShouldBeTrue(); // Different subject also cached perms.IsPublishAllowed("baz.bar").ShouldBeFalse(); perms.IsPublishAllowed("baz.bar").ShouldBeFalse(); } // Go: Permissions — delivery allowed check [Fact] public void Delivery_allowed_respects_deny_list() { var perms = ClientPermissions.Build(new Permissions { Subscribe = new SubjectPermission { Deny = ["blocked.>"] }, }); perms.ShouldNotBeNull(); perms.IsDeliveryAllowed("normal.subject").ShouldBeTrue(); perms.IsDeliveryAllowed("blocked.secret").ShouldBeFalse(); } }