diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props index 3040008..4d6b182 100644 --- a/src/Directory.Packages.props +++ b/src/Directory.Packages.props @@ -8,10 +8,10 @@ - - - - + + + + @@ -21,7 +21,7 @@ - + diff --git a/src/SecuritySystem.Abstractions/ExternalSystem/Management/FakePrincipalManagementService.cs b/src/SecuritySystem.Abstractions/ExternalSystem/Management/FakePrincipalManagementService.cs index 1a7a2cd..754ca0d 100644 --- a/src/SecuritySystem.Abstractions/ExternalSystem/Management/FakePrincipalManagementService.cs +++ b/src/SecuritySystem.Abstractions/ExternalSystem/Management/FakePrincipalManagementService.cs @@ -8,7 +8,7 @@ public class FakePrincipalManagementService : IPrincipalManagementService { public Type PrincipalType => throw new InvalidOperationException(); - public Task CreatePrincipalAsync(string principalName, IEnumerable typedPermissions, CancellationToken cancellationToken = default) + public Task CreatePrincipalAsync(string principalName, IEnumerable managedPermissions, CancellationToken cancellationToken = default) { throw new InvalidOperationException(); } @@ -23,7 +23,7 @@ public Task RemovePrincipalAsync(UserCredential userCredential, b throw new InvalidOperationException(); } - public Task> UpdatePermissionsAsync(UserCredential userCredential, IEnumerable typedPermissions, CancellationToken cancellationToken) + public Task> UpdatePermissionsAsync(UserCredential userCredential, IEnumerable managedPermissions, CancellationToken cancellationToken) { throw new InvalidOperationException(); } diff --git a/src/SecuritySystem.Abstractions/ExternalSystem/Management/IPrincipalManagementService.cs b/src/SecuritySystem.Abstractions/ExternalSystem/Management/IPrincipalManagementService.cs index e0032e4..73df437 100644 --- a/src/SecuritySystem.Abstractions/ExternalSystem/Management/IPrincipalManagementService.cs +++ b/src/SecuritySystem.Abstractions/ExternalSystem/Management/IPrincipalManagementService.cs @@ -8,12 +8,12 @@ public interface IPrincipalManagementService { Type PrincipalType { get; } - Task CreatePrincipalAsync(string principalName, IEnumerable typedPermissions, CancellationToken cancellationToken = default); + Task CreatePrincipalAsync(string principalName, IEnumerable managedPermissions, CancellationToken cancellationToken = default); Task UpdatePrincipalNameAsync(UserCredential userCredential, string principalName, CancellationToken cancellationToken); Task RemovePrincipalAsync(UserCredential userCredential, bool force, CancellationToken cancellationToken = default); - Task> UpdatePermissionsAsync(UserCredential userCredential, IEnumerable typedPermissions, + Task> UpdatePermissionsAsync(UserCredential userCredential, IEnumerable managedPermissions, CancellationToken cancellationToken = default); } \ No newline at end of file diff --git a/src/SecuritySystem.Abstractions/ExternalSystem/Management/ManagedPermission.cs b/src/SecuritySystem.Abstractions/ExternalSystem/Management/ManagedPermission.cs index 497f2a7..37dad88 100644 --- a/src/SecuritySystem.Abstractions/ExternalSystem/Management/ManagedPermission.cs +++ b/src/SecuritySystem.Abstractions/ExternalSystem/Management/ManagedPermission.cs @@ -1,8 +1,36 @@ -namespace SecuritySystem.ExternalSystem.Management; +using System.Collections.Immutable; -public record ManagedPermission : ManagedPermissionData +namespace SecuritySystem.ExternalSystem.Management; + +public record ManagedPermission { public required SecurityIdentity Identity { get; init; } - public required bool IsVirtual { get; init; } + public bool ForceApplyIdentity { get; init; } + + public bool IsVirtual { get; init; } + + public required SecurityRole SecurityRole { get; init; } + + public PermissionPeriod Period { get; init; } = PermissionPeriod.Eternity; + + public string Comment { get; init; } = ""; + + public SecurityIdentity DelegatedFrom { get; init; } = SecurityIdentity.Default; + + public ImmutableDictionary Restrictions { get; init; } = []; + + public ImmutableDictionary ExtendedData { get; init; } = []; + + + public static implicit operator ManagedPermission(SecurityRole securityRole) => new() { SecurityRole = securityRole, Identity = SecurityIdentity.Default }; + + public ManagedPermission WithExtendedData(string key, object value) + { + var newExtendedData = this.ExtendedData.ToDictionary(); + + newExtendedData[key] = value; + + return this with { ExtendedData = newExtendedData.ToImmutableDictionary() }; + } } \ No newline at end of file diff --git a/src/SecuritySystem.Abstractions/ExternalSystem/Management/ManagedPermissionData.cs b/src/SecuritySystem.Abstractions/ExternalSystem/Management/ManagedPermissionData.cs deleted file mode 100644 index 836aaba..0000000 --- a/src/SecuritySystem.Abstractions/ExternalSystem/Management/ManagedPermissionData.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System.Collections.Immutable; - -namespace SecuritySystem.ExternalSystem.Management; - -public record ManagedPermissionData -{ - public required SecurityRole SecurityRole { get; init; } - - public PermissionPeriod Period { get; init; } = PermissionPeriod.Eternity; - - public string Comment { get; init; } = ""; - - public ImmutableDictionary Restrictions { get; init; } = []; - - public ImmutableDictionary ExtendedData { get; init; } = []; - - public static implicit operator ManagedPermissionData(SecurityRole securityRole) => new() { SecurityRole = securityRole }; -} \ No newline at end of file diff --git a/src/SecuritySystem.Abstractions/ExternalSystem/Management/ManagedPermissionDataExtensions.cs b/src/SecuritySystem.Abstractions/ExternalSystem/Management/ManagedPermissionDataExtensions.cs deleted file mode 100644 index af635d5..0000000 --- a/src/SecuritySystem.Abstractions/ExternalSystem/Management/ManagedPermissionDataExtensions.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System.Collections.Immutable; - -namespace SecuritySystem.ExternalSystem.Management; - -public static class ManagedPermissionDataExtensions -{ - public static TManagedPermissionData WithExtendedData(this TManagedPermissionData managedPermissionData, string key, object value) - where TManagedPermissionData : ManagedPermissionData - { - var newExtendedData = managedPermissionData.ExtendedData.ToDictionary(); - - newExtendedData[key] = value; - - return managedPermissionData with { ExtendedData = newExtendedData.ToImmutableDictionary() }; - } -} \ No newline at end of file diff --git a/src/SecuritySystem.Abstractions/ExternalSystem/Management/ManagedPrincipal.cs b/src/SecuritySystem.Abstractions/ExternalSystem/Management/ManagedPrincipal.cs index 0266e8d..d6ffd4f 100644 --- a/src/SecuritySystem.Abstractions/ExternalSystem/Management/ManagedPrincipal.cs +++ b/src/SecuritySystem.Abstractions/ExternalSystem/Management/ManagedPrincipal.cs @@ -1,3 +1,5 @@ -namespace SecuritySystem.ExternalSystem.Management; +using System.Collections.Immutable; -public record ManagedPrincipal(ManagedPrincipalHeader Header, IReadOnlyList Permissions); +namespace SecuritySystem.ExternalSystem.Management; + +public record ManagedPrincipal(ManagedPrincipalHeader Header, ImmutableArray Permissions); diff --git a/src/SecuritySystem.Abstractions/ExternalSystem/Management/PermissionData.cs b/src/SecuritySystem.Abstractions/ExternalSystem/Management/PermissionData.cs index d58d9bb..8abd732 100644 --- a/src/SecuritySystem.Abstractions/ExternalSystem/Management/PermissionData.cs +++ b/src/SecuritySystem.Abstractions/ExternalSystem/Management/PermissionData.cs @@ -1,20 +1,22 @@ -namespace SecuritySystem.ExternalSystem.Management; +using System.Collections.Immutable; + +namespace SecuritySystem.ExternalSystem.Management; public abstract record PermissionData { - public abstract Type PermissionTypeType { get; } + public abstract Type PermissionType { get; } } public abstract record PermissionData(TPermission Permission) : PermissionData { - public override Type PermissionTypeType { get; } = typeof(TPermission); + public override Type PermissionType { get; } = typeof(TPermission); } -public record PermissionData(TPermission Permission, IReadOnlyList Restrictions) +public record PermissionData(TPermission Permission, ImmutableArray Restrictions) : PermissionData(Permission) { public PermissionData(TPermission permission, IEnumerable restrictions) - : this(permission, restrictions.ToList()) + : this(permission, [..restrictions]) { } } \ No newline at end of file diff --git a/src/SecuritySystem.Abstractions/ExternalSystem/Management/PrincipalData.cs b/src/SecuritySystem.Abstractions/ExternalSystem/Management/PrincipalData.cs index ab8b7af..efb8a4e 100644 --- a/src/SecuritySystem.Abstractions/ExternalSystem/Management/PrincipalData.cs +++ b/src/SecuritySystem.Abstractions/ExternalSystem/Management/PrincipalData.cs @@ -1,4 +1,6 @@ -namespace SecuritySystem.ExternalSystem.Management; +using System.Collections.Immutable; + +namespace SecuritySystem.ExternalSystem.Management; public abstract record PrincipalData { @@ -11,10 +13,10 @@ public abstract record PrincipalData public record PrincipalData( TPrincipal Principal, - IReadOnlyList> PermissionDataList) : PrincipalData(Principal) + ImmutableArray> PermissionDataList) : PrincipalData(Principal) { public PrincipalData(TPrincipal principal, IEnumerable> permissionDataList) - : this(principal, permissionDataList.ToList()) + : this(principal, [..permissionDataList]) { } diff --git a/src/SecuritySystem.Abstractions/PermissionPeriod.cs b/src/SecuritySystem.Abstractions/PermissionPeriod.cs index 61d2d59..a65557e 100644 --- a/src/SecuritySystem.Abstractions/PermissionPeriod.cs +++ b/src/SecuritySystem.Abstractions/PermissionPeriod.cs @@ -13,5 +13,16 @@ public bool IsIntersected(PermissionPeriod otherPeriod) return start1 <= end2 && start2 <= end1; } + public bool Contains(PermissionPeriod otherPeriod) + { + var start1 = this.StartDate ?? DateTime.MinValue; + var end1 = this.EndDate ?? DateTime.MaxValue; + + var start2 = otherPeriod.StartDate ?? DateTime.MinValue; + var end2 = otherPeriod.EndDate ?? DateTime.MaxValue; + + return start1 <= start2 && end2 <= end1; + } + public static PermissionPeriod Eternity { get; } = new (null, null); } \ No newline at end of file diff --git a/src/SecuritySystem.Abstractions/SecurityAccessor/SecurityAccessorData.cs b/src/SecuritySystem.Abstractions/SecurityAccessor/SecurityAccessorData.cs index edd5b6a..815d684 100644 --- a/src/SecuritySystem.Abstractions/SecurityAccessor/SecurityAccessorData.cs +++ b/src/SecuritySystem.Abstractions/SecurityAccessor/SecurityAccessorData.cs @@ -1,4 +1,6 @@ -namespace SecuritySystem.SecurityAccessor; +using System.Collections.Immutable; + +namespace SecuritySystem.SecurityAccessor; public abstract record SecurityAccessorData { @@ -6,13 +8,13 @@ public abstract record SecurityAccessorData public static SecurityAccessorData Empty { get; } = Return(); - public static SecurityAccessorData Return(params string[] items) => new FixedSecurityAccessorData(items); + public static SecurityAccessorData Return(params string[] items) => new FixedSecurityAccessorData([..items]); public static SecurityAccessorData Return(IEnumerable items) => Return(items.ToArray()); public static SecurityAccessorData TryReturn(string? item) => string.IsNullOrWhiteSpace(item) ? Empty : Return(item); - public record FixedSecurityAccessorData(IReadOnlyList Items) : SecurityAccessorData; + public record FixedSecurityAccessorData(ImmutableArray Items) : SecurityAccessorData; public record InfinitySecurityAccessorData : SecurityAccessorData; diff --git a/src/SecuritySystem.Abstractions/SecurityContextInfo/ISecurityContextInfoSource.cs b/src/SecuritySystem.Abstractions/SecurityContextInfo/ISecurityContextInfoSource.cs index cc37f7b..6a0510d 100644 --- a/src/SecuritySystem.Abstractions/SecurityContextInfo/ISecurityContextInfoSource.cs +++ b/src/SecuritySystem.Abstractions/SecurityContextInfo/ISecurityContextInfoSource.cs @@ -1,9 +1,11 @@ -// ReSharper disable once CheckNamespace +using System.Collections.Immutable; + +// ReSharper disable once CheckNamespace namespace SecuritySystem; public interface ISecurityContextInfoSource { - IReadOnlyList SecurityContextInfoList { get; } + ImmutableArray SecurityContextInfoList { get; } SecurityContextInfo GetSecurityContextInfo(Type type); diff --git a/src/SecuritySystem.Abstractions/SecurityRole/ISecurityRoleSource.cs b/src/SecuritySystem.Abstractions/SecurityRole/ISecurityRoleSource.cs index 4e3cbb0..c76e452 100644 --- a/src/SecuritySystem.Abstractions/SecurityRole/ISecurityRoleSource.cs +++ b/src/SecuritySystem.Abstractions/SecurityRole/ISecurityRoleSource.cs @@ -1,9 +1,11 @@ -// ReSharper disable once CheckNamespace +using System.Collections.Immutable; + +// ReSharper disable once CheckNamespace namespace SecuritySystem; public interface ISecurityRoleSource { - IReadOnlyList SecurityRoles { get; } + ImmutableArray SecurityRoles { get; } FullSecurityRole GetSecurityRole(SecurityRole securityRole); diff --git a/src/SecuritySystem.Abstractions/SecurityRole/SecurityRoleInfo.cs b/src/SecuritySystem.Abstractions/SecurityRole/SecurityRoleInfo.cs index 7a0530c..60b6c60 100644 --- a/src/SecuritySystem.Abstractions/SecurityRole/SecurityRoleInfo.cs +++ b/src/SecuritySystem.Abstractions/SecurityRole/SecurityRoleInfo.cs @@ -1,4 +1,5 @@ -using HierarchicalExpand; +using System.Collections.Immutable; +using HierarchicalExpand; // ReSharper disable once CheckNamespace namespace SecuritySystem; @@ -9,9 +10,9 @@ public record SecurityRoleInfo(TypedSecurityIdentity Identity) public SecurityPathRestriction Restriction { get; init; } = SecurityPathRestriction.Default; - public IReadOnlyList Operations { get; init; } = []; + public ImmutableArray Operations { get; init; } = []; - public IReadOnlyList Children { get; init; } = []; + public ImmutableArray Children { get; init; } = []; public string? Description { get; init; } diff --git a/src/SecuritySystem.Abstractions/SecurityRule/SecurityRuleExtensions.cs b/src/SecuritySystem.Abstractions/SecurityRule/SecurityRuleExtensions.cs index d3e95e2..6e93d80 100644 --- a/src/SecuritySystem.Abstractions/SecurityRule/SecurityRuleExtensions.cs +++ b/src/SecuritySystem.Abstractions/SecurityRule/SecurityRuleExtensions.cs @@ -161,6 +161,11 @@ public TSecurityRule TryApply(HierarchicalExpandType? customExpandType) => customExpandType == null || securityRule.CustomExpandType != null ? securityRule : securityRule with { CustomExpandType = customExpandType }; + + public TSecurityRule WithDefaultCustoms() => + securityRule.CustomCredential == null && securityRule.CustomExpandType == null && securityRule.CustomRestriction == null + ? securityRule + : securityRule with { CustomCredential = null, CustomExpandType = null, CustomRestriction = null }; } @@ -178,7 +183,7 @@ public TSecurityRule ForceApply(SecurityRuleCredential? customCredential) => : securityRule with { CustomCredential = customCredential }; public TSecurityRule WithDefaultCredential() => - securityRule with { CustomCredential = null }; + securityRule.CustomCredential == null ? securityRule : securityRule with { CustomCredential = null }; public TResult WithDefaultCredential(Func selector) where TResult : SecurityRule => diff --git a/src/SecuritySystem.Abstractions/Services/ISecurityRolesIdentsResolver.cs b/src/SecuritySystem.Abstractions/Services/ISecurityRolesIdentsResolver.cs index 8822fcd..f5b7cbd 100644 --- a/src/SecuritySystem.Abstractions/Services/ISecurityRolesIdentsResolver.cs +++ b/src/SecuritySystem.Abstractions/Services/ISecurityRolesIdentsResolver.cs @@ -1,6 +1,8 @@ -namespace SecuritySystem.Services; +using System.Collections.Immutable; + +namespace SecuritySystem.Services; public interface ISecurityRolesIdentsResolver { - IReadOnlyDictionary Resolve(DomainSecurityRule.RoleBaseSecurityRule securityRule, bool includeVirtual = false); + ImmutableDictionary Resolve(DomainSecurityRule.RoleBaseSecurityRule securityRule, bool includeVirtual = false); } \ No newline at end of file diff --git a/src/SecuritySystem.Configurator/Handlers/GetPrincipalHandler.cs b/src/SecuritySystem.Configurator/Handlers/GetPrincipalHandler.cs index f902942..24a50a2 100644 --- a/src/SecuritySystem.Configurator/Handlers/GetPrincipalHandler.cs +++ b/src/SecuritySystem.Configurator/Handlers/GetPrincipalHandler.cs @@ -44,7 +44,8 @@ private async Task> GetPermissionsAsync(UserCredential userC Role = permission.SecurityRole.Name, RoleId = securityRoleSource.GetSecurityRole(permission.SecurityRole).Identity.GetId().ToString()!, Comment = permission.Comment, - StartDate = permission.Period.StartDate, + DelegatedFromId = permission.DelegatedFrom.GetId().ToString()!, + StartDate = permission.Period.StartDate, EndDate = permission.Period.EndDate, Contexts = permission .Restrictions diff --git a/src/SecuritySystem.Configurator/Handlers/UpdatePermissionsHandler.cs b/src/SecuritySystem.Configurator/Handlers/UpdatePermissionsHandler.cs index de4bdeb..ab91e5f 100644 --- a/src/SecuritySystem.Configurator/Handlers/UpdatePermissionsHandler.cs +++ b/src/SecuritySystem.Configurator/Handlers/UpdatePermissionsHandler.cs @@ -24,9 +24,9 @@ public async Task Execute(HttpContext context, CancellationToken cancellationTok var permissions = await this.ParseRequestBodyAsync>(context); - var typedPermissions = permissions.Select(this.ToManagedPermission).ToList(); + var managedPermissions = permissions.Select(this.ToManagedPermission).ToList(); - var mergeResult = await principalManagementService.UpdatePermissionsAsync(context.ExtractSecurityIdentity(), typedPermissions, cancellationToken); + var mergeResult = await principalManagementService.UpdatePermissionsAsync(context.ExtractSecurityIdentity(), managedPermissions, cancellationToken); if (configuratorIntegrationEvents != null) { @@ -66,6 +66,7 @@ from restriction in permission.Contexts SecurityRole = securityRoleSource.GetSecurityRole(new UntypedSecurityIdentity(permission.RoleId)), Period = new PermissionPeriod(permission.StartDate, permission.EndDate), Comment = permission.Comment, + DelegatedFrom = new UntypedSecurityIdentity(permission.DelegatedFromId), Restrictions = restrictionsRequest.ToImmutableDictionary() }; } @@ -86,6 +87,8 @@ private class RequestBodyDto public List Contexts { get; set; } = default!; + public string DelegatedFromId { get; set; } = default!; + public class ContextDto { public string Id { get; set; } = default!; diff --git a/src/SecuritySystem.Configurator/Models/PermissionDto.cs b/src/SecuritySystem.Configurator/Models/PermissionDto.cs index e92f370..8cd4734 100644 --- a/src/SecuritySystem.Configurator/Models/PermissionDto.cs +++ b/src/SecuritySystem.Configurator/Models/PermissionDto.cs @@ -10,6 +10,8 @@ public record PermissionDto public required string Comment { get; init; } + public required string DelegatedFromId { get; init; } + public required IReadOnlyList Contexts { get; init; } public required DateTime? StartDate { get; init; } diff --git a/src/SecuritySystem.GeneralPermission.Runtime/GeneralPrincipalManagementService.cs b/src/SecuritySystem.GeneralPermission.Runtime/GeneralPrincipalManagementService.cs index b8b0b16..3643c9f 100644 --- a/src/SecuritySystem.GeneralPermission.Runtime/GeneralPrincipalManagementService.cs +++ b/src/SecuritySystem.GeneralPermission.Runtime/GeneralPrincipalManagementService.cs @@ -40,8 +40,8 @@ public class GeneralPrincipalManagementService( public Type PrincipalType => this.InnerService.PrincipalType; - public Task CreatePrincipalAsync(string principalName, IEnumerable typedPermissions, CancellationToken cancellationToken = default) => - this.InnerService.CreatePrincipalAsync(principalName, typedPermissions, cancellationToken); + public Task CreatePrincipalAsync(string principalName, IEnumerable managedPermissions, CancellationToken cancellationToken = default) => + this.InnerService.CreatePrincipalAsync(principalName, managedPermissions, cancellationToken); public Task UpdatePrincipalNameAsync(UserCredential userCredential, string principalName, CancellationToken cancellationToken) => this.InnerService.UpdatePrincipalNameAsync(userCredential, principalName, cancellationToken); @@ -50,8 +50,8 @@ public Task RemovePrincipalAsync(UserCredential userCredential, b this.InnerService.RemovePrincipalAsync(userCredential, force, cancellationToken); public Task> UpdatePermissionsAsync(UserCredential userCredential, - IEnumerable typedPermissions, CancellationToken cancellationToken = default) => - this.InnerService.UpdatePermissionsAsync(userCredential, typedPermissions, cancellationToken); + IEnumerable managedPermissions, CancellationToken cancellationToken = default) => + this.InnerService.UpdatePermissionsAsync(userCredential, managedPermissions, cancellationToken); } public class GeneralPrincipalManagementService( @@ -66,20 +66,20 @@ public class GeneralPrincipalManagementService principalVisualIdentityInfo) : IPrincipalManagementService - where TPrincipal : class, new() - where TPermission : class, new() - where TPermissionRestriction : class, new() + where TPrincipal : class + where TPermission : class + where TPermissionRestriction : class { public Type PrincipalType { get; } = typeof(TPrincipal); public async Task CreatePrincipalAsync( string principalName, - IEnumerable typedPermissions, + IEnumerable managedPermissions, CancellationToken cancellationToken) { var principal = await principalDomainService.GetOrCreateAsync(principalName, cancellationToken); - var result = await this.UpdatePermissionsAsync(principal, [], typedPermissions, cancellationToken); + var result = await this.UpdatePermissionsAsync(principal, [], managedPermissions, cancellationToken); return new PrincipalData(principal, result.AddingItems.Cast>()); @@ -115,38 +115,30 @@ private async Task await this.ToPermissionData(dbPermission, cancellationToken)); + var permissionsData = await dbPermissions.SyncWhenAll(async dbPermission => await permissionRestrictionLoader.ToPermissionData(dbPermission, cancellationToken)); return new PrincipalData(dbPrincipal, permissionsData); } - private async Task> ToPermissionData(TPermission dbPermission, - CancellationToken cancellationToken) - { - var dbRestrictions = await permissionRestrictionLoader.LoadAsync(dbPermission, cancellationToken); - - return new PermissionData(dbPermission, dbRestrictions); - } - public async Task> UpdatePermissionsAsync( UserCredential userCredential, - IEnumerable typedPermissions, + IEnumerable managedPermissions, CancellationToken cancellationToken) { var dbPrincipal = await principalUserSource.GetUserAsync(userCredential, cancellationToken); var dbPermissions = await permissionLoader.LoadAsync(dbPrincipal, cancellationToken); - return await this.UpdatePermissionsAsync(dbPrincipal, dbPermissions, typedPermissions, cancellationToken); + return await this.UpdatePermissionsAsync(dbPrincipal, dbPermissions, managedPermissions, cancellationToken); } private async Task> UpdatePermissionsAsync( TPrincipal dbPrincipal, List dbPermissions, - IEnumerable typedPermissions, + IEnumerable managedPermissions, CancellationToken cancellationToken) { - var permissionMergeResult = dbPermissions.GetMergeResult(typedPermissions, permissionIdentityExtractor.Extract, + var permissionMergeResult = dbPermissions.GetMergeResult(managedPermissions, permissionIdentityExtractor.Extract, p => p.Identity.IsDefault ? new object() : permissionIdentityExtractor.Converter.Convert(p.Identity)); var newPermissions = await this.CreatePermissionsAsync(dbPrincipal, permissionMergeResult.AddingItems, cancellationToken); @@ -155,7 +147,7 @@ private async Task> UpdatePermission var removingPermissions = await permissionMergeResult.RemovingItems.SyncWhenAll(async oldDbPermission => { - var result = await this.ToPermissionData(oldDbPermission, cancellationToken); + var result = await permissionRestrictionLoader.ToPermissionData(oldDbPermission, cancellationToken); foreach (var dbRestriction in result.Restrictions) { @@ -180,10 +172,10 @@ await principalValidator.ValidateAsync( private async Task[]> CreatePermissionsAsync( TPrincipal dbPrincipal, - IEnumerable typedPermissions, + IEnumerable managedPermissions, CancellationToken cancellationToken) { - return await typedPermissions.SyncWhenAll(managedPermission => permissionManagementService.CreatePermissionAsync(dbPrincipal, managedPermission, cancellationToken)); + return await managedPermissions.SyncWhenAll(managedPermission => permissionManagementService.CreatePermissionAsync(dbPrincipal, managedPermission, cancellationToken)); } private async Task<(PermissionData PermissonData, bool Updated)[]> UpdatePermissionsAsync( diff --git a/src/SecuritySystem.GeneralPermission.Runtime/IPermissionRestrictionLoader.cs b/src/SecuritySystem.GeneralPermission.Runtime/IPermissionRestrictionLoader.cs index fc3e7d4..ac22bb3 100644 --- a/src/SecuritySystem.GeneralPermission.Runtime/IPermissionRestrictionLoader.cs +++ b/src/SecuritySystem.GeneralPermission.Runtime/IPermissionRestrictionLoader.cs @@ -1,6 +1,15 @@ -namespace SecuritySystem.GeneralPermission; +using SecuritySystem.ExternalSystem.Management; -public interface IPermissionRestrictionLoader +namespace SecuritySystem.GeneralPermission; + +public interface IPermissionRestrictionLoader { Task> LoadAsync(TPermission permission, CancellationToken cancellationToken); + + async Task> ToPermissionData(TPermission dbPermission, CancellationToken cancellationToken) + { + var dbRestrictions = await this.LoadAsync(dbPermission, cancellationToken); + + return new PermissionData(dbPermission, dbRestrictions); + } } \ No newline at end of file diff --git a/src/SecuritySystem.GeneralPermission.Runtime/ManagedPermissionMapper.cs b/src/SecuritySystem.GeneralPermission.Runtime/ManagedPermissionMapper.cs index 2164c79..af159d2 100644 --- a/src/SecuritySystem.GeneralPermission.Runtime/ManagedPermissionMapper.cs +++ b/src/SecuritySystem.GeneralPermission.Runtime/ManagedPermissionMapper.cs @@ -7,11 +7,13 @@ using SecuritySystem.Services; using System.Collections.Immutable; +using CommonFramework.IdentitySource; namespace SecuritySystem.GeneralPermission; public class PermissionManagementService( IServiceProxyFactory serviceProxyFactory, + IIdentityInfoSource identityInfoSource, IPermissionBindingInfoSource bindingInfoSource, IGeneralPermissionBindingInfoSource generalBindingInfoSource, IGeneralPermissionRestrictionBindingInfoSource restrictionBindingInfoSource) @@ -25,20 +27,24 @@ public class PermissionManagementService) + var permissionIdentityInfo = identityInfoSource.GetIdentityInfo(bindingInfo.PermissionType); + + var innerServiceType = typeof(PermissionManagementService<,,,,,,>) .MakeGenericType( bindingInfo.PrincipalType, bindingInfo.PermissionType, generalBindingInfo.SecurityRoleType, restrictionBindingInfo.PermissionRestrictionType, restrictionBindingInfo.SecurityContextTypeType, - restrictionBindingInfo.SecurityContextObjectIdentType); + restrictionBindingInfo.SecurityContextObjectIdentType, + permissionIdentityInfo.IdentityType); return serviceProxyFactory.Create>( innerServiceType, bindingInfo, generalBindingInfo, - restrictionBindingInfo); + restrictionBindingInfo, + permissionIdentityInfo); }); private IPermissionManagementService InnerService => this.lazyInnerService.Value; @@ -54,11 +60,11 @@ public virtual Task> CreateP this.InnerService.UpdatePermission(dbPermission, managedPermission, cancellationToken); } -public class PermissionManagementService( +public class PermissionManagementService( PermissionBindingInfo bindingInfo, GeneralPermissionBindingInfo generalBindingInfo, GeneralPermissionRestrictionBindingInfo restrictionBindingInfo, - + IdentityInfo permissionIdentityInfo, IPermissionSecurityRoleResolver permissionSecurityRoleResolver, IRawPermissionRestrictionLoader rawPermissionRestrictionLoader, ISecurityIdentityExtractor permissionSecurityIdentityExtractor, @@ -69,7 +75,10 @@ public class PermissionManagementService securityRoleIdentityExtractor, ISecurityIdentityExtractor securityContextTypeIdentityExtractor, - IGenericRepository genericRepository) + ISecurityIdentityExtractor permissionIdentityExtractor, + ISecurityIdentityConverter permissionIdentityConverter, + IGenericRepository genericRepository, + ISecurityRepository permissionRepository) : IPermissionManagementService where TPermission : class, new() @@ -77,7 +86,7 @@ public class PermissionManagementService ToManagedPermissionAsync(TPermission dbPermission, CancellationToken cancellationToken) => new() @@ -87,6 +96,9 @@ public async Task ToManagedPermissionAsync(TPermission dbPerm SecurityRole = permissionSecurityRoleResolver.Resolve(dbPermission), Period = bindingInfo.GetSafePeriod(dbPermission), Comment = bindingInfo.GetSafeComment(dbPermission), + DelegatedFrom = bindingInfo.DelegatedFrom?.Getter.Invoke(dbPermission) is { } delegatedFromPermission + ? permissionIdentityExtractor.Extract(delegatedFromPermission) + : SecurityIdentity.Default, Restrictions = (await rawPermissionRestrictionLoader.LoadAsync(dbPermission, cancellationToken)).ToImmutableDictionary() }; @@ -95,9 +107,9 @@ public async Task> CreatePer ManagedPermission managedPermission, CancellationToken cancellationToken) { - if (!managedPermission.Identity.IsDefault || managedPermission.IsVirtual) + if (managedPermission.IsVirtual || (!managedPermission.Identity.IsDefault && !managedPermission.ForceApplyIdentity)) { - throw new SecuritySystemException("wrong typed permission"); + throw new SecuritySystemException("wrong permission"); } var securityRole = securityRoleSource.GetSecurityRole(managedPermission.SecurityRole); @@ -106,6 +118,13 @@ public async Task> CreatePer var newDbPermission = new TPermission(); + if (!managedPermission.Identity.IsDefault) + { + var permissionIdentity = permissionIdentityConverter.Convert(managedPermission.Identity); + + permissionIdentityInfo.Id.Setter.Invoke(newDbPermission, permissionIdentity.Id); + } + bindingInfo.Principal.Setter(newDbPermission, dbPrincipal); generalBindingInfo.SecurityRole.Setter(newDbPermission, dbRole); @@ -113,6 +132,15 @@ public async Task> CreatePer bindingInfo.PermissionEndDate?.Setter(newDbPermission, managedPermission.Period.EndDate); bindingInfo.PermissionComment?.Setter(newDbPermission, managedPermission.Comment); + if (!managedPermission.DelegatedFrom.IsDefault) + { + var delegatedFromAccessors = bindingInfo.DelegatedFrom ?? throw new InvalidOperationException("Delegated Permission Binding not initialized"); + + var delegatedFromPermission = await permissionRepository.GetObjectAsync(managedPermission.DelegatedFrom, cancellationToken); + + delegatedFromAccessors.Setter(newDbPermission, delegatedFromPermission); + } + await genericRepository.SaveAsync(newDbPermission, cancellationToken); var newPermissionRestrictions = await managedPermission.Restrictions.SyncWhenAll(async restrictionGroup => @@ -145,9 +173,21 @@ public async Task> CreatePer ManagedPermission managedPermission, CancellationToken cancellationToken) { - if (managedPermission.Identity.IsDefault || managedPermission.IsVirtual) + if (managedPermission.IsVirtual || managedPermission.Identity.IsDefault) + { + throw new SecuritySystemException("wrong permission"); + } + + if (!managedPermission.DelegatedFrom.IsDefault) { - throw new SecuritySystemException("wrong typed permission"); + var delegatedFromAccessors = bindingInfo.DelegatedFrom ?? throw new InvalidOperationException("Delegated Permission Binding not initialized"); + + var delegatedFromPermission = await permissionRepository.GetObjectAsync(managedPermission.DelegatedFrom, cancellationToken); + + if (delegatedFromPermission != delegatedFromAccessors.Getter(dbPermission)) + { + throw new InvalidOperationException("Delegated source can't be changed"); + } } var securityRole = generalBindingInfo diff --git a/src/SecuritySystem.GeneralPermission.Runtime/ManagedPrincipalConverter.cs b/src/SecuritySystem.GeneralPermission.Runtime/ManagedPrincipalConverter.cs index 2c02e59..712027c 100644 --- a/src/SecuritySystem.GeneralPermission.Runtime/ManagedPrincipalConverter.cs +++ b/src/SecuritySystem.GeneralPermission.Runtime/ManagedPrincipalConverter.cs @@ -36,12 +36,12 @@ public class ManagedPrincipalConverter ToManagedPrincipalAsync(TPrincipal principal, CancellationToken cancellationToken) + public async Task ToManagedPrincipalAsync(TPrincipal dbPrincipal, CancellationToken cancellationToken) { - var permissions = await permissionLoader.LoadAsync(principal, cancellationToken); + var dbPermissions = await permissionLoader.LoadAsync(dbPrincipal, cancellationToken); - return new ManagedPrincipal( - headerConverter.Convert(principal), - await permissions.SyncWhenAll(permission => permissionManagementService.ToManagedPermissionAsync(permission, cancellationToken))); + var permissions = await dbPermissions.SyncWhenAll(permission => permissionManagementService.ToManagedPermissionAsync(permission, cancellationToken)); + + return new ManagedPrincipal(headerConverter.Convert(dbPrincipal), [..permissions]); } } \ No newline at end of file diff --git a/src/SecuritySystem.GeneralPermission.Runtime/PermissionRestrictionTypeFilterFactory.cs b/src/SecuritySystem.GeneralPermission.Runtime/PermissionRestrictionTypeFilterFactory.cs index 2af83fa..9f6e89b 100644 --- a/src/SecuritySystem.GeneralPermission.Runtime/PermissionRestrictionTypeFilterFactory.cs +++ b/src/SecuritySystem.GeneralPermission.Runtime/PermissionRestrictionTypeFilterFactory.cs @@ -46,7 +46,7 @@ public class PermissionRestrictionTypeFilterFactory cache = new(); + private readonly ConcurrentDictionary cache = []; public Expression> CreateFilter() where TSecurityContext : class, ISecurityContext diff --git a/src/SecuritySystem.GeneralPermission.Runtime/RawPermissionConverter.cs b/src/SecuritySystem.GeneralPermission.Runtime/RawPermissionConverter.cs index e1ab73c..5142721 100644 --- a/src/SecuritySystem.GeneralPermission.Runtime/RawPermissionConverter.cs +++ b/src/SecuritySystem.GeneralPermission.Runtime/RawPermissionConverter.cs @@ -62,7 +62,7 @@ public Dictionary ConvertPermission(DomainSecurityRule.RoleBaseSecu private TSecurityContextObjectIdent[] ApplySecurityContextFilter(Array securityContextIdents, SecurityContextRestrictionFilterInfo restrictionFilterInfo) { - return new Func, IReadOnlyList>( + return new Func, TSecurityContextObjectIdent[]>( this.ApplySecurityContextFilter) .CreateGenericMethod(restrictionFilterInfo.SecurityContextType) .Invoke(this, securityContextIdents, restrictionFilterInfo); diff --git a/src/SecuritySystem.GeneralPermission.Runtime/Validation/DisplayPermissionService.cs b/src/SecuritySystem.GeneralPermission.Runtime/Validation/DisplayPermissionService.cs index 2512d24..aae546b 100644 --- a/src/SecuritySystem.GeneralPermission.Runtime/Validation/DisplayPermissionService.cs +++ b/src/SecuritySystem.GeneralPermission.Runtime/Validation/DisplayPermissionService.cs @@ -32,11 +32,9 @@ public class DisplayPermissionService( generalBindingInfo); }); - - public string ToString(PermissionData permissionData) => this.lazyInnerService.Value.ToString(permissionData); + public string Format(PermissionData permissionData) => this.lazyInnerService.Value.Format(permissionData); } - public class DisplayPermissionService( PermissionBindingInfo bindingInfo, GeneralPermissionBindingInfo generalBindingInfo, @@ -47,7 +45,7 @@ public class DisplayPermissionService where TSecurityRole : class { - public string ToString(PermissionData permissionData) + public string Format(PermissionData permissionData) { return this.GetPermissionVisualParts(permissionData).Join(" | "); } @@ -72,11 +70,11 @@ private IEnumerable GetPermissionVisualParts(PermissionData v.Name).Join(", ")}"; + yield return $"{securityContextInfo.Name}: {securityContextList.Select(v => v.Name).Join(", ")}"; } } } \ No newline at end of file diff --git a/src/SecuritySystem.GeneralPermission.Runtime/Validation/IDisplayPermissionService.cs b/src/SecuritySystem.GeneralPermission.Runtime/Validation/IDisplayPermissionService.cs index 30cee96..bea1795 100644 --- a/src/SecuritySystem.GeneralPermission.Runtime/Validation/IDisplayPermissionService.cs +++ b/src/SecuritySystem.GeneralPermission.Runtime/Validation/IDisplayPermissionService.cs @@ -4,5 +4,5 @@ namespace SecuritySystem.GeneralPermission.Validation; public interface IDisplayPermissionService { - string ToString(PermissionData permissionData); + string Format(PermissionData permissionData); } \ No newline at end of file diff --git a/src/SecuritySystem.GeneralPermission.Runtime/Validation/Permission/PermissionDelegationValidator.cs b/src/SecuritySystem.GeneralPermission.Runtime/Validation/Permission/PermissionDelegationValidator.cs new file mode 100644 index 0000000..5bd4810 --- /dev/null +++ b/src/SecuritySystem.GeneralPermission.Runtime/Validation/Permission/PermissionDelegationValidator.cs @@ -0,0 +1,179 @@ +using CommonFramework; +using CommonFramework.GenericRepository; +using CommonFramework.VisualIdentitySource; + +using GenericQueryable; + +using HierarchicalExpand; + +using SecuritySystem.ExternalSystem.Management; +using SecuritySystem.Services; +using SecuritySystem.Validation; + +namespace SecuritySystem.GeneralPermission.Validation.Permission; + +public class PermissionDelegationValidator( + IServiceProxyFactory serviceProxyFactory, + IPermissionBindingInfoSource bindingInfoSource) + : IPermissionValidator +{ + private readonly Lazy> lazyInnerService = new(() => + { + var bindingInfo = bindingInfoSource.GetForPermission(typeof(TPermission)); + + var innerServiceType = typeof(PermissionDelegationValidator<,,>) + .MakeGenericType( + bindingInfo.PrincipalType, + bindingInfo.PermissionType, + typeof(TPermissionRestriction)); + + return serviceProxyFactory.Create>(innerServiceType, bindingInfo); + }); + + public Task ValidateAsync(PermissionData value, CancellationToken cancellationToken) => + this.lazyInnerService.Value.ValidateAsync(value, cancellationToken); +} + +public class PermissionDelegationValidator( + PermissionBindingInfo permissionBindingInfo, + IPermissionRestrictionRawConverter permissionRestrictionRawConverter, + IDomainObjectDisplayService domainObjectDisplayService, + ISecurityContextInfoSource securityContextInfoSource, + ISecurityRoleSource securityRoleSource, + IPermissionSecurityRoleResolver permissionSecurityRoleResolver, + IQueryableSource queryableSource, + IPermissionRestrictionLoader permissionRestrictionLoader, + IHierarchicalObjectExpanderFactory hierarchicalObjectExpanderFactory) + : IPermissionValidator + + where TPrincipal : class + where TPermission : class +{ + public async Task ValidateAsync(PermissionData permissionData, CancellationToken cancellationToken) + { + if (permissionBindingInfo.DelegatedFrom == null) + { + return; + } + + var permission = permissionData.Permission; + + var delegatedFrom = permissionBindingInfo.DelegatedFrom.Getter(permission); + + if (delegatedFrom != null) + { + if (permissionBindingInfo.Principal.Getter(delegatedFrom) == permissionBindingInfo.Principal.Getter(permission)) + { + throw new SecuritySystemValidationException("Invalid delegation target: the permission cannot be delegated to its original principal"); + } + + var delegatedFromData = await permissionRestrictionLoader.ToPermissionData(delegatedFrom, cancellationToken); + + this.ValidatePermissionDelegatedFrom(permissionData, delegatedFromData); + } + + var subPermissions = await queryableSource + .GetQueryable() + .Where(permissionBindingInfo.DelegatedFrom.Path.Select(p => p == permission)) + .GenericToListAsync(cancellationToken); + + foreach (var subPermission in subPermissions) + { + var subPermissionData = await permissionRestrictionLoader.ToPermissionData(subPermission, cancellationToken); + + this.ValidatePermissionDelegatedFrom(subPermissionData, permissionData); + } + } + + private void ValidatePermissionDelegatedFrom( + PermissionData subPermissionData, + PermissionData delegatedFromData) + { + var subPermission = subPermissionData.Permission; + var delegatedFrom = delegatedFromData.Permission; + + if (!this.IsCorrectRoleSubset(subPermission, delegatedFrom)) + { + throw new SecuritySystemValidationException( + $"Invalid delegated permission role: the selected role \"{permissionSecurityRoleResolver.Resolve(subPermission)}\" is not a subset of \"{permissionSecurityRoleResolver.Resolve(delegatedFrom)}\""); + } + + if (!this.IsCorrectPeriodSubset(subPermission, delegatedFrom)) + { + throw new SecuritySystemValidationException( + $"Invalid delegated permission period: the selected period \"{permissionBindingInfo.GetSafePeriod(subPermission)}\" is not a subset of \"{permissionBindingInfo.GetSafePeriod(delegatedFrom)}\""); + } + + { + var invalidSecurityContextDict = this.GetInvalidSecurityContextDict(subPermissionData, delegatedFromData).ToList(); + + if (invalidSecurityContextDict.Any()) + { + throw new SecuritySystemValidationException( + string.Format( + "Invalid security context delegation: the security contexts of \"{1}\" exceed those granted by \"{0}\": {2}", + domainObjectDisplayService.ToString(permissionBindingInfo.Principal.Getter(delegatedFrom)), + domainObjectDisplayService.ToString(permissionBindingInfo.Principal.Getter(subPermission)), + invalidSecurityContextDict.Join( + " | ", + g => + { + var invalidValues = g.Value.Length == 0 + ? "Unrestricted" + : g.Value.OfType().Join(", "); + + return $"{g.Key.Name}: {invalidValues}"; + }))); + } + } + } + + private bool IsCorrectRoleSubset(TPermission subPermission, TPermission delegatedFrom) => + + permissionSecurityRoleResolver.Resolve(delegatedFrom) + .GetAllElements(role => role.Information.Children.Select(securityRoleSource.GetSecurityRole)) + .Contains(permissionSecurityRoleResolver.Resolve(subPermission)); + + private bool IsCorrectPeriodSubset(TPermission subPermission, TPermission delegatedFrom) + { + return permissionBindingInfo.GetSafePeriod(delegatedFrom).Contains(permissionBindingInfo.GetSafePeriod(subPermission)); + } + + private IEnumerable> GetInvalidSecurityContextDict( + PermissionData subPermissionData, + PermissionData delegatedFromData) + { + var subPermissionRestrictionDict = permissionRestrictionRawConverter.Convert(subPermissionData.Restrictions); + + var delegatedFromRestrictionDict = permissionRestrictionRawConverter.Convert(delegatedFromData.Restrictions); + + foreach (var securityContextInfo in securityContextInfoSource.SecurityContextInfoList) + { + var delegatedFromRestrictions = delegatedFromRestrictionDict.GetValueOrDefault(securityContextInfo.Type); + + if (delegatedFromRestrictions != null) + { + var subRestrictions = subPermissionRestrictionDict.GetValueOrDefault(securityContextInfo.Type); + + if (subRestrictions == null) + { + yield return new(securityContextInfo, Array.Empty()); + } + else + { + var expandedChildren = hierarchicalObjectExpanderFactory + .Create(securityContextInfo.Type) + .Expand(delegatedFromRestrictions, HierarchicalExpandType.Children); + + var missedAccessElements = subRestrictions.Cast().Except(expandedChildren.OfType()).ToArray(); + + if (missedAccessElements.Length > 0) + { + yield return new (securityContextInfo, missedAccessElements); + } + } + } + } + } +} + diff --git a/src/SecuritySystem.GeneralPermission.Runtime/Validation/PermissionDataComparer.cs b/src/SecuritySystem.GeneralPermission.Runtime/Validation/PermissionDataComparer.cs index 416499f..68cb3a2 100644 --- a/src/SecuritySystem.GeneralPermission.Runtime/Validation/PermissionDataComparer.cs +++ b/src/SecuritySystem.GeneralPermission.Runtime/Validation/PermissionDataComparer.cs @@ -120,6 +120,6 @@ orderby securityContextTypeIdentityInfo.Id.Getter(g.Key) public int GetHashCode(PermissionData permissionData) { - return permissionData.Restrictions.Count ^ generalBindingInfo.SecurityRole.Getter(permissionData.Permission).GetHashCode(); + return permissionData.Restrictions.Length ^ generalBindingInfo.SecurityRole.Getter(permissionData.Permission).GetHashCode(); } } \ No newline at end of file diff --git a/src/SecuritySystem.GeneralPermission.Runtime/Validation/Principal/PrincipalUniquePermissionValidator.cs b/src/SecuritySystem.GeneralPermission.Runtime/Validation/Principal/PrincipalUniquePermissionValidator.cs index 238cf58..d80b006 100644 --- a/src/SecuritySystem.GeneralPermission.Runtime/Validation/Principal/PrincipalUniquePermissionValidator.cs +++ b/src/SecuritySystem.GeneralPermission.Runtime/Validation/Principal/PrincipalUniquePermissionValidator.cs @@ -25,7 +25,7 @@ public async Task ValidateAsync(PrincipalData 0) { - var messageBody = duplicates.Join(",", g => $"({displayPermissionService.ToString(g.Key)})"); + var messageBody = duplicates.Join(",", g => $"({displayPermissionService.Format(g.Key)})"); var message = $"Principal \"{principalVisualIdentityInfo.Name.Getter(principalData.Principal)}\" has duplicate permissions: {messageBody}"; diff --git a/src/SecuritySystem.GeneralPermission/DependencyInjection/GeneralPermissionSettings.cs b/src/SecuritySystem.GeneralPermission/DependencyInjection/GeneralPermissionSettings.cs index 4157f8b..dc65a5f 100644 --- a/src/SecuritySystem.GeneralPermission/DependencyInjection/GeneralPermissionSettings.cs +++ b/src/SecuritySystem.GeneralPermission/DependencyInjection/GeneralPermissionSettings.cs @@ -18,6 +18,8 @@ public class GeneralPermissionSettings>? descriptionPath; + private Expression>? delegatedFromPath; + private bool? isReadonly; public Type? PermissionEqualityComparerType { get; private set; } @@ -32,6 +34,7 @@ public TPermissionBindingInfo ApplyOptionalPaths(TPermis .PipeMaybe(this.startDateAccessors, (b, v) => b with { PermissionStartDate = v }) .PipeMaybe(this.endDatedAccessors, (b, v) => b with { PermissionEndDate = v }) .PipeMaybe(this.commentPath, (b, v) => b with { PermissionComment = v.ToPropertyAccessors() }) + .PipeMaybe(this.delegatedFromPath, (b, v) => b with { DelegatedFrom = v.ToPropertyAccessors() }) .PipeMaybe(this.isReadonly, (b, v) => b with { IsReadonly = v }); } @@ -69,6 +72,14 @@ public IGeneralPermissionSettings SetPermissionDelegation( + Expression> newDelegatedFromPath) + { + this.delegatedFromPath = newDelegatedFromPath; + + return this; + } + public IGeneralPermissionSettings SetSecurityRoleDescription( Expression>? newDescriptionPath) { diff --git a/src/SecuritySystem.GeneralPermission/DependencyInjection/IGeneralPermissionSettings.cs b/src/SecuritySystem.GeneralPermission/DependencyInjection/IGeneralPermissionSettings.cs index 7f0f73c..2417807 100644 --- a/src/SecuritySystem.GeneralPermission/DependencyInjection/IGeneralPermissionSettings.cs +++ b/src/SecuritySystem.GeneralPermission/DependencyInjection/IGeneralPermissionSettings.cs @@ -19,6 +19,9 @@ public IGeneralPermissionSettings SetPermissionComment( Expression> commentPath); + IGeneralPermissionSettings SetPermissionDelegation( + Expression> delegatedFromPath); + IGeneralPermissionSettings SetSecurityRoleDescription( Expression>? descriptionPath); diff --git a/src/SecuritySystem.GeneralPermission/DependencyInjection/SecuritySystemSettingsExtensions.cs b/src/SecuritySystem.GeneralPermission/DependencyInjection/SecuritySystemSettingsExtensions.cs index f7289ab..9bd12d2 100644 --- a/src/SecuritySystem.GeneralPermission/DependencyInjection/SecuritySystemSettingsExtensions.cs +++ b/src/SecuritySystem.GeneralPermission/DependencyInjection/SecuritySystemSettingsExtensions.cs @@ -112,6 +112,7 @@ private ISecuritySystemSettings AddGeneralServices() .AddScoped(typeof(IPermissionRestrictionFilterFactory<>), typeof(PermissionRestrictionFilterFactory<>)) .AddScoped(typeof(IRawPermissionConverter<>), typeof(RawPermissionConverter<>)) .AddSingleton(typeof(IPermissionSecurityRoleFilterFactory<>), typeof(PermissionSecurityRoleFilterFactory<>)) + .AddSingleton(typeof(IPermissionSecurityRoleIdentsFilterFactory<>), typeof(PermissionSecurityRoleIdentsFilterFactory<>)) .AddScoped(typeof(IPermissionFilterFactory<>), typeof(PermissionFilterFactory<>)) .AddScoped() .AddScoped(typeof(ISecurityRoleInitializer<>), typeof(SecurityRoleInitializer<>)) @@ -126,6 +127,7 @@ private ISecuritySystemSettings AddGeneralServices() .AddScoped(typeof(IPrincipalValidator<,,>), typeof(PrincipalUniquePermissionValidator<,,>)) .AddKeyedScoped(typeof(IPermissionValidator<,>), "Root", typeof(PermissionRootValidator<,>)) .AddSingleton(typeof(IPermissionValidator<,>), typeof(PermissionRequiredContextValidator<,>)) + .AddScoped(typeof(IPermissionValidator<,>), typeof(PermissionDelegationValidator<,>)) .AddKeyedScoped(typeof(IPermissionRestrictionValidator<>), "Root", typeof(PermissionRestrictionRootValidator<>)) .AddSingleton(typeof(IPermissionRestrictionValidator<>), typeof(AllowedTypePermissionRestrictionValidator<>)) .AddScoped(typeof(IPermissionRestrictionValidator<>), typeof(ExistsPermissionRestrictionValidator<>)) diff --git a/src/SecuritySystem.Runtime/Builders/AccessorsBuilder/ByIdentsFilterBuilder.cs b/src/SecuritySystem.Runtime/Builders/AccessorsBuilder/ByIdentsFilterBuilder.cs index 0b6b769..9cb9633 100644 --- a/src/SecuritySystem.Runtime/Builders/AccessorsBuilder/ByIdentsFilterBuilder.cs +++ b/src/SecuritySystem.Runtime/Builders/AccessorsBuilder/ByIdentsFilterBuilder.cs @@ -25,10 +25,10 @@ public override Expression> GetAccessorsFilter(TDomainOb { var securityObjects = this.GetSecurityObjects(domainObject).ToArray(); - var allowGrandAccess = securityContextRestriction?.Required != true; + var allowsUnrestrictedAccess = securityContextRestriction?.Required != true; - var grandAccessExpr = allowGrandAccess - ? permissionRestrictionSource.GetGrandAccessExpr() + var unrestrictedFilter = allowsUnrestrictedAccess + ? permissionRestrictionSource.GetUnrestrictedFilter() : _ => false; if (securityObjects.Any()) @@ -37,13 +37,13 @@ public override Expression> GetAccessorsFilter(TDomainOb .Create(typeof(TSecurityContext)) .Expand(securityObjects.Select(identityInfo.Id.Getter), expandType.Reverse()); - return grandAccessExpr.BuildOr(permissionRestrictionSource.GetContainsIdentsExpr(securityIdents)); + return unrestrictedFilter.BuildOr(permissionRestrictionSource.GetContainsIdentsExpr(securityIdents)); } else { if (contextSecurityPath.Required) { - return grandAccessExpr; + return unrestrictedFilter; } else { diff --git a/src/SecuritySystem.Runtime/Builders/MaterializedBuilder/ByIdentsFilterBuilder.cs b/src/SecuritySystem.Runtime/Builders/MaterializedBuilder/ByIdentsFilterBuilder.cs index d8b28e1..57ce339 100644 --- a/src/SecuritySystem.Runtime/Builders/MaterializedBuilder/ByIdentsFilterBuilder.cs +++ b/src/SecuritySystem.Runtime/Builders/MaterializedBuilder/ByIdentsFilterBuilder.cs @@ -15,9 +15,9 @@ public sealed override Expression> GetSecurityFilterEx } else { - var allowGrandAccess = securityContextRestriction?.Required != true; + var allowsUnrestrictedAccess = securityContextRestriction?.Required != true; - return _ => allowGrandAccess; + return _ => allowsUnrestrictedAccess; } } diff --git a/src/SecuritySystem.Runtime/Builders/MaterializedBuilder/ManyContextFilterBuilder.cs b/src/SecuritySystem.Runtime/Builders/MaterializedBuilder/ManyContextFilterBuilder.cs index 95971ce..386968c 100644 --- a/src/SecuritySystem.Runtime/Builders/MaterializedBuilder/ManyContextFilterBuilder.cs +++ b/src/SecuritySystem.Runtime/Builders/MaterializedBuilder/ManyContextFilterBuilder.cs @@ -42,9 +42,9 @@ protected override Expression> GetSecurityFilterExpres } else { - var grandAccessFilter = securityPath.Expression.Select(securityObjects => !securityObjects.Any()); + var unrestrictedFilter = securityPath.Expression.Select(securityObjects => !securityObjects.Any()); - return grandAccessFilter.BuildOr(containsFilterExpr); + return unrestrictedFilter.BuildOr(containsFilterExpr); } } } diff --git a/src/SecuritySystem.Runtime/Builders/MaterializedBuilder/SingleContextFilterBuilder.cs b/src/SecuritySystem.Runtime/Builders/MaterializedBuilder/SingleContextFilterBuilder.cs index 4515405..d63040e 100644 --- a/src/SecuritySystem.Runtime/Builders/MaterializedBuilder/SingleContextFilterBuilder.cs +++ b/src/SecuritySystem.Runtime/Builders/MaterializedBuilder/SingleContextFilterBuilder.cs @@ -25,9 +25,9 @@ protected override Expression> GetSecurityFilterExpres } else { - var grandAccessFilter = securityPath.Expression.Select(securityObject => securityObject == null); + var unrestrictedFilter = securityPath.Expression.Select(securityObject => securityObject == null); - return grandAccessFilter.BuildOr(containsFilterExpr); + return unrestrictedFilter.BuildOr(containsFilterExpr); } } } \ No newline at end of file diff --git a/src/SecuritySystem.Runtime/Builders/QueryBuilder/ManyContextFilterBuilder.cs b/src/SecuritySystem.Runtime/Builders/QueryBuilder/ManyContextFilterBuilder.cs index 1e9b661..9f66bbd 100644 --- a/src/SecuritySystem.Runtime/Builders/QueryBuilder/ManyContextFilterBuilder.cs +++ b/src/SecuritySystem.Runtime/Builders/QueryBuilder/ManyContextFilterBuilder.cs @@ -23,10 +23,10 @@ public class ManyContextFilterBuilder> GetSecurityFilterExpression(HierarchicalExpandType expandType) { - var allowGrandAccess = securityContextRestriction?.Required != true; + var allowsUnrestrictedAccess = securityContextRestriction?.Required != true; - var grandAccessExpr = allowGrandAccess - ? permissionRestrictionSource.GetGrandAccessExpr() + var unrestrictedFilter = allowsUnrestrictedAccess + ? permissionRestrictionSource.GetUnrestrictedFilter() : _ => false; var getIdents = permissionRestrictionSource.GetIdentsExpr(); @@ -43,14 +43,14 @@ public override Expression> GetSecurityFi { if (securityPath.Required) { - return (domainObject, permission) => ee.Evaluate(grandAccessExpr, permission) + return (domainObject, permission) => ee.Evaluate(unrestrictedFilter, permission) || ee.Evaluate(securityPath.SecurityPathQ, domainObject) .Any(item => ee.Evaluate(expandExpressionQ, permission).Contains(ee.Evaluate(identityInfo.Id.Path, item))); } else { - return (domainObject, permission) => ee.Evaluate(grandAccessExpr, permission) + return (domainObject, permission) => ee.Evaluate(unrestrictedFilter, permission) || !ee.Evaluate(securityPath.SecurityPathQ, domainObject).Any() @@ -62,14 +62,14 @@ public override Expression> GetSecurityFi { if (securityPath.Required) { - return (domainObject, permission) => ee.Evaluate(grandAccessExpr, permission) + return (domainObject, permission) => ee.Evaluate(unrestrictedFilter, permission) || ee.Evaluate(securityPath.Expression, domainObject) .Any(item => ee.Evaluate(expandExpressionQ, permission).Contains(ee.Evaluate(identityInfo.Id.Path, item))); } else { - return (domainObject, permission) => ee.Evaluate(grandAccessExpr, permission) + return (domainObject, permission) => ee.Evaluate(unrestrictedFilter, permission) || !ee.Evaluate(securityPath.Expression, domainObject).Any() diff --git a/src/SecuritySystem.Runtime/Builders/QueryBuilder/SingleContextFilterBuilder.cs b/src/SecuritySystem.Runtime/Builders/QueryBuilder/SingleContextFilterBuilder.cs index 97b8c7d..2a61b8b 100644 --- a/src/SecuritySystem.Runtime/Builders/QueryBuilder/SingleContextFilterBuilder.cs +++ b/src/SecuritySystem.Runtime/Builders/QueryBuilder/SingleContextFilterBuilder.cs @@ -23,10 +23,10 @@ public class SingleContextFilterBuilder> GetSecurityFilterExpression(HierarchicalExpandType expandType) { - var allowGrandAccess = securityContextRestriction?.Required != true; + var allowsUnrestrictedAccess = securityContextRestriction?.Required != true; - var grandAccessExpr = allowGrandAccess - ? permissionRestrictionSource.GetGrandAccessExpr() + var unrestrictedFilter = allowsUnrestrictedAccess + ? permissionRestrictionSource.GetUnrestrictedFilter() : _ => false; var getIdents = permissionRestrictionSource.GetIdentsExpr(); @@ -45,7 +45,7 @@ public override Expression> GetSecurityFi { return (domainObject, permission) => - ee.Evaluate(grandAccessExpr, permission) + ee.Evaluate(unrestrictedFilter, permission) || ee.Evaluate(expandExpressionQ, permission).Contains( ee.Evaluate(fullIdPath, domainObject)); @@ -54,7 +54,7 @@ public override Expression> GetSecurityFi { return (domainObject, permission) => - ee.Evaluate(grandAccessExpr, permission) + ee.Evaluate(unrestrictedFilter, permission) || ee.Evaluate(securityPath.Expression, domainObject) == null diff --git a/src/SecuritySystem.Runtime/Expanders/SecurityOperationExpander.cs b/src/SecuritySystem.Runtime/Expanders/SecurityOperationExpander.cs index 8ed218b..ad80aaa 100644 --- a/src/SecuritySystem.Runtime/Expanders/SecurityOperationExpander.cs +++ b/src/SecuritySystem.Runtime/Expanders/SecurityOperationExpander.cs @@ -5,7 +5,7 @@ namespace SecuritySystem.Expanders; public class SecurityOperationExpander(ISecurityRoleSource securityRoleSource, ISecurityOperationInfoSource securityOperationInfoSource) : ISecurityOperationExpander { - private readonly ConcurrentDictionary cache = new(); + private readonly ConcurrentDictionary cache = []; public DomainSecurityRule.NonExpandedRolesSecurityRule Expand(DomainSecurityRule.OperationSecurityRule baseSecurityRule) { diff --git a/src/SecuritySystem.Runtime/Expanders/SecurityRoleExpander.cs b/src/SecuritySystem.Runtime/Expanders/SecurityRoleExpander.cs index 060242b..44509f4 100644 --- a/src/SecuritySystem.Runtime/Expanders/SecurityRoleExpander.cs +++ b/src/SecuritySystem.Runtime/Expanders/SecurityRoleExpander.cs @@ -7,7 +7,7 @@ namespace SecuritySystem.Expanders; public class SecurityRoleGroupExpander(ISecurityRoleSource securityRoleSource, IExpandedRoleGroupSecurityRuleSetOptimizer setOptimizer) : ISecurityRoleGroupExpander { - private readonly ConcurrentDictionary cache = new(); + private readonly ConcurrentDictionary cache = []; private readonly ConcurrentDictionary innerCache = new(); diff --git a/src/SecuritySystem.Runtime/ExternalSystem/IPermissionRestrictionSource.cs b/src/SecuritySystem.Runtime/ExternalSystem/IPermissionRestrictionSource.cs index a82026e..4d50427 100644 --- a/src/SecuritySystem.Runtime/ExternalSystem/IPermissionRestrictionSource.cs +++ b/src/SecuritySystem.Runtime/ExternalSystem/IPermissionRestrictionSource.cs @@ -6,7 +6,7 @@ namespace SecuritySystem.ExternalSystem; public interface IPermissionRestrictionSource { - Expression> GetGrandAccessExpr(); + Expression> GetUnrestrictedFilter(); } public interface IPermissionRestrictionSource : IPermissionRestrictionSource @@ -16,6 +16,6 @@ public interface IPermissionRestrictionSource> GetContainsIdentsExpr(IEnumerable idents) => this.GetIdentsExpr() .Select(restrictionIdents => restrictionIdents.Any(restrictionIdent => idents.Contains(restrictionIdent))); - Expression> IPermissionRestrictionSource.GetGrandAccessExpr() + Expression> IPermissionRestrictionSource.GetUnrestrictedFilter() => this.GetIdentsExpr().Select(v => !v.Any()); } \ No newline at end of file diff --git a/src/SecuritySystem.Runtime/ExternalSystem/Management/RootPrincipalSourceService.cs b/src/SecuritySystem.Runtime/ExternalSystem/Management/RootPrincipalSourceService.cs index 232263e..45d6a31 100644 --- a/src/SecuritySystem.Runtime/ExternalSystem/Management/RootPrincipalSourceService.cs +++ b/src/SecuritySystem.Runtime/ExternalSystem/Management/RootPrincipalSourceService.cs @@ -50,7 +50,7 @@ into g select new ManagedPrincipal( g.Key with { IsVirtual = g.All(p => p.Header.IsVirtual) }, - g.SelectMany(p => p.Permissions).ToList()); + [..g.SelectMany(p => p.Permissions)]); return request.SingleOrDefault(() => throw getOverflowException()); } diff --git a/src/SecuritySystem.Runtime/PermissionBindingInfo.cs b/src/SecuritySystem.Runtime/PermissionBindingInfo.cs index 452134d..bff3608 100644 --- a/src/SecuritySystem.Runtime/PermissionBindingInfo.cs +++ b/src/SecuritySystem.Runtime/PermissionBindingInfo.cs @@ -19,6 +19,8 @@ public abstract record PermissionBindingInfo : PermissionBindingInf public PropertyAccessors? PermissionComment { get; init; } + public PropertyAccessors? DelegatedFrom { get; init; } + public PropertyAccessors? PermissionStartDate { get; init; } public PropertyAccessors? PermissionEndDate { get; init; } diff --git a/src/SecuritySystem.Runtime/Providers/RoleBaseSecurityPathProvider.cs b/src/SecuritySystem.Runtime/Providers/RoleBaseSecurityPathProvider.cs index 607d5d6..5452b40 100644 --- a/src/SecuritySystem.Runtime/Providers/RoleBaseSecurityPathProvider.cs +++ b/src/SecuritySystem.Runtime/Providers/RoleBaseSecurityPathProvider.cs @@ -41,5 +41,5 @@ public AccessResult GetAccessResult(TDomainObject domainObject) } public SecurityAccessorData GetAccessorData(TDomainObject domainObject) => - new SecurityAccessorData.FixedSecurityAccessorData(this.lazyAccessorsFilter.Value.GetAccessorsFunc(domainObject).ToList()); + new SecurityAccessorData.FixedSecurityAccessorData([..this.lazyAccessorsFilter.Value.GetAccessorsFunc(domainObject)]); } diff --git a/src/SecuritySystem.Runtime/SecurityContextInfo/SecurityContextInfoSource.cs b/src/SecuritySystem.Runtime/SecurityContextInfo/SecurityContextInfoSource.cs index f93b384..492eed9 100644 --- a/src/SecuritySystem.Runtime/SecurityContextInfo/SecurityContextInfoSource.cs +++ b/src/SecuritySystem.Runtime/SecurityContextInfo/SecurityContextInfoSource.cs @@ -1,7 +1,9 @@ -using SecuritySystem.Services; +using CommonFramework; + +using SecuritySystem.Services; using System.Collections.Concurrent; -using CommonFramework; +using System.Collections.Immutable; // ReSharper disable once CheckNamespace namespace SecuritySystem; @@ -21,7 +23,7 @@ public class SecurityContextInfoSource : ISecurityContextInfoSource public SecurityContextInfoSource(IServiceProxyFactory serviceProxyFactory, IEnumerable securityContextInfoList) { - this.SecurityContextInfoList = securityContextInfoList.ToList(); + this.SecurityContextInfoList = [..securityContextInfoList]; this.typeDict = this.SecurityContextInfoList.ToDictionary(v => v.Type); this.identityDict = this.typeDict.Values.ToDictionary(v => v.Identity); @@ -32,7 +34,7 @@ public SecurityContextInfoSource(IServiceProxyFactory serviceProxyFactory, IEnum this.SecurityContextInfoList.Select(sr => sr.Identity.IdentType).Distinct()); } - public IReadOnlyList SecurityContextInfoList { get; } + public ImmutableArray SecurityContextInfoList { get; } public virtual SecurityContextInfo GetSecurityContextInfo(Type type) => this.typeDict[type]; diff --git a/src/SecuritySystem.Runtime/SecurityRole/SecurityRoleSource.cs b/src/SecuritySystem.Runtime/SecurityRole/SecurityRoleSource.cs index 7c7b96b..b2daebe 100644 --- a/src/SecuritySystem.Runtime/SecurityRole/SecurityRoleSource.cs +++ b/src/SecuritySystem.Runtime/SecurityRole/SecurityRoleSource.cs @@ -3,6 +3,8 @@ using SecuritySystem.Services; using System.Collections.Concurrent; +using System.Collections.Immutable; + // ReSharper disable once CheckNamespace namespace SecuritySystem; @@ -18,7 +20,7 @@ public class SecurityRoleSource : ISecurityRoleSource public SecurityRoleSource(IServiceProxyFactory serviceProxyFactory, IEnumerable securityRoles) { - this.SecurityRoles = securityRoles.ToList(); + this.SecurityRoles = [..securityRoles]; this.identityDict = this.SecurityRoles.ToDictionary(v => v.Identity); @@ -29,7 +31,7 @@ public SecurityRoleSource(IServiceProxyFactory serviceProxyFactory, IEnumerable< this.SecurityRoles.Select(sr => sr.Identity.IdentType).Distinct()); } - public IReadOnlyList SecurityRoles { get; } + public ImmutableArray SecurityRoles { get; } public FullSecurityRole GetSecurityRole(SecurityRole securityRole) => this.GetSecurityRole(securityRole.Name); diff --git a/src/SecuritySystem.Runtime/SecurityRuleInfo/ClientSecurityRuleResolver.cs b/src/SecuritySystem.Runtime/SecurityRuleInfo/ClientSecurityRuleResolver.cs index 300c0e7..8db97d9 100644 --- a/src/SecuritySystem.Runtime/SecurityRuleInfo/ClientSecurityRuleResolver.cs +++ b/src/SecuritySystem.Runtime/SecurityRuleInfo/ClientSecurityRuleResolver.cs @@ -6,7 +6,7 @@ public class ClientSecurityRuleResolver( IDomainSecurityRoleExtractor domainSecurityRoleExtractor, IClientSecurityRuleInfoSource clientSecurityRuleInfoSource) : IClientSecurityRuleResolver { - private readonly ConcurrentDictionary cache = new(); + private readonly ConcurrentDictionary cache = []; public IEnumerable Resolve(SecurityRole securityRole) => this.cache.GetOrAdd(securityRole, _ => diff --git a/src/SecuritySystem.Runtime/Services/AvailablePermissionFilterFactory.cs b/src/SecuritySystem.Runtime/Services/AvailablePermissionFilterFactory.cs index aa32c11..20026fd 100644 --- a/src/SecuritySystem.Runtime/Services/AvailablePermissionFilterFactory.cs +++ b/src/SecuritySystem.Runtime/Services/AvailablePermissionFilterFactory.cs @@ -34,8 +34,7 @@ public class AvailablePermissionFilterFactory( PermissionBindingInfo bindingInfo, TimeProvider timeProvider, IUserNameResolver userNameResolver, - ISecurityRolesIdentsResolver securityRolesIdentsResolver, - IPermissionSecurityRoleFilterFactory permissionSecurityRoleFilterFactory, + IPermissionSecurityRoleIdentsFilterFactory permissionSecurityRoleIdentsFilterFactory, IPermissionFilterFactory permissionFilterFactory, SecurityRuleCredential defaultSecurityRuleCredential, VisualIdentityInfo principalVisualIdentityInfo) : IAvailablePermissionFilterFactory @@ -57,10 +56,7 @@ private IEnumerable>> GetFilterElements(Domai yield return bindingInfo.Principal.Path.Select(principalVisualIdentityInfo.Name.Path).Select(name => name == principalName); } - foreach (var (securityRoleIdentType, securityRoleIdents) in securityRolesIdentsResolver.Resolve(securityRule)) - { - yield return permissionSecurityRoleFilterFactory.CreateFilter(securityRoleIdentType, securityRoleIdents); - } + yield return permissionSecurityRoleIdentsFilterFactory.CreateFilter(securityRule); foreach (var securityContextRestriction in securityRule.GetSafeSecurityContextRestrictions()) { diff --git a/src/SecuritySystem.Runtime/Services/IManagedPrincipalConverter.cs b/src/SecuritySystem.Runtime/Services/IManagedPrincipalConverter.cs index 86341a5..768853e 100644 --- a/src/SecuritySystem.Runtime/Services/IManagedPrincipalConverter.cs +++ b/src/SecuritySystem.Runtime/Services/IManagedPrincipalConverter.cs @@ -4,5 +4,5 @@ namespace SecuritySystem.Services; public interface IManagedPrincipalConverter { - Task ToManagedPrincipalAsync(TPrincipal principal, CancellationToken cancellationToken); + Task ToManagedPrincipalAsync(TPrincipal dbPrincipal, CancellationToken cancellationToken); } \ No newline at end of file diff --git a/src/SecuritySystem.Runtime/Services/IPermissionBindingInfoSource.cs b/src/SecuritySystem.Runtime/Services/IPermissionBindingInfoSource.cs index f59ce14..4549203 100644 --- a/src/SecuritySystem.Runtime/Services/IPermissionBindingInfoSource.cs +++ b/src/SecuritySystem.Runtime/Services/IPermissionBindingInfoSource.cs @@ -4,5 +4,7 @@ public interface IPermissionBindingInfoSource { PermissionBindingInfo GetForPermission(Type permissionType); + PermissionBindingInfo GetForPermission() => (PermissionBindingInfo)this.GetForPermission(typeof(TPermission)); + PermissionBindingInfo GetForPrincipal(Type principalType); } \ No newline at end of file diff --git a/src/SecuritySystem.Runtime/Services/IPermissionSecurityRoleIdentsFilterFactory.cs b/src/SecuritySystem.Runtime/Services/IPermissionSecurityRoleIdentsFilterFactory.cs new file mode 100644 index 0000000..2aee41a --- /dev/null +++ b/src/SecuritySystem.Runtime/Services/IPermissionSecurityRoleIdentsFilterFactory.cs @@ -0,0 +1,8 @@ +using System.Linq.Expressions; + +namespace SecuritySystem.Services; + +public interface IPermissionSecurityRoleIdentsFilterFactory +{ + Expression> CreateFilter(DomainSecurityRule.RoleBaseSecurityRule securityRule); +} \ No newline at end of file diff --git a/src/SecuritySystem.Runtime/Services/PermissionSecurityRoleIdentsFilterFactory.cs b/src/SecuritySystem.Runtime/Services/PermissionSecurityRoleIdentsFilterFactory.cs new file mode 100644 index 0000000..75a2d66 --- /dev/null +++ b/src/SecuritySystem.Runtime/Services/PermissionSecurityRoleIdentsFilterFactory.cs @@ -0,0 +1,24 @@ +using System.Collections.Concurrent; +using System.Linq.Expressions; + +using CommonFramework; + +namespace SecuritySystem.Services; + +public class PermissionSecurityRoleIdentsFilterFactory( + IPermissionSecurityRoleFilterFactory permissionSecurityRoleFilterFactory, + ISecurityRolesIdentsResolver securityRolesIdentsResolver) : IPermissionSecurityRoleIdentsFilterFactory +{ + private readonly ConcurrentDictionary>> cache = []; + + public Expression> CreateFilter(DomainSecurityRule.RoleBaseSecurityRule securityRule) => + this.cache.GetOrAdd(securityRule.WithDefaultCustoms(), _ => this.GetFilterElements(securityRule).BuildAnd()); + + private IEnumerable>> GetFilterElements(DomainSecurityRule.RoleBaseSecurityRule securityRule) + { + foreach (var (securityRoleIdentType, securityRoleIdents) in securityRolesIdentsResolver.Resolve(securityRule)) + { + yield return permissionSecurityRoleFilterFactory.CreateFilter(securityRoleIdentType, securityRoleIdents); + } + } +} \ No newline at end of file diff --git a/src/SecuritySystem.Runtime/Services/PrincipalDataSecurityIdentityExtractor.cs b/src/SecuritySystem.Runtime/Services/PrincipalDataSecurityIdentityExtractor.cs index be288b6..8ce4e22 100644 --- a/src/SecuritySystem.Runtime/Services/PrincipalDataSecurityIdentityExtractor.cs +++ b/src/SecuritySystem.Runtime/Services/PrincipalDataSecurityIdentityExtractor.cs @@ -8,7 +8,7 @@ namespace SecuritySystem.Services; public class PrincipalDataSecurityIdentityExtractor(IServiceProxyFactory serviceProxyFactory) : IPrincipalDataSecurityIdentityExtractor { - private readonly ConcurrentDictionary cache = new(); + private readonly ConcurrentDictionary cache = []; public TypedSecurityIdentity Extract(PrincipalData principalData) { diff --git a/src/SecuritySystem.Runtime/Services/SecurityRolesIdentsResolver.cs b/src/SecuritySystem.Runtime/Services/SecurityRolesIdentsResolver.cs index afeba0a..b1e1030 100644 --- a/src/SecuritySystem.Runtime/Services/SecurityRolesIdentsResolver.cs +++ b/src/SecuritySystem.Runtime/Services/SecurityRolesIdentsResolver.cs @@ -9,11 +9,11 @@ namespace SecuritySystem.Services; public class SecurityRolesIdentsResolver(ISecurityRuleExpander securityRuleExpander, ISecurityRoleSource securityRoleSource) : ISecurityRolesIdentsResolver { - private readonly ConcurrentDictionary<(DomainSecurityRule.RoleBaseSecurityRule, bool), ImmutableDictionary> cache = new(); + private readonly ConcurrentDictionary<(DomainSecurityRule.RoleBaseSecurityRule, bool), ImmutableDictionary> cache = []; - public IReadOnlyDictionary Resolve(DomainSecurityRule.RoleBaseSecurityRule baseSecurityRule, bool includeVirtual = false) - { - return this.cache.GetOrAdd((baseSecurityRule.WithDefaultCredential(), includeVirtual), pair => + public ImmutableDictionary Resolve(DomainSecurityRule.RoleBaseSecurityRule baseSecurityRule, bool includeVirtual = false) => + + this.cache.GetOrAdd((baseSecurityRule.WithDefaultCustoms(), includeVirtual), pair => securityRuleExpander .FullRoleExpand(pair.Item1) @@ -25,5 +25,4 @@ public IReadOnlyDictionary Resolve(DomainSecurityRule.RoleBaseSecur .Select(sr => sr.Identity) .GroupBy(i => i.IdentType, i => i.GetId()) .ToImmutableDictionary(g => g.Key, g => g.ToArray(g.Key))); - } } \ No newline at end of file diff --git a/src/SecuritySystem.Testing/AdministratorsRoleList.cs b/src/SecuritySystem.Testing/AdministratorsRoleList.cs index b08d821..1bb181a 100644 --- a/src/SecuritySystem.Testing/AdministratorsRoleList.cs +++ b/src/SecuritySystem.Testing/AdministratorsRoleList.cs @@ -1,6 +1,8 @@ -namespace SecuritySystem.Testing; +using System.Collections.Immutable; -public record AdministratorsRoleList(IReadOnlyList Roles) +namespace SecuritySystem.Testing; + +public record AdministratorsRoleList(ImmutableArray Roles) { public static AdministratorsRoleList Default { get; } = new([SecurityRole.Administrator, SecurityRole.SystemIntegration]); } \ No newline at end of file diff --git a/src/SecuritySystem.Testing/RootUserCredentialManager.cs b/src/SecuritySystem.Testing/RootUserCredentialManager.cs index e7bfd55..ddba4ea 100644 --- a/src/SecuritySystem.Testing/RootUserCredentialManager.cs +++ b/src/SecuritySystem.Testing/RootUserCredentialManager.cs @@ -24,7 +24,7 @@ public SecurityIdentity CreatePrincipal() public async Task CreatePrincipalAsync(CancellationToken cancellationToken = default) { - return await this.ManagerEvaluator.EvaluateAsync(TestingScopeMode.Write, manger => manger.CreatePrincipalAsync(cancellationToken)); + return await this.ManagerEvaluator.EvaluateAsync(TestingScopeMode.Write, manager => manager.CreatePrincipalAsync(cancellationToken)); } public SecurityIdentity SetAdminRole() @@ -34,35 +34,35 @@ public SecurityIdentity SetAdminRole() public Task SetAdminRoleAsync(CancellationToken cancellationToken = default) { - return this.SetRoleAsync(administratorsRoleList.Roles.Select(securityRole => new ManagedPermissionData { SecurityRole = securityRole }).ToArray(), + return this.SetRoleAsync(administratorsRoleList.Roles.Select(securityRole => (ManagedPermission)securityRole).ToArray(), cancellationToken); } - public SecurityIdentity SetRole(params ManagedPermissionData[] permissions) + public SecurityIdentity SetRole(params ManagedPermission[] permissions) { return this.SetRoleAsync(permissions).GetAwaiter().GetResult(); } - public async Task SetRoleAsync(ManagedPermissionData permission, CancellationToken cancellationToken = default) + public async Task SetRoleAsync(ManagedPermission permission, CancellationToken cancellationToken = default) { return await this.SetRoleAsync([permission], cancellationToken); } - public async Task SetRoleAsync(ManagedPermissionData[] permissions, CancellationToken cancellationToken = default) + public async Task SetRoleAsync(ManagedPermission[] permissions, CancellationToken cancellationToken = default) { await this.ClearRolesAsync(cancellationToken); return await this.AddRoleAsync(permissions, cancellationToken); } - public SecurityIdentity AddRole(params ManagedPermissionData[] permissions) => + public SecurityIdentity AddRole(params ManagedPermission[] permissions) => this.AddRoleAsync(permissions).GetAwaiter().GetResult(); - public async Task AddRoleAsync(ManagedPermissionData permission, CancellationToken cancellationToken = default) => + public async Task AddRoleAsync(ManagedPermission permission, CancellationToken cancellationToken = default) => await this.AddRoleAsync([permission], cancellationToken); - public async Task AddRoleAsync(ManagedPermissionData[] permissions, CancellationToken cancellationToken = default) => - await this.ManagerEvaluator.EvaluateAsync(TestingScopeMode.Write, async manger => await manger.AddUserRoleAsync(permissions, cancellationToken)); + public async Task AddRoleAsync(ManagedPermission[] permissions, CancellationToken cancellationToken = default) => + await this.ManagerEvaluator.EvaluateAsync(TestingScopeMode.Write, async manager => await manager.AddUserRoleAsync(permissions, cancellationToken)); public void ClearRoles() { diff --git a/src/SecuritySystem.Testing/TestPermission.cs b/src/SecuritySystem.Testing/TestPermission.cs index 0461ec4..8152ab5 100644 --- a/src/SecuritySystem.Testing/TestPermission.cs +++ b/src/SecuritySystem.Testing/TestPermission.cs @@ -26,6 +26,10 @@ public TestPermission() public string Comment { get; set; } = ""; + public SecurityIdentity Identity { get; init; } = SecurityIdentity.Default; + + public SecurityIdentity DelegatedFrom { get; init; } = SecurityIdentity.Default; + public TypedSecurityIdentity? GetSingle() where TSecurityContext : ISecurityContext where TIdent : notnull @@ -57,7 +61,7 @@ public TypedSecurityIdentity[] GetMany() { var arr = (TIdent[]?)this.Restrictions.GetValueOrDefault(typeof(TSecurityContext)); - var value = arr ?? Array.Empty(); + var value = arr ?? []; return value.Select(TypedSecurityIdentity.Create).ToArray(); } @@ -76,14 +80,19 @@ public void SetMany(TypedSecurityIdentity[] va } } - public ManagedPermissionData ToManagedPermissionData() => new() + public ManagedPermission ToManagedPermissionData() => new() { + Identity = this.Identity, + ForceApplyIdentity = true, + SecurityRole = this.SecurityRole ?? throw new InvalidOperationException($"{nameof(this.SecurityRole)} not initialized"), Period = this.Period, + IsVirtual = false, Comment = this.Comment, + DelegatedFrom = this.DelegatedFrom, Restrictions = this.Restrictions.Where(pair => pair.Value.Length > 0).ToImmutableDictionary(), ExtendedData = this.ExtendedData.ToImmutableDictionary() }; - public static implicit operator ManagedPermissionData(TestPermission testPermissionBuilder) => testPermissionBuilder.ToManagedPermissionData(); + public static implicit operator ManagedPermission(TestPermission testPermissionBuilder) => testPermissionBuilder.ToManagedPermissionData(); } \ No newline at end of file diff --git a/src/SecuritySystem.Testing/UserCredentialManager.cs b/src/SecuritySystem.Testing/UserCredentialManager.cs index 0fb3079..84e4c50 100644 --- a/src/SecuritySystem.Testing/UserCredentialManager.cs +++ b/src/SecuritySystem.Testing/UserCredentialManager.cs @@ -31,19 +31,8 @@ public async Task CreatePrincipalAsync(CancellationToken cance return securityIdentityExtractor.Extract(principalData); } - public async Task AddUserRoleAsync(ManagedPermissionData[] testPermissions, CancellationToken cancellationToken = default) + public async Task AddUserRoleAsync(ManagedPermission[] newPermissions, CancellationToken cancellationToken = default) { - var newPermissions = testPermissions.Select(testPermission => new ManagedPermission - { - Identity = SecurityIdentity.Default, - IsVirtual = false, - SecurityRole = testPermission.SecurityRole, - Period = testPermission.Period, - Comment = testPermission.Comment, - Restrictions = testPermission.Restrictions, - ExtendedData = testPermission.ExtendedData - }); - var existsPrincipal = await principalSourceService.TryGetPrincipalAsync(this.userCredential, cancellationToken); if (existsPrincipal == null) @@ -54,7 +43,7 @@ public async Task AddUserRoleAsync(ManagedPermissionData[] tes } else { - var updatedPrincipal = existsPrincipal with { Permissions = existsPrincipal.Permissions.Concat(newPermissions).ToList() }; + var updatedPrincipal = existsPrincipal with { Permissions = [..existsPrincipal.Permissions, .. newPermissions] }; await principalManagementService.UpdatePermissionsAsync( updatedPrincipal.Header.Identity, diff --git a/src/SecuritySystem.VirtualPermission.Runtime/VirtualPermissionRestrictionSource.cs b/src/SecuritySystem.VirtualPermission.Runtime/VirtualPermissionRestrictionSource.cs index 2562f62..6772b4d 100644 --- a/src/SecuritySystem.VirtualPermission.Runtime/VirtualPermissionRestrictionSource.cs +++ b/src/SecuritySystem.VirtualPermission.Runtime/VirtualPermissionRestrictionSource.cs @@ -26,12 +26,12 @@ public class VirtualPermissionRestrictionSource>> GetIdentsExpr() => virtualBindingInfo.GetRestrictionsExpr(identityInfo, restrictionFilterInfo?.GetPureFilter(serviceProvider)); - public Expression> GetGrandAccessExpr() => this.GetManyGrandAccessExpr().BuildAnd(); + public Expression> GetUnrestrictedFilter() => this.GetManyUnrestrictedFilter().BuildAnd(); public Expression> GetContainsIdentsExpr(IEnumerable idents) => this.GetManyContainsIdentsExpr(idents).BuildOr(); - private IEnumerable>> GetManyGrandAccessExpr() + private IEnumerable>> GetManyUnrestrictedFilter() { foreach (var restrictionPath in virtualBindingInfo.Restrictions) { diff --git a/src/SecuritySystem.VirtualPermission.Runtime/VirtualPrincipalSourceService.cs b/src/SecuritySystem.VirtualPermission.Runtime/VirtualPrincipalSourceService.cs index 7ff0c9e..4ada8f1 100644 --- a/src/SecuritySystem.VirtualPermission.Runtime/VirtualPrincipalSourceService.cs +++ b/src/SecuritySystem.VirtualPermission.Runtime/VirtualPrincipalSourceService.cs @@ -107,7 +107,7 @@ public async Task> GetPrincipalsAsync(string .Where(bindingInfo.Principal.Path.Select(p => p == principal)) .GenericToListAsync(cancellationToken); - return new ManagedPrincipal(header, permissions.Select(this.ToManagedPermission).ToList()); + return new ManagedPrincipal(header, [..permissions.Select(this.ToManagedPermission)]); } } @@ -131,6 +131,9 @@ private ManagedPermission ToManagedPermission(TPermission permission) SecurityRole = virtualBindingInfo.SecurityRole, Period = bindingInfo.GetSafePeriod(permission), Comment = bindingInfo.GetSafeComment(permission), + DelegatedFrom = bindingInfo.DelegatedFrom?.Getter.Invoke(permission) is { } delegatedFromPermission + ? permissionIdentityExtractor.Extract(delegatedFromPermission) + : SecurityIdentity.Default, Restrictions = restrictions }; } diff --git a/src/SecuritySystem.VirtualPermission/DependencyInjection/IVirtualBindingInfoRootSettingsBuilder.cs b/src/SecuritySystem.VirtualPermission/DependencyInjection/IVirtualBindingInfoRootSettingsBuilder.cs index a4aa562..a037e26 100644 --- a/src/SecuritySystem.VirtualPermission/DependencyInjection/IVirtualBindingInfoRootSettingsBuilder.cs +++ b/src/SecuritySystem.VirtualPermission/DependencyInjection/IVirtualBindingInfoRootSettingsBuilder.cs @@ -1,6 +1,5 @@ -using System.Linq.Expressions; - -using CommonFramework; +using CommonFramework; +using System.Linq.Expressions; namespace SecuritySystem.VirtualPermission.DependencyInjection; @@ -17,5 +16,7 @@ IVirtualBindingInfoRootSettingsBuilder SetPeriod( IVirtualBindingInfoRootSettingsBuilder SetComment(Expression> commentPath); + IVirtualBindingInfoRootSettingsBuilder SetPermissionDelegation(Expression> newDelegatedFromPath); + IVirtualBindingInfoRootSettingsBuilder ForRole(SecurityRole securityRole, Action>? init = null); } \ No newline at end of file diff --git a/src/SecuritySystem.VirtualPermission/DependencyInjection/SecuritySystemSettingsExtensions.cs b/src/SecuritySystem.VirtualPermission/DependencyInjection/SecuritySystemSettingsExtensions.cs index 84477b4..c346e0f 100644 --- a/src/SecuritySystem.VirtualPermission/DependencyInjection/SecuritySystemSettingsExtensions.cs +++ b/src/SecuritySystem.VirtualPermission/DependencyInjection/SecuritySystemSettingsExtensions.cs @@ -31,10 +31,10 @@ public ISecuritySystemSettings AddVirtualPermission( services.AddSingleton(bindingInfo); services.TryAddSingleton(); + var serviceType = typeof(VirtualPrincipalSourceService<>).MakeGenericType(bindingInfo.PermissionType); + foreach (var virtualBindingInfo in virtualBindingInfoList) { - var serviceType = typeof(VirtualPrincipalSourceService<>).MakeGenericType(bindingInfo.PermissionType); - services.AddScopedFrom(factory => factory.Create(serviceType, virtualBindingInfo)); } diff --git a/src/SecuritySystem.VirtualPermission/DependencyInjection/VirtualBindingInfoRootSettingsBuilder.cs b/src/SecuritySystem.VirtualPermission/DependencyInjection/VirtualBindingInfoRootSettingsBuilder.cs index b381a25..245f302 100644 --- a/src/SecuritySystem.VirtualPermission/DependencyInjection/VirtualBindingInfoRootSettingsBuilder.cs +++ b/src/SecuritySystem.VirtualPermission/DependencyInjection/VirtualBindingInfoRootSettingsBuilder.cs @@ -34,6 +34,13 @@ public IVirtualBindingInfoRootSettingsBuilder SetComment(Expression return this; } + public IVirtualBindingInfoRootSettingsBuilder SetPermissionDelegation( + Expression> newDelegatedFromPath) + { + this.PermissionBindingInit.Add(permissionBinding => permissionBinding with { DelegatedFrom = newDelegatedFromPath.ToPropertyAccessors() }); + + return this; + } public IVirtualBindingInfoRootSettingsBuilder ForRole(SecurityRole securityRole, Action>? init = null) diff --git a/src/SecuritySystem/DependencyInjection/InitializedSecurityRoleSource.cs b/src/SecuritySystem/DependencyInjection/InitializedSecurityRoleSource.cs index da8011e..8b1782f 100644 --- a/src/SecuritySystem/DependencyInjection/InitializedSecurityRoleSource.cs +++ b/src/SecuritySystem/DependencyInjection/InitializedSecurityRoleSource.cs @@ -1,4 +1,6 @@ -namespace SecuritySystem.DependencyInjection; +using System.Collections.Immutable; + +namespace SecuritySystem.DependencyInjection; public class InitializedSecurityRoleSource(IEnumerable securityRoles) : IInitializedSecurityRoleSource { @@ -7,7 +9,7 @@ public IEnumerable GetSecurityRoles() return securityRoles.Select(sr => this.GetInitializedRole(sr.FullSecurityRole)); } - protected virtual IReadOnlyList ExceptAdministratorRoles { get; } = [SecurityRole.Administrator, SecurityRole.SystemIntegration]; + protected virtual ImmutableArray ExceptAdministratorRoles { get; } = [SecurityRole.Administrator, SecurityRole.SystemIntegration]; private FullSecurityRole GetInitializedRole(FullSecurityRole securityRole) { @@ -19,7 +21,7 @@ private FullSecurityRole GetInitializedRole(FullSecurityRole securityRole) var newInfo = securityRole.Information with { - Children = info.Children.Concat(otherRoles).Distinct().ToList(), + Children = [..info.Children.Concat(otherRoles).Distinct()], Restriction = SecurityPathRestriction.Ignored }; diff --git a/src/_Example/ExampleApp.Domain/Auth/General/Permission.cs b/src/_Example/ExampleApp.Domain/Auth/General/Permission.cs index 443a002..91acf86 100644 --- a/src/_Example/ExampleApp.Domain/Auth/General/Permission.cs +++ b/src/_Example/ExampleApp.Domain/Auth/General/Permission.cs @@ -8,6 +8,8 @@ public class Permission public virtual required SecurityRole SecurityRole { get; init; } + public virtual Permission? DelegatedFrom { get; init; } + public DateTime StartDate { get; set; } public DateTime? EndDate { get; set; } diff --git a/src/_Example/ExampleApp.Infrastructure/DependencyInjection/ExtendedPermissionManagementService.cs b/src/_Example/ExampleApp.Infrastructure/DependencyInjection/ExtendedPermissionManagementService.cs index a3c656b..044279a 100644 --- a/src/_Example/ExampleApp.Infrastructure/DependencyInjection/ExtendedPermissionManagementService.cs +++ b/src/_Example/ExampleApp.Infrastructure/DependencyInjection/ExtendedPermissionManagementService.cs @@ -1,21 +1,21 @@ using CommonFramework; using CommonFramework.GenericRepository; - -using AuthGeneral = ExampleApp.Domain.Auth.General; - +using CommonFramework.IdentitySource; +using SecuritySystem.ExternalSystem.Management; using SecuritySystem.GeneralPermission; using SecuritySystem.Services; -using SecuritySystem.ExternalSystem.Management; +using AuthGeneral = ExampleApp.Domain.Auth.General; namespace ExampleApp.Infrastructure.DependencyInjection; public class ExtendedPermissionManagementService( IServiceProxyFactory serviceProxyFactory, + IIdentityInfoSource identityInfoSource, IPermissionBindingInfoSource bindingInfoSource, IGeneralPermissionBindingInfoSource generalBindingInfoSource, IGeneralPermissionRestrictionBindingInfoSource restrictionBindingInfoSource, IGenericRepository genericRepository) : - PermissionManagementService(serviceProxyFactory, bindingInfoSource, + PermissionManagementService(serviceProxyFactory, identityInfoSource, bindingInfoSource, generalBindingInfoSource, restrictionBindingInfoSource) { private const string ExtendedKey = nameof(AuthGeneral.Permission.ExtendedValue); diff --git a/src/_Example/ExampleApp.Infrastructure/DependencyInjection/ServiceCollectionExtensions.cs b/src/_Example/ExampleApp.Infrastructure/DependencyInjection/ServiceCollectionExtensions.cs index 9eb2345..19326bc 100644 --- a/src/_Example/ExampleApp.Infrastructure/DependencyInjection/ServiceCollectionExtensions.cs +++ b/src/_Example/ExampleApp.Infrastructure/DependencyInjection/ServiceCollectionExtensions.cs @@ -130,6 +130,7 @@ private IServiceCollection AddSecuritySystem() (permission, startDate) => permission.StartDate = startDate ?? DateTime.MinValue), new PropertyAccessors(v => v.EndDate)) .SetPermissionComment(v => v.Comment) + .SetPermissionDelegation(v => v.DelegatedFrom) .SetPermissionManagementService())); } } diff --git a/src/_Example/ExampleApp.Infrastructure/TestDbContext.cs b/src/_Example/ExampleApp.Infrastructure/TestDbContext.cs index feab8d3..f2c7425 100644 --- a/src/_Example/ExampleApp.Infrastructure/TestDbContext.cs +++ b/src/_Example/ExampleApp.Infrastructure/TestDbContext.cs @@ -147,6 +147,7 @@ private void InitGeneralPermission(ModelBuilder modelBuilder) entity.HasOne(e => e.SecurityRole).WithMany().HasForeignKey($"{nameof(Permission.SecurityRole)}{DefaultIdPostfix}").IsRequired(); entity.HasOne(e => e.Principal).WithMany().HasForeignKey($"{nameof(Permission.Principal)}{DefaultIdPostfix}").IsRequired(); + entity.HasOne(e => e.DelegatedFrom).WithMany().HasForeignKey($"{nameof(Permission.DelegatedFrom)}{DefaultIdPostfix}").IsRequired(false); entity.Property(e => e.Comment).IsRequired().HasMaxLength(DefaultMaxLength); entity.Property(e => e.ExtendedValue).IsRequired().HasMaxLength(DefaultMaxLength); diff --git a/src/_Example/ExampleApp.IntegrationTests/GeneralPermissionTests.cs b/src/_Example/ExampleApp.IntegrationTests/GeneralPermissionTests.cs index f90f219..59ae5ef 100644 --- a/src/_Example/ExampleApp.IntegrationTests/GeneralPermissionTests.cs +++ b/src/_Example/ExampleApp.IntegrationTests/GeneralPermissionTests.cs @@ -6,6 +6,7 @@ using GenericQueryable; using Microsoft.Extensions.DependencyInjection; +using SecuritySystem; using SecuritySystem.AvailableSecurity; using SecuritySystem.DomainServices; using SecuritySystem.Testing; @@ -14,6 +15,23 @@ namespace ExampleApp.IntegrationTests; public class GeneralPermissionTests : TestBase { + [Fact] + public async Task SetRoleAsync_ShouldPreservePermissionIdentity() + { + // Arrange + var testPermission = new TestPermission(ExampleRoles.DefaultRole) { Identity = TypedSecurityIdentity.Create(Guid.NewGuid()) }; + + // Act + var principalIdentity = await this.AuthManager.For("TestPrincipal").SetRoleAsync(testPermission, this.CancellationToken); + + // Assert + var managedPrincipal = await this.AuthManager.For(principalIdentity).GetPrincipalAsync(this.CancellationToken); + + var managedPermission = managedPrincipal.Permissions.Should().ContainSingle().Subject; + + managedPermission.Identity.Should().Be(testPermission.Identity); + } + [Fact] public async Task AssignGeneralPermission_PermissionResolved() { @@ -27,7 +45,7 @@ public async Task AssignGeneralPermission_PermissionResolved() var testPermission = new TestPermission(testRole) { BusinessUnit = buIdentity }; var principalIdentity = await this.AuthManager.For(principalName).SetRoleAsync(testPermission, this.CancellationToken); - this.AuthManager.For(principalName).LoginAs(); + this.AuthManager.For(principalIdentity).LoginAs(); await using var scope = this.RootServiceProvider.CreateAsyncScope(); var availableSecurityRoleSource = scope.ServiceProvider.GetRequiredService(); @@ -54,15 +72,13 @@ public async Task AssignGeneralPermission_PermissionResolved() public async Task AssignGeneralPermission_WithRootBu_AllTestObjectsResolved() { // Arrange - var principalName = "TestPrincipal"; - var buIdentity = await this.AuthManager.GetSecurityContextIdentityAsync("TestRootBu", this.CancellationToken); var testRole = ExampleRoles.BuManager; var testPermission = new TestPermission(testRole) { BusinessUnit = buIdentity }; - var principalId = await this.AuthManager.For(principalName).SetRoleAsync([testPermission, ExampleRoles.DefaultRole], this.CancellationToken); + var principalId = await this.AuthManager.For("TestPrincipal").SetRoleAsync([testPermission, ExampleRoles.DefaultRole], this.CancellationToken); this.AuthManager.For(principalId).LoginAs(); await using var scope = this.RootServiceProvider.CreateAsyncScope(); diff --git a/src/_Example/ExampleApp.IntegrationTests/PermissionDelegatedFromTests.cs b/src/_Example/ExampleApp.IntegrationTests/PermissionDelegatedFromTests.cs new file mode 100644 index 0000000..5e39978 --- /dev/null +++ b/src/_Example/ExampleApp.IntegrationTests/PermissionDelegatedFromTests.cs @@ -0,0 +1,205 @@ +using ExampleApp.Application; +using ExampleApp.Domain; + +using SecuritySystem; +using SecuritySystem.GeneralPermission; +using SecuritySystem.Testing; +using SecuritySystem.Validation; + +namespace ExampleApp.IntegrationTests; + +public class PermissionDelegationFromTests : TestBase +{ + [Fact] + public async Task SetRoleAsync_ShouldPreserveDelegatedFromIdentity() + { + // Arrange + var sourcePrincipalName = "DelegatedFromPrincipal"; + var targetPrincipalName = "TargetPrincipal"; + var delegatedFromPermission = new TestPermission(ExampleRoles.DefaultRole) { Identity = TypedSecurityIdentity.Create(Guid.NewGuid()) }; + + var subPermission = new TestPermission(ExampleRoles.DefaultRole) + { Identity = TypedSecurityIdentity.Create(Guid.NewGuid()), DelegatedFrom = delegatedFromPermission.Identity }; + + + await this.AuthManager.For(sourcePrincipalName).SetRoleAsync(delegatedFromPermission, this.CancellationToken); + + // Act + var principalIdentity = await this.AuthManager.For(targetPrincipalName).SetRoleAsync(subPermission, this.CancellationToken); + + // Assert + var managedPrincipal = await this.AuthManager.For(principalIdentity).GetPrincipalAsync(this.CancellationToken); + + var managedPermission = managedPrincipal.Permissions.Should().ContainSingle().Subject; + + managedPermission.Identity.Should().Be(subPermission.Identity); + managedPermission.DelegatedFrom.Should().Be(subPermission.DelegatedFrom); + } + + [Fact] + public async Task AddRoleAsync_ShouldThrow_WhenDelegatingToOriginalPrincipal() + { + // Arrange + var delegatedFromPermission = new TestPermission(SecurityRole.Administrator) + { Identity = TypedSecurityIdentity.Create(Guid.NewGuid()) }; + + var subPermission = new TestPermission(ExampleRoles.DefaultRole) { DelegatedFrom = delegatedFromPermission.Identity }; + + + var principalIdentity = await this.AuthManager.For("DelegatedFromPrincipal").SetRoleAsync(delegatedFromPermission, this.CancellationToken); + + // Act + var action = () => this.AuthManager.For(principalIdentity).AddRoleAsync(subPermission, this.CancellationToken); + + // Assert + var error = await action.Should().ThrowAsync(); + + error.And.Message.Should().Be("Invalid delegation target: the permission cannot be delegated to its original principal"); + } + + [Fact] + public async Task SetRoleAsync_ShouldPreserveDelegatedFrom_WhenAssigningToChildBusinessUnit() + { + // Arrange + var sourcePrincipalName = "DelegatedFromPrincipal"; + var targetPrincipalName = "TargetPrincipal"; + var sourceBuIdentity = await this.AuthManager.GetSecurityContextIdentityAsync("TestRootBu", this.CancellationToken); + var targetBuIdentity = await this.AuthManager.GetSecurityContextIdentityAsync($"Test{nameof(BusinessUnit)}1", this.CancellationToken); + + var delegatedFromPermission = new TestPermission(ExampleRoles.DefaultRole) + { Identity = TypedSecurityIdentity.Create(Guid.NewGuid()), BusinessUnit = sourceBuIdentity }; + + var subPermission = new TestPermission(ExampleRoles.DefaultRole) + { Identity = TypedSecurityIdentity.Create(Guid.NewGuid()), BusinessUnit = targetBuIdentity, DelegatedFrom = delegatedFromPermission.Identity }; + + + await this.AuthManager.For(sourcePrincipalName).SetRoleAsync(delegatedFromPermission, this.CancellationToken); + + // Act + var principalIdentity = await this.AuthManager.For(targetPrincipalName).SetRoleAsync(subPermission, this.CancellationToken); + + // Assert + var managedPrincipal = await this.AuthManager.For(principalIdentity).GetPrincipalAsync(this.CancellationToken); + + var managedPermission = managedPrincipal.Permissions.Should().ContainSingle().Subject; + + managedPermission.Identity.Should().Be(subPermission.Identity); + managedPermission.DelegatedFrom.Should().Be(subPermission.DelegatedFrom); + managedPermission.Restrictions.Should().BeEquivalentTo(subPermission.Restrictions); + } + + [Fact] + public async Task SetRoleAsync_ShouldThrow_WhenDelegationExceedsSourceBusinessUnit() + { + // Arrange + var sourcePrincipalName = "DelegatedFromPrincipal"; + var targetPrincipalName = "TargetPrincipal"; + var sourceBuIdentity = await this.AuthManager.GetSecurityContextIdentityAsync("TestRootBu", this.CancellationToken); + var invalidObjects = $"{nameof(BusinessUnit)}: Unrestricted"; + + var delegatedFromPermission = new TestPermission(ExampleRoles.DefaultRole) + { Identity = TypedSecurityIdentity.Create(Guid.NewGuid()), BusinessUnit = sourceBuIdentity }; + + var subPermission = new TestPermission(ExampleRoles.DefaultRole) { DelegatedFrom = delegatedFromPermission.Identity }; + + + await this.AuthManager.For(sourcePrincipalName).SetRoleAsync(delegatedFromPermission, this.CancellationToken); + + // Act + var action = () => this.AuthManager.For(targetPrincipalName).SetRoleAsync(subPermission, this.CancellationToken); + + // Assert + var error = await action.Should().ThrowAsync(); + + error.And.Message.Should() + .Be( + $"Invalid security context delegation: the security contexts of \"{targetPrincipalName}\" exceed those granted by \"{sourcePrincipalName}\": {invalidObjects}"); + } + + [Fact] + public async Task SetRoleAsync_WhenTargetBusinessUnitExceedsSource_ShouldFail() + { + // Arrange + var sourcePrincipalName = "DelegatedFromPrincipal"; + var targetPrincipalName = "TargetPrincipal"; + var sourceBuIdentity = await this.AuthManager.GetSecurityContextIdentityAsync($"Test{nameof(BusinessUnit)}1", this.CancellationToken); + var targetBuIdentity = await this.AuthManager.GetSecurityContextIdentityAsync($"Test{nameof(BusinessUnit)}2", this.CancellationToken); + var invalidObjects = $"{nameof(BusinessUnit)}: {targetBuIdentity.Id}"; + + var delegatedFromPermission = new TestPermission(ExampleRoles.DefaultRole) + { Identity = TypedSecurityIdentity.Create(Guid.NewGuid()), BusinessUnit = sourceBuIdentity }; + + var subPermission = new TestPermission(ExampleRoles.DefaultRole) { BusinessUnit = targetBuIdentity, DelegatedFrom = delegatedFromPermission.Identity }; + + + await this.AuthManager.For(sourcePrincipalName).SetRoleAsync(delegatedFromPermission, this.CancellationToken); + + // Act + var action = () => this.AuthManager.For(targetPrincipalName).SetRoleAsync(subPermission, this.CancellationToken); + + // Assert + var error = await action.Should().ThrowAsync(); + + error.And.Message.Should() + .Be( + $"Invalid security context delegation: the security contexts of \"{targetPrincipalName}\" exceed those granted by \"{sourcePrincipalName}\": {invalidObjects}"); + } + + [Fact] + public async Task SetRoleAsync_ShouldThrow_WhenDelegatedRoleIsNotSubsetOfSource() + { + // Arrange + var sourcePrincipalName = "DelegatedFromPrincipal"; + var targetPrincipalName = "TargetPrincipal"; + + var sourceRole = ExampleRoles.DefaultRole; + var targetRole = SecurityRole.Administrator; + + var delegatedFromPermission = new TestPermission(sourceRole) + { Identity = TypedSecurityIdentity.Create(Guid.NewGuid()) }; + + var subPermission = new TestPermission(targetRole) { DelegatedFrom = delegatedFromPermission.Identity }; + + await this.AuthManager.For(sourcePrincipalName).SetRoleAsync(delegatedFromPermission, this.CancellationToken); + + // Act + var action = () => this.AuthManager.For(targetPrincipalName).SetRoleAsync(subPermission, this.CancellationToken); + + // Assert + var error = await action.Should().ThrowAsync(); + + error.And.Message.Should() + .Be( + $"Invalid delegated permission role: the selected role \"{targetRole}\" is not a subset of \"{sourceRole}\""); + } + + [Fact] + public async Task SetRoleAsync_ShouldThrow_WhenDelegatedPeriodIsNotSubsetOfSource() + { + // Arrange + var sourcePrincipalName = "DelegatedFromPrincipal"; + var targetPrincipalName = "TargetPrincipal"; + + var today = DateTime.Today; + + var sourcePeriod = new PermissionPeriod(today, today); + var targetPeriod = PermissionPeriod.Eternity with { StartDate = DateTime.MinValue }; + + var expectedErrorMessage = $"Invalid delegated permission period: the selected period \"{targetPeriod}\" is not a subset of \"{sourcePeriod}\""; + + var delegatedFromPermission = new TestPermission(ExampleRoles.DefaultRole) + { Identity = TypedSecurityIdentity.Create(Guid.NewGuid()), Period = sourcePeriod }; + + var subPermission = new TestPermission(ExampleRoles.DefaultRole) { Period = targetPeriod, DelegatedFrom = delegatedFromPermission.Identity }; + + await this.AuthManager.For(sourcePrincipalName).SetRoleAsync(delegatedFromPermission, this.CancellationToken); + + // Act + var action = () => this.AuthManager.For(targetPrincipalName).SetRoleAsync(subPermission, this.CancellationToken); + + // Assert + var error = await action.Should().ThrowAsync(); + + error.And.Message.Should().Be(expectedErrorMessage); + } +} \ No newline at end of file diff --git a/src/_Example/ExampleApp.IntegrationTests/ExtendedValuePermissionTests.cs b/src/_Example/ExampleApp.IntegrationTests/PermissionExtendedDataTests.cs similarity index 87% rename from src/_Example/ExampleApp.IntegrationTests/ExtendedValuePermissionTests.cs rename to src/_Example/ExampleApp.IntegrationTests/PermissionExtendedDataTests.cs index c5fff39..713b28e 100644 --- a/src/_Example/ExampleApp.IntegrationTests/ExtendedValuePermissionTests.cs +++ b/src/_Example/ExampleApp.IntegrationTests/PermissionExtendedDataTests.cs @@ -4,7 +4,7 @@ namespace ExampleApp.IntegrationTests; -public class ExtendedValuePermissionTests : TestBase +public class PermissionExtendedDataTests : TestBase { [Fact] public async Task SetRoleAsync_WithExtendedValue_ShouldPersistExtendedData() @@ -14,7 +14,7 @@ public async Task SetRoleAsync_WithExtendedValue_ShouldPersistExtendedData() var extendedValue = "abc"; - var testPermission = new TestPermission(ExampleRoles.DefaultRole) { ExtendedValue = extendedValue }.ToManagedPermissionData(); + var testPermission = new TestPermission(ExampleRoles.DefaultRole) { ExtendedValue = extendedValue }; // Act var principalIdentity = await this.AuthManager.For(principalName).SetRoleAsync(testPermission, this.CancellationToken); diff --git a/src/_Example/ExampleApp.IntegrationTests/RestrictionFilterTests.cs b/src/_Example/ExampleApp.IntegrationTests/RestrictionFilterTests.cs index a952fb4..39678f3 100644 --- a/src/_Example/ExampleApp.IntegrationTests/RestrictionFilterTests.cs +++ b/src/_Example/ExampleApp.IntegrationTests/RestrictionFilterTests.cs @@ -67,7 +67,7 @@ public async Task CreatePermissionWithRestrictionFilter_ApplyCorrectBusinessUnit [Fact] - public async Task CreateCustomRestrictionRule_ApplyGrandPermission_OnlyCorrectBuFounded() + public async Task CreateCustomRestrictionRule_ApplyUnrestrictedPermission_OnlyCorrectBuFounded() { // Arrange await this.AuthManager.For(this.testLogin).SetRoleAsync(this.securityRole, this.CancellationToken); @@ -95,7 +95,7 @@ public async Task CreateCustomRestrictionRule_ApplySingleCorrectBU_OnlyCorrectBu } //[Fact] - //public async Task CreateCustomRestrictionRule_SearchAccessorsForGrandPermission_EmployeeFounded() + //public async Task CreateCustomRestrictionRule_SearchAccessorsForUnrestrictedPermission_EmployeeFounded() //{ // // Arrange // await this.AuthManager.For(this.testLogin).SetRoleAsync(this.securityRole, this.CancellationToken); diff --git a/src/__SolutionItems/CommonAssemblyInfo.cs b/src/__SolutionItems/CommonAssemblyInfo.cs index 66cc063..2ace09d 100644 --- a/src/__SolutionItems/CommonAssemblyInfo.cs +++ b/src/__SolutionItems/CommonAssemblyInfo.cs @@ -3,7 +3,7 @@ [assembly: AssemblyProduct("SecuritySystem")] [assembly: AssemblyCompany("IvAt")] -[assembly: AssemblyVersion("2.2.1.0")] +[assembly: AssemblyVersion("2.2.2.0")] [assembly: AssemblyInformationalVersion("changes at build")] #if DEBUG