diff --git a/src/NATS.Server/Imports/ExportAuth.cs b/src/NATS.Server/Imports/ExportAuth.cs new file mode 100644 index 0000000..14200e1 --- /dev/null +++ b/src/NATS.Server/Imports/ExportAuth.cs @@ -0,0 +1,25 @@ +using NATS.Server.Auth; + +namespace NATS.Server.Imports; + +public sealed class ExportAuth +{ + public bool TokenRequired { get; init; } + public uint AccountPosition { get; init; } + public HashSet? ApprovedAccounts { get; init; } + public Dictionary? RevokedAccounts { get; init; } + + public bool IsAuthorized(Account account) + { + if (RevokedAccounts != null && RevokedAccounts.ContainsKey(account.Name)) + return false; + + if (ApprovedAccounts == null && !TokenRequired && AccountPosition == 0) + return true; + + if (ApprovedAccounts != null) + return ApprovedAccounts.Contains(account.Name); + + return false; + } +} diff --git a/src/NATS.Server/Imports/ExportMap.cs b/src/NATS.Server/Imports/ExportMap.cs new file mode 100644 index 0000000..410830a --- /dev/null +++ b/src/NATS.Server/Imports/ExportMap.cs @@ -0,0 +1,8 @@ +namespace NATS.Server.Imports; + +public sealed class ExportMap +{ + public Dictionary Streams { get; } = new(StringComparer.Ordinal); + public Dictionary Services { get; } = new(StringComparer.Ordinal); + public Dictionary Responses { get; } = new(StringComparer.Ordinal); +} diff --git a/src/NATS.Server/Imports/ImportMap.cs b/src/NATS.Server/Imports/ImportMap.cs new file mode 100644 index 0000000..a136c54 --- /dev/null +++ b/src/NATS.Server/Imports/ImportMap.cs @@ -0,0 +1,18 @@ +namespace NATS.Server.Imports; + +public sealed class ImportMap +{ + public List Streams { get; } = []; + public Dictionary> Services { get; } = new(StringComparer.Ordinal); + + public void AddServiceImport(ServiceImport si) + { + if (!Services.TryGetValue(si.From, out var list)) + { + list = []; + Services[si.From] = list; + } + + list.Add(si); + } +} diff --git a/src/NATS.Server/Imports/ServiceExport.cs b/src/NATS.Server/Imports/ServiceExport.cs new file mode 100644 index 0000000..0b4a9ed --- /dev/null +++ b/src/NATS.Server/Imports/ServiceExport.cs @@ -0,0 +1,13 @@ +using NATS.Server.Auth; + +namespace NATS.Server.Imports; + +public sealed class ServiceExport +{ + public ExportAuth Auth { get; init; } = new(); + public Account? Account { get; init; } + public ServiceResponseType ResponseType { get; init; } = ServiceResponseType.Singleton; + public TimeSpan ResponseThreshold { get; init; } = TimeSpan.FromMinutes(2); + public ServiceLatency? Latency { get; init; } + public bool AllowTrace { get; init; } +} diff --git a/src/NATS.Server/Imports/ServiceImport.cs b/src/NATS.Server/Imports/ServiceImport.cs new file mode 100644 index 0000000..20d5536 --- /dev/null +++ b/src/NATS.Server/Imports/ServiceImport.cs @@ -0,0 +1,21 @@ +using NATS.Server.Auth; +using NATS.Server.Subscriptions; + +namespace NATS.Server.Imports; + +public sealed class ServiceImport +{ + public required Account DestinationAccount { get; init; } + public required string From { get; init; } + public required string To { get; init; } + public SubjectTransform? Transform { get; init; } + public ServiceExport? Export { get; init; } + public ServiceResponseType ResponseType { get; init; } = ServiceResponseType.Singleton; + public byte[]? Sid { get; set; } + public bool IsResponse { get; init; } + public bool UsePub { get; init; } + public bool Invalid { get; set; } + public bool Share { get; init; } + public bool Tracking { get; init; } + public long TimestampTicks { get; set; } +} diff --git a/src/NATS.Server/Imports/ServiceLatency.cs b/src/NATS.Server/Imports/ServiceLatency.cs new file mode 100644 index 0000000..0ee37fc --- /dev/null +++ b/src/NATS.Server/Imports/ServiceLatency.cs @@ -0,0 +1,7 @@ +namespace NATS.Server.Imports; + +public sealed class ServiceLatency +{ + public int SamplingPercentage { get; init; } = 100; + public string Subject { get; init; } = string.Empty; +} diff --git a/src/NATS.Server/Imports/ServiceResponseType.cs b/src/NATS.Server/Imports/ServiceResponseType.cs new file mode 100644 index 0000000..a1297ee --- /dev/null +++ b/src/NATS.Server/Imports/ServiceResponseType.cs @@ -0,0 +1,8 @@ +namespace NATS.Server.Imports; + +public enum ServiceResponseType +{ + Singleton, + Streamed, + Chunked, +} diff --git a/src/NATS.Server/Imports/StreamExport.cs b/src/NATS.Server/Imports/StreamExport.cs new file mode 100644 index 0000000..9ac6753 --- /dev/null +++ b/src/NATS.Server/Imports/StreamExport.cs @@ -0,0 +1,6 @@ +namespace NATS.Server.Imports; + +public sealed class StreamExport +{ + public ExportAuth Auth { get; init; } = new(); +} diff --git a/src/NATS.Server/Imports/StreamImport.cs b/src/NATS.Server/Imports/StreamImport.cs new file mode 100644 index 0000000..832950d --- /dev/null +++ b/src/NATS.Server/Imports/StreamImport.cs @@ -0,0 +1,14 @@ +using NATS.Server.Auth; +using NATS.Server.Subscriptions; + +namespace NATS.Server.Imports; + +public sealed class StreamImport +{ + public required Account SourceAccount { get; init; } + public required string From { get; init; } + public required string To { get; init; } + public SubjectTransform? Transform { get; init; } + public bool UsePub { get; init; } + public bool Invalid { get; set; } +} diff --git a/tests/NATS.Server.Tests/ImportExportTests.cs b/tests/NATS.Server.Tests/ImportExportTests.cs new file mode 100644 index 0000000..2945a27 --- /dev/null +++ b/tests/NATS.Server.Tests/ImportExportTests.cs @@ -0,0 +1,75 @@ +using NATS.Server.Auth; +using NATS.Server.Imports; + +namespace NATS.Server.Tests; + +public class ImportExportTests +{ + [Fact] + public void ExportAuth_public_export_authorizes_any_account() + { + var auth = new ExportAuth(); + var account = new Account("test"); + auth.IsAuthorized(account).ShouldBeTrue(); + } + + [Fact] + public void ExportAuth_approved_accounts_restricts_access() + { + var auth = new ExportAuth { ApprovedAccounts = ["allowed"] }; + var allowed = new Account("allowed"); + var denied = new Account("denied"); + auth.IsAuthorized(allowed).ShouldBeTrue(); + auth.IsAuthorized(denied).ShouldBeFalse(); + } + + [Fact] + public void ExportAuth_revoked_account_denied() + { + var auth = new ExportAuth + { + ApprovedAccounts = ["test"], + RevokedAccounts = new() { ["test"] = DateTimeOffset.UtcNow.ToUnixTimeSeconds() }, + }; + var account = new Account("test"); + auth.IsAuthorized(account).ShouldBeFalse(); + } + + [Fact] + public void ServiceResponseType_defaults_to_singleton() + { + var import = new ServiceImport + { + DestinationAccount = new Account("dest"), + From = "requests.>", + To = "api.>", + }; + import.ResponseType.ShouldBe(ServiceResponseType.Singleton); + } + + [Fact] + public void ExportMap_stores_and_retrieves_exports() + { + var map = new ExportMap(); + map.Services["api.>"] = new ServiceExport { Account = new Account("svc") }; + map.Streams["events.>"] = new StreamExport(); + + map.Services.ShouldContainKey("api.>"); + map.Streams.ShouldContainKey("events.>"); + } + + [Fact] + public void ImportMap_stores_service_imports() + { + var map = new ImportMap(); + var si = new ServiceImport + { + DestinationAccount = new Account("dest"), + From = "requests.>", + To = "api.>", + }; + map.AddServiceImport(si); + map.Services.ShouldContainKey("requests.>"); + map.Services["requests.>"].Count.ShouldBe(1); + } +}