mirror of
https://github.com/esiur/esiur-dotnet.git
synced 2026-07-30 17:30:40 +00:00
Managers
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user