feat: add service import shadowing detection (Gap 9.10)
Implements ServiceImportShadowed, GetShadowedServiceImports, HasShadowedImports, and CheckServiceImportShadowing on Account to detect when local SubList subscriptions would intercept messages before a service import can receive them. Adds ShadowCheckResult record and 10 tests covering exact, wildcard, and gt-wildcard shadowing scenarios.
This commit is contained in:
@@ -777,9 +777,89 @@ public sealed class Account : IDisposable
|
|||||||
/// <summary>Returns a snapshot of all reply subjects currently in the reverse response map.</summary>
|
/// <summary>Returns a snapshot of all reply subjects currently in the reverse response map.</summary>
|
||||||
public IReadOnlyList<string> GetReverseResponseMapKeys() => [.. _reverseResponseMap.Keys];
|
public IReadOnlyList<string> GetReverseResponseMapKeys() => [.. _reverseResponseMap.Keys];
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Checks whether any local subscription in this account's SubList would shadow
|
||||||
|
/// (intercept) messages on the given service import subject, preventing the import
|
||||||
|
/// from receiving them.
|
||||||
|
/// Go reference: accounts.go serviceImportShadowed (~line 2015).
|
||||||
|
/// </summary>
|
||||||
|
public bool ServiceImportShadowed(string importSubject)
|
||||||
|
{
|
||||||
|
var matchResult = SubList.Match(importSubject);
|
||||||
|
return matchResult.PlainSubs.Length > 0 || matchResult.QueueSubs.Length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns all service import subjects registered on this account that are currently
|
||||||
|
/// shadowed by a local subscription in the SubList.
|
||||||
|
/// Go reference: accounts.go serviceImportShadowed (~line 2015).
|
||||||
|
/// </summary>
|
||||||
|
public IReadOnlyList<string> GetShadowedServiceImports()
|
||||||
|
{
|
||||||
|
var shadowed = new List<string>();
|
||||||
|
foreach (var subject in Imports.Services.Keys)
|
||||||
|
{
|
||||||
|
if (ServiceImportShadowed(subject))
|
||||||
|
shadowed.Add(subject);
|
||||||
|
}
|
||||||
|
return shadowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns <see langword="true"/> when at least one registered service import subject
|
||||||
|
/// is shadowed by a local subscription.
|
||||||
|
/// Go reference: accounts.go serviceImportShadowed (~line 2015).
|
||||||
|
/// </summary>
|
||||||
|
public bool HasShadowedImports
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
foreach (var subject in Imports.Services.Keys)
|
||||||
|
{
|
||||||
|
if (ServiceImportShadowed(subject))
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns a detailed <see cref="ShadowCheckResult"/> for the given import subject,
|
||||||
|
/// including the list of local subscription subjects that shadow it.
|
||||||
|
/// Go reference: accounts.go serviceImportShadowed (~line 2015).
|
||||||
|
/// </summary>
|
||||||
|
public ShadowCheckResult CheckServiceImportShadowing(string importSubject)
|
||||||
|
{
|
||||||
|
var matchResult = SubList.Match(importSubject);
|
||||||
|
var shadowingSubs = new List<string>();
|
||||||
|
|
||||||
|
foreach (var sub in matchResult.PlainSubs)
|
||||||
|
shadowingSubs.Add(sub.Subject);
|
||||||
|
|
||||||
|
foreach (var queueGroup in matchResult.QueueSubs)
|
||||||
|
foreach (var sub in queueGroup)
|
||||||
|
shadowingSubs.Add(sub.Subject);
|
||||||
|
|
||||||
|
bool isShadowed = shadowingSubs.Count > 0;
|
||||||
|
return new ShadowCheckResult(isShadowed, importSubject, shadowingSubs);
|
||||||
|
}
|
||||||
|
|
||||||
public void Dispose() => SubList.Dispose();
|
public void Dispose() => SubList.Dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Result of <see cref="Account.CheckServiceImportShadowing"/> describing whether a service import
|
||||||
|
/// subject is intercepted by a local subscription and which subscriptions are responsible.
|
||||||
|
/// Go reference: accounts.go serviceImportShadowed (~line 2015).
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="IsShadowed">Whether any local subscription shadows the import subject.</param>
|
||||||
|
/// <param name="ImportSubject">The service import subject that was checked.</param>
|
||||||
|
/// <param name="ShadowingSubscriptions">The subjects of local subscriptions that match the import subject.</param>
|
||||||
|
public sealed record ShadowCheckResult(
|
||||||
|
bool IsShadowed,
|
||||||
|
string ImportSubject,
|
||||||
|
IReadOnlyList<string> ShadowingSubscriptions);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Carries the result of a <see cref="Account.CheckServiceResponse"/> call.
|
/// Carries the result of a <see cref="Account.CheckServiceResponse"/> call.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
170
tests/NATS.Server.Tests/Auth/ImportShadowingTests.cs
Normal file
170
tests/NATS.Server.Tests/Auth/ImportShadowingTests.cs
Normal file
@@ -0,0 +1,170 @@
|
|||||||
|
// Tests for service import shadowing detection.
|
||||||
|
// Go reference: accounts.go serviceImportShadowed (~line 2015).
|
||||||
|
|
||||||
|
using NATS.Server.Auth;
|
||||||
|
using NATS.Server.Imports;
|
||||||
|
using NATS.Server.Subscriptions;
|
||||||
|
|
||||||
|
namespace NATS.Server.Tests.Auth;
|
||||||
|
|
||||||
|
public class ImportShadowingTests
|
||||||
|
{
|
||||||
|
private static Account CreateAccount(string name) => new(name);
|
||||||
|
|
||||||
|
private static Subscription MakeSub(string subject) =>
|
||||||
|
new() { Subject = subject, Sid = subject };
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adds a service import entry directly to the account's import map (bypassing
|
||||||
|
/// export/cycle checks) so that shadowing tests can exercise the import map iteration.
|
||||||
|
/// </summary>
|
||||||
|
private static void RegisterServiceImport(Account account, string fromSubject)
|
||||||
|
{
|
||||||
|
var dest = CreateAccount("Dest");
|
||||||
|
var si = new ServiceImport
|
||||||
|
{
|
||||||
|
DestinationAccount = dest,
|
||||||
|
From = fromSubject,
|
||||||
|
To = fromSubject,
|
||||||
|
};
|
||||||
|
account.Imports.AddServiceImport(si);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Go reference: accounts.go serviceImportShadowed (~line 2015).
|
||||||
|
[Fact]
|
||||||
|
public void ServiceImportShadowed_NoLocalSubs_ReturnsFalse()
|
||||||
|
{
|
||||||
|
var account = CreateAccount("A");
|
||||||
|
|
||||||
|
var result = account.ServiceImportShadowed("orders.create");
|
||||||
|
|
||||||
|
result.ShouldBeFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Go reference: accounts.go serviceImportShadowed (~line 2015).
|
||||||
|
[Fact]
|
||||||
|
public void ServiceImportShadowed_ExactMatch_ReturnsTrue()
|
||||||
|
{
|
||||||
|
var account = CreateAccount("A");
|
||||||
|
account.SubList.Insert(MakeSub("orders.create"));
|
||||||
|
|
||||||
|
var result = account.ServiceImportShadowed("orders.create");
|
||||||
|
|
||||||
|
result.ShouldBeTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Go reference: accounts.go serviceImportShadowed (~line 2015).
|
||||||
|
[Fact]
|
||||||
|
public void ServiceImportShadowed_WildcardMatch_ReturnsTrue()
|
||||||
|
{
|
||||||
|
// Local subscription "orders.*" shadows import on "orders.create"
|
||||||
|
var account = CreateAccount("A");
|
||||||
|
account.SubList.Insert(MakeSub("orders.*"));
|
||||||
|
|
||||||
|
var result = account.ServiceImportShadowed("orders.create");
|
||||||
|
|
||||||
|
result.ShouldBeTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Go reference: accounts.go serviceImportShadowed (~line 2015).
|
||||||
|
[Fact]
|
||||||
|
public void ServiceImportShadowed_GtWildcard_ReturnsTrue()
|
||||||
|
{
|
||||||
|
// Local subscription "orders.>" shadows import on "orders.create.new"
|
||||||
|
var account = CreateAccount("A");
|
||||||
|
account.SubList.Insert(MakeSub("orders.>"));
|
||||||
|
|
||||||
|
var result = account.ServiceImportShadowed("orders.create.new");
|
||||||
|
|
||||||
|
result.ShouldBeTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Go reference: accounts.go serviceImportShadowed (~line 2015).
|
||||||
|
[Fact]
|
||||||
|
public void ServiceImportShadowed_NoMatch_ReturnsFalse()
|
||||||
|
{
|
||||||
|
// Local subscription "users.*" does NOT shadow import on "orders.create"
|
||||||
|
var account = CreateAccount("A");
|
||||||
|
account.SubList.Insert(MakeSub("users.*"));
|
||||||
|
|
||||||
|
var result = account.ServiceImportShadowed("orders.create");
|
||||||
|
|
||||||
|
result.ShouldBeFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Go reference: accounts.go serviceImportShadowed (~line 2015).
|
||||||
|
[Fact]
|
||||||
|
public void GetShadowedServiceImports_ReturnsOnlyShadowed()
|
||||||
|
{
|
||||||
|
var account = CreateAccount("A");
|
||||||
|
|
||||||
|
// Register two service imports
|
||||||
|
RegisterServiceImport(account, "orders.create");
|
||||||
|
RegisterServiceImport(account, "users.profile");
|
||||||
|
|
||||||
|
// Only add a local sub that shadows "orders.create"
|
||||||
|
account.SubList.Insert(MakeSub("orders.create"));
|
||||||
|
|
||||||
|
var shadowed = account.GetShadowedServiceImports();
|
||||||
|
|
||||||
|
shadowed.Count.ShouldBe(1);
|
||||||
|
shadowed.ShouldContain("orders.create");
|
||||||
|
shadowed.ShouldNotContain("users.profile");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Go reference: accounts.go serviceImportShadowed (~line 2015).
|
||||||
|
[Fact]
|
||||||
|
public void HasShadowedImports_True_WhenShadowed()
|
||||||
|
{
|
||||||
|
var account = CreateAccount("A");
|
||||||
|
|
||||||
|
RegisterServiceImport(account, "orders.create");
|
||||||
|
account.SubList.Insert(MakeSub("orders.create"));
|
||||||
|
|
||||||
|
account.HasShadowedImports.ShouldBeTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Go reference: accounts.go serviceImportShadowed (~line 2015).
|
||||||
|
[Fact]
|
||||||
|
public void HasShadowedImports_False_WhenNone()
|
||||||
|
{
|
||||||
|
var account = CreateAccount("A");
|
||||||
|
|
||||||
|
RegisterServiceImport(account, "orders.create");
|
||||||
|
// No local subs — nothing shadows the import
|
||||||
|
|
||||||
|
account.HasShadowedImports.ShouldBeFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Go reference: accounts.go serviceImportShadowed (~line 2015).
|
||||||
|
[Fact]
|
||||||
|
public void CheckServiceImportShadowing_ReturnsShadowingSubscriptions()
|
||||||
|
{
|
||||||
|
var account = CreateAccount("A");
|
||||||
|
account.SubList.Insert(MakeSub("orders.*"));
|
||||||
|
account.SubList.Insert(MakeSub("orders.>"));
|
||||||
|
|
||||||
|
var result = account.CheckServiceImportShadowing("orders.create");
|
||||||
|
|
||||||
|
result.IsShadowed.ShouldBeTrue();
|
||||||
|
result.ImportSubject.ShouldBe("orders.create");
|
||||||
|
result.ShadowingSubscriptions.Count.ShouldBeGreaterThan(0);
|
||||||
|
// Both wildcard subs match "orders.create"
|
||||||
|
result.ShadowingSubscriptions.ShouldContain("orders.*");
|
||||||
|
result.ShadowingSubscriptions.ShouldContain("orders.>");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Go reference: accounts.go serviceImportShadowed (~line 2015).
|
||||||
|
[Fact]
|
||||||
|
public void CheckServiceImportShadowing_NotShadowed()
|
||||||
|
{
|
||||||
|
var account = CreateAccount("A");
|
||||||
|
account.SubList.Insert(MakeSub("users.*"));
|
||||||
|
|
||||||
|
var result = account.CheckServiceImportShadowing("orders.create");
|
||||||
|
|
||||||
|
result.IsShadowed.ShouldBeFalse();
|
||||||
|
result.ImportSubject.ShouldBe("orders.create");
|
||||||
|
result.ShadowingSubscriptions.Count.ShouldBe(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user