E6: Add IsSystemAccount property to Account, mark $SYS account as system, add IsSystemSubject/IsSubscriptionAllowed/GetSubListForSubject helpers to route $SYS.> subjects to the system account's SubList and block non-system accounts from subscribing. E7: Add ConfigReloader.ReloadAsync and ApplyDiff for structured async reload, add ConfigReloadResult/ConfigApplyResult types. SIGHUP handler already wired via PosixSignalRegistration in HandleSignals. E8: Add PropagateAuthChanges to re-evaluate connected clients after auth config reload, disconnecting clients whose credentials no longer pass authentication with -ERR 'Authorization Violation'.
257 lines
9.9 KiB
C#
257 lines
9.9 KiB
C#
// Port of Go server/accounts_test.go — TestSystemAccountDefaultCreation,
|
|
// TestSystemAccountSysSubjectRouting, TestNonSystemAccountCannotSubscribeToSys.
|
|
// Reference: golang/nats-server/server/accounts_test.go, server.go — initSystemAccount.
|
|
|
|
using System.Net;
|
|
using System.Net.Sockets;
|
|
using System.Text;
|
|
using Microsoft.Extensions.Logging.Abstractions;
|
|
using NATS.Server.Auth;
|
|
|
|
namespace NATS.Server.Tests.Auth;
|
|
|
|
/// <summary>
|
|
/// Tests for the $SYS system account functionality including:
|
|
/// - Default system account creation with IsSystemAccount flag
|
|
/// - $SYS.> subject routing to the system account's SubList
|
|
/// - Non-system accounts blocked from subscribing to $SYS.> subjects
|
|
/// - System account event publishing
|
|
/// Reference: Go server/accounts.go — isSystemAccount, isReservedSubject.
|
|
/// </summary>
|
|
public class SystemAccountTests
|
|
{
|
|
// ─── Helpers ────────────────────────────────────────────────────────────
|
|
|
|
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 async Task<Socket> RawConnectAsync(int port)
|
|
{
|
|
var sock = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
|
|
await sock.ConnectAsync(IPAddress.Loopback, port);
|
|
var buf = new byte[4096];
|
|
await sock.ReceiveAsync(buf, SocketFlags.None);
|
|
return sock;
|
|
}
|
|
|
|
private static async Task<string> ReadUntilAsync(Socket sock, string expected, int timeoutMs = 5000)
|
|
{
|
|
using var cts = new CancellationTokenSource(timeoutMs);
|
|
var sb = new StringBuilder();
|
|
var buf = new byte[4096];
|
|
while (!sb.ToString().Contains(expected, StringComparison.Ordinal))
|
|
{
|
|
int n;
|
|
try { n = await sock.ReceiveAsync(buf, SocketFlags.None, cts.Token); }
|
|
catch (OperationCanceledException) { break; }
|
|
if (n == 0) break;
|
|
sb.Append(Encoding.ASCII.GetString(buf, 0, n));
|
|
}
|
|
return sb.ToString();
|
|
}
|
|
|
|
// ─── Tests ──────────────────────────────────────────────────────────────
|
|
|
|
/// <summary>
|
|
/// Verifies that the server creates a $SYS system account by default with
|
|
/// IsSystemAccount set to true.
|
|
/// Reference: Go server/server.go — initSystemAccount.
|
|
/// </summary>
|
|
[Fact]
|
|
public void Default_system_account_is_created()
|
|
{
|
|
var options = new NatsOptions { Port = 0 };
|
|
using var server = new NatsServer(options, NullLoggerFactory.Instance);
|
|
|
|
server.SystemAccount.ShouldNotBeNull();
|
|
server.SystemAccount.Name.ShouldBe(Account.SystemAccountName);
|
|
server.SystemAccount.IsSystemAccount.ShouldBeTrue();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verifies that the system account constant matches "$SYS".
|
|
/// </summary>
|
|
[Fact]
|
|
public void System_account_name_constant_is_correct()
|
|
{
|
|
Account.SystemAccountName.ShouldBe("$SYS");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verifies that a non-system account does not have IsSystemAccount set.
|
|
/// </summary>
|
|
[Fact]
|
|
public void Regular_account_is_not_system_account()
|
|
{
|
|
var account = new Account("test-account");
|
|
account.IsSystemAccount.ShouldBeFalse();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verifies that IsSystemAccount can be explicitly set on an account.
|
|
/// </summary>
|
|
[Fact]
|
|
public void IsSystemAccount_can_be_set()
|
|
{
|
|
var account = new Account("custom-sys") { IsSystemAccount = true };
|
|
account.IsSystemAccount.ShouldBeTrue();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verifies that IsSystemSubject correctly identifies $SYS subjects.
|
|
/// Reference: Go server/server.go — isReservedSubject.
|
|
/// </summary>
|
|
[Theory]
|
|
[InlineData("$SYS", true)]
|
|
[InlineData("$SYS.ACCOUNT.test.CONNECT", true)]
|
|
[InlineData("$SYS.SERVER.abc.STATSZ", true)]
|
|
[InlineData("$SYS.REQ.SERVER.PING.VARZ", true)]
|
|
[InlineData("foo.bar", false)]
|
|
[InlineData("$G", false)]
|
|
[InlineData("SYS.test", false)]
|
|
[InlineData("$JS.API.STREAM.LIST", false)]
|
|
[InlineData("$SYS.", true)]
|
|
public void IsSystemSubject_identifies_sys_subjects(string subject, bool expected)
|
|
{
|
|
NatsServer.IsSystemSubject(subject).ShouldBe(expected);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verifies that the system account is listed among server accounts.
|
|
/// </summary>
|
|
[Fact]
|
|
public void System_account_is_in_server_accounts()
|
|
{
|
|
var options = new NatsOptions { Port = 0 };
|
|
using var server = new NatsServer(options, NullLoggerFactory.Instance);
|
|
|
|
var accounts = server.GetAccounts().ToList();
|
|
accounts.ShouldContain(a => a.Name == Account.SystemAccountName && a.IsSystemAccount);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verifies that IsSubscriptionAllowed blocks non-system accounts from $SYS.> subjects.
|
|
/// Reference: Go server/accounts.go — isReservedForSys.
|
|
/// </summary>
|
|
[Fact]
|
|
public void Non_system_account_cannot_subscribe_to_sys_subjects()
|
|
{
|
|
var options = new NatsOptions { Port = 0 };
|
|
using var server = new NatsServer(options, NullLoggerFactory.Instance);
|
|
|
|
var regularAccount = new Account("regular");
|
|
|
|
server.IsSubscriptionAllowed(regularAccount, "$SYS.SERVER.abc.STATSZ").ShouldBeFalse();
|
|
server.IsSubscriptionAllowed(regularAccount, "$SYS.ACCOUNT.test.CONNECT").ShouldBeFalse();
|
|
server.IsSubscriptionAllowed(regularAccount, "$SYS.REQ.SERVER.PING.VARZ").ShouldBeFalse();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verifies that the system account IS allowed to subscribe to $SYS.> subjects.
|
|
/// </summary>
|
|
[Fact]
|
|
public void System_account_can_subscribe_to_sys_subjects()
|
|
{
|
|
var options = new NatsOptions { Port = 0 };
|
|
using var server = new NatsServer(options, NullLoggerFactory.Instance);
|
|
|
|
server.IsSubscriptionAllowed(server.SystemAccount, "$SYS.SERVER.abc.STATSZ").ShouldBeTrue();
|
|
server.IsSubscriptionAllowed(server.SystemAccount, "$SYS.ACCOUNT.test.CONNECT").ShouldBeTrue();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verifies that any account can subscribe to non-$SYS subjects.
|
|
/// </summary>
|
|
[Fact]
|
|
public void Any_account_can_subscribe_to_regular_subjects()
|
|
{
|
|
var options = new NatsOptions { Port = 0 };
|
|
using var server = new NatsServer(options, NullLoggerFactory.Instance);
|
|
|
|
var regularAccount = new Account("regular");
|
|
|
|
server.IsSubscriptionAllowed(regularAccount, "foo.bar").ShouldBeTrue();
|
|
server.IsSubscriptionAllowed(regularAccount, "$JS.API.STREAM.LIST").ShouldBeTrue();
|
|
server.IsSubscriptionAllowed(server.SystemAccount, "foo.bar").ShouldBeTrue();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verifies that GetSubListForSubject routes $SYS subjects to the system account's SubList.
|
|
/// Reference: Go server/server.go — sublist routing for internal subjects.
|
|
/// </summary>
|
|
[Fact]
|
|
public void GetSubListForSubject_routes_sys_to_system_account()
|
|
{
|
|
var options = new NatsOptions { Port = 0 };
|
|
using var server = new NatsServer(options, NullLoggerFactory.Instance);
|
|
|
|
var globalAccount = server.GetOrCreateAccount(Account.GlobalAccountName);
|
|
|
|
// $SYS subjects should route to the system account's SubList
|
|
var sysList = server.GetSubListForSubject(globalAccount, "$SYS.SERVER.abc.STATSZ");
|
|
sysList.ShouldBeSameAs(server.SystemAccount.SubList);
|
|
|
|
// Regular subjects should route to the specified account's SubList
|
|
var regularList = server.GetSubListForSubject(globalAccount, "foo.bar");
|
|
regularList.ShouldBeSameAs(globalAccount.SubList);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verifies that the EventSystem publishes to the system account's SubList
|
|
/// and that internal subscriptions for monitoring are registered there.
|
|
/// The subscriptions are wired up during StartAsync via InitEventTracking.
|
|
/// </summary>
|
|
[Fact]
|
|
public async Task Event_system_subscribes_in_system_account()
|
|
{
|
|
var (server, _, cts) = await StartServerAsync(new NatsOptions());
|
|
try
|
|
{
|
|
// The system account's SubList should have subscriptions registered
|
|
// by the internal event system (VARZ, HEALTHZ, etc.)
|
|
server.EventSystem.ShouldNotBeNull();
|
|
server.SystemAccount.SubList.Count.ShouldBeGreaterThan(0u);
|
|
}
|
|
finally
|
|
{
|
|
await cts.CancelAsync();
|
|
server.Dispose();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verifies that the global account is separate from the system account.
|
|
/// </summary>
|
|
[Fact]
|
|
public void Global_and_system_accounts_are_separate()
|
|
{
|
|
var options = new NatsOptions { Port = 0 };
|
|
using var server = new NatsServer(options, NullLoggerFactory.Instance);
|
|
|
|
var globalAccount = server.GetOrCreateAccount(Account.GlobalAccountName);
|
|
var systemAccount = server.SystemAccount;
|
|
|
|
globalAccount.ShouldNotBeSameAs(systemAccount);
|
|
globalAccount.Name.ShouldBe(Account.GlobalAccountName);
|
|
systemAccount.Name.ShouldBe(Account.SystemAccountName);
|
|
globalAccount.IsSystemAccount.ShouldBeFalse();
|
|
systemAccount.IsSystemAccount.ShouldBeTrue();
|
|
globalAccount.SubList.ShouldNotBeSameAs(systemAccount.SubList);
|
|
}
|
|
}
|