diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props index d7c1557..3040008 100644 --- a/src/Directory.Packages.props +++ b/src/Directory.Packages.props @@ -3,24 +3,24 @@ true - - - - - - - - - - + + + + + + + + + + - - - - - - + + + + + + diff --git a/src/SecuritySystem.Abstractions/ExternalSystem/Management/ManagedPermissionData.cs b/src/SecuritySystem.Abstractions/ExternalSystem/Management/ManagedPermissionData.cs index 9be7bfd..836aaba 100644 --- a/src/SecuritySystem.Abstractions/ExternalSystem/Management/ManagedPermissionData.cs +++ b/src/SecuritySystem.Abstractions/ExternalSystem/Management/ManagedPermissionData.cs @@ -1,4 +1,6 @@ -namespace SecuritySystem.ExternalSystem.Management; +using System.Collections.Immutable; + +namespace SecuritySystem.ExternalSystem.Management; public record ManagedPermissionData { @@ -8,7 +10,9 @@ public record ManagedPermissionData public string Comment { get; init; } = ""; - public IReadOnlyDictionary Restrictions { get; init; } = new Dictionary(); + 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 new file mode 100644 index 0000000..af635d5 --- /dev/null +++ b/src/SecuritySystem.Abstractions/ExternalSystem/Management/ManagedPermissionDataExtensions.cs @@ -0,0 +1,16 @@ +using System.Collections.Immutable; + +namespace SecuritySystem.ExternalSystem.Management; + +public static class ManagedPermissionDataExtensions +{ + public static TManagedPermissionData WithExtendedData(this TManagedPermissionData managedPermissionData, string key, object value) + where TManagedPermissionData : ManagedPermissionData + { + var newExtendedData = managedPermissionData.ExtendedData.ToDictionary(); + + newExtendedData[key] = value; + + return managedPermissionData with { ExtendedData = newExtendedData.ToImmutableDictionary() }; + } +} \ No newline at end of file diff --git a/src/SecuritySystem.Abstractions/ExternalSystem/Management/ManagedPrincipalHeader.cs b/src/SecuritySystem.Abstractions/ExternalSystem/Management/ManagedPrincipalHeader.cs index a0a5701..1d7c1db 100644 --- a/src/SecuritySystem.Abstractions/ExternalSystem/Management/ManagedPrincipalHeader.cs +++ b/src/SecuritySystem.Abstractions/ExternalSystem/Management/ManagedPrincipalHeader.cs @@ -1,3 +1,3 @@ namespace SecuritySystem.ExternalSystem.Management; -public record ManagedPrincipalHeader(SecurityIdentity Identity, string Name, bool IsVirtual); +public record ManagedPrincipalHeader(SecurityIdentity Identity, string Name, bool IsVirtual); \ No newline at end of file diff --git a/src/SecuritySystem.Configurator/Handlers/UpdatePermissionsHandler.cs b/src/SecuritySystem.Configurator/Handlers/UpdatePermissionsHandler.cs index 9929c1b..de4bdeb 100644 --- a/src/SecuritySystem.Configurator/Handlers/UpdatePermissionsHandler.cs +++ b/src/SecuritySystem.Configurator/Handlers/UpdatePermissionsHandler.cs @@ -1,4 +1,6 @@ -using Microsoft.AspNetCore.Http; +using CommonFramework; + +using Microsoft.AspNetCore.Http; using SecuritySystem.Attributes; using SecuritySystem.Configurator.Interfaces; @@ -64,7 +66,7 @@ from restriction in permission.Contexts SecurityRole = securityRoleSource.GetSecurityRole(new UntypedSecurityIdentity(permission.RoleId)), Period = new PermissionPeriod(permission.StartDate, permission.EndDate), Comment = permission.Comment, - Restrictions = restrictionsRequest.ToDictionary() + Restrictions = restrictionsRequest.ToImmutableDictionary() }; } diff --git a/src/SecuritySystem.DiTests/Services/ExamplePermissionSource.cs b/src/SecuritySystem.DiTests/Services/ExamplePermissionSource.cs index 5f83200..82487ff 100644 --- a/src/SecuritySystem.DiTests/Services/ExamplePermissionSource.cs +++ b/src/SecuritySystem.DiTests/Services/ExamplePermissionSource.cs @@ -4,7 +4,7 @@ namespace SecuritySystem.DiTests.Services; -public class ExamplePermissionSource(TestPermissionData data, DomainSecurityRule.ExpandedRoleGroupSecurityRule securityRule) : IPermissionSource +public class ExamplePermissionSource(TestPermissions data, DomainSecurityRule.ExpandedRoleGroupSecurityRule securityRule) : IPermissionSource { public bool HasAccess() => throw new NotImplementedException(); diff --git a/src/SecuritySystem.DiTests/Services/ExamplePermissionSystem.cs b/src/SecuritySystem.DiTests/Services/ExamplePermissionSystem.cs index f0cd340..54fbc7c 100644 --- a/src/SecuritySystem.DiTests/Services/ExamplePermissionSystem.cs +++ b/src/SecuritySystem.DiTests/Services/ExamplePermissionSystem.cs @@ -3,7 +3,7 @@ namespace SecuritySystem.DiTests.Services; -public class ExamplePermissionSystem(ISecurityRuleExpander securityRuleExpander, TestPermissionData data) : IPermissionSystem +public class ExamplePermissionSystem(ISecurityRuleExpander securityRuleExpander, TestPermissions data) : IPermissionSystem { public Type PermissionType => throw new NotImplementedException(); diff --git a/src/SecuritySystem.DiTests/Services/ExamplePermissionSystemFactory.cs b/src/SecuritySystem.DiTests/Services/ExamplePermissionSystemFactory.cs index 4168396..bae08fc 100644 --- a/src/SecuritySystem.DiTests/Services/ExamplePermissionSystemFactory.cs +++ b/src/SecuritySystem.DiTests/Services/ExamplePermissionSystemFactory.cs @@ -3,7 +3,7 @@ namespace SecuritySystem.DiTests.Services; -public class ExamplePermissionSystemFactory(ISecurityRuleExpander securityRuleExpander, TestPermissionData data) : IPermissionSystemFactory +public class ExamplePermissionSystemFactory(ISecurityRuleExpander securityRuleExpander, TestPermissions data) : IPermissionSystemFactory { public IPermissionSystem Create(SecurityRuleCredential securityRuleCredential) => new ExamplePermissionSystem(securityRuleExpander, data); diff --git a/src/SecuritySystem.DiTests/Services/TestPermissionData.cs b/src/SecuritySystem.DiTests/Services/TestPermissionData.cs deleted file mode 100644 index 0d8114b..0000000 --- a/src/SecuritySystem.DiTests/Services/TestPermissionData.cs +++ /dev/null @@ -1,3 +0,0 @@ -namespace SecuritySystem.DiTests.Services; - -public record TestPermissionData(List Permissions); diff --git a/src/SecuritySystem.DiTests/Services/TestPermissions.cs b/src/SecuritySystem.DiTests/Services/TestPermissions.cs new file mode 100644 index 0000000..a6cf187 --- /dev/null +++ b/src/SecuritySystem.DiTests/Services/TestPermissions.cs @@ -0,0 +1,3 @@ +namespace SecuritySystem.DiTests.Services; + +public record TestPermissions(List Permissions); diff --git a/src/SecuritySystem.DiTests/TestBase.cs b/src/SecuritySystem.DiTests/TestBase.cs index e812f0e..0cb22bf 100644 --- a/src/SecuritySystem.DiTests/TestBase.cs +++ b/src/SecuritySystem.DiTests/TestBase.cs @@ -94,6 +94,6 @@ protected virtual IServiceCollection CreateServices(IServiceCollection serviceCo .AddRelativeDomainPath((Employee employee) => employee) .AddSingleton(typeof(TestCheckboxConditionFactory<>)) - .AddSingleton(_ => new TestPermissionData(this.GetPermissions().ToList())); + .AddSingleton(_ => new TestPermissions(this.GetPermissions().ToList())); } } \ No newline at end of file diff --git a/src/SecuritySystem.GeneralPermission.Runtime/GeneralPrincipalManagementService.cs b/src/SecuritySystem.GeneralPermission.Runtime/GeneralPrincipalManagementService.cs index badafc3..b8b0b16 100644 --- a/src/SecuritySystem.GeneralPermission.Runtime/GeneralPrincipalManagementService.cs +++ b/src/SecuritySystem.GeneralPermission.Runtime/GeneralPrincipalManagementService.cs @@ -2,8 +2,6 @@ using CommonFramework.GenericRepository; using CommonFramework.VisualIdentitySource; -using GenericQueryable; - using Microsoft.Extensions.DependencyInjection; using SecuritySystem.Credential; @@ -18,35 +16,24 @@ public class GeneralPrincipalManagementService( IServiceProxyFactory serviceProxyFactory, IVisualIdentityInfoSource visualIdentityInfoSource, IEnumerable bindingInfoList, - IGeneralPermissionBindingInfoSource generalBindingInfoSource, IGeneralPermissionRestrictionBindingInfoSource restrictionBindingInfoSource) : IPrincipalManagementService { private readonly Lazy lazyInnerService = new(() => { - var bindingInfo = bindingInfoList.Single(bi => !bi.IsReadonly); - - var generalBindingInfo = generalBindingInfoSource.GetForPermission(bindingInfo.PermissionType); + var bindingInfo = bindingInfoList.Single( + bi => !bi.IsReadonly, + () => new InvalidOperationException("No writable management service was found"), + () => new InvalidOperationException("Multiple writable management services were found")); var restrictionBindingInfo = restrictionBindingInfoSource.GetForPermission(bindingInfo.PermissionType); var principalVisualIdentityInfo = visualIdentityInfoSource.GetVisualIdentityInfo(bindingInfo.PrincipalType); - var innerServiceType = typeof(GeneralPrincipalManagementService<,,,,,>) - .MakeGenericType( - bindingInfo.PrincipalType, - bindingInfo.PermissionType, - generalBindingInfo.SecurityRoleType, - restrictionBindingInfo.PermissionRestrictionType, - restrictionBindingInfo.SecurityContextTypeType, - restrictionBindingInfo.SecurityContextObjectIdentType); - - return serviceProxyFactory.Create( - innerServiceType, - bindingInfo, - generalBindingInfo, - restrictionBindingInfo, - principalVisualIdentityInfo); + var innerServiceType = typeof(GeneralPrincipalManagementService<,,>) + .MakeGenericType(bindingInfo.PrincipalType, bindingInfo.PermissionType, restrictionBindingInfo.PermissionRestrictionType); + + return serviceProxyFactory.Create(innerServiceType, principalVisualIdentityInfo); }); private IPrincipalManagementService InnerService => this.lazyInnerService.Value; @@ -67,34 +54,21 @@ public Task> UpdatePermissionsAsync( this.InnerService.UpdatePermissionsAsync(userCredential, typedPermissions, cancellationToken); } -public class GeneralPrincipalManagementService( - PermissionBindingInfo bindingInfo, - GeneralPermissionBindingInfo generalBindingInfo, - GeneralPermissionRestrictionBindingInfo restrictionBindingInfo, - ISecurityRepository securityRoleRepository, - IQueryableSource queryableSource, +public class GeneralPrincipalManagementService( [FromKeyedServices("Root")] IPrincipalValidator principalValidator, - ISecurityRoleSource securityRoleSource, IGenericRepository genericRepository, - ISecurityRepository securityContextTypeRepository, - ISecurityContextInfoSource securityContextInfoSource, IPrincipalDomainService principalDomainService, IUserSource principalUserSource, ISecurityIdentityExtractor permissionIdentityExtractor, - ISecurityIdentityExtractor securityRoleIdentityExtractor, - ISecurityIdentityExtractor securityContextTypeIdentityExtractor, IPermissionLoader permissionLoader, IPermissionRestrictionLoader permissionRestrictionLoader, + IPermissionManagementService permissionManagementService, VisualIdentityInfo principalVisualIdentityInfo) : IPrincipalManagementService where TPrincipal : class, new() where TPermission : class, new() - where TSecurityRole : class where TPermissionRestriction : class, new() - where TSecurityContextType : class - where TSecurityContextObjectIdent : notnull { public Type PrincipalType { get; } = typeof(TPrincipal); @@ -209,141 +183,13 @@ private async Task[]> Create IEnumerable typedPermissions, CancellationToken cancellationToken) { - return await typedPermissions.SyncWhenAll(managedPermission => this.CreatePermissionAsync(dbPrincipal, managedPermission, cancellationToken)); - } - - private async Task> CreatePermissionAsync( - TPrincipal dbPrincipal, - ManagedPermission managedPermission, - CancellationToken cancellationToken) - { - if (!managedPermission.Identity.IsDefault || managedPermission.IsVirtual) - { - throw new SecuritySystemException("wrong typed permission"); - } - - var securityRole = securityRoleSource.GetSecurityRole(managedPermission.SecurityRole); - - var dbRole = await securityRoleRepository.GetObjectAsync(securityRole.Identity, cancellationToken); - - var newDbPermission = new TPermission(); - - bindingInfo.Principal.Setter(newDbPermission, dbPrincipal); - generalBindingInfo.SecurityRole.Setter(newDbPermission, dbRole); - - bindingInfo.PermissionStartDate?.Setter(newDbPermission, managedPermission.Period.StartDate); - bindingInfo.PermissionEndDate?.Setter(newDbPermission, managedPermission.Period.EndDate); - bindingInfo.PermissionComment?.Setter(newDbPermission, managedPermission.Comment); - - await genericRepository.SaveAsync(newDbPermission, cancellationToken); - - var newPermissionRestrictions = await managedPermission.Restrictions.SyncWhenAll(async restrictionGroup => - { - var securityContextTypeIdentity = securityContextInfoSource.GetSecurityContextInfo(restrictionGroup.Key).Identity; - - var dbSecurityContextType = await securityContextTypeRepository.GetObjectAsync(securityContextTypeIdentity, cancellationToken); - - var newPermissionRestrictions = await restrictionGroup.Value.Cast().SyncWhenAll(async securityContextId => - { - var newDbPermissionRestriction = new TPermissionRestriction(); - - restrictionBindingInfo.Permission.Setter(newDbPermissionRestriction, newDbPermission); - restrictionBindingInfo.SecurityContextObjectId.Setter(newDbPermissionRestriction, securityContextId); - restrictionBindingInfo.SecurityContextType.Setter(newDbPermissionRestriction, dbSecurityContextType); - - await genericRepository.SaveAsync(newDbPermissionRestriction, cancellationToken); - - return newDbPermissionRestriction; - }); - - return newPermissionRestrictions; - }); - - return new PermissionData(newDbPermission, newPermissionRestrictions.SelectMany()); + return await typedPermissions.SyncWhenAll(managedPermission => permissionManagementService.CreatePermissionAsync(dbPrincipal, managedPermission, cancellationToken)); } private async Task<(PermissionData PermissonData, bool Updated)[]> UpdatePermissionsAsync( IReadOnlyList<(TPermission, ManagedPermission)> permissionPairs, CancellationToken cancellationToken) { - return await permissionPairs.SyncWhenAll(permissionPair => this.UpdatePermission(permissionPair.Item1, permissionPair.Item2, cancellationToken)); - } - - private async Task<(PermissionData PermissonData, bool Updated)> UpdatePermission(TPermission dbPermission, - ManagedPermission managedPermission, CancellationToken cancellationToken) - { - if (managedPermission.Identity.IsDefault || managedPermission.IsVirtual) - { - throw new SecuritySystemException("wrong typed permission"); - } - - var securityRole = generalBindingInfo - .SecurityRole - .Getter(dbPermission) - .Pipe(securityRoleIdentityExtractor.Extract) - .Pipe(securityRoleSource.GetSecurityRole); - - if (securityRole != managedPermission.SecurityRole) - { - throw new SecuritySystemException("Permission role can't be changed"); - } - - var dbRestrictions = await queryableSource.GetQueryable() - .Where(restrictionBindingInfo.Permission.Path.Select(p => p == dbPermission)) - .GenericToListAsync(cancellationToken); - - var restrictionMergeResult = dbRestrictions.GetMergeResult( - managedPermission.Restrictions - .ChangeKey(t => securityContextInfoSource.GetSecurityContextInfo(t).Identity) - .SelectMany(pair => pair.Value.Cast().Select(securityContextId => (pair.Key, securityContextId))), - pr => ( - securityContextTypeIdentityExtractor.Extract(restrictionBindingInfo.SecurityContextType.Getter(pr)), - restrictionBindingInfo.SecurityContextObjectId.Getter(pr)), - pair => pair); - - if (restrictionMergeResult.IsEmpty - && (bindingInfo.PermissionComment == null || bindingInfo.PermissionComment.Getter(dbPermission) == managedPermission.Comment) - && (bindingInfo.PermissionStartDate == null || bindingInfo.PermissionStartDate.Getter(dbPermission) == managedPermission.Period.StartDate) - && (bindingInfo.PermissionEndDate == null || bindingInfo.PermissionEndDate.Getter(dbPermission) == managedPermission.Period.EndDate)) - { - var permissionData = new PermissionData(dbPermission, - restrictionMergeResult.CombineItems.Select(v => v.Item1)); - - return (permissionData, false); - } - else - { - bindingInfo.PermissionComment?.Setter.Invoke(dbPermission, managedPermission.Comment); - bindingInfo.PermissionStartDate?.Setter.Invoke(dbPermission, managedPermission.Period.StartDate); - bindingInfo.PermissionEndDate?.Setter.Invoke(dbPermission, managedPermission.Period.EndDate); - - var newPermissionRestrictions = await restrictionMergeResult.AddingItems.SyncWhenAll(async restriction => - { - var newPermissionRestriction = new TPermissionRestriction(); - - var dbSecurityContextType = - await securityContextTypeRepository.GetObjectAsync(restriction.Key, cancellationToken); - - restrictionBindingInfo.Permission.Setter(newPermissionRestriction, dbPermission); - restrictionBindingInfo.SecurityContextObjectId.Setter(newPermissionRestriction, restriction.securityContextId); - restrictionBindingInfo.SecurityContextType.Setter(newPermissionRestriction, dbSecurityContextType); - - await genericRepository.SaveAsync(newPermissionRestriction, cancellationToken); - - return newPermissionRestriction; - }); - - foreach (var dbRestriction in restrictionMergeResult.RemovingItems) - { - await genericRepository.RemoveAsync(dbRestriction, cancellationToken); - } - - var permissionData = new PermissionData(dbPermission, - restrictionMergeResult.CombineItems.Select(v => v.Item1).Concat(newPermissionRestrictions)); - - await genericRepository.SaveAsync(dbPermission, cancellationToken); - - return (permissionData, true); - } + return await permissionPairs.SyncWhenAll(permissionPair => permissionManagementService.UpdatePermission(permissionPair.Item1, permissionPair.Item2, cancellationToken)); } } \ No newline at end of file diff --git a/src/SecuritySystem.GeneralPermission.Runtime/IPermissionManagementService.cs b/src/SecuritySystem.GeneralPermission.Runtime/IPermissionManagementService.cs new file mode 100644 index 0000000..7ad788a --- /dev/null +++ b/src/SecuritySystem.GeneralPermission.Runtime/IPermissionManagementService.cs @@ -0,0 +1,18 @@ +using SecuritySystem.ExternalSystem.Management; + +namespace SecuritySystem.GeneralPermission; + +public interface IPermissionManagementService +{ + Task ToManagedPermissionAsync(TPermission permission, CancellationToken cancellationToken); + + Task> CreatePermissionAsync( + TPrincipal dbPrincipal, + ManagedPermission managedPermission, + CancellationToken cancellationToken); + + Task<(PermissionData PermissonData, bool Updated)> UpdatePermission( + TPermission dbPermission, + ManagedPermission managedPermission, + CancellationToken cancellationToken); +} \ No newline at end of file diff --git a/src/SecuritySystem.GeneralPermission.Runtime/ManagedPermissionMapper.cs b/src/SecuritySystem.GeneralPermission.Runtime/ManagedPermissionMapper.cs new file mode 100644 index 0000000..2164c79 --- /dev/null +++ b/src/SecuritySystem.GeneralPermission.Runtime/ManagedPermissionMapper.cs @@ -0,0 +1,222 @@ +using CommonFramework; +using CommonFramework.GenericRepository; + +using GenericQueryable; + +using SecuritySystem.ExternalSystem.Management; +using SecuritySystem.Services; + +using System.Collections.Immutable; + +namespace SecuritySystem.GeneralPermission; + +public class PermissionManagementService( + IServiceProxyFactory serviceProxyFactory, + IPermissionBindingInfoSource bindingInfoSource, + IGeneralPermissionBindingInfoSource generalBindingInfoSource, + IGeneralPermissionRestrictionBindingInfoSource restrictionBindingInfoSource) + : IPermissionManagementService +{ + private readonly Lazy> lazyInnerService = new(() => + { + var bindingInfo = bindingInfoSource.GetForPermission(typeof(TPermission)); + + var generalBindingInfo = generalBindingInfoSource.GetForPermission(bindingInfo.PermissionType); + + var restrictionBindingInfo = restrictionBindingInfoSource.GetForPermission(bindingInfo.PermissionType); + + var innerServiceType = typeof(PermissionManagementService<,,,,,>) + .MakeGenericType( + bindingInfo.PrincipalType, + bindingInfo.PermissionType, + generalBindingInfo.SecurityRoleType, + restrictionBindingInfo.PermissionRestrictionType, + restrictionBindingInfo.SecurityContextTypeType, + restrictionBindingInfo.SecurityContextObjectIdentType); + + return serviceProxyFactory.Create>( + innerServiceType, + bindingInfo, + generalBindingInfo, + restrictionBindingInfo); + }); + + private IPermissionManagementService InnerService => this.lazyInnerService.Value; + + public virtual Task ToManagedPermissionAsync(TPermission dbPermission, CancellationToken cancellationToken) => + this.InnerService.ToManagedPermissionAsync(dbPermission, cancellationToken); + + public virtual Task> CreatePermissionAsync(TPrincipal dbPrincipal, ManagedPermission managedPermission, CancellationToken cancellationToken) => + this.InnerService.CreatePermissionAsync(dbPrincipal, managedPermission, cancellationToken); + + public virtual Task<(PermissionData PermissonData, bool Updated)> UpdatePermission(TPermission dbPermission, + ManagedPermission managedPermission, CancellationToken cancellationToken) => + this.InnerService.UpdatePermission(dbPermission, managedPermission, cancellationToken); +} + +public class PermissionManagementService( + PermissionBindingInfo bindingInfo, + GeneralPermissionBindingInfo generalBindingInfo, + GeneralPermissionRestrictionBindingInfo restrictionBindingInfo, + + IPermissionSecurityRoleResolver permissionSecurityRoleResolver, + IRawPermissionRestrictionLoader rawPermissionRestrictionLoader, + ISecurityIdentityExtractor permissionSecurityIdentityExtractor, + ISecurityRepository securityRoleRepository, + IQueryableSource queryableSource, + ISecurityRoleSource securityRoleSource, + ISecurityRepository securityContextTypeRepository, + ISecurityContextInfoSource securityContextInfoSource, + ISecurityIdentityExtractor securityRoleIdentityExtractor, + ISecurityIdentityExtractor securityContextTypeIdentityExtractor, + IGenericRepository genericRepository) + : IPermissionManagementService + + where TPermission : class, new() + where TSecurityRole : class + where TPermissionRestriction : class, new() + where TSecurityContextType : class + where TSecurityContextObjectIdent : notnull + +{ + public async Task ToManagedPermissionAsync(TPermission dbPermission, CancellationToken cancellationToken) => + new() + { + Identity = permissionSecurityIdentityExtractor.Extract(dbPermission), + IsVirtual = bindingInfo.IsReadonly, + SecurityRole = permissionSecurityRoleResolver.Resolve(dbPermission), + Period = bindingInfo.GetSafePeriod(dbPermission), + Comment = bindingInfo.GetSafeComment(dbPermission), + Restrictions = (await rawPermissionRestrictionLoader.LoadAsync(dbPermission, cancellationToken)).ToImmutableDictionary() + }; + + public async Task> CreatePermissionAsync( + TPrincipal dbPrincipal, + ManagedPermission managedPermission, + CancellationToken cancellationToken) + { + if (!managedPermission.Identity.IsDefault || managedPermission.IsVirtual) + { + throw new SecuritySystemException("wrong typed permission"); + } + + var securityRole = securityRoleSource.GetSecurityRole(managedPermission.SecurityRole); + + var dbRole = await securityRoleRepository.GetObjectAsync(securityRole.Identity, cancellationToken); + + var newDbPermission = new TPermission(); + + bindingInfo.Principal.Setter(newDbPermission, dbPrincipal); + generalBindingInfo.SecurityRole.Setter(newDbPermission, dbRole); + + bindingInfo.PermissionStartDate?.Setter(newDbPermission, managedPermission.Period.StartDate); + bindingInfo.PermissionEndDate?.Setter(newDbPermission, managedPermission.Period.EndDate); + bindingInfo.PermissionComment?.Setter(newDbPermission, managedPermission.Comment); + + await genericRepository.SaveAsync(newDbPermission, cancellationToken); + + var newPermissionRestrictions = await managedPermission.Restrictions.SyncWhenAll(async restrictionGroup => + { + var securityContextTypeIdentity = securityContextInfoSource.GetSecurityContextInfo(restrictionGroup.Key).Identity; + + var dbSecurityContextType = await securityContextTypeRepository.GetObjectAsync(securityContextTypeIdentity, cancellationToken); + + var newPermissionRestrictions = await restrictionGroup.Value.Cast().SyncWhenAll(async securityContextId => + { + var newDbPermissionRestriction = new TPermissionRestriction(); + + restrictionBindingInfo.Permission.Setter(newDbPermissionRestriction, newDbPermission); + restrictionBindingInfo.SecurityContextObjectId.Setter(newDbPermissionRestriction, securityContextId); + restrictionBindingInfo.SecurityContextType.Setter(newDbPermissionRestriction, dbSecurityContextType); + + await genericRepository.SaveAsync(newDbPermissionRestriction, cancellationToken); + + return newDbPermissionRestriction; + }); + + return newPermissionRestrictions; + }); + + return new PermissionData(newDbPermission, newPermissionRestrictions.SelectMany()); + } + + public async Task<(PermissionData PermissonData, bool Updated)> UpdatePermission( + TPermission dbPermission, + ManagedPermission managedPermission, + CancellationToken cancellationToken) + { + if (managedPermission.Identity.IsDefault || managedPermission.IsVirtual) + { + throw new SecuritySystemException("wrong typed permission"); + } + + var securityRole = generalBindingInfo + .SecurityRole + .Getter(dbPermission) + .Pipe(securityRoleIdentityExtractor.Extract) + .Pipe(securityRoleSource.GetSecurityRole); + + if (securityRole != managedPermission.SecurityRole) + { + throw new SecuritySystemException("Permission role can't be changed"); + } + + var dbRestrictions = await queryableSource.GetQueryable() + .Where(restrictionBindingInfo.Permission.Path.Select(p => p == dbPermission)) + .GenericToListAsync(cancellationToken); + + var restrictionMergeResult = dbRestrictions.GetMergeResult( + managedPermission.Restrictions + .ChangeKey(t => securityContextInfoSource.GetSecurityContextInfo(t).Identity) + .SelectMany(pair => pair.Value.Cast().Select(securityContextId => (pair.Key, securityContextId))), + pr => ( + securityContextTypeIdentityExtractor.Extract(restrictionBindingInfo.SecurityContextType.Getter(pr)), + restrictionBindingInfo.SecurityContextObjectId.Getter(pr)), + pair => pair); + + if (restrictionMergeResult.IsEmpty + && (bindingInfo.PermissionComment == null || bindingInfo.PermissionComment.Getter(dbPermission) == managedPermission.Comment) + && (bindingInfo.PermissionStartDate == null || bindingInfo.PermissionStartDate.Getter(dbPermission) == managedPermission.Period.StartDate) + && (bindingInfo.PermissionEndDate == null || bindingInfo.PermissionEndDate.Getter(dbPermission) == managedPermission.Period.EndDate)) + { + var permissionData = new PermissionData(dbPermission, + restrictionMergeResult.CombineItems.Select(v => v.Item1)); + + return (permissionData, false); + } + else + { + bindingInfo.PermissionComment?.Setter.Invoke(dbPermission, managedPermission.Comment); + bindingInfo.PermissionStartDate?.Setter.Invoke(dbPermission, managedPermission.Period.StartDate); + bindingInfo.PermissionEndDate?.Setter.Invoke(dbPermission, managedPermission.Period.EndDate); + + var newPermissionRestrictions = await restrictionMergeResult.AddingItems.SyncWhenAll(async restriction => + { + var newPermissionRestriction = new TPermissionRestriction(); + + var dbSecurityContextType = + await securityContextTypeRepository.GetObjectAsync(restriction.Key, cancellationToken); + + restrictionBindingInfo.Permission.Setter(newPermissionRestriction, dbPermission); + restrictionBindingInfo.SecurityContextObjectId.Setter(newPermissionRestriction, restriction.securityContextId); + restrictionBindingInfo.SecurityContextType.Setter(newPermissionRestriction, dbSecurityContextType); + + await genericRepository.SaveAsync(newPermissionRestriction, cancellationToken); + + return newPermissionRestriction; + }); + + foreach (var dbRestriction in restrictionMergeResult.RemovingItems) + { + await genericRepository.RemoveAsync(dbRestriction, cancellationToken); + } + + var permissionData = new PermissionData(dbPermission, + restrictionMergeResult.CombineItems.Select(v => v.Item1).Concat(newPermissionRestrictions)); + + await genericRepository.SaveAsync(dbPermission, cancellationToken); + + return (permissionData, true); + } + } +} \ No newline at end of file diff --git a/src/SecuritySystem.GeneralPermission.Runtime/ManagedPrincipalConverter.cs b/src/SecuritySystem.GeneralPermission.Runtime/ManagedPrincipalConverter.cs index 83edf8b..2c02e59 100644 --- a/src/SecuritySystem.GeneralPermission.Runtime/ManagedPrincipalConverter.cs +++ b/src/SecuritySystem.GeneralPermission.Runtime/ManagedPrincipalConverter.cs @@ -5,31 +5,34 @@ namespace SecuritySystem.GeneralPermission; -public class ManagedPrincipalConverter(IServiceProxyFactory serviceProxyFactory, IPermissionBindingInfoSource bindingInfoSource) +public class ManagedPrincipalConverter( + IServiceProxyFactory serviceProxyFactory, + IPermissionBindingInfoSource bindingInfoSource, + IGeneralPermissionRestrictionBindingInfoSource restrictionBindingInfoSource) : IManagedPrincipalConverter { private readonly Lazy> lazyInnerService = new(() => { var bindingInfo = bindingInfoSource.GetForPrincipal(typeof(TPrincipal)); - var innerServiceType = typeof(ManagedPrincipalConverter<,>).MakeGenericType( + var restrictionBindingInfo = restrictionBindingInfoSource.GetForPermission(bindingInfo.PermissionType); + + var innerServiceType = typeof(ManagedPrincipalConverter<,,>).MakeGenericType( bindingInfo.PrincipalType, - bindingInfo.PermissionType); + bindingInfo.PermissionType, + restrictionBindingInfo.PermissionRestrictionType); - return serviceProxyFactory.Create>(innerServiceType, bindingInfo); + return serviceProxyFactory.Create>(innerServiceType); }); public Task ToManagedPrincipalAsync(TPrincipal principal, CancellationToken cancellationToken) => this.lazyInnerService.Value.ToManagedPrincipalAsync(principal, cancellationToken); } -public class ManagedPrincipalConverter( - PermissionBindingInfo bindingInfo, +public class ManagedPrincipalConverter( IManagedPrincipalHeaderConverter headerConverter, - IPermissionSecurityRoleResolver permissionSecurityRoleResolver, IPermissionLoader permissionLoader, - IRawPermissionRestrictionLoader rawPermissionRestrictionLoader, - ISecurityIdentityExtractor permissionSecurityIdentityExtractor) : IManagedPrincipalConverter + IPermissionManagementService permissionManagementService) : IManagedPrincipalConverter where TPrincipal : class where TPermission : class { @@ -39,17 +42,6 @@ public async Task ToManagedPrincipalAsync(TPrincipal principal return new ManagedPrincipal( headerConverter.Convert(principal), - await permissions.SyncWhenAll(permission => this.ToManagedPermissionAsync(permission, cancellationToken))); + await permissions.SyncWhenAll(permission => permissionManagementService.ToManagedPermissionAsync(permission, cancellationToken))); } - - private async Task ToManagedPermissionAsync(TPermission permission, CancellationToken cancellationToken) => - new() - { - Identity = permissionSecurityIdentityExtractor.Extract(permission), - IsVirtual = bindingInfo.IsReadonly, - SecurityRole = permissionSecurityRoleResolver.Resolve(permission), - Period = bindingInfo.GetSafePeriod(permission), - Comment = bindingInfo.GetSafeComment(permission), - Restrictions = await rawPermissionRestrictionLoader.LoadAsync(permission, cancellationToken) - }; } \ No newline at end of file diff --git a/src/SecuritySystem.GeneralPermission/DependencyInjection/GeneralPermissionSettings.cs b/src/SecuritySystem.GeneralPermission/DependencyInjection/GeneralPermissionSettings.cs index d17401b..4157f8b 100644 --- a/src/SecuritySystem.GeneralPermission/DependencyInjection/GeneralPermissionSettings.cs +++ b/src/SecuritySystem.GeneralPermission/DependencyInjection/GeneralPermissionSettings.cs @@ -1,10 +1,12 @@ using System.Linq.Expressions; using CommonFramework; +using SecuritySystem.GeneralPermission.Validation; namespace SecuritySystem.GeneralPermission.DependencyInjection; -public class GeneralPermissionSettings : IGeneralPermissionSettings +public class GeneralPermissionSettings : + IGeneralPermissionSettings where TPermission : class where TSecurityRole : notnull { @@ -18,6 +20,10 @@ public class GeneralPermissionSettings : private bool? isReadonly; + public Type? PermissionEqualityComparerType { get; private set; } + + public Type? PermissionManagementServiceType { get; private set; } + public TPermissionBindingInfo ApplyOptionalPaths(TPermissionBindingInfo bindingInfo) where TPermissionBindingInfo : PermissionBindingInfo @@ -36,7 +42,7 @@ public TGeneralPermissionBindingInfo ApplyGeneralOptionalPaths b with { SecurityRoleDescription = v.ToPropertyAccessors() }); } - public IGeneralPermissionSettings SetPermissionPeriod( + public IGeneralPermissionSettings SetPermissionPeriod( PropertyAccessors? startDatePropertyAccessor, PropertyAccessors? endDatePropertyAccessor) { @@ -46,7 +52,7 @@ public IGeneralPermissionSettings SetPermissionPerio return this; } - public IGeneralPermissionSettings SetPermissionPeriod( + public IGeneralPermissionSettings SetPermissionPeriod( Expression>? startDatePath, Expression>? endDatePath) { @@ -55,24 +61,43 @@ public IGeneralPermissionSettings SetPermissionPerio endDatePath == null ? null : new PropertyAccessors(endDatePath)); } - public IGeneralPermissionSettings SetPermissionComment(Expression> newCommentPath) + public IGeneralPermissionSettings SetPermissionComment( + Expression> newCommentPath) { this.commentPath = newCommentPath; return this; } - public IGeneralPermissionSettings SetSecurityRoleDescription(Expression>? newDescriptionPath) + public IGeneralPermissionSettings SetSecurityRoleDescription( + Expression>? newDescriptionPath) { this.descriptionPath = newDescriptionPath; return this; } - public IGeneralPermissionSettings SetReadonly(bool value = true) + public IGeneralPermissionSettings SetReadonly(bool value = true) { this.isReadonly = value; return this; } + + public IGeneralPermissionSettings SetPermissionEqualityComparer() + where TComparer : IPermissionEqualityComparer + { + this.PermissionEqualityComparerType = typeof(TComparer); + + return this; + } + + public IGeneralPermissionSettings SetPermissionManagementService< + TPermissionManagementService>() + where TPermissionManagementService : IPermissionManagementService + { + this.PermissionManagementServiceType = typeof(TPermissionManagementService); + + return this; + } } \ No newline at end of file diff --git a/src/SecuritySystem.GeneralPermission/DependencyInjection/IGeneralPermissionSettings.cs b/src/SecuritySystem.GeneralPermission/DependencyInjection/IGeneralPermissionSettings.cs index 8e900b4..7f0f73c 100644 --- a/src/SecuritySystem.GeneralPermission/DependencyInjection/IGeneralPermissionSettings.cs +++ b/src/SecuritySystem.GeneralPermission/DependencyInjection/IGeneralPermissionSettings.cs @@ -2,21 +2,31 @@ using System.Linq.Expressions; +using SecuritySystem.GeneralPermission.Validation; + namespace SecuritySystem.GeneralPermission.DependencyInjection; -public interface IGeneralPermissionSettings +public interface IGeneralPermissionSettings { - public IGeneralPermissionSettings SetPermissionPeriod( + public IGeneralPermissionSettings SetPermissionPeriod( PropertyAccessors? startDatePropertyAccessor, PropertyAccessors? endDatePropertyAccessor); - public IGeneralPermissionSettings SetPermissionPeriod( + public IGeneralPermissionSettings SetPermissionPeriod( Expression>? startDatePath, Expression>? endDatePath); - IGeneralPermissionSettings SetPermissionComment(Expression> commentPath); + IGeneralPermissionSettings SetPermissionComment( + Expression> commentPath); + + IGeneralPermissionSettings SetSecurityRoleDescription( + Expression>? descriptionPath); + + IGeneralPermissionSettings SetReadonly(bool value = true); - IGeneralPermissionSettings SetSecurityRoleDescription(Expression>? descriptionPath); + IGeneralPermissionSettings SetPermissionEqualityComparer() + where TComparer : IPermissionEqualityComparer; - IGeneralPermissionSettings SetReadonly(bool value = true); + IGeneralPermissionSettings SetPermissionManagementService() + where TPermissionManagementService : IPermissionManagementService; } \ No newline at end of file diff --git a/src/SecuritySystem.GeneralPermission/DependencyInjection/SecuritySystemSettingsExtensions.cs b/src/SecuritySystem.GeneralPermission/DependencyInjection/SecuritySystemSettingsExtensions.cs index 597dc2d..f7289ab 100644 --- a/src/SecuritySystem.GeneralPermission/DependencyInjection/SecuritySystemSettingsExtensions.cs +++ b/src/SecuritySystem.GeneralPermission/DependencyInjection/SecuritySystemSettingsExtensions.cs @@ -1,4 +1,5 @@ using CommonFramework; +using CommonFramework.DependencyInjection; using Microsoft.Extensions.DependencyInjection; @@ -22,9 +23,13 @@ public static class SecuritySystemSettingsExtensions public ISecuritySystemSettings AddGeneralPermission( PermissionBindingInfo bindingInfo, GeneralPermissionBindingInfo generalBindingInfo, - GeneralPermissionRestrictionBindingInfo restrictionBindingInfo) + GeneralPermissionRestrictionBindingInfo restrictionBindingInfo, + Type? permissionEqualityComparerType, + Type? permissionManagementServiceType) { return securitySystemSettings + .AddGeneralServices() + .AddPermissionSystem(sp => new GeneralPermissionSystemFactory(sp, bindingInfo)) .AddExtensions(services => services @@ -32,51 +37,20 @@ public ISecuritySystemSettings AddGeneralPermission( .AddSingleton(generalBindingInfo) .AddSingleton(restrictionBindingInfo) - .AddSingleton(typeof(IPermissionRestrictionTypeFilterFactory<>), typeof(PermissionRestrictionTypeFilterFactory<>)) - .AddScoped(typeof(IPermissionRestrictionFilterFactory<>), typeof(PermissionRestrictionFilterFactory<>)) - .AddScoped(typeof(IRawPermissionConverter<>), typeof(RawPermissionConverter<>)) - .AddSingleton(typeof(IPermissionSecurityRoleFilterFactory<>), typeof(PermissionSecurityRoleFilterFactory<>)) - .AddScoped(typeof(IPermissionFilterFactory<>), typeof(PermissionFilterFactory<>)) - - - .AddScoped() - .AddScoped(typeof(ISecurityRoleInitializer<>), typeof(SecurityRoleInitializer<>)) - - .AddScoped() - .AddScoped(typeof(ISecurityContextInitializer<>), typeof(SecurityContextInitializer<>)) - - .AddScoped(typeof(IManagedPrincipalConverter<>), typeof(ManagedPrincipalConverter<>)) - - .AddScoped(typeof(IDisplayPermissionService<,>), typeof(DisplayPermissionService<,>)) - .AddSingleton(typeof(IPermissionEqualityComparer<,>), typeof(PermissionEqualityComparer<,>)) - - .AddKeyedScoped(typeof(IPrincipalValidator<,,>), "Root", typeof(PrincipalRootValidator<,,>)) - .AddScoped(typeof(IPrincipalValidator<,,>), typeof(PrincipalUniquePermissionValidator<,,>)) - - .AddKeyedScoped(typeof(IPermissionValidator<,>), "Root", typeof(PermissionRootValidator<,>)) - .AddSingleton(typeof(IPermissionValidator<,>), typeof(PermissionRequiredContextValidator<,>)) - - .AddKeyedScoped(typeof(IPermissionRestrictionValidator<>), "Root", typeof(PermissionRestrictionRootValidator<>)) - .AddSingleton(typeof(IPermissionRestrictionValidator<>), typeof(AllowedTypePermissionRestrictionValidator<>)) - .AddScoped(typeof(IPermissionRestrictionValidator<>), typeof(ExistsPermissionRestrictionValidator<>)) - .AddScoped(typeof(IPermissionRestrictionValidator<>), typeof(AllowedFilterPermissionRestrictionValidator<>)) + .PipeMaybe(permissionEqualityComparerType, (sc, v) => sc + .AddScoped( + typeof(IPermissionEqualityComparer<,>) + .MakeGenericType(restrictionBindingInfo.PermissionType, restrictionBindingInfo.PermissionRestrictionType), v)) - .AddScoped(typeof(IPermissionLoader<,>), typeof(PermissionLoader<,>)) - .AddScoped(typeof(IPermissionRestrictionLoader<,>), typeof(PermissionRestrictionLoader<,>)) - .AddScoped(typeof(IRawPermissionRestrictionLoader<>), typeof(RawPermissionRestrictionLoader<>)) + .PipeMaybe(permissionManagementServiceType, (sc, v) => sc + .AddScoped( + typeof(IPermissionManagementService<,,>) + .MakeGenericType(bindingInfo.PrincipalType, restrictionBindingInfo.PermissionType, restrictionBindingInfo.PermissionRestrictionType), v)) - .AddSingleton(typeof(IPermissionRestrictionRawConverter<>), typeof(PermissionRestrictionRawConverter<>)) - .AddSingleton(typeof(IPermissionSecurityRoleResolver<>), typeof(PermissionSecurityRoleResolver<>)) - .AddSingleton(typeof(IPermissionRestrictionSecurityContextTypeResolver<>), typeof(PermissionRestrictionSecurityContextTypeResolver<>)) - - .AddSingleton() + .AddScoped(typeof(IPrincipalSourceService), typeof(GeneralPrincipalSourceService<>).MakeGenericType(bindingInfo.PrincipalType))); + } - .AddSingleton() - .AddSingleton() - .AddScoped(typeof(IPrincipalSourceService), typeof(GeneralPrincipalSourceService<>).MakeGenericType(bindingInfo.PrincipalType))) - .Pipe(!bindingInfo.IsReadonly, v => v.SetPrincipalManagementService()); - } public ISecuritySystemSettings AddGeneralPermission( @@ -85,7 +59,7 @@ public ISecuritySystemSettings AddGeneralPermission( Expression> permissionPath, Expression> securityContextTypePath, Expression> securityContextObjectIdPath, - Action>? setupAction = null) + Action>? setupAction = null) where TPrincipal : class where TPermission : class where TSecurityRole : class @@ -93,7 +67,7 @@ public ISecuritySystemSettings AddGeneralPermission( where TSecurityContextType : class where TSecurityContextObjectIdent : notnull { - var settings = new GeneralPermissionSettings(); + var settings = new GeneralPermissionSettings(); setupAction?.Invoke(settings); @@ -115,7 +89,60 @@ public ISecuritySystemSettings AddGeneralPermission( SecurityContextObjectId = securityContextObjectIdPath.ToPropertyAccessors() }; - return securitySystemSettings.AddGeneralPermission(bindingInfo, generalBindingInfo, restrictionBindingInfo); + return securitySystemSettings.AddGeneralPermission( + bindingInfo, + generalBindingInfo, + restrictionBindingInfo, + settings.PermissionEqualityComparerType, + settings.PermissionManagementServiceType); + } + + private ISecuritySystemSettings AddGeneralServices() + { + return securitySystemSettings + .AddExtensions(services => + { + if (services.AlreadyInitialized()) + { + return; + } + + services + .AddSingleton(typeof(IPermissionRestrictionTypeFilterFactory<>), typeof(PermissionRestrictionTypeFilterFactory<>)) + .AddScoped(typeof(IPermissionRestrictionFilterFactory<>), typeof(PermissionRestrictionFilterFactory<>)) + .AddScoped(typeof(IRawPermissionConverter<>), typeof(RawPermissionConverter<>)) + .AddSingleton(typeof(IPermissionSecurityRoleFilterFactory<>), typeof(PermissionSecurityRoleFilterFactory<>)) + .AddScoped(typeof(IPermissionFilterFactory<>), typeof(PermissionFilterFactory<>)) + .AddScoped() + .AddScoped(typeof(ISecurityRoleInitializer<>), typeof(SecurityRoleInitializer<>)) + .AddScoped() + .AddScoped(typeof(ISecurityContextInitializer<>), typeof(SecurityContextInitializer<>)) + .AddScoped(typeof(IManagedPrincipalConverter<>), typeof(ManagedPrincipalConverter<>)) + .AddScoped(typeof(IDisplayPermissionService<,>), typeof(DisplayPermissionService<,>)) + + .AddSingleton(typeof(IPermissionEqualityComparer<,>), typeof(PermissionEqualityComparer<,>)) + + .AddKeyedScoped(typeof(IPrincipalValidator<,,>), "Root", typeof(PrincipalRootValidator<,,>)) + .AddScoped(typeof(IPrincipalValidator<,,>), typeof(PrincipalUniquePermissionValidator<,,>)) + .AddKeyedScoped(typeof(IPermissionValidator<,>), "Root", typeof(PermissionRootValidator<,>)) + .AddSingleton(typeof(IPermissionValidator<,>), typeof(PermissionRequiredContextValidator<,>)) + .AddKeyedScoped(typeof(IPermissionRestrictionValidator<>), "Root", typeof(PermissionRestrictionRootValidator<>)) + .AddSingleton(typeof(IPermissionRestrictionValidator<>), typeof(AllowedTypePermissionRestrictionValidator<>)) + .AddScoped(typeof(IPermissionRestrictionValidator<>), typeof(ExistsPermissionRestrictionValidator<>)) + .AddScoped(typeof(IPermissionRestrictionValidator<>), typeof(AllowedFilterPermissionRestrictionValidator<>)) + .AddScoped(typeof(IPermissionLoader<,>), typeof(PermissionLoader<,>)) + .AddScoped(typeof(IPermissionRestrictionLoader<,>), typeof(PermissionRestrictionLoader<,>)) + .AddScoped(typeof(IRawPermissionRestrictionLoader<>), typeof(RawPermissionRestrictionLoader<>)) + .AddSingleton(typeof(IPermissionRestrictionRawConverter<>), typeof(PermissionRestrictionRawConverter<>)) + .AddSingleton(typeof(IPermissionSecurityRoleResolver<>), typeof(PermissionSecurityRoleResolver<>)) + .AddSingleton(typeof(IPermissionRestrictionSecurityContextTypeResolver<>), typeof(PermissionRestrictionSecurityContextTypeResolver<>)) + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddScoped(typeof(IPermissionManagementService<,,>), typeof(PermissionManagementService<,,>)); + }) + + .SetPrincipalManagementService(); } } } \ No newline at end of file diff --git a/src/SecuritySystem.Runtime/Services/SecurityRolesIdentsResolver.cs b/src/SecuritySystem.Runtime/Services/SecurityRolesIdentsResolver.cs index cfa2023..afeba0a 100644 --- a/src/SecuritySystem.Runtime/Services/SecurityRolesIdentsResolver.cs +++ b/src/SecuritySystem.Runtime/Services/SecurityRolesIdentsResolver.cs @@ -1,4 +1,5 @@ using System.Collections.Concurrent; +using System.Collections.Immutable; using CommonFramework; @@ -8,7 +9,7 @@ namespace SecuritySystem.Services; public class SecurityRolesIdentsResolver(ISecurityRuleExpander securityRuleExpander, ISecurityRoleSource securityRoleSource) : ISecurityRolesIdentsResolver { - private readonly ConcurrentDictionary<(DomainSecurityRule.RoleBaseSecurityRule, bool), Dictionary> cache = new(); + private readonly ConcurrentDictionary<(DomainSecurityRule.RoleBaseSecurityRule, bool), ImmutableDictionary> cache = new(); public IReadOnlyDictionary Resolve(DomainSecurityRule.RoleBaseSecurityRule baseSecurityRule, bool includeVirtual = false) { @@ -23,6 +24,6 @@ public IReadOnlyDictionary Resolve(DomainSecurityRule.RoleBaseSecur .Where(sr => includeVirtual || !sr.Information.IsVirtual) .Select(sr => sr.Identity) .GroupBy(i => i.IdentType, i => i.GetId()) - .ToDictionary(g => g.Key, g => g.ToArray(g.Key))); + .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 fb1c56b..0461ec4 100644 --- a/src/SecuritySystem.Testing/TestPermission.cs +++ b/src/SecuritySystem.Testing/TestPermission.cs @@ -1,4 +1,6 @@ -using SecuritySystem.ExternalSystem.Management; +using System.Collections.Immutable; + +using SecuritySystem.ExternalSystem.Management; namespace SecuritySystem.Testing; @@ -18,9 +20,11 @@ public TestPermission() public PermissionPeriod Period { get; set; } = PermissionPeriod.Eternity; - public Dictionary Restrictions { get; } = new(); + public Dictionary Restrictions { get; } = []; + + public Dictionary ExtendedData { get; set; } = []; - public string Comment { get; set; } + public string Comment { get; set; } = ""; public TypedSecurityIdentity? GetSingle() where TSecurityContext : ISecurityContext @@ -72,21 +76,14 @@ public void SetMany(TypedSecurityIdentity[] va } } - public ManagedPermissionData ToManagedPermissionData() + public ManagedPermissionData ToManagedPermissionData() => new() { - if (this.SecurityRole is null) - { - throw new InvalidOperationException($"{nameof(this.SecurityRole)} not initialized"); - } - - return new ManagedPermissionData - { - SecurityRole = this.SecurityRole, - Period = this.Period, - Comment = this.Comment, - Restrictions = this.Restrictions.Where(pair => pair.Value.Length > 0).ToDictionary() - }; - } + SecurityRole = this.SecurityRole ?? throw new InvalidOperationException($"{nameof(this.SecurityRole)} not initialized"), + Period = this.Period, + Comment = this.Comment, + Restrictions = this.Restrictions.Where(pair => pair.Value.Length > 0).ToImmutableDictionary(), + ExtendedData = this.ExtendedData.ToImmutableDictionary() + }; public static implicit operator ManagedPermissionData(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 1cbdb9b..0fb3079 100644 --- a/src/SecuritySystem.Testing/UserCredentialManager.cs +++ b/src/SecuritySystem.Testing/UserCredentialManager.cs @@ -40,7 +40,8 @@ public async Task AddUserRoleAsync(ManagedPermissionData[] tes SecurityRole = testPermission.SecurityRole, Period = testPermission.Period, Comment = testPermission.Comment, - Restrictions = testPermission.Restrictions + Restrictions = testPermission.Restrictions, + ExtendedData = testPermission.ExtendedData }); var existsPrincipal = await principalSourceService.TryGetPrincipalAsync(this.userCredential, cancellationToken); diff --git a/src/SecuritySystem.VirtualPermission.Runtime/VirtualPermissionBindingInfo.cs b/src/SecuritySystem.VirtualPermission.Runtime/VirtualPermissionBindingInfo.cs index ae8b965..fd954c8 100644 --- a/src/SecuritySystem.VirtualPermission.Runtime/VirtualPermissionBindingInfo.cs +++ b/src/SecuritySystem.VirtualPermission.Runtime/VirtualPermissionBindingInfo.cs @@ -1,4 +1,6 @@ -using CommonFramework; +using System.Collections.Immutable; + +using CommonFramework; using CommonFramework.ExpressionEvaluate; using CommonFramework.IdentitySource; @@ -12,14 +14,15 @@ public abstract record VirtualPermissionBindingInfo public required SecurityRole SecurityRole { get; init; } - public IReadOnlyList Restrictions { get; init; } = []; + public ImmutableList Restrictions { get; init; } = []; - public IEnumerable GetSecurityContextTypes() - { - return this.Restrictions - .Select(restrictionPath => restrictionPath.ReturnType.GetCollectionElementTypeOrSelf()) - .Distinct(); - } + public ImmutableList SecurityContextTypes => + field ??= + [ + ..this.Restrictions + .Select(restrictionPath => restrictionPath.ReturnType.GetCollectionElementTypeOrSelf()) + .Distinct() + ]; } public record VirtualPermissionBindingInfo : VirtualPermissionBindingInfo diff --git a/src/SecuritySystem.VirtualPermission.Runtime/VirtualPermissionBindingInfoValidator.cs b/src/SecuritySystem.VirtualPermission.Runtime/VirtualPermissionBindingInfoValidator.cs index 6fd6d10..81a4b08 100644 --- a/src/SecuritySystem.VirtualPermission.Runtime/VirtualPermissionBindingInfoValidator.cs +++ b/src/SecuritySystem.VirtualPermission.Runtime/VirtualPermissionBindingInfoValidator.cs @@ -35,7 +35,7 @@ private void InternalValidate(VirtualPermissionBindingInfo virtualBindingInfo) if (securityContextRestrictions != null) { - var bindingContextTypes = virtualBindingInfo.GetSecurityContextTypes().ToList(); + var bindingContextTypes = virtualBindingInfo.SecurityContextTypes; var invalidTypes = bindingContextTypes.Except(securityContextRestrictions.Select(r => r.SecurityContextType)).ToList(); diff --git a/src/SecuritySystem.VirtualPermission.Runtime/VirtualPrincipalSourceService.cs b/src/SecuritySystem.VirtualPermission.Runtime/VirtualPrincipalSourceService.cs index 24b50b6..7ff0c9e 100644 --- a/src/SecuritySystem.VirtualPermission.Runtime/VirtualPrincipalSourceService.cs +++ b/src/SecuritySystem.VirtualPermission.Runtime/VirtualPrincipalSourceService.cs @@ -1,16 +1,18 @@ -using CommonFramework; +using System.Linq.Expressions; +using System.Reflection; + +using CommonFramework; using CommonFramework.ExpressionEvaluate; using CommonFramework.GenericRepository; using CommonFramework.IdentitySource; using CommonFramework.VisualIdentitySource; + using GenericQueryable; + using SecuritySystem.Credential; using SecuritySystem.ExternalSystem.Management; using SecuritySystem.Services; using SecuritySystem.UserSource; -using System.Linq.Expressions; -using System.Reflection; -using System.Xml.Linq; namespace SecuritySystem.VirtualPermission; @@ -71,10 +73,7 @@ public class VirtualPrincipalSourceService( public Type PrincipalType { get; } = typeof(TPrincipal); - public async Task> GetPrincipalsAsync( - string nameFilter, - int limit, - CancellationToken cancellationToken) + public async Task> GetPrincipalsAsync(string nameFilter, int limit, CancellationToken cancellationToken) { return await queryableSource .GetQueryable() @@ -117,13 +116,13 @@ private ManagedPermission ToManagedPermission(TPermission permission) var getRestrictionsMethod = this.GetType().GetMethod(nameof(this.GetRestrictionArray), BindingFlags.Instance | BindingFlags.NonPublic)!; var restrictions = virtualBindingInfo - .GetSecurityContextTypes() + .SecurityContextTypes .Select(identityInfoSource.GetIdentityInfo) .Select(identityInfo => (identityInfo.DomainObjectType, getRestrictionsMethod .MakeGenericMethod(identityInfo.DomainObjectType, identityInfo.IdentityType) .Invoke(this, permission, identityInfo))) - .ToDictionary(); + .ToImmutableDictionary(); return new ManagedPermission { @@ -136,9 +135,7 @@ private ManagedPermission ToManagedPermission(TPermission permission) }; } - public async Task> GetLinkedPrincipalsAsync( - IEnumerable securityRoles, - CancellationToken cancellationToken) + public async Task> GetLinkedPrincipalsAsync(IEnumerable securityRoles, CancellationToken cancellationToken) { if (securityRoles.Contains(virtualBindingInfo.SecurityRole)) { @@ -154,7 +151,8 @@ public async Task> GetLinkedPrincipalsAsync( } } - private Array GetRestrictionArray(TPermission permission, + private TSecurityContextIdent[] GetRestrictionArray( + TPermission permission, IdentityInfo identityInfo) where TSecurityContext : ISecurityContext where TSecurityContextIdent : notnull @@ -162,7 +160,8 @@ private Array GetRestrictionArray(TPerm return this.GetRestrictionIdents(permission, identityInfo).ToArray(); } - private IEnumerable GetRestrictionIdents(TPermission permission, + private IEnumerable GetRestrictionIdents( + TPermission permission, IdentityInfo identityInfo) where TSecurityContext : ISecurityContext where TSecurityContextIdent : notnull diff --git a/src/SecuritySystem.VirtualPermission/DependencyInjection/VirtualBindingInfoSettingsBuilder.cs b/src/SecuritySystem.VirtualPermission/DependencyInjection/VirtualBindingInfoSettingsBuilder.cs index bf8385b..18ca64f 100644 --- a/src/SecuritySystem.VirtualPermission/DependencyInjection/VirtualBindingInfoSettingsBuilder.cs +++ b/src/SecuritySystem.VirtualPermission/DependencyInjection/VirtualBindingInfoSettingsBuilder.cs @@ -1,4 +1,5 @@ -using System.Linq.Expressions; +using System.Collections.Immutable; +using System.Linq.Expressions; using CommonFramework; @@ -13,7 +14,7 @@ public IVirtualBindingInfoSettingsBuilder AddRestriction>> path) where TSecurityContext : ISecurityContext { - this.initList.Add(v => v with { Restrictions = v.Restrictions.Concat([path]).ToList() }); + this.initList.Add(v => v with { Restrictions = v.Restrictions.Concat([path]).ToImmutableList() }); return this; } @@ -22,7 +23,7 @@ public IVirtualBindingInfoSettingsBuilder AddRestriction> path) where TSecurityContext : ISecurityContext { - this.initList.Add(v => v with { Restrictions = v.Restrictions.Concat([path]).ToList() }); + this.initList.Add(v => v with { Restrictions = v.Restrictions.Concat([path]).ToImmutableList() }); return this; } diff --git a/src/_Example/ExampleApp.Domain/Auth/General/Permission.cs b/src/_Example/ExampleApp.Domain/Auth/General/Permission.cs index 56429fc..443a002 100644 --- a/src/_Example/ExampleApp.Domain/Auth/General/Permission.cs +++ b/src/_Example/ExampleApp.Domain/Auth/General/Permission.cs @@ -13,4 +13,6 @@ public class Permission public DateTime? EndDate { get; set; } public string Comment { get; set; } = ""; + + public string ExtendedValue { get; set; } = ""; } \ No newline at end of file diff --git a/src/_Example/ExampleApp.Domain/Auth/General/SecurityRole.cs b/src/_Example/ExampleApp.Domain/Auth/General/SecurityRole.cs index e3ab44a..0c04fe0 100644 --- a/src/_Example/ExampleApp.Domain/Auth/General/SecurityRole.cs +++ b/src/_Example/ExampleApp.Domain/Auth/General/SecurityRole.cs @@ -6,5 +6,5 @@ public class SecurityRole public required string Name { get; set; } - public required string Description { get; set; } + public required string Description { get; set; } = ""; } \ No newline at end of file diff --git a/src/_Example/ExampleApp.Infrastructure/DependencyInjection/ExtendedPermissionManagementService.cs b/src/_Example/ExampleApp.Infrastructure/DependencyInjection/ExtendedPermissionManagementService.cs new file mode 100644 index 0000000..a3c656b --- /dev/null +++ b/src/_Example/ExampleApp.Infrastructure/DependencyInjection/ExtendedPermissionManagementService.cs @@ -0,0 +1,59 @@ +using CommonFramework; +using CommonFramework.GenericRepository; + +using AuthGeneral = ExampleApp.Domain.Auth.General; + +using SecuritySystem.GeneralPermission; +using SecuritySystem.Services; +using SecuritySystem.ExternalSystem.Management; + +namespace ExampleApp.Infrastructure.DependencyInjection; + +public class ExtendedPermissionManagementService( + IServiceProxyFactory serviceProxyFactory, + IPermissionBindingInfoSource bindingInfoSource, + IGeneralPermissionBindingInfoSource generalBindingInfoSource, + IGeneralPermissionRestrictionBindingInfoSource restrictionBindingInfoSource, + IGenericRepository genericRepository) : + PermissionManagementService(serviceProxyFactory, bindingInfoSource, + generalBindingInfoSource, restrictionBindingInfoSource) +{ + private const string ExtendedKey = nameof(AuthGeneral.Permission.ExtendedValue); + + public override async Task> CreatePermissionAsync( + AuthGeneral.Principal dbPrincipal, + ManagedPermission managedPermission, + CancellationToken cancellationToken) + { + var baseResult = await base.CreatePermissionAsync(dbPrincipal, managedPermission, cancellationToken); + + if (managedPermission.ExtendedData.TryGetValue(ExtendedKey, out var extendedValue)) + { + baseResult.Permission.ExtendedValue = (string)extendedValue; + + await genericRepository.SaveAsync(baseResult.Permission, cancellationToken); + } + + return baseResult; + } + + public override async Task ToManagedPermissionAsync(AuthGeneral.Permission dbPermission, CancellationToken cancellationToken) + { + var baseResult = await base.ToManagedPermissionAsync(dbPermission, cancellationToken); + + return baseResult.WithExtendedData(ExtendedKey, dbPermission.ExtendedValue); + } + + public override async Task<(PermissionData PermissonData, bool Updated)> UpdatePermission( + AuthGeneral.Permission dbPermission, ManagedPermission managedPermission, CancellationToken cancellationToken) + { + var baseResult = await base.UpdatePermission(dbPermission, managedPermission, cancellationToken); + + if (managedPermission.ExtendedData.TryGetValue(ExtendedKey, out var extendedValue) && (string)extendedValue != dbPermission.ExtendedValue) + { + throw new InvalidOperationException($"{ExtendedKey} can't be changed"); + } + + return baseResult; + } +} \ No newline at end of file diff --git a/src/_Example/ExampleApp.Infrastructure/DependencyInjection/ServiceCollectionExtensions.cs b/src/_Example/ExampleApp.Infrastructure/DependencyInjection/ServiceCollectionExtensions.cs index 58d5470..9eb2345 100644 --- a/src/_Example/ExampleApp.Infrastructure/DependencyInjection/ServiceCollectionExtensions.cs +++ b/src/_Example/ExampleApp.Infrastructure/DependencyInjection/ServiceCollectionExtensions.cs @@ -129,7 +129,8 @@ private IServiceCollection AddSecuritySystem() v => v.StartDate, (permission, startDate) => permission.StartDate = startDate ?? DateTime.MinValue), new PropertyAccessors(v => v.EndDate)) - .SetPermissionComment(v => v.Comment))); + .SetPermissionComment(v => v.Comment) + .SetPermissionManagementService())); } } } \ No newline at end of file diff --git a/src/_Example/ExampleApp.Infrastructure/TestDbContext.cs b/src/_Example/ExampleApp.Infrastructure/TestDbContext.cs index e962c09..feab8d3 100644 --- a/src/_Example/ExampleApp.Infrastructure/TestDbContext.cs +++ b/src/_Example/ExampleApp.Infrastructure/TestDbContext.cs @@ -137,7 +137,9 @@ private void InitGeneralPermission(ModelBuilder modelBuilder) entity.HasIndex(e => e.Name).IsUnique(); entity.Property(e => e.Name).IsRequired().HasMaxLength(DefaultMaxLength); - } + + entity.Property(e => e.Description).IsRequired().HasMaxLength(DefaultMaxLength); + } { var entity = modelBuilder.Entity().ToTable(nameof(Permission), AuthSchema); @@ -145,6 +147,9 @@ 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.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/ExtendedValuePermissionTests.cs new file mode 100644 index 0000000..c5fff39 --- /dev/null +++ b/src/_Example/ExampleApp.IntegrationTests/ExtendedValuePermissionTests.cs @@ -0,0 +1,30 @@ +using ExampleApp.Application; + +using SecuritySystem.Testing; + +namespace ExampleApp.IntegrationTests; + +public class ExtendedValuePermissionTests : 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/_Example/ExampleApp.IntegrationTests/TestPermissionExtensions.cs b/src/_Example/ExampleApp.IntegrationTests/TestPermissionExtensions.cs index c18f425..b8d30a1 100644 --- a/src/_Example/ExampleApp.IntegrationTests/TestPermissionExtensions.cs +++ b/src/_Example/ExampleApp.IntegrationTests/TestPermissionExtensions.cs @@ -7,6 +7,8 @@ namespace ExampleApp.IntegrationTests; public static class TestPermissionExtensions { + public const string ExtendedKey = nameof(Domain.Auth.General.Permission.ExtendedValue); + extension(TestPermission testPermission) { public TypedSecurityIdentity? BusinessUnit @@ -20,5 +22,11 @@ public TypedSecurityIdentity[] BusinessUnits get => testPermission.GetMany(); set => testPermission.SetMany(value); } + + public string ExtendedValue + { + get => (string)testPermission.ExtendedData[ExtendedKey]; + set => testPermission.ExtendedData[ExtendedKey] = value; + } } -} +} \ No newline at end of file diff --git a/src/__SolutionItems/CommonAssemblyInfo.cs b/src/__SolutionItems/CommonAssemblyInfo.cs index a341232..66cc063 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.0.0")] +[assembly: AssemblyVersion("2.2.1.0")] [assembly: AssemblyInformationalVersion("changes at build")] #if DEBUG