feat(config): add system account, SIGHUP reload, and auth change propagation (E6+E7+E8)
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'.
This commit is contained in:
@@ -365,9 +365,10 @@ public sealed class NatsServer : IMessageRouter, ISubListAccess, IDisposable
|
||||
_globalAccount = new Account(Account.GlobalAccountName);
|
||||
_accounts[Account.GlobalAccountName] = _globalAccount;
|
||||
|
||||
// Create $SYS system account (stub -- no internal subscriptions yet)
|
||||
_systemAccount = new Account("$SYS");
|
||||
_accounts["$SYS"] = _systemAccount;
|
||||
// Create $SYS system account and mark it as the system account.
|
||||
// Reference: Go server/server.go — initSystemAccount, accounts.go — isSystemAccount().
|
||||
_systemAccount = new Account(Account.SystemAccountName) { IsSystemAccount = true };
|
||||
_accounts[Account.SystemAccountName] = _systemAccount;
|
||||
|
||||
// Create system internal client and event system
|
||||
var sysClientId = Interlocked.Increment(ref _nextClientId);
|
||||
@@ -1265,6 +1266,43 @@ public sealed class NatsServer : IMessageRouter, ISubListAccess, IDisposable
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if the subject belongs to the $SYS subject space.
|
||||
/// Reference: Go server/server.go — isReservedSubject.
|
||||
/// </summary>
|
||||
public static bool IsSystemSubject(string subject)
|
||||
=> subject.StartsWith("$SYS.", StringComparison.Ordinal) || subject == "$SYS";
|
||||
|
||||
/// <summary>
|
||||
/// Checks whether the given account is allowed to subscribe to the specified subject.
|
||||
/// Non-system accounts cannot subscribe to $SYS.> subjects.
|
||||
/// Reference: Go server/accounts.go — isReservedForSys.
|
||||
/// </summary>
|
||||
public bool IsSubscriptionAllowed(Account? account, string subject)
|
||||
{
|
||||
if (!IsSystemSubject(subject))
|
||||
return true;
|
||||
|
||||
// System account is always allowed
|
||||
if (account != null && account.IsSystemAccount)
|
||||
return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the SubList appropriate for a given subject: system account SubList
|
||||
/// for $SYS.> subjects, or the provided account's SubList for everything else.
|
||||
/// Reference: Go server/server.go — sublist routing for internal subjects.
|
||||
/// </summary>
|
||||
public SubList GetSubListForSubject(Account? account, string subject)
|
||||
{
|
||||
if (IsSystemSubject(subject))
|
||||
return _systemAccount.SubList;
|
||||
|
||||
return account?.SubList ?? _globalAccount.SubList;
|
||||
}
|
||||
|
||||
public void SendInternalMsg(string subject, string? reply, object? msg)
|
||||
{
|
||||
_eventSystem?.Enqueue(new PublishMessage { Subject = subject, Reply = reply, Body = msg });
|
||||
@@ -1653,9 +1691,79 @@ public sealed class NatsServer : IMessageRouter, ISubListAccess, IDisposable
|
||||
|
||||
if (hasAuthChanges)
|
||||
{
|
||||
// Rebuild auth service with new options
|
||||
// Rebuild auth service with new options, then propagate changes to connected clients
|
||||
var oldAuthService = _authService;
|
||||
_authService = AuthService.Build(_options);
|
||||
_logger.LogInformation("Authorization configuration reloaded");
|
||||
|
||||
// Re-evaluate connected clients against the new auth config.
|
||||
// Clients that no longer pass authentication are disconnected with AUTH_EXPIRED.
|
||||
// Reference: Go server/reload.go — applyOptions / reloadAuthorization.
|
||||
PropagateAuthChanges();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Re-evaluates all connected clients against the current auth configuration.
|
||||
/// Clients whose credentials no longer pass authentication are disconnected
|
||||
/// with an "Authorization Violation" error via SendErrAndCloseAsync, which
|
||||
/// properly drains the outbound channel before closing the socket.
|
||||
/// Reference: Go server/reload.go — reloadAuthorization, client.go — applyAccountLimits.
|
||||
/// </summary>
|
||||
internal void PropagateAuthChanges()
|
||||
{
|
||||
if (!_authService.IsAuthRequired)
|
||||
{
|
||||
// Auth was disabled — all existing clients are fine
|
||||
return;
|
||||
}
|
||||
|
||||
var clientsToDisconnect = new List<NatsClient>();
|
||||
|
||||
foreach (var client in _clients.Values)
|
||||
{
|
||||
if (client.ClientOpts == null)
|
||||
continue; // Client hasn't sent CONNECT yet
|
||||
|
||||
var context = new ClientAuthContext
|
||||
{
|
||||
Opts = client.ClientOpts,
|
||||
Nonce = [], // Nonce is only used at connect time; re-evaluation skips it
|
||||
ClientCertificate = client.TlsState?.PeerCert,
|
||||
};
|
||||
|
||||
var result = _authService.Authenticate(context);
|
||||
if (result == null)
|
||||
{
|
||||
_logger.LogInformation(
|
||||
"Client {ClientId} credentials no longer valid after auth reload, disconnecting",
|
||||
client.Id);
|
||||
clientsToDisconnect.Add(client);
|
||||
}
|
||||
}
|
||||
|
||||
// Disconnect clients that failed re-authentication.
|
||||
// Use SendErrAndCloseAsync which queues the -ERR, completes the outbound channel,
|
||||
// waits for the write loop to drain, then cancels the client.
|
||||
var disconnectTasks = new List<Task>(clientsToDisconnect.Count);
|
||||
foreach (var client in clientsToDisconnect)
|
||||
{
|
||||
disconnectTasks.Add(client.SendErrAndCloseAsync(
|
||||
NatsProtocol.ErrAuthorizationViolation,
|
||||
ClientClosedReason.AuthenticationExpired));
|
||||
}
|
||||
|
||||
// Wait for all disconnects to complete (with timeout to avoid blocking reload)
|
||||
if (disconnectTasks.Count > 0)
|
||||
{
|
||||
Task.WhenAll(disconnectTasks)
|
||||
.WaitAsync(TimeSpan.FromSeconds(5))
|
||||
.ConfigureAwait(ConfigureAwaitOptions.SuppressThrowing)
|
||||
.GetAwaiter().GetResult();
|
||||
|
||||
_logger.LogInformation(
|
||||
"Disconnected {Count} client(s) after auth configuration reload",
|
||||
clientsToDisconnect.Count);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user