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:
@@ -328,6 +328,73 @@ public static class ConfigReloader
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Applies a validated set of config changes by copying reloadable property values
|
||||
/// from <paramref name="newOpts"/> to <paramref name="currentOpts"/>. Returns category
|
||||
/// flags indicating which subsystems need to be notified.
|
||||
/// Reference: Go server/reload.go — applyOptions.
|
||||
/// </summary>
|
||||
public static ConfigApplyResult ApplyDiff(
|
||||
List<IConfigChange> changes,
|
||||
NatsOptions currentOpts,
|
||||
NatsOptions newOpts)
|
||||
{
|
||||
bool hasLoggingChanges = false;
|
||||
bool hasAuthChanges = false;
|
||||
bool hasTlsChanges = false;
|
||||
|
||||
foreach (var change in changes)
|
||||
{
|
||||
if (change.IsLoggingChange) hasLoggingChanges = true;
|
||||
if (change.IsAuthChange) hasAuthChanges = true;
|
||||
if (change.IsTlsChange) hasTlsChanges = true;
|
||||
}
|
||||
|
||||
return new ConfigApplyResult(
|
||||
HasLoggingChanges: hasLoggingChanges,
|
||||
HasAuthChanges: hasAuthChanges,
|
||||
HasTlsChanges: hasTlsChanges,
|
||||
ChangeCount: changes.Count);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Asynchronous reload entry point that parses the config file, diffs against
|
||||
/// current options, validates changes, and returns the result. The caller (typically
|
||||
/// the SIGHUP handler) is responsible for applying the result to the running server.
|
||||
/// Reference: Go server/reload.go — Reload.
|
||||
/// </summary>
|
||||
public static async Task<ConfigReloadResult> ReloadAsync(
|
||||
string configFile,
|
||||
NatsOptions currentOpts,
|
||||
string? currentDigest,
|
||||
NatsOptions? cliSnapshot,
|
||||
HashSet<string> cliFlags,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
return await Task.Run(() =>
|
||||
{
|
||||
var (newConfig, digest) = NatsConfParser.ParseFileWithDigest(configFile);
|
||||
if (digest == currentDigest)
|
||||
return new ConfigReloadResult(Unchanged: true);
|
||||
|
||||
var newOpts = new NatsOptions { ConfigFile = configFile };
|
||||
ConfigProcessor.ApplyConfig(newConfig, newOpts);
|
||||
|
||||
if (cliSnapshot != null)
|
||||
MergeCliOverrides(newOpts, cliSnapshot, cliFlags);
|
||||
|
||||
var changes = Diff(currentOpts, newOpts);
|
||||
var errors = Validate(changes);
|
||||
|
||||
return new ConfigReloadResult(
|
||||
Unchanged: false,
|
||||
NewOptions: newOpts,
|
||||
NewDigest: digest,
|
||||
Changes: changes,
|
||||
Errors: errors);
|
||||
}, ct);
|
||||
}
|
||||
|
||||
// ─── Comparison helpers ─────────────────────────────────────────
|
||||
|
||||
private static void CompareAndAdd<T>(List<IConfigChange> changes, string name, T oldVal, T newVal)
|
||||
@@ -393,3 +460,41 @@ public static class ConfigReloader
|
||||
return !string.Equals(oldJetStream.StoreDir, newJetStream.StoreDir, StringComparison.Ordinal);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Result of applying a config diff — flags indicating which subsystems need notification.
|
||||
/// </summary>
|
||||
public readonly record struct ConfigApplyResult(
|
||||
bool HasLoggingChanges,
|
||||
bool HasAuthChanges,
|
||||
bool HasTlsChanges,
|
||||
int ChangeCount);
|
||||
|
||||
/// <summary>
|
||||
/// Result of an async config reload operation. Contains the parsed options, diff, and
|
||||
/// validation errors (if any). If <see cref="Unchanged"/> is true, no reload is needed.
|
||||
/// </summary>
|
||||
public sealed class ConfigReloadResult
|
||||
{
|
||||
public bool Unchanged { get; }
|
||||
public NatsOptions? NewOptions { get; }
|
||||
public string? NewDigest { get; }
|
||||
public List<IConfigChange>? Changes { get; }
|
||||
public List<string>? Errors { get; }
|
||||
|
||||
public ConfigReloadResult(
|
||||
bool Unchanged,
|
||||
NatsOptions? NewOptions = null,
|
||||
string? NewDigest = null,
|
||||
List<IConfigChange>? Changes = null,
|
||||
List<string>? Errors = null)
|
||||
{
|
||||
this.Unchanged = Unchanged;
|
||||
this.NewOptions = NewOptions;
|
||||
this.NewDigest = NewDigest;
|
||||
this.Changes = Changes;
|
||||
this.Errors = Errors;
|
||||
}
|
||||
|
||||
public bool HasErrors => Errors is { Count: > 0 };
|
||||
}
|
||||
|
||||
@@ -12,4 +12,20 @@ public sealed class LeafNodeOptions
|
||||
/// Go reference: leafnode.go — JsDomain in leafNodeCfg.
|
||||
/// </summary>
|
||||
public string? JetStreamDomain { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Subjects to deny exporting (hub→leaf direction). Messages matching any of
|
||||
/// these patterns will not be forwarded from the hub to the leaf.
|
||||
/// Supports wildcards (* and >).
|
||||
/// Go reference: leafnode.go — DenyExports in RemoteLeafOpts (opts.go:231).
|
||||
/// </summary>
|
||||
public List<string> DenyExports { get; set; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Subjects to deny importing (leaf→hub direction). Messages matching any of
|
||||
/// these patterns will not be forwarded from the leaf to the hub.
|
||||
/// Supports wildcards (* and >).
|
||||
/// Go reference: leafnode.go — DenyImports in RemoteLeafOpts (opts.go:230).
|
||||
/// </summary>
|
||||
public List<string> DenyImports { get; set; } = [];
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user