mirror of
https://github.com/esiur/esiur-dotnet.git
synced 2026-07-30 17:30:40 +00:00
Managers
This commit is contained in:
@@ -1,5 +1,6 @@
|
|||||||
using Esiur.Data.Types;
|
using Esiur.Data.Types;
|
||||||
using Esiur.Protocol;
|
using Esiur.Protocol;
|
||||||
|
using Esiur.Resource;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections;
|
using System.Collections;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
@@ -23,6 +24,8 @@ namespace Esiur.Core
|
|||||||
|
|
||||||
internal StreamMode StreamMode { get; private set; }
|
internal StreamMode StreamMode { get; private set; }
|
||||||
internal bool Pausable { 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;
|
internal volatile bool Ended;
|
||||||
|
|
||||||
@@ -75,6 +78,12 @@ namespace Esiur.Core
|
|||||||
CallbackId = callbackId;
|
CallbackId = callbackId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal void BindOperation(IResource resource, FunctionDef function)
|
||||||
|
{
|
||||||
|
Resource = resource;
|
||||||
|
Function = function;
|
||||||
|
}
|
||||||
|
|
||||||
internal void InitializeStream(StreamMode streamMode, bool pausable)
|
internal void InitializeStream(StreamMode streamMode, bool pausable)
|
||||||
{
|
{
|
||||||
StreamMode = streamMode;
|
StreamMode = streamMode;
|
||||||
|
|||||||
@@ -209,12 +209,14 @@ public class EventDef : MemberDef
|
|||||||
|
|
||||||
return DefinitionAttributeReader.Apply(ei, new EventDef()
|
return DefinitionAttributeReader.Apply(ei, new EventDef()
|
||||||
{
|
{
|
||||||
|
Definition = schema,
|
||||||
Name = name,
|
Name = name,
|
||||||
ArgumentType = evtType,
|
ArgumentType = evtType,
|
||||||
Index = index,
|
Index = index,
|
||||||
Inherited = ei.DeclaringType != type,
|
Inherited = ei.DeclaringType != type,
|
||||||
Annotations = annotations,
|
Annotations = annotations,
|
||||||
EventInfo = ei,
|
EventInfo = ei,
|
||||||
|
MemberPolicyAttributes = Attribute.GetCustomAttributes(ei, true),
|
||||||
AutoDelivered = autoDeliveryAttr != null,
|
AutoDelivered = autoDeliveryAttr != null,
|
||||||
Historical = historicalAttr != null,
|
Historical = historicalAttr != null,
|
||||||
OrderingControl = orderingAttr?.Control ?? OrderingControl.Strict,
|
OrderingControl = orderingAttr?.Control ?? OrderingControl.Strict,
|
||||||
|
|||||||
@@ -367,6 +367,7 @@ public class FunctionDef : MemberDef
|
|||||||
|
|
||||||
return DefinitionAttributeReader.Apply(mi, new FunctionDef()
|
return DefinitionAttributeReader.Apply(mi, new FunctionDef()
|
||||||
{
|
{
|
||||||
|
Definition = schema,
|
||||||
Name = name,
|
Name = name,
|
||||||
Index = index,
|
Index = index,
|
||||||
Inherited = mi.DeclaringType != type,
|
Inherited = mi.DeclaringType != type,
|
||||||
@@ -379,6 +380,7 @@ public class FunctionDef : MemberDef
|
|||||||
Idempotent = mi.GetCustomAttribute<IdempotentAttribute>(true) != null,
|
Idempotent = mi.GetCustomAttribute<IdempotentAttribute>(true) != null,
|
||||||
Cancellable = mi.GetCustomAttribute<CancellableAttribute>(true) != null,
|
Cancellable = mi.GetCustomAttribute<CancellableAttribute>(true) != null,
|
||||||
RatePolicyName = mi.GetCustomAttribute<RateControlAttribute>(true)?.PolicyName,
|
RatePolicyName = mi.GetCustomAttribute<RateControlAttribute>(true)?.PolicyName,
|
||||||
|
MemberPolicyAttributes = Attribute.GetCustomAttributes(mi, true),
|
||||||
StreamMode = streamMode,
|
StreamMode = streamMode,
|
||||||
Pausable = pausable,
|
Pausable = pausable,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ using System.Threading.Tasks;
|
|||||||
namespace Esiur.Data.Types;
|
namespace Esiur.Data.Types;
|
||||||
public class MemberDef
|
public class MemberDef
|
||||||
{
|
{
|
||||||
|
IReadOnlyList<Attribute> memberPolicyAttributes = Array.Empty<Attribute>();
|
||||||
|
|
||||||
// Core fields
|
// Core fields
|
||||||
public byte Index { get; set; }
|
public byte Index { get; set; }
|
||||||
@@ -24,6 +25,18 @@ public class MemberDef
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public string? RatePolicyName { get; set; }
|
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!;
|
public TypeDef Definition { get; set; } = null!;
|
||||||
|
|
||||||
// Human-readable metadata
|
// Human-readable metadata
|
||||||
|
|||||||
@@ -312,12 +312,17 @@ public class PropertyDef : MemberDef
|
|||||||
|
|
||||||
return DefinitionAttributeReader.Apply(pi, new PropertyDef()
|
return DefinitionAttributeReader.Apply(pi, new PropertyDef()
|
||||||
{
|
{
|
||||||
|
Definition = typeDef,
|
||||||
Name = name,
|
Name = name,
|
||||||
Index = index,
|
Index = index,
|
||||||
Inherited = pi.DeclaringType != type,
|
Inherited = pi.DeclaringType != type,
|
||||||
ValueType = propType,
|
ValueType = propType,
|
||||||
PropertyInfo = pi,
|
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,
|
ReadOnly = readOnlyAttr != null,
|
||||||
Volatile = volatileAttr != null,
|
Volatile = volatileAttr != null,
|
||||||
Historical = historicalAttr != null,
|
Historical = historicalAttr != null,
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ using Esiur.Net;
|
|||||||
using Esiur.Net.Packets;
|
using Esiur.Net.Packets;
|
||||||
using Esiur.Resource;
|
using Esiur.Resource;
|
||||||
using Esiur.Security.Authority;
|
using Esiur.Security.Authority;
|
||||||
|
using Esiur.Security.Management;
|
||||||
using Esiur.Security.Permissions;
|
using Esiur.Security.Permissions;
|
||||||
using Esiur.Security.RateLimiting;
|
using Esiur.Security.RateLimiting;
|
||||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||||
@@ -55,63 +56,110 @@ partial class EpConnection
|
|||||||
|
|
||||||
internal bool IsRateControlBlocked => Volatile.Read(ref _rateControlBlocked) != 0;
|
internal bool IsRateControlBlocked => Volatile.Read(ref _rateControlBlocked) != 0;
|
||||||
|
|
||||||
bool TryApplyRateControl(
|
bool TryApplyManagers(
|
||||||
MemberDef member,
|
MemberDef member,
|
||||||
IResource? resource,
|
IResource? resource,
|
||||||
ActionType action,
|
ActionType action,
|
||||||
uint callback,
|
uint callback,
|
||||||
out TimeSpan delay)
|
ErrorType denialErrorType,
|
||||||
|
ExceptionCode denialCode,
|
||||||
|
out TimeSpan delay,
|
||||||
|
IEnumerable<IResourceManager> managers = null,
|
||||||
|
bool supportsDelay = false)
|
||||||
{
|
{
|
||||||
delay = TimeSpan.Zero;
|
delay = TimeSpan.Zero;
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(member.RatePolicyName))
|
|
||||||
return true;
|
|
||||||
|
|
||||||
if (IsRateControlBlocked)
|
if (IsRateControlBlocked)
|
||||||
return false;
|
|
||||||
|
|
||||||
var warehouse = Instance.Warehouse;
|
|
||||||
var policy = warehouse.TryGetRatePolicy(member.RatePolicyName);
|
|
||||||
|
|
||||||
if (policy == null)
|
|
||||||
{
|
{
|
||||||
DenyRateControlledRequest(
|
SendError(
|
||||||
|
ErrorType.Management,
|
||||||
callback,
|
callback,
|
||||||
$"Rate policy `{member.RatePolicyName}` is not registered.");
|
(ushort)ExceptionCode.RateLimitExceeded,
|
||||||
|
"The connection is blocked by rate control.");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var warehouse = Instance.Warehouse;
|
||||||
|
|
||||||
try
|
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,
|
warehouse,
|
||||||
this,
|
this,
|
||||||
_session,
|
_session,
|
||||||
resource,
|
resource,
|
||||||
member,
|
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(
|
if (evaluation.RateControl == Ruling.Denied)
|
||||||
callback,
|
DenyRateControlledRequest(
|
||||||
$"Rate policy `{member.RatePolicyName}` denied `{member.Fullname}`.");
|
callback,
|
||||||
|
evaluation.RateControlDenialReason ?? "Rate control denied the operation.",
|
||||||
|
false);
|
||||||
|
|
||||||
|
SendError(denialErrorType, callback, (ushort)denialCode);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
delay = context.Delay > TimeSpan.Zero ? context.Delay : TimeSpan.Zero;
|
if (evaluation.RateControl == Ruling.Denied)
|
||||||
return true;
|
{
|
||||||
|
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)
|
catch (Exception exception)
|
||||||
{
|
{
|
||||||
DenyRateControlledRequest(
|
Global.Log(exception);
|
||||||
callback,
|
SendError(denialErrorType, callback, (ushort)denialCode);
|
||||||
$"Rate policy `{member.RatePolicyName}` failed: {exception.Message}");
|
|
||||||
return false;
|
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 configuration = Instance.Warehouse.Configuration.RateControl;
|
||||||
var now = DateTime.UtcNow;
|
var now = DateTime.UtcNow;
|
||||||
@@ -136,11 +184,12 @@ partial class EpConnection
|
|||||||
Interlocked.Exchange(ref _rateControlBlocked, 1) == 0;
|
Interlocked.Exchange(ref _rateControlBlocked, 1) == 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
SendError(
|
if (sendError)
|
||||||
ErrorType.Management,
|
SendError(
|
||||||
callback,
|
ErrorType.Management,
|
||||||
(ushort)ExceptionCode.RateLimitExceeded,
|
callback,
|
||||||
message);
|
(ushort)ExceptionCode.RateLimitExceeded,
|
||||||
|
message);
|
||||||
|
|
||||||
if (shouldBlock)
|
if (shouldBlock)
|
||||||
_ = CloseRateControlledConnectionAsync(configuration.ConnectionBlockDelay);
|
_ = CloseRateControlledConnectionAsync(configuration.ConnectionBlockDelay);
|
||||||
@@ -944,11 +993,15 @@ partial class EpConnection
|
|||||||
{
|
{
|
||||||
if (res != null)
|
if (res != null)
|
||||||
{
|
{
|
||||||
if (res.Instance.Applicable(_session, ActionType.Attach, null) == Ruling.Denied)
|
if (!TryApplyManagers(
|
||||||
{
|
null,
|
||||||
SendError(ErrorType.Management, callback, 6);
|
res,
|
||||||
|
ActionType.Attach,
|
||||||
|
callback,
|
||||||
|
ErrorType.Management,
|
||||||
|
ExceptionCode.AttachDenied,
|
||||||
|
out _))
|
||||||
return;
|
return;
|
||||||
}
|
|
||||||
|
|
||||||
var r = res as IResource;
|
var r = res as IResource;
|
||||||
|
|
||||||
@@ -1007,11 +1060,15 @@ partial class EpConnection
|
|||||||
{
|
{
|
||||||
if (res != null)
|
if (res != null)
|
||||||
{
|
{
|
||||||
if (res.Instance.Applicable(_session, ActionType.Attach, null) == Ruling.Denied)
|
if (!TryApplyManagers(
|
||||||
{
|
null,
|
||||||
SendError(ErrorType.Management, callback, 6);
|
res,
|
||||||
|
ActionType.Attach,
|
||||||
|
callback,
|
||||||
|
ErrorType.Management,
|
||||||
|
ExceptionCode.AttachDenied,
|
||||||
|
out _))
|
||||||
return;
|
return;
|
||||||
}
|
|
||||||
|
|
||||||
var r = res as IResource;
|
var r = res as IResource;
|
||||||
|
|
||||||
@@ -1059,6 +1116,15 @@ partial class EpConnection
|
|||||||
{
|
{
|
||||||
if (res != null)
|
if (res != null)
|
||||||
{
|
{
|
||||||
|
if (!TryApplyManagers(
|
||||||
|
null,
|
||||||
|
res,
|
||||||
|
ActionType.Detach,
|
||||||
|
callback,
|
||||||
|
ErrorType.Management,
|
||||||
|
ExceptionCode.NotAllowed,
|
||||||
|
out _))
|
||||||
|
return;
|
||||||
|
|
||||||
// unsubscribe
|
// unsubscribe
|
||||||
Unsubscribe(res);
|
Unsubscribe(res);
|
||||||
@@ -1121,11 +1187,15 @@ partial class EpConnection
|
|||||||
var store = r.Instance.Store;
|
var store = r.Instance.Store;
|
||||||
|
|
||||||
// check security
|
// check security
|
||||||
if (store.Instance.Applicable(_session, ActionType.CreateResource, null) != Ruling.Allowed)
|
if (!TryApplyManagers(
|
||||||
{
|
null,
|
||||||
SendError(ErrorType.Management, callback, (ushort)ExceptionCode.CreateDenied);
|
store,
|
||||||
|
ActionType.CreateResource,
|
||||||
|
callback,
|
||||||
|
ErrorType.Management,
|
||||||
|
ExceptionCode.CreateDenied,
|
||||||
|
out _))
|
||||||
return;
|
return;
|
||||||
}
|
|
||||||
|
|
||||||
Instance.Warehouse.New(localTypeDef.DefinedType, path,
|
Instance.Warehouse.New(localTypeDef.DefinedType, path,
|
||||||
new ResourceContext(0,
|
new ResourceContext(0,
|
||||||
@@ -1166,11 +1236,20 @@ partial class EpConnection
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (r.Instance.Store.Instance.Applicable(_session, ActionType.Delete, null) != Ruling.Allowed)
|
var deleteManagers = r.Instance.Managers
|
||||||
{
|
.ToArray()
|
||||||
SendError(ErrorType.Management, callback, (ushort)ExceptionCode.DeleteDenied);
|
.Concat(r.Instance.Store.Instance.Managers.ToArray());
|
||||||
|
|
||||||
|
if (!TryApplyManagers(
|
||||||
|
null,
|
||||||
|
r,
|
||||||
|
ActionType.Delete,
|
||||||
|
callback,
|
||||||
|
ErrorType.Management,
|
||||||
|
ExceptionCode.DeleteDenied,
|
||||||
|
out _,
|
||||||
|
deleteManagers))
|
||||||
return;
|
return;
|
||||||
}
|
|
||||||
|
|
||||||
if (Instance.Warehouse.Remove(r))
|
if (Instance.Warehouse.Remove(r))
|
||||||
SendReply(EpPacketReply.Completed, callback);
|
SendReply(EpPacketReply.Completed, callback);
|
||||||
@@ -1205,11 +1284,15 @@ partial class EpConnection
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (resource.Instance.Applicable(this._session, ActionType.Rename, null) != Ruling.Allowed)
|
if (!TryApplyManagers(
|
||||||
{
|
null,
|
||||||
SendError(ErrorType.Management, callback, (ushort)ExceptionCode.RenameDenied);
|
resource,
|
||||||
|
ActionType.Rename,
|
||||||
|
callback,
|
||||||
|
ErrorType.Management,
|
||||||
|
ExceptionCode.RenameDenied,
|
||||||
|
out _))
|
||||||
return;
|
return;
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
resource.Instance.Name = name;
|
resource.Instance.Name = name;
|
||||||
@@ -1240,11 +1323,15 @@ partial class EpConnection
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (r.Instance.Applicable(_session, ActionType.ViewTypeDef, null) == Ruling.Denied)
|
if (!TryApplyManagers(
|
||||||
{
|
null,
|
||||||
SendError(ErrorType.Management, callback, (ushort)ExceptionCode.NotAllowed);
|
r,
|
||||||
|
ActionType.ViewTypeDef,
|
||||||
|
callback,
|
||||||
|
ErrorType.Management,
|
||||||
|
ExceptionCode.NotAllowed,
|
||||||
|
out _))
|
||||||
return;
|
return;
|
||||||
}
|
|
||||||
|
|
||||||
// make sure the resource is a local type def.
|
// make sure the resource is a local type def.
|
||||||
if (r.Instance.Definition is LocalTypeDef localTypeDef)
|
if (r.Instance.Definition is LocalTypeDef localTypeDef)
|
||||||
@@ -1272,19 +1359,33 @@ partial class EpConnection
|
|||||||
|
|
||||||
var classNames = (string[])value;
|
var classNames = (string[])value;
|
||||||
|
|
||||||
var typeDefs = new List<ulong>();
|
var typeDefs = new List<LocalTypeDef>();
|
||||||
|
|
||||||
foreach (var className in classNames)
|
foreach (var className in classNames)
|
||||||
{
|
{
|
||||||
//@TODO: need to search in remoteTypeDefs as well
|
//@TODO: need to search in remoteTypeDefs as well
|
||||||
var typeDef = Instance.Warehouse.GetLocalTypeDefByName(className);
|
var typeDef = Instance.Warehouse.GetLocalTypeDefByName(className);
|
||||||
if (typeDef != null)
|
if (typeDef != null)
|
||||||
typeDefs.Add(typeDef.Id);
|
typeDefs.Add(typeDef);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeDefs.Count > 0)
|
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
|
else
|
||||||
{
|
{
|
||||||
@@ -1304,6 +1405,18 @@ partial class EpConnection
|
|||||||
|
|
||||||
if (t != null)
|
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));
|
SendReply(EpPacketReply.Completed, callback, t.Compose(this));
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@@ -1326,6 +1439,16 @@ partial class EpConnection
|
|||||||
{
|
{
|
||||||
if (r != null)
|
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));
|
SendReply(EpPacketReply.Completed, callback, r.Instance.Definition.Compose(this));
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@@ -1349,11 +1472,15 @@ partial class EpConnection
|
|||||||
SendError(ErrorType.Management, callback, (ushort)ExceptionCode.ResourceNotFound);
|
SendError(ErrorType.Management, callback, (ushort)ExceptionCode.ResourceNotFound);
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
if (r.Instance.Applicable(_session, ActionType.Attach, null) == Ruling.Denied)
|
if (!TryApplyManagers(
|
||||||
{
|
null,
|
||||||
SendError(ErrorType.Management, callback, (ushort)ExceptionCode.ResourceNotFound);
|
r,
|
||||||
|
ActionType.Attach,
|
||||||
|
callback,
|
||||||
|
ErrorType.Management,
|
||||||
|
ExceptionCode.ResourceNotFound,
|
||||||
|
out _))
|
||||||
return;
|
return;
|
||||||
}
|
|
||||||
|
|
||||||
SendReply(EpPacketReply.Completed, callback, r);
|
SendReply(EpPacketReply.Completed, callback, r);
|
||||||
}
|
}
|
||||||
@@ -1378,15 +1505,21 @@ partial class EpConnection
|
|||||||
SendError(ErrorType.Management, callback, (ushort)ExceptionCode.ResourceNotFound);
|
SendError(ErrorType.Management, callback, (ushort)ExceptionCode.ResourceNotFound);
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
if (r.Instance.Applicable(_session, ActionType.Attach, null) == Ruling.Denied)
|
if (!TryApplyManagers(
|
||||||
{
|
null,
|
||||||
SendError(ErrorType.Management, callback, (ushort)ExceptionCode.NotAllowed);
|
r,
|
||||||
|
ActionType.Attach,
|
||||||
|
callback,
|
||||||
|
ErrorType.Management,
|
||||||
|
ExceptionCode.NotAllowed,
|
||||||
|
out _))
|
||||||
return;
|
return;
|
||||||
}
|
|
||||||
|
|
||||||
r.Instance.Children<IResource>().Then(children =>
|
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);
|
SendReply(EpPacketReply.Completed, callback, list);
|
||||||
}).Error(e =>
|
}).Error(e =>
|
||||||
{
|
{
|
||||||
@@ -1445,6 +1578,17 @@ partial class EpConnection
|
|||||||
return;
|
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 =>
|
Codec.ParseAsync(tdu.Data, offset, this, null).Then(pr =>
|
||||||
{
|
{
|
||||||
if (pr.Value is AsyncReply reply)
|
if (pr.Value is AsyncReply reply)
|
||||||
@@ -1464,7 +1608,7 @@ partial class EpConnection
|
|||||||
// return;
|
// 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 =>
|
}).Error(x =>
|
||||||
{
|
{
|
||||||
@@ -1479,7 +1623,7 @@ partial class EpConnection
|
|||||||
this.Socket.Unhold();
|
this.Socket.Unhold();
|
||||||
|
|
||||||
// @TODO: Make managers for procedure calls
|
// @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));
|
}).Error(x => SendError(ErrorType.Management, callback, (ushort)ExceptionCode.ParseError));
|
||||||
}
|
}
|
||||||
@@ -1518,6 +1662,18 @@ partial class EpConnection
|
|||||||
return;
|
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 =>
|
Codec.ParseAsync(tdu.Data, offset, this, null).Then(pr =>
|
||||||
{
|
{
|
||||||
if (pr.Value is AsyncReply reply)
|
if (pr.Value is AsyncReply reply)
|
||||||
@@ -1538,7 +1694,7 @@ partial class EpConnection
|
|||||||
// return;
|
// return;
|
||||||
//}
|
//}
|
||||||
|
|
||||||
InvokeFunction(fd, callback, results, EpPacketRequest.StaticCall, null);
|
InvokeFunction(fd, callback, results, EpPacketRequest.StaticCall, managerDelay, null);
|
||||||
|
|
||||||
}).Error(x =>
|
}).Error(x =>
|
||||||
{
|
{
|
||||||
@@ -1555,7 +1711,7 @@ partial class EpConnection
|
|||||||
// @TODO: Make managers for static calls
|
// @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));
|
}).Error(x => SendError(ErrorType.Management, callback, (ushort)ExceptionCode.ParseError));
|
||||||
}
|
}
|
||||||
@@ -1586,6 +1742,17 @@ partial class EpConnection
|
|||||||
return;
|
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 =>
|
Codec.ParseAsync(tdu.Data, offset, this, null).Then(pr =>
|
||||||
{
|
{
|
||||||
if (pr.Value is AsyncReply asyncReply)
|
if (pr.Value is AsyncReply asyncReply)
|
||||||
@@ -1599,30 +1766,26 @@ partial class EpConnection
|
|||||||
|
|
||||||
if (r is EpResource)
|
if (r is EpResource)
|
||||||
{
|
{
|
||||||
var rt = (r as EpResource)._Invoke(index, result);
|
ExecuteRateControlled(callback, managerDelay, () =>
|
||||||
if (rt != null)
|
|
||||||
{
|
{
|
||||||
rt.Then(res =>
|
var rt = (r as EpResource)._Invoke(index, result);
|
||||||
|
if (rt != null)
|
||||||
{
|
{
|
||||||
SendReply(EpPacketReply.Completed, callback, res);
|
rt.Then(res =>
|
||||||
});
|
{
|
||||||
}
|
SendReply(EpPacketReply.Completed, callback, res);
|
||||||
else
|
});
|
||||||
{
|
}
|
||||||
// function not found on a distributed object
|
else
|
||||||
SendError(ErrorType.Management, callback, (ushort)ExceptionCode.MethodNotFound);
|
{
|
||||||
}
|
// function not found on a distributed object
|
||||||
|
SendError(ErrorType.Management, callback, (ushort)ExceptionCode.MethodNotFound);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
if (r.Instance.Applicable(_session, ActionType.Execute, ft) == Ruling.Denied)
|
InvokeFunction(ft, callback, result, EpPacketRequest.InvokeFunction, managerDelay, r);
|
||||||
{
|
|
||||||
SendError(ErrorType.Management, callback,
|
|
||||||
(ushort)ExceptionCode.InvokeDenied);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
InvokeFunction(ft, callback, result, EpPacketRequest.InvokeFunction, r);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -1635,30 +1798,26 @@ partial class EpConnection
|
|||||||
|
|
||||||
if (r is EpResource)
|
if (r is EpResource)
|
||||||
{
|
{
|
||||||
var rt = (r as EpResource)._Invoke(index, pr.Value);
|
ExecuteRateControlled(callback, managerDelay, () =>
|
||||||
if (rt != null)
|
|
||||||
{
|
{
|
||||||
rt.Then(res =>
|
var rt = (r as EpResource)._Invoke(index, pr.Value);
|
||||||
|
if (rt != null)
|
||||||
{
|
{
|
||||||
SendReply(EpPacketReply.Completed, callback, res);
|
rt.Then(res =>
|
||||||
});
|
{
|
||||||
}
|
SendReply(EpPacketReply.Completed, callback, res);
|
||||||
else
|
});
|
||||||
{
|
}
|
||||||
// function not found on a distributed object
|
else
|
||||||
SendError(ErrorType.Management, callback, (ushort)ExceptionCode.MethodNotFound);
|
{
|
||||||
}
|
// function not found on a distributed object
|
||||||
|
SendError(ErrorType.Management, callback, (ushort)ExceptionCode.MethodNotFound);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
if (r.Instance.Applicable(_session, ActionType.Execute, ft) == Ruling.Denied)
|
InvokeFunction(ft, callback, pr.Value, EpPacketRequest.InvokeFunction, managerDelay, r);
|
||||||
{
|
|
||||||
SendError(ErrorType.Management, callback,
|
|
||||||
(ushort)ExceptionCode.InvokeDenied);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
InvokeFunction(ft, callback, pr.Value, EpPacketRequest.InvokeFunction, r);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}).Error(x => SendError(ErrorType.Management, callback, (ushort)ExceptionCode.ParseError)); ;
|
}).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(
|
ExecuteRateControlled(
|
||||||
callback,
|
callback,
|
||||||
delay,
|
managerDelay,
|
||||||
() => InvokeFunctionCore(ft, callback, arguments, actionType, target));
|
() => InvokeFunctionCore(ft, callback, arguments, actionType, target));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1811,6 +1968,8 @@ partial class EpConnection
|
|||||||
|
|
||||||
object rt;
|
object rt;
|
||||||
|
|
||||||
|
context?.BindOperation(target as IResource, ft);
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
rt = ft.MethodInfo.Invoke(target, args);
|
rt = ft.MethodInfo.Invoke(target, args);
|
||||||
@@ -1827,6 +1986,7 @@ partial class EpConnection
|
|||||||
if (ft.StreamMode != StreamMode.None)
|
if (ft.StreamMode != StreamMode.None)
|
||||||
{
|
{
|
||||||
context ??= new InvocationContext(this, callback);
|
context ??= new InvocationContext(this, callback);
|
||||||
|
context.BindOperation(target as IResource, ft);
|
||||||
context.InitializeStream(ft.StreamMode, ft.Pausable);
|
context.InitializeStream(ft.StreamMode, ft.Pausable);
|
||||||
|
|
||||||
if (ft.StreamMode == StreamMode.Pull && context.SetAsyncEnumerable(rt))
|
if (ft.StreamMode == StreamMode.Pull && context.SetAsyncEnumerable(rt))
|
||||||
@@ -2041,10 +2201,24 @@ partial class EpConnection
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!TryApplyManagers(
|
||||||
|
context.Function,
|
||||||
|
context.Resource,
|
||||||
|
ActionType.PullStream,
|
||||||
|
callback,
|
||||||
|
ErrorType.Management,
|
||||||
|
ExceptionCode.NotAllowed,
|
||||||
|
out var managerDelay,
|
||||||
|
supportsDelay: true))
|
||||||
|
return;
|
||||||
|
|
||||||
Task.Run(async () =>
|
Task.Run(async () =>
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
if (managerDelay > TimeSpan.Zero)
|
||||||
|
await Task.Delay(managerDelay).ConfigureAwait(false);
|
||||||
|
|
||||||
var next = await context.PullAsync();
|
var next = await context.PullAsync();
|
||||||
if (next.HasValue)
|
if (next.HasValue)
|
||||||
SendChunk(executionCallback, next.Value);
|
SendChunk(executionCallback, next.Value);
|
||||||
@@ -2070,7 +2244,25 @@ partial class EpConnection
|
|||||||
{
|
{
|
||||||
var executionCallback = ParseExecutionCallback(tdu);
|
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,
|
SendError(ErrorType.Management, callback, (ushort)ExceptionCode.NotAllowed,
|
||||||
"The target execution is not active.");
|
"The target execution is not active.");
|
||||||
@@ -2099,7 +2291,24 @@ partial class EpConnection
|
|||||||
var executionCallback = ParseExecutionCallback(tdu);
|
var executionCallback = ParseExecutionCallback(tdu);
|
||||||
var context = GetInvocation(executionCallback);
|
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,
|
SendError(ErrorType.Management, callback, (ushort)ExceptionCode.NotAllowed,
|
||||||
"The target execution is not pausable or is already halted.");
|
"The target execution is not pausable or is already halted.");
|
||||||
@@ -2114,7 +2323,24 @@ partial class EpConnection
|
|||||||
var executionCallback = ParseExecutionCallback(tdu);
|
var executionCallback = ParseExecutionCallback(tdu);
|
||||||
var context = GetInvocation(executionCallback);
|
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,
|
SendError(ErrorType.Management, callback, (ushort)ExceptionCode.NotAllowed,
|
||||||
"The target execution is not pausable or is not halted.");
|
"The target execution is not pausable or is not halted.");
|
||||||
@@ -2151,6 +2377,16 @@ partial class EpConnection
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!TryApplyManagers(
|
||||||
|
et,
|
||||||
|
r,
|
||||||
|
ActionType.Subscribe,
|
||||||
|
callback,
|
||||||
|
ErrorType.Management,
|
||||||
|
ExceptionCode.NotAllowed,
|
||||||
|
out _))
|
||||||
|
return;
|
||||||
|
|
||||||
if (r is EpResource)
|
if (r is EpResource)
|
||||||
{
|
{
|
||||||
(r as EpResource).Subscribe(et).Then(x =>
|
(r as EpResource).Subscribe(et).Then(x =>
|
||||||
@@ -2210,6 +2446,16 @@ partial class EpConnection
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!TryApplyManagers(
|
||||||
|
et,
|
||||||
|
r,
|
||||||
|
ActionType.Unsubscribe,
|
||||||
|
callback,
|
||||||
|
ErrorType.Management,
|
||||||
|
ExceptionCode.NotAllowed,
|
||||||
|
out _))
|
||||||
|
return;
|
||||||
|
|
||||||
if (r is EpResource)
|
if (r is EpResource)
|
||||||
{
|
{
|
||||||
(r as EpResource).Unsubscribe(et).Then(x =>
|
(r as EpResource).Unsubscribe(et).Then(x =>
|
||||||
@@ -2274,18 +2520,15 @@ partial class EpConnection
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (r.Instance.Applicable(_session, ActionType.SetProperty, pt, this) == Ruling.Denied)
|
if (!TryApplyManagers(
|
||||||
{
|
|
||||||
SendError(ErrorType.Exception, callback, (ushort)ExceptionCode.SetPropertyDenied);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!TryApplyRateControl(
|
|
||||||
pt,
|
pt,
|
||||||
r,
|
r,
|
||||||
ActionType.SetProperty,
|
ActionType.SetProperty,
|
||||||
callback,
|
callback,
|
||||||
out var rateControlDelay))
|
ErrorType.Exception,
|
||||||
|
ExceptionCode.SetPropertyDenied,
|
||||||
|
out var rateControlDelay,
|
||||||
|
supportsDelay: true))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
|
||||||
@@ -3359,7 +3602,7 @@ partial class EpConnection
|
|||||||
if (!info.Receivers(_session))
|
if (!info.Receivers(_session))
|
||||||
return;
|
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;
|
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;
|
return;
|
||||||
|
|
||||||
// compose the packet
|
// compose the packet
|
||||||
|
|||||||
@@ -7,7 +7,10 @@ namespace Esiur.Resource;
|
|||||||
/// Associates a registered auditing-manager implementation with a resource type.
|
/// Associates a registered auditing-manager implementation with a resource type.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = true)]
|
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = true)]
|
||||||
public sealed class AuditingManagerAttribute<T> : Attribute
|
public sealed class AuditingManagerAttribute<T> : ResourceManagerAttribute
|
||||||
where T : IAuditingManager
|
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.
|
/// Associates a registered permissions-manager implementation with a resource type.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = true)]
|
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = true)]
|
||||||
public sealed class PermissionsManagerAttribute<T> : Attribute
|
public sealed class PermissionsManagerAttribute<T> : ResourceManagerAttribute
|
||||||
where T : IPermissionsManager
|
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.
|
/// Associates a registered rate-control-manager implementation with a resource type.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = true)]
|
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = true)]
|
||||||
public sealed class RateControlManagerAttribute<T> : Attribute
|
public sealed class RateControlManagerAttribute<T> : ResourceManagerAttribute
|
||||||
where T : IRateControlManager
|
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; }
|
||||||
|
}
|
||||||
@@ -8,6 +8,7 @@ using System.Runtime.CompilerServices;
|
|||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using Esiur.Misc;
|
using Esiur.Misc;
|
||||||
using Esiur.Security.Permissions;
|
using Esiur.Security.Permissions;
|
||||||
|
using Esiur.Security.Management;
|
||||||
using Esiur.Security.Authority;
|
using Esiur.Security.Authority;
|
||||||
using Esiur.Proxy;
|
using Esiur.Proxy;
|
||||||
using Esiur.Core;
|
using Esiur.Core;
|
||||||
@@ -30,7 +31,7 @@ public class Instance
|
|||||||
WeakReference<IResource> resource;
|
WeakReference<IResource> resource;
|
||||||
IStore store;
|
IStore store;
|
||||||
TypeDef definition;
|
TypeDef definition;
|
||||||
AutoList<IPermissionsManager, Instance> managers;
|
AutoList<IResourceManager, Instance> managers;
|
||||||
|
|
||||||
|
|
||||||
public event PropertyModifiedEvent PropertyModified;
|
public event PropertyModifiedEvent PropertyModified;
|
||||||
@@ -696,36 +697,55 @@ public class Instance
|
|||||||
/// <returns>Ruling.</returns>
|
/// <returns>Ruling.</returns>
|
||||||
public Ruling Applicable(Session session, ActionType action, MemberDef member, object inquirer = null)
|
public Ruling Applicable(Session session, ActionType action, MemberDef member, object inquirer = null)
|
||||||
{
|
{
|
||||||
IResource res;
|
var context = MakeManagerContext(session, action, member, inquirer);
|
||||||
if (this.resource.TryGetTarget(out res))
|
return Warehouse.EvaluatePermissions(context, managers.ToArray());
|
||||||
{
|
}
|
||||||
//return store.Applicable(res, session, action, member, inquirer);
|
|
||||||
|
|
||||||
foreach (IPermissionsManager manager in managers)
|
/// <summary>
|
||||||
{
|
/// Evaluates the Warehouse defaults and this resource's manager snapshot.
|
||||||
var r = manager.Applicable(res, session, action, member, inquirer);
|
/// </summary>
|
||||||
if (r != Ruling.DontCare)
|
public ResourceManagerEvaluation EvaluateManagers(
|
||||||
return r;
|
Session session,
|
||||||
}
|
ActionType action,
|
||||||
|
MemberDef member,
|
||||||
}
|
object inquirer = null)
|
||||||
|
{
|
||||||
// Apply default permissions if no manager is applicable or if the resource is not available.
|
var context = MakeManagerContext(session, action, member, inquirer);
|
||||||
if (action == ActionType.GetProperty
|
return Warehouse.EvaluateManagers(context, managers.ToArray());
|
||||||
|| action == ActionType.ViewTypeDef
|
}
|
||||||
|| action == ActionType.ReceiveEvent
|
|
||||||
|| action == ActionType.Attach
|
|
||||||
|| action == ActionType.Execute)
|
|
||||||
return Ruling.Allowed;
|
|
||||||
else
|
|
||||||
return Ruling.Denied;
|
|
||||||
|
|
||||||
|
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>
|
/// <summary>
|
||||||
/// Execution managers.
|
/// Execution managers.
|
||||||
/// </summary>
|
/// </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;
|
public readonly Warehouse Warehouse;
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -747,7 +767,8 @@ public class Instance
|
|||||||
//this.attributes = new KeyList<string, object>(this);
|
//this.attributes = new KeyList<string, object>(this);
|
||||||
//children = new AutoList<IResource, Instance>(this);
|
//children = new AutoList<IResource, Instance>(this);
|
||||||
//parents = 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.OnAdd += Children_OnAdd;
|
||||||
//children.OnRemoved += Children_OnRemoved;
|
//children.OnRemoved += Children_OnRemoved;
|
||||||
//parents.OnAdd += Parents_OnAdd;
|
//parents.OnAdd += Parents_OnAdd;
|
||||||
|
|||||||
@@ -1,19 +1,22 @@
|
|||||||
using Esiur.Data;
|
using Esiur.Data;
|
||||||
using Esiur.Security.Authority;
|
using Esiur.Security.Authority;
|
||||||
using Esiur.Security.Permissions;
|
using Esiur.Security.Permissions;
|
||||||
|
using Esiur.Security.Management;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
using System.Security.Principal;
|
using System.Security.Principal;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
|
||||||
namespace Esiur.Resource
|
namespace Esiur.Resource
|
||||||
{
|
{
|
||||||
public class ResourceContext:IResourceContext
|
public class ResourceContext : IResourceContext, IResourceManagersContext
|
||||||
{
|
{
|
||||||
public ulong Age { get; }
|
public ulong Age { get; }
|
||||||
public Map<string, object> Attributes { get; }
|
public Map<string, object> Attributes { get; }
|
||||||
public Map<string, object> Properties { get; }
|
public Map<string, object> Properties { get; }
|
||||||
public IPermissionsManager PermissionsManager { get; }
|
public IPermissionsManager PermissionsManager { get; }
|
||||||
|
public IReadOnlyList<IResourceManager> ResourceManagers { get; }
|
||||||
|
|
||||||
public ResourceContext(ulong age, Map<string, object> attributes, Map<string, object> properties, IPermissionsManager permissionsManager)
|
public ResourceContext(ulong age, Map<string, object> attributes, Map<string, object> properties, IPermissionsManager permissionsManager)
|
||||||
{
|
{
|
||||||
@@ -21,6 +24,35 @@ namespace Esiur.Resource
|
|||||||
Attributes = attributes;
|
Attributes = attributes;
|
||||||
Properties = properties;
|
Properties = properties;
|
||||||
PermissionsManager = permissionsManager;
|
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()
|
//public virtual void Build()
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ using Esiur.Protocol;
|
|||||||
using Esiur.Proxy;
|
using Esiur.Proxy;
|
||||||
using Esiur.Security.Authority;
|
using Esiur.Security.Authority;
|
||||||
using Esiur.Security.Cryptography;
|
using Esiur.Security.Cryptography;
|
||||||
|
using Esiur.Security.Management;
|
||||||
using Esiur.Security.Permissions;
|
using Esiur.Security.Permissions;
|
||||||
using Esiur.Security.RateLimiting;
|
using Esiur.Security.RateLimiting;
|
||||||
using Org.BouncyCastle.Asn1.Cms;
|
using Org.BouncyCastle.Asn1.Cms;
|
||||||
@@ -91,7 +92,10 @@ public class Warehouse
|
|||||||
Map<string, IAuthenticationProvider> _authenticationProviders = new Map<string, IAuthenticationProvider>();
|
Map<string, IAuthenticationProvider> _authenticationProviders = new Map<string, IAuthenticationProvider>();
|
||||||
readonly ConcurrentDictionary<string, IEncryptionProvider> _encryptionProviders
|
readonly ConcurrentDictionary<string, IEncryptionProvider> _encryptionProviders
|
||||||
= new ConcurrentDictionary<string, IEncryptionProvider>(StringComparer.Ordinal);
|
= 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
|
readonly ConcurrentDictionary<string, RatePolicy> _ratePolicies
|
||||||
= new ConcurrentDictionary<string, RatePolicy>(StringComparer.Ordinal);
|
= 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>
|
/// <summary>
|
||||||
/// Registers a named rate policy referenced by RateControl attributes.
|
/// Registers a named rate policy referenced by RateControl attributes.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -241,6 +608,8 @@ public class Warehouse
|
|||||||
{
|
{
|
||||||
Configuration = configuration ?? throw new ArgumentNullException(nameof(configuration));
|
Configuration = configuration ?? throw new ArgumentNullException(nameof(configuration));
|
||||||
|
|
||||||
|
RegisterManager(new NamedRateControlManager(), true);
|
||||||
|
|
||||||
Protocols.Add("EP",
|
Protocols.Add("EP",
|
||||||
async (name, context)
|
async (name, context)
|
||||||
=> await New<EpConnection>(name, context));
|
=> await New<EpConnection>(name, context));
|
||||||
@@ -540,11 +909,14 @@ public class Warehouse
|
|||||||
}
|
}
|
||||||
|
|
||||||
var resourceReference = new WeakReference<IResource>(resource);
|
var resourceReference = new WeakReference<IResource>(resource);
|
||||||
|
var resourceManagers = ResolveResourceManagers(resource.GetType(), resourceContext);
|
||||||
|
|
||||||
var resourceId = (uint)Interlocked.Increment(ref _resourceCounter);
|
var resourceId = (uint)Interlocked.Increment(ref _resourceCounter);
|
||||||
|
|
||||||
resource.Instance = new Instance(this, resourceId, instanceName, resource, store, resourceContext?.Age ?? 0);
|
resource.Instance = new Instance(this, resourceId, instanceName, resource, store, resourceContext?.Age ?? 0);
|
||||||
|
|
||||||
|
resource.Instance.Managers.AddRange(resourceManagers);
|
||||||
|
|
||||||
if (resourceContext?.Attributes != null)
|
if (resourceContext?.Attributes != null)
|
||||||
resource.Instance.SetAttributes(resourceContext.Attributes);
|
resource.Instance.SetAttributes(resourceContext.Attributes);
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,8 @@ namespace Esiur.Security.Management;
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Evaluates whether a resource operation is admitted by rate-control policy.
|
/// Evaluates whether a resource operation is admitted by rate-control policy.
|
||||||
/// A manager may assign <see cref="ResourceManagerContext.Delay"/> when allowing
|
/// 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>
|
/// </summary>
|
||||||
public interface IRateControlManager : IResourceManager
|
public interface IRateControlManager : IResourceManager
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -10,9 +10,9 @@ using System.Linq;
|
|||||||
namespace Esiur.Security.Management;
|
namespace Esiur.Security.Management;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Immutable metadata describing a resource operation being evaluated by managers.
|
/// Metadata describing a resource operation being evaluated by managers. Operation
|
||||||
/// <see cref="Delay"/> is the sole mutable value and allows rate-control managers to
|
/// identity is immutable; <see cref="Delay"/> and <see cref="DenialReason"/> let a
|
||||||
/// request deferred execution.
|
/// manager return admission details.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class ResourceManagerContext
|
public sealed class ResourceManagerContext
|
||||||
{
|
{
|
||||||
@@ -23,9 +23,16 @@ public sealed class ResourceManagerContext
|
|||||||
public Session? Session { get; }
|
public Session? Session { get; }
|
||||||
public IResource? Resource { get; }
|
public IResource? Resource { get; }
|
||||||
public MemberDef? Member { get; }
|
public MemberDef? Member { get; }
|
||||||
|
public TypeDef? TypeDefinition { get; }
|
||||||
public ActionType Action { get; }
|
public ActionType Action { get; }
|
||||||
public object? Inquirer { 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>
|
/// <summary>
|
||||||
/// Gets the local attributes that configure manager policy for the target member.
|
/// Gets the local attributes that configure manager policy for the target member.
|
||||||
/// The collection is a defensive, read-only snapshot.
|
/// The collection is a defensive, read-only snapshot.
|
||||||
@@ -33,10 +40,17 @@ public sealed class ResourceManagerContext
|
|||||||
public IReadOnlyList<Attribute> MemberPolicyAttributes => _memberPolicyAttributes;
|
public IReadOnlyList<Attribute> MemberPolicyAttributes => _memberPolicyAttributes;
|
||||||
|
|
||||||
/// <summary>
|
/// <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>
|
/// </summary>
|
||||||
public TimeSpan Delay { get; set; }
|
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(
|
public ResourceManagerContext(
|
||||||
Warehouse warehouse,
|
Warehouse warehouse,
|
||||||
EpConnection? connection,
|
EpConnection? connection,
|
||||||
@@ -45,15 +59,19 @@ public sealed class ResourceManagerContext
|
|||||||
MemberDef? member,
|
MemberDef? member,
|
||||||
ActionType action,
|
ActionType action,
|
||||||
object? inquirer = null,
|
object? inquirer = null,
|
||||||
IEnumerable<Attribute>? memberPolicyAttributes = null)
|
IEnumerable<Attribute>? memberPolicyAttributes = null,
|
||||||
|
TypeDef? typeDefinition = null,
|
||||||
|
bool supportsDelay = false)
|
||||||
{
|
{
|
||||||
Warehouse = warehouse ?? throw new ArgumentNullException(nameof(warehouse));
|
Warehouse = warehouse ?? throw new ArgumentNullException(nameof(warehouse));
|
||||||
Connection = connection;
|
Connection = connection;
|
||||||
Session = session;
|
Session = session;
|
||||||
Resource = resource;
|
Resource = resource;
|
||||||
Member = member;
|
Member = member;
|
||||||
|
TypeDefinition = typeDefinition ?? resource?.Instance?.Definition ?? member?.Definition;
|
||||||
Action = action;
|
Action = action;
|
||||||
Inquirer = inquirer;
|
Inquirer = inquirer;
|
||||||
|
SupportsDelay = supportsDelay;
|
||||||
|
|
||||||
var policies = memberPolicyAttributes?.ToArray() ?? Array.Empty<Attribute>();
|
var policies = memberPolicyAttributes?.ToArray() ?? Array.Empty<Attribute>();
|
||||||
if (policies.Any(attribute => attribute == null))
|
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,
|
RemoveChild,
|
||||||
Rename,
|
Rename,
|
||||||
ReceiveEvent,
|
ReceiveEvent,
|
||||||
ViewTypeDef
|
ViewTypeDef,
|
||||||
|
Detach,
|
||||||
|
Subscribe,
|
||||||
|
Unsubscribe,
|
||||||
|
PullStream,
|
||||||
|
TerminateExecution,
|
||||||
|
HaltExecution,
|
||||||
|
ResumeExecution
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ using Esiur.Core;
|
|||||||
using Esiur.Resource;
|
using Esiur.Resource;
|
||||||
using Esiur.Security.Authority;
|
using Esiur.Security.Authority;
|
||||||
using Esiur.Data.Types;
|
using Esiur.Data.Types;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
namespace Esiur.Security.Permissions;
|
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)
|
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)
|
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);
|
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;
|
return (T)res;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -71,7 +71,13 @@ public class EsiurProxyRewrite : IModelFinalizingConvention
|
|||||||
// check if the object exists
|
// check if the object exists
|
||||||
var obj = options.Warehouse.Create(entityType.ClrType, null) as IResource;
|
var obj = options.Warehouse.Create(entityType.ClrType, null) as IResource;
|
||||||
options.Store.TypesByType[entityType.ClrType].PrimaryKey.SetValue(obj, id);
|
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;
|
return obj;
|
||||||
}
|
}
|
||||||
else
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -24,12 +24,11 @@ SOFTWARE.
|
|||||||
|
|
||||||
using Esiur.Core;
|
using Esiur.Core;
|
||||||
using Esiur.Data;
|
using Esiur.Data;
|
||||||
using Esiur.Data.Types;
|
|
||||||
using Esiur.Protocol;
|
using Esiur.Protocol;
|
||||||
using Esiur.Resource;
|
using Esiur.Resource;
|
||||||
using Esiur.Security.Authority;
|
using Esiur.Security.Authority;
|
||||||
using Esiur.Security.Cryptography;
|
using Esiur.Security.Cryptography;
|
||||||
using Esiur.Security.Permissions;
|
using Esiur.Security.Management;
|
||||||
using Esiur.Security.RateLimiting;
|
using Esiur.Security.RateLimiting;
|
||||||
using Esiur.Stores;
|
using Esiur.Stores;
|
||||||
using System;
|
using System;
|
||||||
@@ -55,16 +54,17 @@ internal static class Program
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
var port = FindAvailablePort();
|
var port = FindAvailablePort();
|
||||||
var service = await StartServer(serverWarehouse, port);
|
var server = await StartServer(serverWarehouse, port);
|
||||||
|
|
||||||
connection = await ConnectClient(clientWarehouse, port);
|
connection = await ConnectClient(clientWarehouse, port);
|
||||||
Require(connection.IsEncrypted, "Authenticated connection did not enable AES encryption.");
|
Require(connection.IsEncrypted, "Authenticated connection did not enable AES encryption.");
|
||||||
var remote = await connection.Get("sys/service") as EpResource
|
var remote = await connection.Get("sys/service") as EpResource
|
||||||
?? throw new InvalidOperationException("Remote service was not found.");
|
?? 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 RunRateControlScenarios(remote);
|
||||||
await RunStreamingScenarios(service, remote);
|
await RunStreamingScenarios(server.Service, remote);
|
||||||
|
|
||||||
Console.WriteLine();
|
Console.WriteLine();
|
||||||
Console.WriteLine("All functional scenarios passed.");
|
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.RegisterAuthenticationProvider(new ServerAuthenticationProvider());
|
||||||
warehouse.RegisterEncryptionProvider(new AesEncryptionProvider());
|
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.MaximumPacketSize = 8 * 1024 * 1024;
|
||||||
warehouse.Configuration.Parser.MaximumAllocationSize = 4 * 1024 * 1024;
|
warehouse.Configuration.Parser.MaximumAllocationSize = 4 * 1024 * 1024;
|
||||||
warehouse.Configuration.Parser.MaximumCollectionItems = 65_536;
|
warehouse.Configuration.Parser.MaximumCollectionItems = 65_536;
|
||||||
@@ -111,7 +126,10 @@ internal static class Program
|
|||||||
});
|
});
|
||||||
|
|
||||||
var service = await warehouse.Put("sys/service", new MyService());
|
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
|
var resource1 = await warehouse.Put("sys/service/r1", new MyResource
|
||||||
{
|
{
|
||||||
Description = "Testing 1",
|
Description = "Testing 1",
|
||||||
@@ -144,7 +162,15 @@ internal static class Program
|
|||||||
server.MapCall("temp", () => child2);
|
server.MapCall("temp", () => child2);
|
||||||
|
|
||||||
await warehouse.Open();
|
await warehouse.Open();
|
||||||
return service;
|
return (
|
||||||
|
service,
|
||||||
|
new ManagerScenarioFixture(
|
||||||
|
managerProbe,
|
||||||
|
defaultPermissions,
|
||||||
|
denyPermissions,
|
||||||
|
rateControl,
|
||||||
|
attributeAudit,
|
||||||
|
contextAudit));
|
||||||
}
|
}
|
||||||
|
|
||||||
static async Task<EpConnection> ConnectClient(Warehouse warehouse, ushort port)
|
static async Task<EpConnection> ConnectClient(Warehouse warehouse, ushort port)
|
||||||
@@ -358,18 +384,4 @@ internal static class Program
|
|||||||
return port;
|
return port;
|
||||||
}
|
}
|
||||||
|
|
||||||
sealed class AllowPropertySetPermissions : IPermissionsManager
|
|
||||||
{
|
|
||||||
public Map<string, object> Settings { get; } = new();
|
|
||||||
|
|
||||||
public Ruling Applicable(
|
|
||||||
IResource resource,
|
|
||||||
Session session,
|
|
||||||
ActionType action,
|
|
||||||
MemberDef member,
|
|
||||||
object inquirer = null!)
|
|
||||||
=> action == ActionType.SetProperty ? Ruling.Allowed : Ruling.DontCare;
|
|
||||||
|
|
||||||
public bool Initialize(Map<string, object> settings, IResource resource) => true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ Coverage includes:
|
|||||||
|
|
||||||
- resource attachment and property serialization;
|
- resource attachment and property serialization;
|
||||||
- procedure and resource calls, optional arguments, tuples, and events;
|
- 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;
|
- named rate policies for function calls and property setters;
|
||||||
- rate-limit denial propagation to callers;
|
- rate-limit denial propagation to callers;
|
||||||
- pull streams backed by `IAsyncEnumerable<T>`;
|
- pull streams backed by `IAsyncEnumerable<T>`;
|
||||||
|
|||||||
@@ -104,8 +104,11 @@ public class RateControlIntegrationTests
|
|||||||
QueueLimit = queueLimit,
|
QueueLimit = queueLimit,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
var permissions = new AllowPropertySetPermissions();
|
||||||
|
warehouse.RegisterManager(permissions);
|
||||||
|
|
||||||
var resource = await warehouse.Put("sys/rate", new RateLimitedResource());
|
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)
|
static async Task<EpResource> GetRemote(IntegrationCluster cluster)
|
||||||
|
|||||||
@@ -0,0 +1,285 @@
|
|||||||
|
using Esiur.Data;
|
||||||
|
using Esiur.Data.Types;
|
||||||
|
using Esiur.Protocol;
|
||||||
|
using Esiur.Resource;
|
||||||
|
using Esiur.Security.Authority;
|
||||||
|
using Esiur.Security.Management;
|
||||||
|
using Esiur.Security.Permissions;
|
||||||
|
using Esiur.Stores;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace Esiur.Tests.Unit;
|
||||||
|
|
||||||
|
public class ResourceManagerPipelineTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public async Task Put_WithUnregisteredTypeManager_FailsBeforeAssigningInstance()
|
||||||
|
{
|
||||||
|
var warehouse = await CreateWarehouseAsync();
|
||||||
|
var resource = new AttributeManagedResource();
|
||||||
|
|
||||||
|
var exception = await Assert.ThrowsAsync<InvalidOperationException>(async () =>
|
||||||
|
{
|
||||||
|
await warehouse.Put("sys/unregistered-manager", resource);
|
||||||
|
});
|
||||||
|
|
||||||
|
Assert.Contains(nameof(AttributePermissionsManager), exception.Message);
|
||||||
|
Assert.Null(resource.Instance);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ResourceContext_RequiresTheExactRegisteredManagerInstance()
|
||||||
|
{
|
||||||
|
var warehouse = await CreateWarehouseAsync();
|
||||||
|
var registered = new ContextPermissionsManager();
|
||||||
|
var differentInstance = new ContextPermissionsManager();
|
||||||
|
warehouse.RegisterManager(registered);
|
||||||
|
|
||||||
|
var rejectedResource = new PlainResource();
|
||||||
|
var rejectedContext = new ResourceContext(
|
||||||
|
new IResourceManager[] { differentInstance });
|
||||||
|
|
||||||
|
await Assert.ThrowsAsync<InvalidOperationException>(async () =>
|
||||||
|
{
|
||||||
|
await warehouse.Put("sys/rejected-context-manager", rejectedResource, rejectedContext);
|
||||||
|
});
|
||||||
|
|
||||||
|
Assert.Null(rejectedResource.Instance);
|
||||||
|
|
||||||
|
var acceptedContext = new ResourceContext(
|
||||||
|
new IResourceManager[] { registered });
|
||||||
|
var acceptedResource = await warehouse.Put(
|
||||||
|
"sys/accepted-context-manager",
|
||||||
|
new PlainResource(),
|
||||||
|
acceptedContext);
|
||||||
|
|
||||||
|
Assert.Contains(
|
||||||
|
acceptedResource.Instance!.Managers.ToArray(),
|
||||||
|
manager => ReferenceEquals(manager, registered));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void EvaluateManagers_ExecutesAllPermissionsManagersAndDenialOverrides()
|
||||||
|
{
|
||||||
|
var warehouse = new Warehouse();
|
||||||
|
var defaultAllow = new DefaultAllowPermissionsManager();
|
||||||
|
var localDeny = new DenyPermissionsManager();
|
||||||
|
var trailingObserver = new ObserverPermissionsManager();
|
||||||
|
|
||||||
|
warehouse.RegisterManager(defaultAllow, useAsDefault: true);
|
||||||
|
warehouse.RegisterManager(localDeny);
|
||||||
|
warehouse.RegisterManager(trailingObserver);
|
||||||
|
|
||||||
|
Assert.Same(defaultAllow, warehouse.TryGetManager<DefaultAllowPermissionsManager>());
|
||||||
|
Assert.Contains(typeof(DefaultAllowPermissionsManager), warehouse.GetRegisteredManagerTypes());
|
||||||
|
Assert.Contains(
|
||||||
|
warehouse.GetDefaultManagers(),
|
||||||
|
manager => ReferenceEquals(manager, defaultAllow));
|
||||||
|
|
||||||
|
var context = new ResourceManagerContext(
|
||||||
|
warehouse,
|
||||||
|
null,
|
||||||
|
new Session(),
|
||||||
|
null,
|
||||||
|
new FunctionDef { Name = "Call" },
|
||||||
|
ActionType.Execute);
|
||||||
|
|
||||||
|
var evaluation = warehouse.EvaluateManagers(
|
||||||
|
context,
|
||||||
|
new IResourceManager[] { localDeny, trailingObserver });
|
||||||
|
|
||||||
|
Assert.Equal(1, defaultAllow.Calls);
|
||||||
|
Assert.Equal(1, localDeny.Calls);
|
||||||
|
Assert.Equal(1, trailingObserver.Calls);
|
||||||
|
Assert.Equal(Ruling.Denied, evaluation.Permissions);
|
||||||
|
Assert.False(evaluation.IsAllowed);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void RateControlAllow_DoesNotGrantDefaultDeniedPropertySet()
|
||||||
|
{
|
||||||
|
var warehouse = new Warehouse();
|
||||||
|
var rateManager = new AllowRateControlManager();
|
||||||
|
warehouse.RegisterManager(rateManager, useAsDefault: true);
|
||||||
|
|
||||||
|
var context = new ResourceManagerContext(
|
||||||
|
warehouse,
|
||||||
|
null,
|
||||||
|
new Session(),
|
||||||
|
null,
|
||||||
|
new PropertyDef { Name = "Value" },
|
||||||
|
ActionType.SetProperty);
|
||||||
|
|
||||||
|
var evaluation = warehouse.EvaluateManagers(context);
|
||||||
|
|
||||||
|
Assert.Equal(1, rateManager.Calls);
|
||||||
|
Assert.Equal(Ruling.Allowed, evaluation.RateControl);
|
||||||
|
Assert.Equal(Ruling.Denied, evaluation.Permissions);
|
||||||
|
Assert.False(evaluation.IsAllowed);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task MemberPolicyAttributes_AreLocalAndNotTransportedToRemoteTypeDefs()
|
||||||
|
{
|
||||||
|
var warehouse = await CreateWarehouseAsync();
|
||||||
|
var local = new LocalTypeDef(typeof(MemberPolicyResource), warehouse);
|
||||||
|
|
||||||
|
var localFunction = local.GetFunctionDefByName(nameof(MemberPolicyResource.Call));
|
||||||
|
var localProperty = local.GetPropertyDefByName(nameof(MemberPolicyResource.Value));
|
||||||
|
var localEvent = local.GetEventDefByName(nameof(MemberPolicyResource.Changed));
|
||||||
|
|
||||||
|
Assert.Equal("call-rate", localFunction!.RatePolicyName);
|
||||||
|
Assert.Equal("set-rate", localProperty!.RatePolicyName);
|
||||||
|
Assert.Equal(
|
||||||
|
"call-policy",
|
||||||
|
Assert.Single(localFunction.MemberPolicyAttributes.OfType<LocalMemberPolicyAttribute>()).Name);
|
||||||
|
Assert.Equal(
|
||||||
|
"property-policy",
|
||||||
|
Assert.Single(localProperty.MemberPolicyAttributes.OfType<LocalMemberPolicyAttribute>()).Name);
|
||||||
|
Assert.Equal(
|
||||||
|
"event-policy",
|
||||||
|
Assert.Single(localEvent!.MemberPolicyAttributes.OfType<LocalMemberPolicyAttribute>()).Name);
|
||||||
|
|
||||||
|
var bytes = Codec.Compose(TypeDefInfo.FromTypeDef(local), warehouse, null!);
|
||||||
|
var wireText = Encoding.UTF8.GetString(bytes);
|
||||||
|
Assert.DoesNotContain("call-policy", wireText);
|
||||||
|
Assert.DoesNotContain("property-policy", wireText);
|
||||||
|
Assert.DoesNotContain("event-policy", wireText);
|
||||||
|
Assert.DoesNotContain("call-rate", wireText);
|
||||||
|
Assert.DoesNotContain("set-rate", wireText);
|
||||||
|
|
||||||
|
var connection = await warehouse.Put("sys/type-parser", new EpConnection());
|
||||||
|
connection.RemoteDomain = "remote.test";
|
||||||
|
var remote = await RemoteTypeDef.Parse(
|
||||||
|
new RemoteTypeDef(),
|
||||||
|
connection.RemoteDomain,
|
||||||
|
bytes,
|
||||||
|
connection,
|
||||||
|
Array.Empty<ulong>());
|
||||||
|
|
||||||
|
Assert.Empty(remote.GetFunctionDefByName(nameof(MemberPolicyResource.Call))!
|
||||||
|
.MemberPolicyAttributes);
|
||||||
|
Assert.Empty(remote.GetPropertyDefByName(nameof(MemberPolicyResource.Value))!
|
||||||
|
.MemberPolicyAttributes);
|
||||||
|
Assert.Empty(remote.GetEventDefByName(nameof(MemberPolicyResource.Changed))!
|
||||||
|
.MemberPolicyAttributes);
|
||||||
|
Assert.Null(remote.GetFunctionDefByName(nameof(MemberPolicyResource.Call))!.RatePolicyName);
|
||||||
|
Assert.Null(remote.GetPropertyDefByName(nameof(MemberPolicyResource.Value))!.RatePolicyName);
|
||||||
|
}
|
||||||
|
|
||||||
|
static async Task<Warehouse> CreateWarehouseAsync()
|
||||||
|
{
|
||||||
|
var warehouse = new Warehouse();
|
||||||
|
await warehouse.Put("sys", new MemoryStore());
|
||||||
|
return warehouse;
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract class TestPermissionsManager : IPermissionsManager
|
||||||
|
{
|
||||||
|
readonly Ruling _ruling;
|
||||||
|
|
||||||
|
protected TestPermissionsManager(Ruling ruling)
|
||||||
|
{
|
||||||
|
_ruling = ruling;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int Calls { get; private set; }
|
||||||
|
public Map<string, object> Settings { get; } = new();
|
||||||
|
|
||||||
|
public Ruling Applicable(
|
||||||
|
IResource resource,
|
||||||
|
Session session,
|
||||||
|
ActionType action,
|
||||||
|
MemberDef member,
|
||||||
|
object inquirer = null!)
|
||||||
|
{
|
||||||
|
Calls++;
|
||||||
|
return _ruling;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool Initialize(Map<string, object> settings, IResource resource) => true;
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed class AttributePermissionsManager : TestPermissionsManager
|
||||||
|
{
|
||||||
|
public AttributePermissionsManager() : base(Ruling.DontCare)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed class ContextPermissionsManager : TestPermissionsManager
|
||||||
|
{
|
||||||
|
public ContextPermissionsManager() : base(Ruling.DontCare)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed class DefaultAllowPermissionsManager : TestPermissionsManager
|
||||||
|
{
|
||||||
|
public DefaultAllowPermissionsManager() : base(Ruling.Allowed)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed class DenyPermissionsManager : TestPermissionsManager
|
||||||
|
{
|
||||||
|
public DenyPermissionsManager() : base(Ruling.Denied)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed class ObserverPermissionsManager : TestPermissionsManager
|
||||||
|
{
|
||||||
|
public ObserverPermissionsManager() : base(Ruling.DontCare)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed class AllowRateControlManager : IRateControlManager
|
||||||
|
{
|
||||||
|
public int Calls { get; private set; }
|
||||||
|
|
||||||
|
public Ruling Applicable(ResourceManagerContext context)
|
||||||
|
{
|
||||||
|
Calls++;
|
||||||
|
return Ruling.Allowed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[PermissionsManager<AttributePermissionsManager>]
|
||||||
|
sealed class AttributeManagedResource : Esiur.Resource.Resource
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed class PlainResource : Esiur.Resource.Resource
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Property | AttributeTargets.Event)]
|
||||||
|
sealed class LocalMemberPolicyAttribute : Attribute
|
||||||
|
{
|
||||||
|
public string Name { get; }
|
||||||
|
|
||||||
|
public LocalMemberPolicyAttribute(string name)
|
||||||
|
{
|
||||||
|
Name = name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Export]
|
||||||
|
sealed class MemberPolicyResource : Esiur.Resource.Resource
|
||||||
|
{
|
||||||
|
[RateControl("call-rate")]
|
||||||
|
[LocalMemberPolicy("call-policy")]
|
||||||
|
public void Call()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
[RateControl("set-rate")]
|
||||||
|
[LocalMemberPolicy("property-policy")]
|
||||||
|
public int Value { get; set; }
|
||||||
|
|
||||||
|
[LocalMemberPolicy("event-policy")]
|
||||||
|
public event ResourceEventHandler<int>? Changed;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user