This commit is contained in:
2026-07-15 02:42:16 +03:00
parent be2a24bfd9
commit 3a1b95dbc5
27 changed files with 1767 additions and 197 deletions
@@ -0,0 +1,365 @@
/*
Copyright (c) 2017-2026 Ahmed Kh. Zamil
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
using Esiur.Core;
using Esiur.Data;
using Esiur.Data.Types;
using Esiur.Protocol;
using Esiur.Resource;
using Esiur.Security.Authority;
using Esiur.Security.Management;
using Esiur.Security.Permissions;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
#nullable enable
namespace Esiur.Tests.Functional;
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Property, Inherited = true)]
internal sealed class FunctionalManagerPolicyAttribute : Attribute
{
public string Name { get; }
public Ruling Permissions { get; set; } = Ruling.DontCare;
public Ruling RateControl { get; set; } = Ruling.DontCare;
public FunctionalManagerPolicyAttribute(string name)
{
if (string.IsNullOrWhiteSpace(name))
throw new ArgumentException("A manager policy name is required.", nameof(name));
Name = name;
}
}
internal sealed class ManagerObservation
{
public ActionType Action { get; }
public string? MemberName { get; }
public string? PolicyName { get; }
public string? CallerIdentity { get; }
public bool HasConnection { get; }
public IResource? Resource { get; }
public ManagerObservation(
ActionType action,
string? memberName,
string? policyName,
string? callerIdentity,
bool hasConnection,
IResource? resource)
{
Action = action;
MemberName = memberName;
PolicyName = policyName;
CallerIdentity = callerIdentity;
HasConnection = hasConnection;
Resource = resource;
}
}
internal abstract class ManagerObservationRecorder
{
readonly ConcurrentQueue<ManagerObservation> observations = new();
protected static FunctionalManagerPolicyAttribute? GetPolicy(MemberDef? member)
=> member?.MemberPolicyAttributes
.OfType<FunctionalManagerPolicyAttribute>()
.SingleOrDefault();
protected void Record(
ActionType action,
MemberDef? member,
IResource? resource,
Session? session,
EpConnection? connection)
{
observations.Enqueue(new ManagerObservation(
action,
member?.Name,
GetPolicy(member)?.Name,
session?.RemoteIdentity ?? session?.LocalIdentity,
connection != null,
resource));
}
public ManagerObservation[] ExecuteObservations(string memberName)
=> observations
.Where(observation =>
observation.Action == ActionType.Execute &&
string.Equals(observation.MemberName, memberName, StringComparison.Ordinal))
.ToArray();
}
internal sealed class DefaultAllowPermissionsManager :
ManagerObservationRecorder,
IPermissionsManager
{
public Map<string, object> Settings { get; } = new();
public Ruling Applicable(
IResource resource,
Session session,
ActionType action,
MemberDef member,
object inquirer = null!)
{
Record(action, member, resource, session, inquirer as EpConnection);
return action == ActionType.Execute || action == ActionType.SetProperty
? Ruling.Allowed
: Ruling.DontCare;
}
public bool Initialize(Map<string, object> settings, IResource resource) => true;
}
internal sealed class ProbeDenyPermissionsManager :
ManagerObservationRecorder,
IPermissionsManager
{
public Map<string, object> Settings { get; } = new();
public Ruling Applicable(
IResource resource,
Session session,
ActionType action,
MemberDef member,
object inquirer = null!)
{
Record(action, member, resource, session, inquirer as EpConnection);
return GetPolicy(member)?.Permissions ?? Ruling.DontCare;
}
public bool Initialize(Map<string, object> settings, IResource resource) => true;
}
internal sealed class ProbeRateControlManager :
ManagerObservationRecorder,
IRateControlManager
{
public Ruling Applicable(ResourceManagerContext context)
{
Record(
context.Action,
context.Member,
context.Resource,
context.Session,
context.Connection);
var ruling = GetPolicy(context.Member)?.RateControl ?? Ruling.DontCare;
if (ruling == Ruling.Denied)
context.DenialReason = "The functional rate-control manager denied the operation.";
return ruling;
}
}
internal sealed class AttributeProbeAuditingManager :
ManagerObservationRecorder,
IAuditingManager
{
public Ruling Applicable(ResourceManagerContext context)
{
Record(
context.Action,
context.Member,
context.Resource,
context.Session,
context.Connection);
return Ruling.Allowed;
}
}
internal sealed class ContextProbeAuditingManager :
ManagerObservationRecorder,
IAuditingManager
{
public Ruling Applicable(ResourceManagerContext context)
{
Record(
context.Action,
context.Member,
context.Resource,
context.Session,
context.Connection);
return Ruling.DontCare;
}
}
[Resource]
[PermissionsManager<ProbeDenyPermissionsManager>]
[RateControlManager<ProbeRateControlManager>]
[AuditingManager<AttributeProbeAuditingManager>]
public partial class ManagerProbeResource
{
int allowedExecutions;
int permissionDeniedExecutions;
int rateDeniedExecutions;
internal int AllowedExecutions => Volatile.Read(ref allowedExecutions);
internal int PermissionDeniedExecutions => Volatile.Read(ref permissionDeniedExecutions);
internal int RateDeniedExecutions => Volatile.Read(ref rateDeniedExecutions);
[Export]
[FunctionalManagerPolicy(
"manager-allowed",
Permissions = Ruling.DontCare,
RateControl = Ruling.Allowed)]
public int AllowedCall(int value)
{
Interlocked.Increment(ref allowedExecutions);
return value + 1;
}
[Export]
[FunctionalManagerPolicy(
"manager-permission-denied",
Permissions = Ruling.Denied,
RateControl = Ruling.Allowed)]
public int PermissionDeniedCall()
{
Interlocked.Increment(ref permissionDeniedExecutions);
return permissionDeniedExecutions;
}
[Export]
[FunctionalManagerPolicy(
"manager-rate-denied",
Permissions = Ruling.DontCare,
RateControl = Ruling.Denied)]
public int RateDeniedCall()
{
Interlocked.Increment(ref rateDeniedExecutions);
return rateDeniedExecutions;
}
}
internal sealed class ManagerScenarioFixture
{
public ManagerProbeResource Resource { get; }
public IReadOnlyList<ManagerObservationRecorder> Managers { get; }
public ManagerScenarioFixture(
ManagerProbeResource resource,
params ManagerObservationRecorder[] managers)
{
Resource = resource;
Managers = managers;
}
}
internal static class ManagerScenarios
{
static readonly (string MemberName, string PolicyName)[] expectedCalls =
{
(nameof(ManagerProbeResource.AllowedCall), "manager-allowed"),
(nameof(ManagerProbeResource.PermissionDeniedCall), "manager-permission-denied"),
(nameof(ManagerProbeResource.RateDeniedCall), "manager-rate-denied"),
};
public static async Task Run(EpConnection connection, ManagerScenarioFixture fixture)
{
Console.WriteLine("Registered resource managers");
var remote = await connection.Get("sys/manager-probe") as EpResource
?? throw new InvalidOperationException("The manager probe resource was not found.");
var allowed = GetFunction(remote, nameof(ManagerProbeResource.AllowedCall));
var permissionDenied = GetFunction(remote, nameof(ManagerProbeResource.PermissionDeniedCall));
var rateDenied = GetFunction(remote, nameof(ManagerProbeResource.RateDeniedCall));
var allowedArguments = new Map<byte, object> { [0] = 41 };
var allowedResult = await remote._Invoke(allowed.Index, allowedArguments);
Require(Convert.ToInt32(allowedResult) == 42, "The manager-approved invocation returned an unexpected value.");
await ExpectError(
() => remote._Invoke(permissionDenied.Index, Array.Empty<object>()),
ExceptionCode.InvokeDenied);
await ExpectError(
() => remote._Invoke(rateDenied.Index, Array.Empty<object>()),
ExceptionCode.RateLimitExceeded);
Require(fixture.Resource.AllowedExecutions == 1, "The allowed manager probe did not execute exactly once.");
Require(fixture.Resource.PermissionDeniedExecutions == 0, "A permissions-denied invocation reached the resource.");
Require(fixture.Resource.RateDeniedExecutions == 0, "A rate-denied invocation reached the resource.");
foreach (var manager in fixture.Managers)
{
foreach (var expected in expectedCalls)
{
var observations = manager.ExecuteObservations(expected.MemberName);
Require(
observations.Length == 1,
$"Manager `{manager.GetType().Name}` observed `{expected.MemberName}` {observations.Length} times.");
var observation = observations[0];
Require(
string.Equals(observation.PolicyName, expected.PolicyName, StringComparison.Ordinal),
$"Manager `{manager.GetType().Name}` did not receive the policy for `{expected.MemberName}`.");
Require(
string.Equals(observation.CallerIdentity, "tester", StringComparison.Ordinal),
$"Manager `{manager.GetType().Name}` did not receive the authenticated identity.");
Require(
observation.HasConnection,
$"Manager `{manager.GetType().Name}` did not receive the connection context.");
Require(
ReferenceEquals(observation.Resource, fixture.Resource),
$"Manager `{manager.GetType().Name}` did not receive the target resource.");
}
}
Console.WriteLine(" PASS defaults, type attributes, ResourceContext, deny-overrides, and audit fan-out");
}
static FunctionDef GetFunction(EpResource resource, string name)
=> resource.Instance.Definition.GetFunctionDefByName(name)
?? throw new InvalidOperationException($"Manager probe function `{name}` was not found.");
static async Task ExpectError(Func<AsyncReply> action, ExceptionCode expectedCode)
{
try
{
await action();
}
catch (AsyncException exception) when (exception.Code == expectedCode)
{
return;
}
throw new InvalidOperationException($"The request was expected to fail with `{expectedCode}`.");
}
static void Require(bool condition, string message)
{
if (!condition)
throw new InvalidOperationException(message);
}
}
+34 -22
View File
@@ -24,12 +24,11 @@ SOFTWARE.
using Esiur.Core;
using Esiur.Data;
using Esiur.Data.Types;
using Esiur.Protocol;
using Esiur.Resource;
using Esiur.Security.Authority;
using Esiur.Security.Cryptography;
using Esiur.Security.Permissions;
using Esiur.Security.Management;
using Esiur.Security.RateLimiting;
using Esiur.Stores;
using System;
@@ -55,16 +54,17 @@ internal static class Program
try
{
var port = FindAvailablePort();
var service = await StartServer(serverWarehouse, port);
var server = await StartServer(serverWarehouse, port);
connection = await ConnectClient(clientWarehouse, port);
Require(connection.IsEncrypted, "Authenticated connection did not enable AES encryption.");
var remote = await connection.Get("sys/service") as EpResource
?? throw new InvalidOperationException("Remote service was not found.");
await RunCoreScenarios(connection, service, remote);
await RunCoreScenarios(connection, server.Service, remote);
await ManagerScenarios.Run(connection, server.ManagerScenario);
await RunRateControlScenarios(remote);
await RunStreamingScenarios(service, remote);
await RunStreamingScenarios(server.Service, remote);
Console.WriteLine();
Console.WriteLine("All functional scenarios passed.");
@@ -77,10 +77,25 @@ internal static class Program
}
}
static async Task<MyService> StartServer(Warehouse warehouse, ushort port)
static async Task<(MyService Service, ManagerScenarioFixture ManagerScenario)> StartServer(
Warehouse warehouse,
ushort port)
{
warehouse.RegisterAuthenticationProvider(new ServerAuthenticationProvider());
warehouse.RegisterEncryptionProvider(new AesEncryptionProvider());
var defaultPermissions = new DefaultAllowPermissionsManager();
var denyPermissions = new ProbeDenyPermissionsManager();
var rateControl = new ProbeRateControlManager();
var attributeAudit = new AttributeProbeAuditingManager();
var contextAudit = new ContextProbeAuditingManager();
warehouse.RegisterPermissionsManager(defaultPermissions);
warehouse.RegisterManager(denyPermissions);
warehouse.RegisterRateControlManager(rateControl);
warehouse.RegisterAuditingManager(attributeAudit);
warehouse.RegisterAuditingManager(contextAudit);
warehouse.Configuration.Parser.MaximumPacketSize = 8 * 1024 * 1024;
warehouse.Configuration.Parser.MaximumAllocationSize = 4 * 1024 * 1024;
warehouse.Configuration.Parser.MaximumCollectionItems = 65_536;
@@ -111,7 +126,10 @@ internal static class Program
});
var service = await warehouse.Put("sys/service", new MyService());
service.Instance!.Managers.Add(new AllowPropertySetPermissions());
var managerProbe = await warehouse.Put(
"sys/manager-probe",
new ManagerProbeResource(),
new ResourceContext(new IResourceManager[] { contextAudit }));
var resource1 = await warehouse.Put("sys/service/r1", new MyResource
{
Description = "Testing 1",
@@ -144,7 +162,15 @@ internal static class Program
server.MapCall("temp", () => child2);
await warehouse.Open();
return service;
return (
service,
new ManagerScenarioFixture(
managerProbe,
defaultPermissions,
denyPermissions,
rateControl,
attributeAudit,
contextAudit));
}
static async Task<EpConnection> ConnectClient(Warehouse warehouse, ushort port)
@@ -358,18 +384,4 @@ internal static class Program
return port;
}
sealed class AllowPropertySetPermissions : IPermissionsManager
{
public Map<string, object> Settings { get; } = new();
public Ruling Applicable(
IResource resource,
Session session,
ActionType action,
MemberDef member,
object inquirer = null!)
=> action == ActionType.SetProperty ? Ruling.Allowed : Ruling.DontCare;
public bool Initialize(Map<string, object> settings, IResource resource) => true;
}
}
+2
View File
@@ -8,6 +8,8 @@ Coverage includes:
- resource attachment and property serialization;
- procedure and resource calls, optional arguments, tuples, and events;
- registered resource managers from Warehouse defaults, generic type attributes,
and `ResourceContext`, including deny-overrides and audit fan-out;
- named rate policies for function calls and property setters;
- rate-limit denial propagation to callers;
- pull streams backed by `IAsyncEnumerable<T>`;
@@ -104,8 +104,11 @@ public class RateControlIntegrationTests
QueueLimit = queueLimit,
});
var permissions = new AllowPropertySetPermissions();
warehouse.RegisterManager(permissions);
var resource = await warehouse.Put("sys/rate", new RateLimitedResource());
resource.Instance!.Managers.Add(new AllowPropertySetPermissions());
resource.Instance!.Managers.Add(permissions);
});
static async Task<EpResource> GetRemote(IntegrationCluster cluster)
+285
View File
@@ -0,0 +1,285 @@
using Esiur.Data;
using Esiur.Data.Types;
using Esiur.Protocol;
using Esiur.Resource;
using Esiur.Security.Authority;
using Esiur.Security.Management;
using Esiur.Security.Permissions;
using Esiur.Stores;
using System.Text;
namespace Esiur.Tests.Unit;
public class ResourceManagerPipelineTests
{
[Fact]
public async Task Put_WithUnregisteredTypeManager_FailsBeforeAssigningInstance()
{
var warehouse = await CreateWarehouseAsync();
var resource = new AttributeManagedResource();
var exception = await Assert.ThrowsAsync<InvalidOperationException>(async () =>
{
await warehouse.Put("sys/unregistered-manager", resource);
});
Assert.Contains(nameof(AttributePermissionsManager), exception.Message);
Assert.Null(resource.Instance);
}
[Fact]
public async Task ResourceContext_RequiresTheExactRegisteredManagerInstance()
{
var warehouse = await CreateWarehouseAsync();
var registered = new ContextPermissionsManager();
var differentInstance = new ContextPermissionsManager();
warehouse.RegisterManager(registered);
var rejectedResource = new PlainResource();
var rejectedContext = new ResourceContext(
new IResourceManager[] { differentInstance });
await Assert.ThrowsAsync<InvalidOperationException>(async () =>
{
await warehouse.Put("sys/rejected-context-manager", rejectedResource, rejectedContext);
});
Assert.Null(rejectedResource.Instance);
var acceptedContext = new ResourceContext(
new IResourceManager[] { registered });
var acceptedResource = await warehouse.Put(
"sys/accepted-context-manager",
new PlainResource(),
acceptedContext);
Assert.Contains(
acceptedResource.Instance!.Managers.ToArray(),
manager => ReferenceEquals(manager, registered));
}
[Fact]
public void EvaluateManagers_ExecutesAllPermissionsManagersAndDenialOverrides()
{
var warehouse = new Warehouse();
var defaultAllow = new DefaultAllowPermissionsManager();
var localDeny = new DenyPermissionsManager();
var trailingObserver = new ObserverPermissionsManager();
warehouse.RegisterManager(defaultAllow, useAsDefault: true);
warehouse.RegisterManager(localDeny);
warehouse.RegisterManager(trailingObserver);
Assert.Same(defaultAllow, warehouse.TryGetManager<DefaultAllowPermissionsManager>());
Assert.Contains(typeof(DefaultAllowPermissionsManager), warehouse.GetRegisteredManagerTypes());
Assert.Contains(
warehouse.GetDefaultManagers(),
manager => ReferenceEquals(manager, defaultAllow));
var context = new ResourceManagerContext(
warehouse,
null,
new Session(),
null,
new FunctionDef { Name = "Call" },
ActionType.Execute);
var evaluation = warehouse.EvaluateManagers(
context,
new IResourceManager[] { localDeny, trailingObserver });
Assert.Equal(1, defaultAllow.Calls);
Assert.Equal(1, localDeny.Calls);
Assert.Equal(1, trailingObserver.Calls);
Assert.Equal(Ruling.Denied, evaluation.Permissions);
Assert.False(evaluation.IsAllowed);
}
[Fact]
public void RateControlAllow_DoesNotGrantDefaultDeniedPropertySet()
{
var warehouse = new Warehouse();
var rateManager = new AllowRateControlManager();
warehouse.RegisterManager(rateManager, useAsDefault: true);
var context = new ResourceManagerContext(
warehouse,
null,
new Session(),
null,
new PropertyDef { Name = "Value" },
ActionType.SetProperty);
var evaluation = warehouse.EvaluateManagers(context);
Assert.Equal(1, rateManager.Calls);
Assert.Equal(Ruling.Allowed, evaluation.RateControl);
Assert.Equal(Ruling.Denied, evaluation.Permissions);
Assert.False(evaluation.IsAllowed);
}
[Fact]
public async Task MemberPolicyAttributes_AreLocalAndNotTransportedToRemoteTypeDefs()
{
var warehouse = await CreateWarehouseAsync();
var local = new LocalTypeDef(typeof(MemberPolicyResource), warehouse);
var localFunction = local.GetFunctionDefByName(nameof(MemberPolicyResource.Call));
var localProperty = local.GetPropertyDefByName(nameof(MemberPolicyResource.Value));
var localEvent = local.GetEventDefByName(nameof(MemberPolicyResource.Changed));
Assert.Equal("call-rate", localFunction!.RatePolicyName);
Assert.Equal("set-rate", localProperty!.RatePolicyName);
Assert.Equal(
"call-policy",
Assert.Single(localFunction.MemberPolicyAttributes.OfType<LocalMemberPolicyAttribute>()).Name);
Assert.Equal(
"property-policy",
Assert.Single(localProperty.MemberPolicyAttributes.OfType<LocalMemberPolicyAttribute>()).Name);
Assert.Equal(
"event-policy",
Assert.Single(localEvent!.MemberPolicyAttributes.OfType<LocalMemberPolicyAttribute>()).Name);
var bytes = Codec.Compose(TypeDefInfo.FromTypeDef(local), warehouse, null!);
var wireText = Encoding.UTF8.GetString(bytes);
Assert.DoesNotContain("call-policy", wireText);
Assert.DoesNotContain("property-policy", wireText);
Assert.DoesNotContain("event-policy", wireText);
Assert.DoesNotContain("call-rate", wireText);
Assert.DoesNotContain("set-rate", wireText);
var connection = await warehouse.Put("sys/type-parser", new EpConnection());
connection.RemoteDomain = "remote.test";
var remote = await RemoteTypeDef.Parse(
new RemoteTypeDef(),
connection.RemoteDomain,
bytes,
connection,
Array.Empty<ulong>());
Assert.Empty(remote.GetFunctionDefByName(nameof(MemberPolicyResource.Call))!
.MemberPolicyAttributes);
Assert.Empty(remote.GetPropertyDefByName(nameof(MemberPolicyResource.Value))!
.MemberPolicyAttributes);
Assert.Empty(remote.GetEventDefByName(nameof(MemberPolicyResource.Changed))!
.MemberPolicyAttributes);
Assert.Null(remote.GetFunctionDefByName(nameof(MemberPolicyResource.Call))!.RatePolicyName);
Assert.Null(remote.GetPropertyDefByName(nameof(MemberPolicyResource.Value))!.RatePolicyName);
}
static async Task<Warehouse> CreateWarehouseAsync()
{
var warehouse = new Warehouse();
await warehouse.Put("sys", new MemoryStore());
return warehouse;
}
abstract class TestPermissionsManager : IPermissionsManager
{
readonly Ruling _ruling;
protected TestPermissionsManager(Ruling ruling)
{
_ruling = ruling;
}
public int Calls { get; private set; }
public Map<string, object> Settings { get; } = new();
public Ruling Applicable(
IResource resource,
Session session,
ActionType action,
MemberDef member,
object inquirer = null!)
{
Calls++;
return _ruling;
}
public bool Initialize(Map<string, object> settings, IResource resource) => true;
}
sealed class AttributePermissionsManager : TestPermissionsManager
{
public AttributePermissionsManager() : base(Ruling.DontCare)
{
}
}
sealed class ContextPermissionsManager : TestPermissionsManager
{
public ContextPermissionsManager() : base(Ruling.DontCare)
{
}
}
sealed class DefaultAllowPermissionsManager : TestPermissionsManager
{
public DefaultAllowPermissionsManager() : base(Ruling.Allowed)
{
}
}
sealed class DenyPermissionsManager : TestPermissionsManager
{
public DenyPermissionsManager() : base(Ruling.Denied)
{
}
}
sealed class ObserverPermissionsManager : TestPermissionsManager
{
public ObserverPermissionsManager() : base(Ruling.DontCare)
{
}
}
sealed class AllowRateControlManager : IRateControlManager
{
public int Calls { get; private set; }
public Ruling Applicable(ResourceManagerContext context)
{
Calls++;
return Ruling.Allowed;
}
}
[PermissionsManager<AttributePermissionsManager>]
sealed class AttributeManagedResource : Esiur.Resource.Resource
{
}
sealed class PlainResource : Esiur.Resource.Resource
{
}
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Property | AttributeTargets.Event)]
sealed class LocalMemberPolicyAttribute : Attribute
{
public string Name { get; }
public LocalMemberPolicyAttribute(string name)
{
Name = name;
}
}
[Export]
sealed class MemberPolicyResource : Esiur.Resource.Resource
{
[RateControl("call-rate")]
[LocalMemberPolicy("call-policy")]
public void Call()
{
}
[RateControl("set-rate")]
[LocalMemberPolicy("property-policy")]
public int Value { get; set; }
[LocalMemberPolicy("event-policy")]
public event ResourceEventHandler<int>? Changed;
}
}