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;
}
}