From 8b6fa0abb2af9ecf60d5017b2efef3773e4bd091 Mon Sep 17 00:00:00 2001 From: iatsuta Date: Wed, 18 Feb 2026 23:23:04 +0100 Subject: [PATCH 1/5] step1 --- src/Directory.Packages.props | 10 +- .../Management/ManagedPermissionData.cs | 2 + .../Management/ManagedPrincipal.cs | 6 +- .../Management/PermissionData.cs | 12 +- .../Management/PrincipalData.cs | 8 +- .../PermissionPeriod.cs | 11 ++ .../ISecurityContextInfoSource.cs | 6 +- .../SecurityRole/ISecurityRoleSource.cs | 6 +- .../SecurityRole/SecurityRoleInfo.cs | 7 +- .../SecurityRule/SecurityRuleExtensions.cs | 7 +- .../Services/ISecurityRolesIdentsResolver.cs | 6 +- .../Handlers/GetPrincipalHandler.cs | 3 +- .../Handlers/UpdatePermissionsHandler.cs | 3 + .../Models/PermissionDto.cs | 2 + .../GeneralPrincipalManagementService.cs | 18 +- .../IPermissionRestrictionLoader.cs | 13 +- .../ManagedPermissionMapper.cs | 29 ++- .../ManagedPrincipalConverter.cs | 10 +- .../PermissionRestrictionTypeFilterFactory.cs | 2 +- .../RawPermissionConverter.cs | 2 +- .../Validation/DisplayPermissionService.cs | 10 +- .../Validation/IDisplayPermissionService.cs | 2 +- .../PermissionDelegationValidator.cs | 172 ++++++++++++++++++ .../Validation/PermissionDataComparer.cs | 2 +- .../PrincipalUniquePermissionValidator.cs | 2 +- .../GeneralPermissionSettings.cs | 11 ++ .../IGeneralPermissionSettings.cs | 3 + .../SecuritySystemSettingsExtensions.cs | 2 + .../Expanders/SecurityOperationExpander.cs | 2 +- .../Expanders/SecurityRoleExpander.cs | 2 +- .../Management/RootPrincipalSourceService.cs | 2 +- .../PermissionBindingInfo.cs | 2 + .../SecurityContextInfoSource.cs | 10 +- .../SecurityRole/SecurityRoleSource.cs | 6 +- .../ClientSecurityRuleResolver.cs | 2 +- .../AvailablePermissionFilterFactory.cs | 8 +- .../Services/IManagedPrincipalConverter.cs | 2 +- .../Services/IPermissionBindingInfoSource.cs | 2 + ...rmissionSecurityRoleIdentsFilterFactory.cs | 8 + ...rmissionSecurityRoleIdentsFilterFactory.cs | 24 +++ .../PrincipalDataSecurityIdentityExtractor.cs | 2 +- .../Services/SecurityRolesIdentsResolver.cs | 9 +- src/SecuritySystem.Testing/TestPermission.cs | 5 +- .../UserCredentialManager.cs | 3 +- .../VirtualPrincipalSourceService.cs | 5 +- .../IVirtualBindingInfoRootSettingsBuilder.cs | 7 +- .../SecuritySystemSettingsExtensions.cs | 4 +- .../VirtualBindingInfoRootSettingsBuilder.cs | 7 + .../InitializedSecurityRoleSource.cs | 8 +- .../Auth/General/Permission.cs | 2 + .../ServiceCollectionExtensions.cs | 1 + .../TestDbContext.cs | 1 + ... => PermissionExtendedDataTests - Copy.cs} | 2 +- .../PermissionExtendedDataTests.cs | 30 +++ src/__SolutionItems/CommonAssemblyInfo.cs | 2 +- 55 files changed, 430 insertions(+), 95 deletions(-) create mode 100644 src/SecuritySystem.GeneralPermission.Runtime/Validation/Permission/PermissionDelegationValidator.cs create mode 100644 src/SecuritySystem.Runtime/Services/IPermissionSecurityRoleIdentsFilterFactory.cs create mode 100644 src/SecuritySystem.Runtime/Services/PermissionSecurityRoleIdentsFilterFactory.cs rename src/_Example/ExampleApp.IntegrationTests/{ExtendedValuePermissionTests.cs => PermissionExtendedDataTests - Copy.cs} (94%) create mode 100644 src/_Example/ExampleApp.IntegrationTests/PermissionExtendedDataTests.cs 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/ManagedPermissionData.cs b/src/SecuritySystem.Abstractions/ExternalSystem/Management/ManagedPermissionData.cs index 836aaba..1208a6d 100644 --- a/src/SecuritySystem.Abstractions/ExternalSystem/Management/ManagedPermissionData.cs +++ b/src/SecuritySystem.Abstractions/ExternalSystem/Management/ManagedPermissionData.cs @@ -10,6 +10,8 @@ public record ManagedPermissionData public string Comment { get; init; } = ""; + public SecurityIdentity DelegatedFrom { get; init; } = SecurityIdentity.Default; + public ImmutableDictionary Restrictions { get; init; } = []; public ImmutableDictionary ExtendedData { get; init; } = []; 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/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..d54c030 100644 --- a/src/SecuritySystem.Configurator/Handlers/UpdatePermissionsHandler.cs +++ b/src/SecuritySystem.Configurator/Handlers/UpdatePermissionsHandler.cs @@ -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..7b2ad4d 100644 --- a/src/SecuritySystem.GeneralPermission.Runtime/GeneralPrincipalManagementService.cs +++ b/src/SecuritySystem.GeneralPermission.Runtime/GeneralPrincipalManagementService.cs @@ -66,9 +66,9 @@ 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); @@ -115,19 +115,11 @@ 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, @@ -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) { 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..0aa9b89 100644 --- a/src/SecuritySystem.GeneralPermission.Runtime/ManagedPermissionMapper.cs +++ b/src/SecuritySystem.GeneralPermission.Runtime/ManagedPermissionMapper.cs @@ -69,7 +69,9 @@ public class PermissionManagementService securityRoleIdentityExtractor, ISecurityIdentityExtractor securityContextTypeIdentityExtractor, - IGenericRepository genericRepository) + ISecurityIdentityExtractor permissionIdentityExtractor, + IGenericRepository genericRepository, + ISecurityRepository permissionRepository) : IPermissionManagementService where TPermission : class, new() @@ -77,7 +79,6 @@ public class PermissionManagementService ToManagedPermissionAsync(TPermission dbPermission, CancellationToken cancellationToken) => new() @@ -87,6 +88,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() }; @@ -113,6 +117,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 => @@ -150,6 +163,18 @@ public async Task> CreatePer throw new SecuritySystemException("wrong typed permission"); } + if (!managedPermission.DelegatedFrom.IsDefault) + { + 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 .SecurityRole .Getter(dbPermission) 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..e140c20 --- /dev/null +++ b/src/SecuritySystem.GeneralPermission.Runtime/Validation/Permission/PermissionDelegationValidator.cs @@ -0,0 +1,172 @@ +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("Permission cannot be delegated to the original user"); + } + + 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. Selected role \"{permissionSecurityRoleResolver.Resolve(subPermission)}\" not subset of \"{permissionSecurityRoleResolver.Resolve(delegatedFrom)}\""); + } + + if (!this.IsCorrectPeriodSubset(subPermission, delegatedFrom)) + { + throw new SecuritySystemValidationException( + $"Invalid delegated permission period. Selected period \"{permissionBindingInfo.GetSafePeriod(subPermission)}\" not subset of \"{permissionBindingInfo.GetSafePeriod(delegatedFrom)}\""); + } + + { + var invalidEntityGroups = this.GetInvalidContextDict(subPermissionData, delegatedFromData).ToList(); + + if (invalidEntityGroups.Any()) + { + throw new SecuritySystemValidationException( + string.Format( + "Can't delegate permission from {0} to {1}, because {0} have no access to objects ({2})", + domainObjectDisplayService.ToString(permissionBindingInfo.Principal.Getter(delegatedFrom)), + domainObjectDisplayService.ToString(permissionBindingInfo.Principal.Getter(subPermission)), + invalidEntityGroups.Join( + " | ", + g => $"{g.Key.Name}: {g.Value.OfType().Join(", ")}"))); + } + } + } + + 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> GetInvalidContextDict( + 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 = expandedChildren.OfType().Except(subRestrictions.Cast()).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/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/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/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/TestPermission.cs b/src/SecuritySystem.Testing/TestPermission.cs index 0461ec4..c58b5cc 100644 --- a/src/SecuritySystem.Testing/TestPermission.cs +++ b/src/SecuritySystem.Testing/TestPermission.cs @@ -26,6 +26,8 @@ public TestPermission() public string Comment { get; set; } = ""; + public SecurityIdentity DelegatedFrom { get; init; } = SecurityIdentity.Default; + public TypedSecurityIdentity? GetSingle() where TSecurityContext : ISecurityContext where TIdent : notnull @@ -57,7 +59,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(); } @@ -81,6 +83,7 @@ public void SetMany(TypedSecurityIdentity[] va SecurityRole = this.SecurityRole ?? throw new InvalidOperationException($"{nameof(this.SecurityRole)} not initialized"), Period = this.Period, Comment = this.Comment, + DelegatedFrom = this.DelegatedFrom, Restrictions = this.Restrictions.Where(pair => pair.Value.Length > 0).ToImmutableDictionary(), ExtendedData = this.ExtendedData.ToImmutableDictionary() }; diff --git a/src/SecuritySystem.Testing/UserCredentialManager.cs b/src/SecuritySystem.Testing/UserCredentialManager.cs index 0fb3079..7853840 100644 --- a/src/SecuritySystem.Testing/UserCredentialManager.cs +++ b/src/SecuritySystem.Testing/UserCredentialManager.cs @@ -40,6 +40,7 @@ public async Task AddUserRoleAsync(ManagedPermissionData[] tes SecurityRole = testPermission.SecurityRole, Period = testPermission.Period, Comment = testPermission.Comment, + DelegatedFrom = testPermission.DelegatedFrom, Restrictions = testPermission.Restrictions, ExtendedData = testPermission.ExtendedData }); @@ -54,7 +55,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/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/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/ExtendedValuePermissionTests.cs b/src/_Example/ExampleApp.IntegrationTests/PermissionExtendedDataTests - Copy.cs similarity index 94% rename from src/_Example/ExampleApp.IntegrationTests/ExtendedValuePermissionTests.cs rename to src/_Example/ExampleApp.IntegrationTests/PermissionExtendedDataTests - Copy.cs index c5fff39..83d80f4 100644 --- a/src/_Example/ExampleApp.IntegrationTests/ExtendedValuePermissionTests.cs +++ b/src/_Example/ExampleApp.IntegrationTests/PermissionExtendedDataTests - Copy.cs @@ -4,7 +4,7 @@ namespace ExampleApp.IntegrationTests; -public class ExtendedValuePermissionTests : TestBase +public class PermissionExtendedDataTests : TestBase { [Fact] public async Task SetRoleAsync_WithExtendedValue_ShouldPersistExtendedData() diff --git a/src/_Example/ExampleApp.IntegrationTests/PermissionExtendedDataTests.cs b/src/_Example/ExampleApp.IntegrationTests/PermissionExtendedDataTests.cs new file mode 100644 index 0000000..83d80f4 --- /dev/null +++ b/src/_Example/ExampleApp.IntegrationTests/PermissionExtendedDataTests.cs @@ -0,0 +1,30 @@ +using ExampleApp.Application; + +using SecuritySystem.Testing; + +namespace ExampleApp.IntegrationTests; + +public class PermissionExtendedDataTests : TestBase +{ + [Fact] + public async Task SetRoleAsync_WithExtendedValue_ShouldPersistExtendedData() + { + // Arrange + var principalName = "TestPrincipal"; + + var extendedValue = "abc"; + + var testPermission = new TestPermission(ExampleRoles.DefaultRole) { ExtendedValue = extendedValue }.ToManagedPermissionData(); + + // Act + var principalIdentity = await this.AuthManager.For(principalName).SetRoleAsync(testPermission, this.CancellationToken); + + // Assert + var managedPrincipal = await this.AuthManager.For(principalIdentity).GetPrincipalAsync(this.CancellationToken); + + var managedPermission = managedPrincipal.Permissions.Should().ContainSingle().Subject; + + managedPermission.ExtendedData.GetValueOrDefault(TestPermissionExtensions.ExtendedKey) + .Should().Be(extendedValue); + } +} \ No newline at end of file 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 From 944d815d29724475c1bf3b087905c4bd999dbec1 Mon Sep 17 00:00:00 2001 From: iatsuta Date: Wed, 18 Feb 2026 23:41:01 +0100 Subject: [PATCH 2/5] upd --- .../FakePrincipalManagementService.cs | 4 ++-- .../Management/IPrincipalManagementService.cs | 4 ++-- .../Handlers/UpdatePermissionsHandler.cs | 4 ++-- .../GeneralPrincipalManagementService.cs | 24 +++++++++---------- .../RootUserCredentialManager.cs | 4 ++-- .../PermissionExtendedDataTests - Copy.cs | 4 ++-- 6 files changed, 22 insertions(+), 22 deletions(-) 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.Configurator/Handlers/UpdatePermissionsHandler.cs b/src/SecuritySystem.Configurator/Handlers/UpdatePermissionsHandler.cs index d54c030..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) { diff --git a/src/SecuritySystem.GeneralPermission.Runtime/GeneralPrincipalManagementService.cs b/src/SecuritySystem.GeneralPermission.Runtime/GeneralPrincipalManagementService.cs index 7b2ad4d..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( @@ -74,12 +74,12 @@ public class GeneralPrincipalManagementService 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>()); @@ -122,23 +122,23 @@ private 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); @@ -172,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.Testing/RootUserCredentialManager.cs b/src/SecuritySystem.Testing/RootUserCredentialManager.cs index e7bfd55..e3543c4 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() @@ -62,7 +62,7 @@ public async Task AddRoleAsync(ManagedPermissionData permissio 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)); + await this.ManagerEvaluator.EvaluateAsync(TestingScopeMode.Write, async manager => await manager.AddUserRoleAsync(permissions, cancellationToken)); public void ClearRoles() { diff --git a/src/_Example/ExampleApp.IntegrationTests/PermissionExtendedDataTests - Copy.cs b/src/_Example/ExampleApp.IntegrationTests/PermissionExtendedDataTests - Copy.cs index 83d80f4..ff57f80 100644 --- a/src/_Example/ExampleApp.IntegrationTests/PermissionExtendedDataTests - Copy.cs +++ b/src/_Example/ExampleApp.IntegrationTests/PermissionExtendedDataTests - Copy.cs @@ -4,10 +4,10 @@ namespace ExampleApp.IntegrationTests; -public class PermissionExtendedDataTests : TestBase +public class PermissionDelegatedFromTests : TestBase { [Fact] - public async Task SetRoleAsync_WithExtendedValue_ShouldPersistExtendedData() + public async Task Tet() { // Arrange var principalName = "TestPrincipal"; From 6db58734c20008bac2a62167d917dbefeb5fec06 Mon Sep 17 00:00:00 2001 From: iatsuta Date: Thu, 19 Feb 2026 00:08:32 +0100 Subject: [PATCH 3/5] upd --- .../Management/ManagedPermission.cs | 34 +++++++++++++++++-- .../Management/ManagedPermissionData.cs | 20 ----------- .../ManagedPermissionDataExtensions.cs | 16 --------- .../SecurityAccessor/SecurityAccessorData.cs | 8 +++-- .../ManagedPermissionMapper.cs | 33 +++++++++++++----- .../Providers/RoleBaseSecurityPathProvider.cs | 2 +- .../AdministratorsRoleList.cs | 6 ++-- .../RootUserCredentialManager.cs | 14 ++++---- src/SecuritySystem.Testing/TestPermission.cs | 10 ++++-- .../UserCredentialManager.cs | 14 +------- .../ExtendedPermissionManagementService.cs | 10 +++--- 11 files changed, 86 insertions(+), 81 deletions(-) delete mode 100644 src/SecuritySystem.Abstractions/ExternalSystem/Management/ManagedPermissionData.cs delete mode 100644 src/SecuritySystem.Abstractions/ExternalSystem/Management/ManagedPermissionDataExtensions.cs 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 1208a6d..0000000 --- a/src/SecuritySystem.Abstractions/ExternalSystem/Management/ManagedPermissionData.cs +++ /dev/null @@ -1,20 +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 SecurityIdentity DelegatedFrom { get; init; } = SecurityIdentity.Default; - - 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/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.GeneralPermission.Runtime/ManagedPermissionMapper.cs b/src/SecuritySystem.GeneralPermission.Runtime/ManagedPermissionMapper.cs index 0aa9b89..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, @@ -70,6 +76,7 @@ public class PermissionManagementService securityRoleIdentityExtractor, ISecurityIdentityExtractor securityContextTypeIdentityExtractor, ISecurityIdentityExtractor permissionIdentityExtractor, + ISecurityIdentityConverter permissionIdentityConverter, IGenericRepository genericRepository, ISecurityRepository permissionRepository) : IPermissionManagementService @@ -79,6 +86,7 @@ public class PermissionManagementService ToManagedPermissionAsync(TPermission dbPermission, CancellationToken cancellationToken) => new() @@ -99,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); @@ -110,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); @@ -158,9 +173,9 @@ public async Task> CreatePer ManagedPermission managedPermission, CancellationToken cancellationToken) { - if (managedPermission.Identity.IsDefault || managedPermission.IsVirtual) + if (managedPermission.IsVirtual || managedPermission.Identity.IsDefault) { - throw new SecuritySystemException("wrong typed permission"); + throw new SecuritySystemException("wrong permission"); } if (!managedPermission.DelegatedFrom.IsDefault) 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.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 e3543c4..ddba4ea 100644 --- a/src/SecuritySystem.Testing/RootUserCredentialManager.cs +++ b/src/SecuritySystem.Testing/RootUserCredentialManager.cs @@ -34,34 +34,34 @@ 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) => + 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 c58b5cc..8152ab5 100644 --- a/src/SecuritySystem.Testing/TestPermission.cs +++ b/src/SecuritySystem.Testing/TestPermission.cs @@ -26,6 +26,8 @@ public TestPermission() public string Comment { get; set; } = ""; + public SecurityIdentity Identity { get; init; } = SecurityIdentity.Default; + public SecurityIdentity DelegatedFrom { get; init; } = SecurityIdentity.Default; public TypedSecurityIdentity? GetSingle() @@ -78,15 +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 7853840..84e4c50 100644 --- a/src/SecuritySystem.Testing/UserCredentialManager.cs +++ b/src/SecuritySystem.Testing/UserCredentialManager.cs @@ -31,20 +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, - DelegatedFrom = testPermission.DelegatedFrom, - Restrictions = testPermission.Restrictions, - ExtendedData = testPermission.ExtendedData - }); - var existsPrincipal = await principalSourceService.TryGetPrincipalAsync(this.userCredential, cancellationToken); if (existsPrincipal == null) 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); From 8d659b923dda6a2ee0ca4d4e552a0bd915829c4a Mon Sep 17 00:00:00 2001 From: iatsuta Date: Thu, 19 Feb 2026 01:21:42 +0100 Subject: [PATCH 4/5] upd --- .../PermissionDelegationValidator.cs | 27 +++-- .../GeneralPermissionTests.cs | 24 +++- .../PermissionDelegatedFromTests.cs | 110 ++++++++++++++++++ .../PermissionExtendedDataTests - Copy.cs | 30 ----- .../PermissionExtendedDataTests.cs | 2 +- 5 files changed, 148 insertions(+), 45 deletions(-) create mode 100644 src/_Example/ExampleApp.IntegrationTests/PermissionDelegatedFromTests.cs delete mode 100644 src/_Example/ExampleApp.IntegrationTests/PermissionExtendedDataTests - Copy.cs diff --git a/src/SecuritySystem.GeneralPermission.Runtime/Validation/Permission/PermissionDelegationValidator.cs b/src/SecuritySystem.GeneralPermission.Runtime/Validation/Permission/PermissionDelegationValidator.cs index e140c20..efad568 100644 --- a/src/SecuritySystem.GeneralPermission.Runtime/Validation/Permission/PermissionDelegationValidator.cs +++ b/src/SecuritySystem.GeneralPermission.Runtime/Validation/Permission/PermissionDelegationValidator.cs @@ -64,7 +64,7 @@ public async Task ValidateAsync(PermissionData $"{g.Key.Name}: {g.Value.OfType().Join(", ")}"))); + g => + { + var invalidValues = g.Value.Length == 0 + ? "Global Access" + : g.Value.OfType().Join(", "); + + return $"{g.Key.Name}: {invalidValues}"; + }))); } } } @@ -132,7 +139,7 @@ private bool IsCorrectPeriodSubset(TPermission subPermission, TPermission delega return permissionBindingInfo.GetSafePeriod(delegatedFrom).Contains(permissionBindingInfo.GetSafePeriod(subPermission)); } - private IEnumerable> GetInvalidContextDict( + private IEnumerable> GetInvalidSecurityContextDict( PermissionData subPermissionData, PermissionData delegatedFromData) { @@ -158,7 +165,7 @@ private IEnumerable> GetInvalidContextD .Create(securityContextInfo.Type) .Expand(delegatedFromRestrictions, HierarchicalExpandType.Children); - var missedAccessElements = expandedChildren.OfType().Except(subRestrictions.Cast()).ToArray(); + var missedAccessElements = subRestrictions.Cast().Except(expandedChildren.OfType()).ToArray(); if (missedAccessElements.Length > 0) { 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..74a6a85 --- /dev/null +++ b/src/_Example/ExampleApp.IntegrationTests/PermissionDelegatedFromTests.cs @@ -0,0 +1,110 @@ +using ExampleApp.Application; +using ExampleApp.Domain; + +using SecuritySystem; +using SecuritySystem.Testing; +using SecuritySystem.Validation; + +namespace ExampleApp.IntegrationTests; + +public class PermissionDelegationFromTests : TestBase +{ + [Fact] + public async Task SetRoleAsync_ShouldPreserveDelegatedFromIdentity() + { + // Arrange + var delegatedFromPermission = new TestPermission(ExampleRoles.DefaultRole) { Identity = TypedSecurityIdentity.Create(Guid.NewGuid()) }; + + await this.AuthManager.For("DelegatedFromPrincipal").SetRoleAsync(delegatedFromPermission, this.CancellationToken); + + var subPermission = new TestPermission(ExampleRoles.DefaultRole) + { Identity = TypedSecurityIdentity.Create(Guid.NewGuid()), DelegatedFrom = delegatedFromPermission.Identity }; + + // Act + var principalIdentity = await this.AuthManager.For("TargetPrincipal").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 principalIdentity = await this.AuthManager.For("DelegatedFromPrincipal").SetRoleAsync(delegatedFromPermission, this.CancellationToken); + + var subPermission = new TestPermission(ExampleRoles.DefaultRole) + { Identity = TypedSecurityIdentity.Create(Guid.NewGuid()), DelegatedFrom = delegatedFromPermission.Identity }; + + // 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 rootBuIdentity = await this.AuthManager.GetSecurityContextIdentityAsync("TestRootBu", this.CancellationToken); + var childBuIdentity = + await this.AuthManager.GetSecurityContextIdentityAsync($"Test{nameof(BusinessUnit)}1", this.CancellationToken); + + var delegatedFromPermission = new TestPermission(ExampleRoles.DefaultRole) + { Identity = TypedSecurityIdentity.Create(Guid.NewGuid()), BusinessUnit = rootBuIdentity }; + + await this.AuthManager.For("DelegatedFromPrincipal").SetRoleAsync(delegatedFromPermission, this.CancellationToken); + + var subPermission = new TestPermission(ExampleRoles.DefaultRole) + { Identity = TypedSecurityIdentity.Create(Guid.NewGuid()), BusinessUnit = childBuIdentity, DelegatedFrom = delegatedFromPermission.Identity }; + + // Act + var principalIdentity = await this.AuthManager.For("TargetPrincipal").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 Test4() + { + // Arrange + var rootBuIdentity = await this.AuthManager.GetSecurityContextIdentityAsync("TestRootBu", this.CancellationToken); + + var delegatedFromPermission = new TestPermission(ExampleRoles.BuManager) + { Identity = TypedSecurityIdentity.Create(Guid.NewGuid()), BusinessUnit = rootBuIdentity }; + + await this.AuthManager.For("DelegatedFromPrincipal").SetRoleAsync(delegatedFromPermission, this.CancellationToken); + + var subPermission = new TestPermission(ExampleRoles.BuManager) + { Identity = TypedSecurityIdentity.Create(Guid.NewGuid()), DelegatedFrom = delegatedFromPermission.Identity }; + + // Act + var action = () => this.AuthManager.For("TargetPrincipal").SetRoleAsync(subPermission, this.CancellationToken); + + // Assert + var error = await action.Should().ThrowAsync(); + + error.And.Message.Should() + .Be( + $"Invalid security context delegation: the source principal \"{0}\" does not have access to the following security contexts required for delegation to \"{1}\": {2}"); + } +} \ No newline at end of file diff --git a/src/_Example/ExampleApp.IntegrationTests/PermissionExtendedDataTests - Copy.cs b/src/_Example/ExampleApp.IntegrationTests/PermissionExtendedDataTests - Copy.cs deleted file mode 100644 index ff57f80..0000000 --- a/src/_Example/ExampleApp.IntegrationTests/PermissionExtendedDataTests - Copy.cs +++ /dev/null @@ -1,30 +0,0 @@ -using ExampleApp.Application; - -using SecuritySystem.Testing; - -namespace ExampleApp.IntegrationTests; - -public class PermissionDelegatedFromTests : TestBase -{ - [Fact] - public async Task Tet() - { - // Arrange - var principalName = "TestPrincipal"; - - var extendedValue = "abc"; - - var testPermission = new TestPermission(ExampleRoles.DefaultRole) { ExtendedValue = extendedValue }.ToManagedPermissionData(); - - // Act - var principalIdentity = await this.AuthManager.For(principalName).SetRoleAsync(testPermission, this.CancellationToken); - - // Assert - var managedPrincipal = await this.AuthManager.For(principalIdentity).GetPrincipalAsync(this.CancellationToken); - - var managedPermission = managedPrincipal.Permissions.Should().ContainSingle().Subject; - - managedPermission.ExtendedData.GetValueOrDefault(TestPermissionExtensions.ExtendedKey) - .Should().Be(extendedValue); - } -} \ No newline at end of file diff --git a/src/_Example/ExampleApp.IntegrationTests/PermissionExtendedDataTests.cs b/src/_Example/ExampleApp.IntegrationTests/PermissionExtendedDataTests.cs index 83d80f4..713b28e 100644 --- a/src/_Example/ExampleApp.IntegrationTests/PermissionExtendedDataTests.cs +++ b/src/_Example/ExampleApp.IntegrationTests/PermissionExtendedDataTests.cs @@ -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); From fa09c8ce9ddbc9fe29b82581180ac8bc55fcf885 Mon Sep 17 00:00:00 2001 From: iatsuta Date: Thu, 19 Feb 2026 11:43:29 +0100 Subject: [PATCH 5/5] upd --- .../PermissionDelegationValidator.cs | 4 +- .../AccessorsBuilder/ByIdentsFilterBuilder.cs | 10 +- .../ByIdentsFilterBuilder.cs | 4 +- .../ManyContextFilterBuilder.cs | 4 +- .../SingleContextFilterBuilder.cs | 4 +- .../QueryBuilder/ManyContextFilterBuilder.cs | 14 +- .../SingleContextFilterBuilder.cs | 10 +- .../IPermissionRestrictionSource.cs | 4 +- .../VirtualPermissionRestrictionSource.cs | 4 +- .../PermissionDelegatedFromTests.cs | 141 +++++++++++++++--- .../RestrictionFilterTests.cs | 4 +- 11 files changed, 149 insertions(+), 54 deletions(-) diff --git a/src/SecuritySystem.GeneralPermission.Runtime/Validation/Permission/PermissionDelegationValidator.cs b/src/SecuritySystem.GeneralPermission.Runtime/Validation/Permission/PermissionDelegationValidator.cs index efad568..5bd4810 100644 --- a/src/SecuritySystem.GeneralPermission.Runtime/Validation/Permission/PermissionDelegationValidator.cs +++ b/src/SecuritySystem.GeneralPermission.Runtime/Validation/Permission/PermissionDelegationValidator.cs @@ -111,7 +111,7 @@ private void ValidatePermissionDelegatedFrom( { throw new SecuritySystemValidationException( string.Format( - "Invalid security context delegation: the source principal \"{0}\" does not have access to the following security contexts required for delegation to \"{1}\": {2}", + "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( @@ -119,7 +119,7 @@ private void ValidatePermissionDelegatedFrom( g => { var invalidValues = g.Value.Length == 0 - ? "Global Access" + ? "Unrestricted" : g.Value.OfType().Join(", "); return $"{g.Key.Name}: {invalidValues}"; 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/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.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/_Example/ExampleApp.IntegrationTests/PermissionDelegatedFromTests.cs b/src/_Example/ExampleApp.IntegrationTests/PermissionDelegatedFromTests.cs index 74a6a85..5e39978 100644 --- a/src/_Example/ExampleApp.IntegrationTests/PermissionDelegatedFromTests.cs +++ b/src/_Example/ExampleApp.IntegrationTests/PermissionDelegatedFromTests.cs @@ -2,6 +2,7 @@ using ExampleApp.Domain; using SecuritySystem; +using SecuritySystem.GeneralPermission; using SecuritySystem.Testing; using SecuritySystem.Validation; @@ -13,15 +14,18 @@ public class PermissionDelegationFromTests : TestBase public async Task SetRoleAsync_ShouldPreserveDelegatedFromIdentity() { // Arrange + var sourcePrincipalName = "DelegatedFromPrincipal"; + var targetPrincipalName = "TargetPrincipal"; var delegatedFromPermission = new TestPermission(ExampleRoles.DefaultRole) { Identity = TypedSecurityIdentity.Create(Guid.NewGuid()) }; - await this.AuthManager.For("DelegatedFromPrincipal").SetRoleAsync(delegatedFromPermission, this.CancellationToken); - 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("TargetPrincipal").SetRoleAsync(subPermission, this.CancellationToken); + var principalIdentity = await this.AuthManager.For(targetPrincipalName).SetRoleAsync(subPermission, this.CancellationToken); // Assert var managedPrincipal = await this.AuthManager.For(principalIdentity).GetPrincipalAsync(this.CancellationToken); @@ -39,10 +43,10 @@ public async Task AddRoleAsync_ShouldThrow_WhenDelegatingToOriginalPrincipal() var delegatedFromPermission = new TestPermission(SecurityRole.Administrator) { Identity = TypedSecurityIdentity.Create(Guid.NewGuid()) }; - var principalIdentity = await this.AuthManager.For("DelegatedFromPrincipal").SetRoleAsync(delegatedFromPermission, this.CancellationToken); + var subPermission = new TestPermission(ExampleRoles.DefaultRole) { DelegatedFrom = delegatedFromPermission.Identity }; - var subPermission = new TestPermission(ExampleRoles.DefaultRole) - { Identity = TypedSecurityIdentity.Create(Guid.NewGuid()), 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); @@ -57,20 +61,22 @@ public async Task AddRoleAsync_ShouldThrow_WhenDelegatingToOriginalPrincipal() public async Task SetRoleAsync_ShouldPreserveDelegatedFrom_WhenAssigningToChildBusinessUnit() { // Arrange - var rootBuIdentity = await this.AuthManager.GetSecurityContextIdentityAsync("TestRootBu", this.CancellationToken); - var childBuIdentity = - await this.AuthManager.GetSecurityContextIdentityAsync($"Test{nameof(BusinessUnit)}1", this.CancellationToken); + 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 = rootBuIdentity }; - - await this.AuthManager.For("DelegatedFromPrincipal").SetRoleAsync(delegatedFromPermission, this.CancellationToken); + { Identity = TypedSecurityIdentity.Create(Guid.NewGuid()), BusinessUnit = sourceBuIdentity }; var subPermission = new TestPermission(ExampleRoles.DefaultRole) - { Identity = TypedSecurityIdentity.Create(Guid.NewGuid()), BusinessUnit = childBuIdentity, DelegatedFrom = delegatedFromPermission.Identity }; + { 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("TargetPrincipal").SetRoleAsync(subPermission, this.CancellationToken); + var principalIdentity = await this.AuthManager.For(targetPrincipalName).SetRoleAsync(subPermission, this.CancellationToken); // Assert var managedPrincipal = await this.AuthManager.For(principalIdentity).GetPrincipalAsync(this.CancellationToken); @@ -82,29 +88,118 @@ public async Task SetRoleAsync_ShouldPreserveDelegatedFrom_WhenAssigningToChildB 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 Test4() + public async Task SetRoleAsync_WhenTargetBusinessUnitExceedsSource_ShouldFail() { // Arrange - var rootBuIdentity = await this.AuthManager.GetSecurityContextIdentityAsync("TestRootBu", this.CancellationToken); + 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.BuManager) - { Identity = TypedSecurityIdentity.Create(Guid.NewGuid()), BusinessUnit = rootBuIdentity }; + var delegatedFromPermission = new TestPermission(ExampleRoles.DefaultRole) + { Identity = TypedSecurityIdentity.Create(Guid.NewGuid()), BusinessUnit = sourceBuIdentity }; - await this.AuthManager.For("DelegatedFromPrincipal").SetRoleAsync(delegatedFromPermission, this.CancellationToken); + var subPermission = new TestPermission(ExampleRoles.DefaultRole) { BusinessUnit = targetBuIdentity, DelegatedFrom = delegatedFromPermission.Identity }; - var subPermission = new TestPermission(ExampleRoles.BuManager) - { Identity = TypedSecurityIdentity.Create(Guid.NewGuid()), DelegatedFrom = delegatedFromPermission.Identity }; + + await this.AuthManager.For(sourcePrincipalName).SetRoleAsync(delegatedFromPermission, this.CancellationToken); // Act - var action = () => this.AuthManager.For("TargetPrincipal").SetRoleAsync(subPermission, this.CancellationToken); + 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 source principal \"{0}\" does not have access to the following security contexts required for delegation to \"{1}\": {2}"); + $"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/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);