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
@@ -1,5 +1,6 @@
using Esiur.Data.Types;
using Esiur.Protocol;
using Esiur.Resource;
using System;
using System.Collections;
using System.Collections.Generic;
@@ -23,6 +24,8 @@ namespace Esiur.Core
internal StreamMode StreamMode { get; private set; }
internal bool Pausable { get; private set; }
internal IResource Resource { get; private set; }
internal FunctionDef Function { get; private set; }
internal volatile bool Ended;
@@ -75,6 +78,12 @@ namespace Esiur.Core
CallbackId = callbackId;
}
internal void BindOperation(IResource resource, FunctionDef function)
{
Resource = resource;
Function = function;
}
internal void InitializeStream(StreamMode streamMode, bool pausable)
{
StreamMode = streamMode;
+2
View File
@@ -209,12 +209,14 @@ public class EventDef : MemberDef
return DefinitionAttributeReader.Apply(ei, new EventDef()
{
Definition = schema,
Name = name,
ArgumentType = evtType,
Index = index,
Inherited = ei.DeclaringType != type,
Annotations = annotations,
EventInfo = ei,
MemberPolicyAttributes = Attribute.GetCustomAttributes(ei, true),
AutoDelivered = autoDeliveryAttr != null,
Historical = historicalAttr != null,
OrderingControl = orderingAttr?.Control ?? OrderingControl.Strict,
@@ -367,6 +367,7 @@ public class FunctionDef : MemberDef
return DefinitionAttributeReader.Apply(mi, new FunctionDef()
{
Definition = schema,
Name = name,
Index = index,
Inherited = mi.DeclaringType != type,
@@ -379,6 +380,7 @@ public class FunctionDef : MemberDef
Idempotent = mi.GetCustomAttribute<IdempotentAttribute>(true) != null,
Cancellable = mi.GetCustomAttribute<CancellableAttribute>(true) != null,
RatePolicyName = mi.GetCustomAttribute<RateControlAttribute>(true)?.PolicyName,
MemberPolicyAttributes = Attribute.GetCustomAttributes(mi, true),
StreamMode = streamMode,
Pausable = pausable,
});
+13
View File
@@ -8,6 +8,7 @@ using System.Threading.Tasks;
namespace Esiur.Data.Types;
public class MemberDef
{
IReadOnlyList<Attribute> memberPolicyAttributes = Array.Empty<Attribute>();
// Core fields
public byte Index { get; set; }
@@ -24,6 +25,18 @@ public class MemberDef
/// </summary>
public string? RatePolicyName { get; set; }
/// <summary>
/// Local reflection attributes supplied to resource managers while this member
/// is evaluated. This execution metadata is not serialized to remote peers.
/// </summary>
public IReadOnlyList<Attribute> MemberPolicyAttributes
{
get => memberPolicyAttributes;
internal set => memberPolicyAttributes = value is null
? Array.Empty<Attribute>()
: Array.AsReadOnly(value.ToArray());
}
public TypeDef Definition { get; set; } = null!;
// Human-readable metadata
+6 -1
View File
@@ -312,12 +312,17 @@ public class PropertyDef : MemberDef
return DefinitionAttributeReader.Apply(pi, new PropertyDef()
{
Definition = typeDef,
Name = name,
Index = index,
Inherited = pi.DeclaringType != type,
ValueType = propType,
PropertyInfo = pi,
RatePolicyName = pi.GetCustomAttribute<RateControlAttribute>(true)?.PolicyName,
RatePolicyName = (Attribute.GetCustomAttribute(
pi,
typeof(RateControlAttribute),
true) as RateControlAttribute)?.PolicyName,
MemberPolicyAttributes = Attribute.GetCustomAttributes(pi, true),
ReadOnly = readOnlyAttr != null,
Volatile = volatileAttr != null,
Historical = historicalAttr != null,
+374 -131
View File
@@ -30,6 +30,7 @@ using Esiur.Net;
using Esiur.Net.Packets;
using Esiur.Resource;
using Esiur.Security.Authority;
using Esiur.Security.Management;
using Esiur.Security.Permissions;
using Esiur.Security.RateLimiting;
using Microsoft.CodeAnalysis.CSharp.Syntax;
@@ -55,63 +56,110 @@ partial class EpConnection
internal bool IsRateControlBlocked => Volatile.Read(ref _rateControlBlocked) != 0;
bool TryApplyRateControl(
bool TryApplyManagers(
MemberDef member,
IResource? resource,
ActionType action,
uint callback,
out TimeSpan delay)
ErrorType denialErrorType,
ExceptionCode denialCode,
out TimeSpan delay,
IEnumerable<IResourceManager> managers = null,
bool supportsDelay = false)
{
delay = TimeSpan.Zero;
if (string.IsNullOrWhiteSpace(member.RatePolicyName))
return true;
if (IsRateControlBlocked)
return false;
var warehouse = Instance.Warehouse;
var policy = warehouse.TryGetRatePolicy(member.RatePolicyName);
if (policy == null)
{
DenyRateControlledRequest(
SendError(
ErrorType.Management,
callback,
$"Rate policy `{member.RatePolicyName}` is not registered.");
(ushort)ExceptionCode.RateLimitExceeded,
"The connection is blocked by rate control.");
return false;
}
var warehouse = Instance.Warehouse;
try
{
var context = new RateControlContext(
if (resource == null && member?.Definition is LocalTypeDef localTypeDef)
managers ??= warehouse.ResolveResourceManagers(localTypeDef.DefinedType, null);
else if (resource == null && member is FunctionDef functionDef)
managers ??= warehouse.ResolveResourceManagers(functionDef.MethodInfo?.DeclaringType, null);
var context = new ResourceManagerContext(
warehouse,
this,
_session,
resource,
member,
action);
action,
this,
member?.MemberPolicyAttributes);
if (policy.Applicable(context) == Ruling.Denied)
var evaluation = warehouse.EvaluateManagers(context, managers);
delay = evaluation.Delay;
if (evaluation.Permissions == Ruling.Denied ||
evaluation.Auditing == Ruling.Denied)
{
DenyRateControlledRequest(
callback,
$"Rate policy `{member.RatePolicyName}` denied `{member.Fullname}`.");
if (evaluation.RateControl == Ruling.Denied)
DenyRateControlledRequest(
callback,
evaluation.RateControlDenialReason ?? "Rate control denied the operation.",
false);
SendError(denialErrorType, callback, (ushort)denialCode);
return false;
}
delay = context.Delay > TimeSpan.Zero ? context.Delay : TimeSpan.Zero;
return true;
if (evaluation.RateControl == Ruling.Denied)
{
DenyRateControlledRequest(
callback,
evaluation.RateControlDenialReason ?? "Rate control denied the operation.");
return false;
}
if (delay > TimeSpan.Zero && !supportsDelay)
{
DenyRateControlledRequest(
callback,
$"Delayed `{action}` operations are not supported.");
return false;
}
return evaluation.IsAllowed;
}
catch (Exception exception)
{
DenyRateControlledRequest(
callback,
$"Rate policy `{member.RatePolicyName}` failed: {exception.Message}");
Global.Log(exception);
SendError(denialErrorType, callback, (ushort)denialCode);
return false;
}
}
void DenyRateControlledRequest(uint callback, string message)
bool IsOperationAllowed(
IResource resource,
MemberDef member,
ActionType action,
object inquirer = null)
{
var context = new ResourceManagerContext(
Instance.Warehouse,
this,
_session,
resource,
member,
action,
inquirer ?? this,
member?.MemberPolicyAttributes);
return Instance.Warehouse.EvaluateManagers(context).IsAllowed;
}
void DenyRateControlledRequest(uint callback, string message, bool sendError = true)
{
var configuration = Instance.Warehouse.Configuration.RateControl;
var now = DateTime.UtcNow;
@@ -136,11 +184,12 @@ partial class EpConnection
Interlocked.Exchange(ref _rateControlBlocked, 1) == 0;
}
SendError(
ErrorType.Management,
callback,
(ushort)ExceptionCode.RateLimitExceeded,
message);
if (sendError)
SendError(
ErrorType.Management,
callback,
(ushort)ExceptionCode.RateLimitExceeded,
message);
if (shouldBlock)
_ = CloseRateControlledConnectionAsync(configuration.ConnectionBlockDelay);
@@ -944,11 +993,15 @@ partial class EpConnection
{
if (res != null)
{
if (res.Instance.Applicable(_session, ActionType.Attach, null) == Ruling.Denied)
{
SendError(ErrorType.Management, callback, 6);
if (!TryApplyManagers(
null,
res,
ActionType.Attach,
callback,
ErrorType.Management,
ExceptionCode.AttachDenied,
out _))
return;
}
var r = res as IResource;
@@ -1007,11 +1060,15 @@ partial class EpConnection
{
if (res != null)
{
if (res.Instance.Applicable(_session, ActionType.Attach, null) == Ruling.Denied)
{
SendError(ErrorType.Management, callback, 6);
if (!TryApplyManagers(
null,
res,
ActionType.Attach,
callback,
ErrorType.Management,
ExceptionCode.AttachDenied,
out _))
return;
}
var r = res as IResource;
@@ -1059,6 +1116,15 @@ partial class EpConnection
{
if (res != null)
{
if (!TryApplyManagers(
null,
res,
ActionType.Detach,
callback,
ErrorType.Management,
ExceptionCode.NotAllowed,
out _))
return;
// unsubscribe
Unsubscribe(res);
@@ -1121,11 +1187,15 @@ partial class EpConnection
var store = r.Instance.Store;
// check security
if (store.Instance.Applicable(_session, ActionType.CreateResource, null) != Ruling.Allowed)
{
SendError(ErrorType.Management, callback, (ushort)ExceptionCode.CreateDenied);
if (!TryApplyManagers(
null,
store,
ActionType.CreateResource,
callback,
ErrorType.Management,
ExceptionCode.CreateDenied,
out _))
return;
}
Instance.Warehouse.New(localTypeDef.DefinedType, path,
new ResourceContext(0,
@@ -1166,11 +1236,20 @@ partial class EpConnection
return;
}
if (r.Instance.Store.Instance.Applicable(_session, ActionType.Delete, null) != Ruling.Allowed)
{
SendError(ErrorType.Management, callback, (ushort)ExceptionCode.DeleteDenied);
var deleteManagers = r.Instance.Managers
.ToArray()
.Concat(r.Instance.Store.Instance.Managers.ToArray());
if (!TryApplyManagers(
null,
r,
ActionType.Delete,
callback,
ErrorType.Management,
ExceptionCode.DeleteDenied,
out _,
deleteManagers))
return;
}
if (Instance.Warehouse.Remove(r))
SendReply(EpPacketReply.Completed, callback);
@@ -1205,11 +1284,15 @@ partial class EpConnection
return;
}
if (resource.Instance.Applicable(this._session, ActionType.Rename, null) != Ruling.Allowed)
{
SendError(ErrorType.Management, callback, (ushort)ExceptionCode.RenameDenied);
if (!TryApplyManagers(
null,
resource,
ActionType.Rename,
callback,
ErrorType.Management,
ExceptionCode.RenameDenied,
out _))
return;
}
resource.Instance.Name = name;
@@ -1240,11 +1323,15 @@ partial class EpConnection
return;
}
if (r.Instance.Applicable(_session, ActionType.ViewTypeDef, null) == Ruling.Denied)
{
SendError(ErrorType.Management, callback, (ushort)ExceptionCode.NotAllowed);
if (!TryApplyManagers(
null,
r,
ActionType.ViewTypeDef,
callback,
ErrorType.Management,
ExceptionCode.NotAllowed,
out _))
return;
}
// make sure the resource is a local type def.
if (r.Instance.Definition is LocalTypeDef localTypeDef)
@@ -1272,19 +1359,33 @@ partial class EpConnection
var classNames = (string[])value;
var typeDefs = new List<ulong>();
var typeDefs = new List<LocalTypeDef>();
foreach (var className in classNames)
{
//@TODO: need to search in remoteTypeDefs as well
var typeDef = Instance.Warehouse.GetLocalTypeDefByName(className);
if (typeDef != null)
typeDefs.Add(typeDef.Id);
typeDefs.Add(typeDef);
}
if (typeDefs.Count > 0)
{
SendReply(EpPacketReply.Completed, callback, typeDefs.ToArray());
var managers = typeDefs
.SelectMany(typeDef => Instance.Warehouse.ResolveResourceManagers(typeDef.DefinedType, null));
if (!TryApplyManagers(
null,
null,
ActionType.ViewTypeDef,
callback,
ErrorType.Management,
ExceptionCode.NotAllowed,
out _,
managers))
return;
SendReply(EpPacketReply.Completed, callback, typeDefs.Select(typeDef => typeDef.Id).ToArray());
}
else
{
@@ -1304,6 +1405,18 @@ partial class EpConnection
if (t != null)
{
var managers = Instance.Warehouse.ResolveResourceManagers(t.DefinedType, null);
if (!TryApplyManagers(
null,
null,
ActionType.ViewTypeDef,
callback,
ErrorType.Management,
ExceptionCode.NotAllowed,
out _,
managers))
return;
SendReply(EpPacketReply.Completed, callback, t.Compose(this));
}
else
@@ -1326,6 +1439,16 @@ partial class EpConnection
{
if (r != null)
{
if (!TryApplyManagers(
null,
r,
ActionType.ViewTypeDef,
callback,
ErrorType.Management,
ExceptionCode.NotAllowed,
out _))
return;
SendReply(EpPacketReply.Completed, callback, r.Instance.Definition.Compose(this));
}
else
@@ -1349,11 +1472,15 @@ partial class EpConnection
SendError(ErrorType.Management, callback, (ushort)ExceptionCode.ResourceNotFound);
else
{
if (r.Instance.Applicable(_session, ActionType.Attach, null) == Ruling.Denied)
{
SendError(ErrorType.Management, callback, (ushort)ExceptionCode.ResourceNotFound);
if (!TryApplyManagers(
null,
r,
ActionType.Attach,
callback,
ErrorType.Management,
ExceptionCode.ResourceNotFound,
out _))
return;
}
SendReply(EpPacketReply.Completed, callback, r);
}
@@ -1378,15 +1505,21 @@ partial class EpConnection
SendError(ErrorType.Management, callback, (ushort)ExceptionCode.ResourceNotFound);
else
{
if (r.Instance.Applicable(_session, ActionType.Attach, null) == Ruling.Denied)
{
SendError(ErrorType.Management, callback, (ushort)ExceptionCode.NotAllowed);
if (!TryApplyManagers(
null,
r,
ActionType.Attach,
callback,
ErrorType.Management,
ExceptionCode.NotAllowed,
out _))
return;
}
r.Instance.Children<IResource>().Then(children =>
{
var list = children.Where(x => x.Instance.Applicable(_session, ActionType.Attach, null) != Ruling.Denied).ToArray();
var list = children
.Where(x => IsOperationAllowed(x, null, ActionType.Attach))
.ToArray();
SendReply(EpPacketReply.Completed, callback, list);
}).Error(e =>
{
@@ -1445,6 +1578,17 @@ partial class EpConnection
return;
}
if (!TryApplyManagers(
call.Value.Definition,
call.Value.Delegate.Target as IResource,
ActionType.Execute,
callback,
ErrorType.Management,
ExceptionCode.InvokeDenied,
out var managerDelay,
supportsDelay: true))
return;
Codec.ParseAsync(tdu.Data, offset, this, null).Then(pr =>
{
if (pr.Value is AsyncReply reply)
@@ -1464,7 +1608,7 @@ partial class EpConnection
// return;
//}
InvokeFunction(call.Value.Definition, callback, results, EpPacketRequest.ProcedureCall, call.Value.Delegate.Target);
InvokeFunction(call.Value.Definition, callback, results, EpPacketRequest.ProcedureCall, managerDelay, call.Value.Delegate.Target);
}).Error(x =>
{
@@ -1479,7 +1623,7 @@ partial class EpConnection
this.Socket.Unhold();
// @TODO: Make managers for procedure calls
InvokeFunction(call.Value.Definition, callback, pr.Value, EpPacketRequest.ProcedureCall, call.Value.Delegate.Target);
InvokeFunction(call.Value.Definition, callback, pr.Value, EpPacketRequest.ProcedureCall, managerDelay, call.Value.Delegate.Target);
}
}).Error(x => SendError(ErrorType.Management, callback, (ushort)ExceptionCode.ParseError));
}
@@ -1518,6 +1662,18 @@ partial class EpConnection
return;
}
if (!TryApplyManagers(
fd,
null,
ActionType.Execute,
callback,
ErrorType.Management,
ExceptionCode.InvokeDenied,
out var managerDelay,
managers: Instance.Warehouse.ResolveResourceManagers(typeDef.DefinedType, null),
supportsDelay: true))
return;
Codec.ParseAsync(tdu.Data, offset, this, null).Then(pr =>
{
if (pr.Value is AsyncReply reply)
@@ -1538,7 +1694,7 @@ partial class EpConnection
// return;
//}
InvokeFunction(fd, callback, results, EpPacketRequest.StaticCall, null);
InvokeFunction(fd, callback, results, EpPacketRequest.StaticCall, managerDelay, null);
}).Error(x =>
{
@@ -1555,7 +1711,7 @@ partial class EpConnection
// @TODO: Make managers for static calls
InvokeFunction(fd, callback, pr.Value, EpPacketRequest.StaticCall, null);
InvokeFunction(fd, callback, pr.Value, EpPacketRequest.StaticCall, managerDelay, null);
}
}).Error(x => SendError(ErrorType.Management, callback, (ushort)ExceptionCode.ParseError));
}
@@ -1586,6 +1742,17 @@ partial class EpConnection
return;
}
if (!TryApplyManagers(
ft,
r,
ActionType.Execute,
callback,
ErrorType.Management,
ExceptionCode.InvokeDenied,
out var managerDelay,
supportsDelay: true))
return;
Codec.ParseAsync(tdu.Data, offset, this, null).Then(pr =>
{
if (pr.Value is AsyncReply asyncReply)
@@ -1599,30 +1766,26 @@ partial class EpConnection
if (r is EpResource)
{
var rt = (r as EpResource)._Invoke(index, result);
if (rt != null)
ExecuteRateControlled(callback, managerDelay, () =>
{
rt.Then(res =>
var rt = (r as EpResource)._Invoke(index, result);
if (rt != null)
{
SendReply(EpPacketReply.Completed, callback, res);
});
}
else
{
// function not found on a distributed object
SendError(ErrorType.Management, callback, (ushort)ExceptionCode.MethodNotFound);
}
rt.Then(res =>
{
SendReply(EpPacketReply.Completed, callback, res);
});
}
else
{
// function not found on a distributed object
SendError(ErrorType.Management, callback, (ushort)ExceptionCode.MethodNotFound);
}
});
}
else
{
if (r.Instance.Applicable(_session, ActionType.Execute, ft) == Ruling.Denied)
{
SendError(ErrorType.Management, callback,
(ushort)ExceptionCode.InvokeDenied);
return;
}
InvokeFunction(ft, callback, result, EpPacketRequest.InvokeFunction, r);
InvokeFunction(ft, callback, result, EpPacketRequest.InvokeFunction, managerDelay, r);
}
});
}
@@ -1635,30 +1798,26 @@ partial class EpConnection
if (r is EpResource)
{
var rt = (r as EpResource)._Invoke(index, pr.Value);
if (rt != null)
ExecuteRateControlled(callback, managerDelay, () =>
{
rt.Then(res =>
var rt = (r as EpResource)._Invoke(index, pr.Value);
if (rt != null)
{
SendReply(EpPacketReply.Completed, callback, res);
});
}
else
{
// function not found on a distributed object
SendError(ErrorType.Management, callback, (ushort)ExceptionCode.MethodNotFound);
}
rt.Then(res =>
{
SendReply(EpPacketReply.Completed, callback, res);
});
}
else
{
// function not found on a distributed object
SendError(ErrorType.Management, callback, (ushort)ExceptionCode.MethodNotFound);
}
});
}
else
{
if (r.Instance.Applicable(_session, ActionType.Execute, ft) == Ruling.Denied)
{
SendError(ErrorType.Management, callback,
(ushort)ExceptionCode.InvokeDenied);
return;
}
InvokeFunction(ft, callback, pr.Value, EpPacketRequest.InvokeFunction, r);
InvokeFunction(ft, callback, pr.Value, EpPacketRequest.InvokeFunction, managerDelay, r);
}
}
}).Error(x => SendError(ErrorType.Management, callback, (ushort)ExceptionCode.ParseError)); ;
@@ -1667,19 +1826,17 @@ partial class EpConnection
void InvokeFunction(FunctionDef ft, uint callback, object arguments, EpPacketRequest actionType, object target = null)
void InvokeFunction(
FunctionDef ft,
uint callback,
object arguments,
EpPacketRequest actionType,
TimeSpan managerDelay,
object target = null)
{
if (!TryApplyRateControl(
ft,
target as IResource,
ActionType.Execute,
callback,
out var delay))
return;
ExecuteRateControlled(
callback,
delay,
managerDelay,
() => InvokeFunctionCore(ft, callback, arguments, actionType, target));
}
@@ -1811,6 +1968,8 @@ partial class EpConnection
object rt;
context?.BindOperation(target as IResource, ft);
try
{
rt = ft.MethodInfo.Invoke(target, args);
@@ -1827,6 +1986,7 @@ partial class EpConnection
if (ft.StreamMode != StreamMode.None)
{
context ??= new InvocationContext(this, callback);
context.BindOperation(target as IResource, ft);
context.InitializeStream(ft.StreamMode, ft.Pausable);
if (ft.StreamMode == StreamMode.Pull && context.SetAsyncEnumerable(rt))
@@ -2041,10 +2201,24 @@ partial class EpConnection
return;
}
if (!TryApplyManagers(
context.Function,
context.Resource,
ActionType.PullStream,
callback,
ErrorType.Management,
ExceptionCode.NotAllowed,
out var managerDelay,
supportsDelay: true))
return;
Task.Run(async () =>
{
try
{
if (managerDelay > TimeSpan.Zero)
await Task.Delay(managerDelay).ConfigureAwait(false);
var next = await context.PullAsync();
if (next.HasValue)
SendChunk(executionCallback, next.Value);
@@ -2070,7 +2244,25 @@ partial class EpConnection
{
var executionCallback = ParseExecutionCallback(tdu);
if (!TakeInvocation(executionCallback, null, out var context))
var activeContext = GetInvocation(executionCallback);
if (activeContext == null)
{
SendError(ErrorType.Management, callback, (ushort)ExceptionCode.NotAllowed,
"The target execution is not active.");
return;
}
if (!TryApplyManagers(
activeContext.Function,
activeContext.Resource,
ActionType.TerminateExecution,
callback,
ErrorType.Management,
ExceptionCode.NotAllowed,
out _))
return;
if (!TakeInvocation(executionCallback, activeContext, out var context))
{
SendError(ErrorType.Management, callback, (ushort)ExceptionCode.NotAllowed,
"The target execution is not active.");
@@ -2099,7 +2291,24 @@ partial class EpConnection
var executionCallback = ParseExecutionCallback(tdu);
var context = GetInvocation(executionCallback);
if (context == null || !context.Halt())
if (context == null)
{
SendError(ErrorType.Management, callback, (ushort)ExceptionCode.NotAllowed,
"The target execution is not pausable or is already halted.");
return;
}
if (!TryApplyManagers(
context.Function,
context.Resource,
ActionType.HaltExecution,
callback,
ErrorType.Management,
ExceptionCode.NotAllowed,
out _))
return;
if (!context.Halt())
{
SendError(ErrorType.Management, callback, (ushort)ExceptionCode.NotAllowed,
"The target execution is not pausable or is already halted.");
@@ -2114,7 +2323,24 @@ partial class EpConnection
var executionCallback = ParseExecutionCallback(tdu);
var context = GetInvocation(executionCallback);
if (context == null || !context.Resume())
if (context == null)
{
SendError(ErrorType.Management, callback, (ushort)ExceptionCode.NotAllowed,
"The target execution is not pausable or is not halted.");
return;
}
if (!TryApplyManagers(
context.Function,
context.Resource,
ActionType.ResumeExecution,
callback,
ErrorType.Management,
ExceptionCode.NotAllowed,
out _))
return;
if (!context.Resume())
{
SendError(ErrorType.Management, callback, (ushort)ExceptionCode.NotAllowed,
"The target execution is not pausable or is not halted.");
@@ -2151,6 +2377,16 @@ partial class EpConnection
return;
}
if (!TryApplyManagers(
et,
r,
ActionType.Subscribe,
callback,
ErrorType.Management,
ExceptionCode.NotAllowed,
out _))
return;
if (r is EpResource)
{
(r as EpResource).Subscribe(et).Then(x =>
@@ -2210,6 +2446,16 @@ partial class EpConnection
return;
}
if (!TryApplyManagers(
et,
r,
ActionType.Unsubscribe,
callback,
ErrorType.Management,
ExceptionCode.NotAllowed,
out _))
return;
if (r is EpResource)
{
(r as EpResource).Unsubscribe(et).Then(x =>
@@ -2274,18 +2520,15 @@ partial class EpConnection
return;
}
if (r.Instance.Applicable(_session, ActionType.SetProperty, pt, this) == Ruling.Denied)
{
SendError(ErrorType.Exception, callback, (ushort)ExceptionCode.SetPropertyDenied);
return;
}
if (!TryApplyRateControl(
if (!TryApplyManagers(
pt,
r,
ActionType.SetProperty,
callback,
out var rateControlDelay))
ErrorType.Exception,
ExceptionCode.SetPropertyDenied,
out var rateControlDelay,
supportsDelay: true))
return;
@@ -3359,7 +3602,7 @@ partial class EpConnection
if (!info.Receivers(_session))
return;
if (info.Resource.Instance.Applicable(_session, ActionType.ReceiveEvent, info.EventDef, info.Issuer) == Ruling.Denied)
if (!IsOperationAllowed(info.Resource, info.EventDef, ActionType.ReceiveEvent, info.Issuer))
return;
@@ -3385,7 +3628,7 @@ partial class EpConnection
}
}
if (info.Resource.Instance.Applicable(_session, ActionType.ReceiveEvent, info.Definition, null) == Ruling.Denied)
if (!IsOperationAllowed(info.Resource, info.Definition, ActionType.ReceiveEvent))
return;
// compose the packet
@@ -7,7 +7,10 @@ namespace Esiur.Resource;
/// Associates a registered auditing-manager implementation with a resource type.
/// </summary>
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = true)]
public sealed class AuditingManagerAttribute<T> : Attribute
public sealed class AuditingManagerAttribute<T> : ResourceManagerAttribute
where T : IAuditingManager
{
public AuditingManagerAttribute() : base(typeof(T))
{
}
}
@@ -7,7 +7,10 @@ namespace Esiur.Resource;
/// Associates a registered permissions-manager implementation with a resource type.
/// </summary>
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = true)]
public sealed class PermissionsManagerAttribute<T> : Attribute
public sealed class PermissionsManagerAttribute<T> : ResourceManagerAttribute
where T : IPermissionsManager
{
public PermissionsManagerAttribute() : base(typeof(T))
{
}
}
@@ -7,7 +7,10 @@ namespace Esiur.Resource;
/// Associates a registered rate-control-manager implementation with a resource type.
/// </summary>
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = true)]
public sealed class RateControlManagerAttribute<T> : Attribute
public sealed class RateControlManagerAttribute<T> : ResourceManagerAttribute
where T : IRateControlManager
{
public RateControlManagerAttribute() : base(typeof(T))
{
}
}
@@ -0,0 +1,24 @@
using Esiur.Security.Management;
using System;
namespace Esiur.Resource;
/// <summary>
/// Base metadata used to resolve a resource manager from its owning Warehouse.
/// Manager attributes never create instances directly; the declared type must be
/// registered with the Warehouse before the resource is created.
/// </summary>
public abstract class ResourceManagerAttribute : Attribute
{
public Type ManagerType { get; }
protected ResourceManagerAttribute(Type managerType)
{
ManagerType = managerType ?? throw new ArgumentNullException(nameof(managerType));
if (!typeof(IResourceManager).IsAssignableFrom(managerType))
throw new ArgumentException(
$"Manager type `{managerType}` must implement {nameof(IResourceManager)}.",
nameof(managerType));
}
}
@@ -0,0 +1,14 @@
using Esiur.Security.Management;
using System.Collections.Generic;
namespace Esiur.Resource;
/// <summary>
/// Optional trusted creation context that binds registered managers to a resource.
/// Keeping this separate from <see cref="IResourceContext"/> preserves compatibility
/// with existing context implementations.
/// </summary>
public interface IResourceManagersContext
{
IReadOnlyList<IResourceManager> ResourceManagers { get; }
}
+46 -25
View File
@@ -8,6 +8,7 @@ using System.Runtime.CompilerServices;
using System.Reflection;
using Esiur.Misc;
using Esiur.Security.Permissions;
using Esiur.Security.Management;
using Esiur.Security.Authority;
using Esiur.Proxy;
using Esiur.Core;
@@ -30,7 +31,7 @@ public class Instance
WeakReference<IResource> resource;
IStore store;
TypeDef definition;
AutoList<IPermissionsManager, Instance> managers;
AutoList<IResourceManager, Instance> managers;
public event PropertyModifiedEvent PropertyModified;
@@ -696,36 +697,55 @@ public class Instance
/// <returns>Ruling.</returns>
public Ruling Applicable(Session session, ActionType action, MemberDef member, object inquirer = null)
{
IResource res;
if (this.resource.TryGetTarget(out res))
{
//return store.Applicable(res, session, action, member, inquirer);
var context = MakeManagerContext(session, action, member, inquirer);
return Warehouse.EvaluatePermissions(context, managers.ToArray());
}
foreach (IPermissionsManager manager in managers)
{
var r = manager.Applicable(res, session, action, member, inquirer);
if (r != Ruling.DontCare)
return r;
}
}
// Apply default permissions if no manager is applicable or if the resource is not available.
if (action == ActionType.GetProperty
|| action == ActionType.ViewTypeDef
|| action == ActionType.ReceiveEvent
|| action == ActionType.Attach
|| action == ActionType.Execute)
return Ruling.Allowed;
else
return Ruling.Denied;
/// <summary>
/// Evaluates the Warehouse defaults and this resource's manager snapshot.
/// </summary>
public ResourceManagerEvaluation EvaluateManagers(
Session session,
ActionType action,
MemberDef member,
object inquirer = null)
{
var context = MakeManagerContext(session, action, member, inquirer);
return Warehouse.EvaluateManagers(context, managers.ToArray());
}
ResourceManagerContext MakeManagerContext(
Session session,
ActionType action,
MemberDef member,
object inquirer)
{
resource.TryGetTarget(out var res);
return new ResourceManagerContext(
Warehouse,
inquirer as EpConnection,
session,
res,
member,
action,
inquirer,
member?.MemberPolicyAttributes);
}
/// <summary>
/// Execution managers.
/// </summary>
public AutoList<IPermissionsManager, Instance> Managers => managers;
public AutoList<IResourceManager, Instance> Managers => managers;
void ManagerAdded(Instance instance, IResourceManager manager)
{
if (manager != null && Warehouse.IsRegisteredManager(manager))
return;
managers.Remove(manager);
throw new InvalidOperationException(
$"Resource manager `{manager?.GetType()}` must be registered with this Warehouse before it is attached.");
}
public readonly Warehouse Warehouse;
/// <summary>
@@ -747,7 +767,8 @@ public class Instance
//this.attributes = new KeyList<string, object>(this);
//children = new AutoList<IResource, Instance>(this);
//parents = new AutoList<IResource, Instance>(this);
managers = new AutoList<IPermissionsManager, Instance>(this);
managers = new AutoList<IResourceManager, Instance>(this);
managers.OnAdd += ManagerAdded;
//children.OnAdd += Children_OnAdd;
//children.OnRemoved += Children_OnRemoved;
//parents.OnAdd += Parents_OnAdd;
+33 -1
View File
@@ -1,19 +1,22 @@
using Esiur.Data;
using Esiur.Security.Authority;
using Esiur.Security.Permissions;
using Esiur.Security.Management;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Principal;
using System.Text;
namespace Esiur.Resource
{
public class ResourceContext:IResourceContext
public class ResourceContext : IResourceContext, IResourceManagersContext
{
public ulong Age { get; }
public Map<string, object> Attributes { get; }
public Map<string, object> Properties { get; }
public IPermissionsManager PermissionsManager { get; }
public IReadOnlyList<IResourceManager> ResourceManagers { get; }
public ResourceContext(ulong age, Map<string, object> attributes, Map<string, object> properties, IPermissionsManager permissionsManager)
{
@@ -21,6 +24,35 @@ namespace Esiur.Resource
Attributes = attributes;
Properties = properties;
PermissionsManager = permissionsManager;
ResourceManagers = permissionsManager is null
? Array.Empty<IResourceManager>()
: new IResourceManager[] { permissionsManager };
}
/// <summary>
/// Creates a context with one or more locally supplied managers. Every manager
/// must already be registered with the target Warehouse.
/// </summary>
public ResourceContext(
IEnumerable<IResourceManager> resourceManagers,
ulong age = 0,
Map<string, object> attributes = null,
Map<string, object> properties = null)
{
if (resourceManagers == null)
throw new ArgumentNullException(nameof(resourceManagers));
var managers = resourceManagers.ToArray();
if (managers.Any(manager => manager == null))
throw new ArgumentException(
"Resource managers cannot contain null values.",
nameof(resourceManagers));
Age = age;
Attributes = attributes;
Properties = properties;
ResourceManagers = Array.AsReadOnly(managers);
PermissionsManager = managers.OfType<IPermissionsManager>().FirstOrDefault();
}
//public virtual void Build()
+375 -3
View File
@@ -31,6 +31,7 @@ using Esiur.Protocol;
using Esiur.Proxy;
using Esiur.Security.Authority;
using Esiur.Security.Cryptography;
using Esiur.Security.Management;
using Esiur.Security.Permissions;
using Esiur.Security.RateLimiting;
using Org.BouncyCastle.Asn1.Cms;
@@ -91,7 +92,10 @@ public class Warehouse
Map<string, IAuthenticationProvider> _authenticationProviders = new Map<string, IAuthenticationProvider>();
readonly ConcurrentDictionary<string, IEncryptionProvider> _encryptionProviders
= new ConcurrentDictionary<string, IEncryptionProvider>(StringComparer.Ordinal);
List<IPermissionsManager> _permissionsManagers = new List<IPermissionsManager>();
readonly ConcurrentDictionary<Type, IResourceManager> _resourceManagers
= new ConcurrentDictionary<Type, IResourceManager>();
readonly ConcurrentDictionary<Type, byte> _defaultResourceManagerTypes
= new ConcurrentDictionary<Type, byte>();
readonly ConcurrentDictionary<string, RatePolicy> _ratePolicies
= new ConcurrentDictionary<string, RatePolicy>(StringComparer.Ordinal);
@@ -151,11 +155,374 @@ public class Warehouse
}
public void RegisterPermissionsManager(IPermissionsManager manager)
/// <summary>
/// Registers a manager instance by its concrete type. Type attributes and
/// ResourceContext bindings can only reference instances registered here.
/// </summary>
public void RegisterManager(IResourceManager manager, bool useAsDefault = false)
{
_permissionsManagers.Add(manager);
if (manager == null)
throw new ArgumentNullException(nameof(manager));
var managerType = manager.GetType();
var categoryCount =
(manager is IPermissionsManager ? 1 : 0) +
(manager is IRateControlManager ? 1 : 0) +
(manager is IAuditingManager ? 1 : 0);
if (categoryCount == 0)
throw new ArgumentException(
$"Manager `{managerType}` does not implement a supported manager category.",
nameof(manager));
if (categoryCount > 1)
throw new ArgumentException(
$"Manager `{managerType}` must implement exactly one manager category.",
nameof(manager));
if (!_resourceManagers.TryAdd(managerType, manager))
throw new InvalidOperationException(
$"A resource manager of type `{managerType}` is already registered.");
if (useAsDefault)
_defaultResourceManagerTypes.TryAdd(managerType, 0);
}
public void RegisterManager<TManager>(TManager manager, bool useAsDefault = false)
where TManager : class, IResourceManager
=> RegisterManager((IResourceManager)manager, useAsDefault);
public TManager RegisterManager<TManager>(bool useAsDefault = false)
where TManager : class, IResourceManager, new()
{
var manager = new TManager();
RegisterManager(manager, useAsDefault);
return manager;
}
/// <summary>
/// Compatibility registration for Warehouse-wide permissions. Registered
/// permission managers are defaults, matching the original API's intent.
/// </summary>
public void RegisterPermissionsManager(IPermissionsManager manager)
=> RegisterManager(manager, true);
public void RegisterPermissionsManager(
IPermissionsManager manager,
bool useAsDefault)
=> RegisterManager(manager, useAsDefault);
public void RegisterRateControlManager(IRateControlManager manager, bool useAsDefault = false)
=> RegisterManager(manager, useAsDefault);
public void RegisterAuditingManager(IAuditingManager manager, bool useAsDefault = false)
=> RegisterManager(manager, useAsDefault);
/// <summary>
/// Enables or disables a registered manager as a Warehouse-wide default.
/// Multiple defaults may be enabled for the same manager category.
/// </summary>
public void SetDefaultManager(Type managerType, bool enabled = true)
{
if (managerType == null)
throw new ArgumentNullException(nameof(managerType));
if (!_resourceManagers.ContainsKey(managerType))
throw new InvalidOperationException(
$"Resource manager `{managerType}` is not registered.");
if (!enabled && typeof(NamedRateControlManager).IsAssignableFrom(managerType))
throw new InvalidOperationException(
"The built-in named rate-control manager cannot be disabled.");
if (enabled)
_defaultResourceManagerTypes.TryAdd(managerType, 0);
else
_defaultResourceManagerTypes.TryRemove(managerType, out _);
}
public void SetDefaultManager<TManager>(bool enabled = true)
where TManager : IResourceManager
=> SetDefaultManager(typeof(TManager), enabled);
public IResourceManager? TryGetManager(Type managerType)
=> managerType != null && _resourceManagers.TryGetValue(managerType, out var manager)
? manager
: null;
public TManager? TryGetManager<TManager>() where TManager : class, IResourceManager
=> TryGetManager(typeof(TManager)) as TManager;
public bool RemoveManager(Type managerType)
{
if (managerType == null)
return false;
if (typeof(NamedRateControlManager).IsAssignableFrom(managerType))
return false;
_defaultResourceManagerTypes.TryRemove(managerType, out _);
return _resourceManagers.TryRemove(managerType, out _);
}
public Type[] GetRegisteredManagerTypes()
=> _resourceManagers.Keys.OrderBy(type => type.FullName, StringComparer.Ordinal).ToArray();
public IResourceManager[] GetDefaultManagers()
=> _defaultResourceManagerTypes.Keys
.OrderBy(type => type.FullName, StringComparer.Ordinal)
.Select(type => TryGetManager(type))
.Where(manager => manager != null)
.ToArray()!;
internal bool IsRegisteredManager(IResourceManager manager)
=> manager != null &&
_resourceManagers.TryGetValue(manager.GetType(), out var registered) &&
ReferenceEquals(manager, registered);
internal IResourceManager[] ResolveResourceManagers(
Type resourceType,
IResourceContext resourceContext)
{
if (resourceType == null)
throw new ArgumentNullException(nameof(resourceType));
var resolved = new List<IResourceManager>();
var baseType = ResourceProxy.GetBaseType(resourceType);
foreach (var attribute in baseType
.GetCustomAttributes(typeof(ResourceManagerAttribute), true)
.OfType<ResourceManagerAttribute>())
{
var manager = TryGetManager(attribute.ManagerType);
if (manager == null)
throw new InvalidOperationException(
$"Resource manager `{attribute.ManagerType}` declared by `{baseType}` is not registered.");
if (!resolved.Any(existing => ReferenceEquals(existing, manager)))
resolved.Add(manager);
}
if (resourceContext is IResourceManagersContext managerContext)
{
foreach (var manager in managerContext.ResourceManagers ?? Array.Empty<IResourceManager>())
{
if (manager == null)
throw new InvalidOperationException("A ResourceContext supplied a null manager.");
if (!IsRegisteredManager(manager))
throw new InvalidOperationException(
$"Resource manager `{manager.GetType()}` supplied by ResourceContext is not registered with this Warehouse.");
if (!resolved.Any(existing => ReferenceEquals(existing, manager)))
resolved.Add(manager);
}
}
return resolved.ToArray();
}
/// <summary>
/// Asks every applicable manager using independent deny-overrides aggregation
/// for permissions, rate control and auditing.
/// </summary>
public ResourceManagerEvaluation EvaluateManagers(
ResourceManagerContext context,
IEnumerable<IResourceManager>? resourceManagers = null)
{
if (context == null)
throw new ArgumentNullException(nameof(context));
if (!ReferenceEquals(context.Warehouse, this))
throw new ArgumentException(
"The manager context belongs to another Warehouse.",
nameof(context));
var localManagers = resourceManagers?.ToArray()
?? context.Resource?.Instance?.Managers.ToArray()
?? Array.Empty<IResourceManager>();
var managers = new List<IResourceManager>();
foreach (var manager in GetDefaultManagers().Concat(localManagers))
if (manager != null && !managers.Any(existing => ReferenceEquals(existing, manager)))
managers.Add(manager);
var permissionsAllowed = false;
var permissionsDenied = false;
var rateAllowed = false;
var rateDenied = false;
var auditingAllowed = false;
var auditingDenied = false;
var delay = TimeSpan.Zero;
string permissionsReason = null;
string rateReason = null;
string auditingReason = null;
foreach (var manager in managers)
{
var registered = IsRegisteredManager(manager);
context.Delay = TimeSpan.Zero;
context.DenialReason = null;
try
{
if (!registered)
throw new InvalidOperationException(
$"Resource manager `{manager.GetType()}` is not registered with this Warehouse.");
if (manager is IPermissionsManager permissionsManager)
{
var ruling = permissionsManager.Applicable(
context.Resource,
context.Session,
context.Action,
context.Member,
context.Inquirer);
permissionsDenied |= ruling == Ruling.Denied;
permissionsAllowed |= ruling == Ruling.Allowed;
if (ruling == Ruling.Denied && permissionsReason == null)
permissionsReason = context.DenialReason
?? $"Permissions manager `{manager.GetType().Name}` denied the operation.";
}
else if (manager is IRateControlManager rateControlManager)
{
var ruling = rateControlManager.Applicable(context);
rateDenied |= ruling == Ruling.Denied;
rateAllowed |= ruling == Ruling.Allowed;
if (context.Delay > delay)
delay = context.Delay;
if (ruling == Ruling.Denied && rateReason == null)
rateReason = context.DenialReason
?? $"Rate-control manager `{manager.GetType().Name}` denied the operation.";
}
else if (manager is IAuditingManager auditingManager)
{
var ruling = auditingManager.Applicable(context);
auditingDenied |= ruling == Ruling.Denied;
auditingAllowed |= ruling == Ruling.Allowed;
if (ruling == Ruling.Denied && auditingReason == null)
auditingReason = context.DenialReason
?? $"Auditing manager `{manager.GetType().Name}` denied the operation.";
}
}
catch (Exception exception)
{
Global.Log(
"ResourceManager",
LogType.Error,
$"Manager `{manager.GetType()}` failed while evaluating `{context.Action}`: {exception}");
if (manager is IPermissionsManager)
{
permissionsDenied = true;
permissionsReason ??= "A permissions manager failed while evaluating the operation.";
}
else if (manager is IRateControlManager)
{
rateDenied = true;
rateReason ??= "A rate-control manager failed while evaluating the operation.";
}
else if (manager is IAuditingManager)
{
auditingDenied = true;
auditingReason ??= "An auditing manager failed while evaluating the operation.";
}
}
}
var permissions = permissionsDenied
? Ruling.Denied
: permissionsAllowed
? Ruling.Allowed
: DefaultPermissions(context.Action);
var rateControl = rateDenied
? Ruling.Denied
: rateAllowed ? Ruling.Allowed : Ruling.DontCare;
var auditing = auditingDenied
? Ruling.Denied
: auditingAllowed ? Ruling.Allowed : Ruling.DontCare;
context.Delay = delay;
context.DenialReason = permissions == Ruling.Denied
? permissionsReason
: auditing == Ruling.Denied
? auditingReason
: rateReason;
return new ResourceManagerEvaluation(
permissions,
rateControl,
auditing,
delay,
permissionsReason,
rateReason,
auditingReason);
}
internal Ruling EvaluatePermissions(
ResourceManagerContext context,
IEnumerable<IResourceManager> resourceManagers)
{
if (context == null)
throw new ArgumentNullException(nameof(context));
if (!ReferenceEquals(context.Warehouse, this))
throw new ArgumentException(
"The manager context belongs to another Warehouse.",
nameof(context));
var managers = new List<IPermissionsManager>();
foreach (var manager in GetDefaultManagers()
.Concat(resourceManagers ?? Array.Empty<IResourceManager>())
.OfType<IPermissionsManager>())
if (!managers.Any(existing => ReferenceEquals(existing, manager)))
managers.Add(manager);
var allowed = false;
var denied = false;
foreach (var manager in managers)
{
try
{
if (!IsRegisteredManager(manager))
throw new InvalidOperationException(
$"Resource manager `{manager.GetType()}` is not registered with this Warehouse.");
var ruling = manager.Applicable(
context.Resource,
context.Session,
context.Action,
context.Member,
context.Inquirer);
denied |= ruling == Ruling.Denied;
allowed |= ruling == Ruling.Allowed;
}
catch (Exception exception)
{
denied = true;
Global.Log(
"ResourceManager",
LogType.Error,
$"Permissions manager `{manager.GetType()}` failed while evaluating `{context.Action}`: {exception}");
}
}
return denied
? Ruling.Denied
: allowed ? Ruling.Allowed : DefaultPermissions(context.Action);
}
internal static Ruling DefaultPermissions(ActionType action)
=> action == ActionType.GetProperty
|| action == ActionType.ViewTypeDef
|| action == ActionType.ReceiveEvent
|| action == ActionType.Attach
|| action == ActionType.Execute
|| action == ActionType.Detach
|| action == ActionType.Subscribe
|| action == ActionType.Unsubscribe
|| action == ActionType.PullStream
|| action == ActionType.TerminateExecution
|| action == ActionType.HaltExecution
|| action == ActionType.ResumeExecution
? Ruling.Allowed
: Ruling.Denied;
/// <summary>
/// Registers a named rate policy referenced by RateControl attributes.
/// </summary>
@@ -241,6 +608,8 @@ public class Warehouse
{
Configuration = configuration ?? throw new ArgumentNullException(nameof(configuration));
RegisterManager(new NamedRateControlManager(), true);
Protocols.Add("EP",
async (name, context)
=> await New<EpConnection>(name, context));
@@ -540,11 +909,14 @@ public class Warehouse
}
var resourceReference = new WeakReference<IResource>(resource);
var resourceManagers = ResolveResourceManagers(resource.GetType(), resourceContext);
var resourceId = (uint)Interlocked.Increment(ref _resourceCounter);
resource.Instance = new Instance(this, resourceId, instanceName, resource, store, resourceContext?.Age ?? 0);
resource.Instance.Managers.AddRange(resourceManagers);
if (resourceContext?.Attributes != null)
resource.Instance.SetAttributes(resourceContext.Attributes);
@@ -5,7 +5,8 @@ namespace Esiur.Security.Management;
/// <summary>
/// Evaluates whether a resource operation is admitted by rate-control policy.
/// A manager may assign <see cref="ResourceManagerContext.Delay"/> when allowing
/// an operation that should be queued.
/// an operation that should be queued and
/// <see cref="ResourceManagerContext.SupportsDelay"/> is true.
/// </summary>
public interface IRateControlManager : IResourceManager
{
@@ -10,9 +10,9 @@ using System.Linq;
namespace Esiur.Security.Management;
/// <summary>
/// Immutable metadata describing a resource operation being evaluated by managers.
/// <see cref="Delay"/> is the sole mutable value and allows rate-control managers to
/// request deferred execution.
/// Metadata describing a resource operation being evaluated by managers. Operation
/// identity is immutable; <see cref="Delay"/> and <see cref="DenialReason"/> let a
/// manager return admission details.
/// </summary>
public sealed class ResourceManagerContext
{
@@ -23,9 +23,16 @@ public sealed class ResourceManagerContext
public Session? Session { get; }
public IResource? Resource { get; }
public MemberDef? Member { get; }
public TypeDef? TypeDefinition { get; }
public ActionType Action { get; }
public object? Inquirer { get; }
/// <summary>
/// Indicates whether the protocol operation can honor an allowed delayed ruling.
/// Rate managers should inspect this before reserving queued capacity.
/// </summary>
public bool SupportsDelay { get; }
/// <summary>
/// Gets the local attributes that configure manager policy for the target member.
/// The collection is a defensive, read-only snapshot.
@@ -33,10 +40,17 @@ public sealed class ResourceManagerContext
public IReadOnlyList<Attribute> MemberPolicyAttributes => _memberPolicyAttributes;
/// <summary>
/// Gets or sets an optional delay requested by a rate-control manager.
/// Gets or sets an optional delay requested by a rate-control manager. Execute,
/// property-set, and pull-stream operations honor delayed rulings; operations
/// that cannot be delayed fail closed.
/// </summary>
public TimeSpan Delay { get; set; }
/// <summary>
/// Optional public-safe reason supplied by a manager when it denies an operation.
/// </summary>
public string? DenialReason { get; set; }
public ResourceManagerContext(
Warehouse warehouse,
EpConnection? connection,
@@ -45,15 +59,19 @@ public sealed class ResourceManagerContext
MemberDef? member,
ActionType action,
object? inquirer = null,
IEnumerable<Attribute>? memberPolicyAttributes = null)
IEnumerable<Attribute>? memberPolicyAttributes = null,
TypeDef? typeDefinition = null,
bool supportsDelay = false)
{
Warehouse = warehouse ?? throw new ArgumentNullException(nameof(warehouse));
Connection = connection;
Session = session;
Resource = resource;
Member = member;
TypeDefinition = typeDefinition ?? resource?.Instance?.Definition ?? member?.Definition;
Action = action;
Inquirer = inquirer;
SupportsDelay = supportsDelay;
var policies = memberPolicyAttributes?.ToArray() ?? Array.Empty<Attribute>();
if (policies.Any(attribute => attribute == null))
@@ -0,0 +1,43 @@
using Esiur.Security.Permissions;
using System;
namespace Esiur.Security.Management;
/// <summary>
/// Aggregated decisions from each independent manager category.
/// An allow in one category never grants admission in another category.
/// </summary>
public sealed class ResourceManagerEvaluation
{
public Ruling Permissions { get; }
public Ruling RateControl { get; }
public Ruling Auditing { get; }
public TimeSpan Delay { get; }
public string? PermissionsDenialReason { get; }
public string? RateControlDenialReason { get; }
public string? AuditingDenialReason { get; }
public bool IsAllowed =>
Permissions == Ruling.Allowed &&
RateControl != Ruling.Denied &&
Auditing != Ruling.Denied;
internal ResourceManagerEvaluation(
Ruling permissions,
Ruling rateControl,
Ruling auditing,
TimeSpan delay,
string? permissionsDenialReason,
string? rateControlDenialReason,
string? auditingDenialReason)
{
Permissions = permissions;
RateControl = rateControl;
Auditing = auditing;
Delay = delay > TimeSpan.Zero ? delay : TimeSpan.Zero;
PermissionsDenialReason = permissionsDenialReason;
RateControlDenialReason = rateControlDenialReason;
AuditingDenialReason = auditingDenialReason;
}
}
@@ -46,5 +46,12 @@ public enum ActionType
RemoveChild,
Rename,
ReceiveEvent,
ViewTypeDef
ViewTypeDef,
Detach,
Subscribe,
Unsubscribe,
PullStream,
TerminateExecution,
HaltExecution,
ResumeExecution
}
@@ -30,6 +30,7 @@ using Esiur.Core;
using Esiur.Resource;
using Esiur.Security.Authority;
using Esiur.Data.Types;
using System.Linq;
namespace Esiur.Security.Permissions;
@@ -41,7 +42,29 @@ public class StorePermissionsManager : IPermissionsManager
public Ruling Applicable(IResource resource, Session session, ActionType action, MemberDef member, object inquirer = null)
{
return resource.Instance.Store.Instance.Applicable(session, action, member, inquirer);
var storeInstance = resource?.Instance?.Store?.Instance;
if (storeInstance == null)
return Ruling.DontCare;
// Delegate only to managers attached to the store. Re-entering
// Instance.Applicable would run Warehouse defaults (including this manager)
// again and can recurse indefinitely or charge rate policies twice.
var store = storeInstance.Resource;
var allowed = false;
foreach (var manager in storeInstance.Managers
.ToArray()
.OfType<IPermissionsManager>()
.Where(manager => !ReferenceEquals(manager, this)))
{
var ruling = manager.Applicable(store, session, action, member, inquirer);
if (ruling == Ruling.Denied)
return Ruling.Denied;
if (ruling == Ruling.Allowed)
allowed = true;
}
return allowed ? Ruling.Allowed : Ruling.DontCare;
}
public bool Initialize(Map<string,object> settings, IResource resource)
@@ -0,0 +1,53 @@
using Esiur.Security.Management;
using Esiur.Security.Permissions;
using System;
namespace Esiur.Security.RateLimiting;
/// <summary>
/// Bridges the existing named RateControl policy registry into the unified
/// resource-manager pipeline.
/// </summary>
internal sealed class NamedRateControlManager : IRateControlManager
{
public Ruling Applicable(ResourceManagerContext context)
{
if (context.Action != ActionType.Execute &&
context.Action != ActionType.SetProperty)
return Ruling.DontCare;
var policyName = context.Member?.RatePolicyName;
if (string.IsNullOrWhiteSpace(policyName))
return Ruling.DontCare;
if (context.Connection is null || context.Session is null || context.Member is null)
{
context.DenialReason = $"Rate policy `{policyName}` requires an authenticated connection.";
return Ruling.Denied;
}
var policy = context.Warehouse.TryGetRatePolicy(policyName);
if (policy is null)
{
context.DenialReason = $"Rate policy `{policyName}` is not registered.";
return Ruling.Denied;
}
var rateContext = new RateControlContext(
context.Warehouse,
context.Connection,
context.Session,
context.Resource,
context.Member,
context.Action);
var ruling = policy.Applicable(rateContext);
if (rateContext.Delay > context.Delay)
context.Delay = rateContext.Delay;
if (ruling == Ruling.Denied && string.IsNullOrWhiteSpace(context.DenialReason))
context.DenialReason = $"Rate policy `{policyName}` denied `{context.Member.Fullname}`.";
return ruling;
}
}
@@ -131,7 +131,13 @@ public static class EsiurExtensions
var id = store.TypesByType[typeof(T)].PrimaryKey.GetValue(resource);
await options.Warehouse.Put($"{store.Instance.Name}/{typeof(T).Name}/{id}", res, new ResourceContext(0, null, null, manager));
var resourceContext = manager == null
? null
: new ResourceContext(new[] { manager });
await options.Warehouse.Put(
$"{store.Instance.Name}/{typeof(T).Name}/{id}",
res,
resourceContext);
return (T)res;
}
@@ -71,7 +71,13 @@ public class EsiurProxyRewrite : IModelFinalizingConvention
// check if the object exists
var obj = options.Warehouse.Create(entityType.ClrType, null) as IResource;
options.Store.TypesByType[entityType.ClrType].PrimaryKey.SetValue(obj, id);
options.Warehouse.Put($"{options.Store.Instance.Name}/{entityType.ClrType.Name}/{id}", obj, new ResourceContext(0, null, null, manager)).Wait();
var resourceContext = manager == null
? null
: new ResourceContext(new[] { manager });
options.Warehouse.Put(
$"{options.Store.Instance.Name}/{entityType.ClrType.Name}/{id}",
obj,
resourceContext).Wait();
return obj;
}
else
@@ -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;
}
}