From 46f6e00fe30f85e5c446922cb7593ce09e13502d Mon Sep 17 00:00:00 2001 From: Patrick <135162612+plavoie-BC@users.noreply.github.com> Date: Thu, 30 Apr 2026 10:00:34 -0700 Subject: [PATCH 1/7] [ARCH-SPIKE] Refactor identity permissions and claims handling Co-authored-by: Copilot --- .../PermissionOrAuthorizationHandler.cs | 40 +++ .../RoleOrPermissionAuthorizationHandler.cs | 44 +++ .../Identity/CurrentUser.cs | 2 +- .../Identity/IdentityExtensionMethods.cs | 19 -- .../IdentityProfileLoginAdminHandler.cs | 21 -- .../LoginHandlers/IdentityProfileLoginBase.cs | 5 +- .../IdentityProfileLoginUserHandler.cs | 40 +-- .../Identity/PermissionChecker.cs | 118 -------- .../Identity/PolicyRegistrant.cs | 251 ++---------------- 9 files changed, 116 insertions(+), 424 deletions(-) create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.Web/Identity/Authorization/PermissionOrAuthorizationHandler.cs create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.Web/Identity/Authorization/RoleOrPermissionAuthorizationHandler.cs delete mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.Web/Identity/IdentityExtensionMethods.cs delete mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.Web/Identity/PermissionChecker.cs diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Identity/Authorization/PermissionOrAuthorizationHandler.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Identity/Authorization/PermissionOrAuthorizationHandler.cs new file mode 100644 index 0000000000..7adb9b86e1 --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Identity/Authorization/PermissionOrAuthorizationHandler.cs @@ -0,0 +1,40 @@ +using Microsoft.AspNetCore.Authorization; +using System.Threading.Tasks; +using Volo.Abp.Authorization.Permissions; +using Volo.Abp.DependencyInjection; + +namespace Unity.GrantManager.Web.Identity.Authorization; + +public class PermissionOrRequirement : IAuthorizationRequirement +{ + public string[] Permissions { get; } + + public PermissionOrRequirement(params string[] permissions) + { + Permissions = permissions; + } +} + +public class PermissionOrAuthorizationHandler : AuthorizationHandler, ITransientDependency +{ + private readonly IPermissionChecker _permissionChecker; + + public PermissionOrAuthorizationHandler(IPermissionChecker permissionChecker) + { + _permissionChecker = permissionChecker; + } + + protected override async Task HandleRequirementAsync( + AuthorizationHandlerContext context, + PermissionOrRequirement requirement) + { + foreach (var permission in requirement.Permissions) + { + if (await _permissionChecker.IsGrantedAsync(context.User, permission)) + { + context.Succeed(requirement); + return; + } + } + } +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Identity/Authorization/RoleOrPermissionAuthorizationHandler.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Identity/Authorization/RoleOrPermissionAuthorizationHandler.cs new file mode 100644 index 0000000000..2701b9ba35 --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Identity/Authorization/RoleOrPermissionAuthorizationHandler.cs @@ -0,0 +1,44 @@ +using Microsoft.AspNetCore.Authorization; +using System.Threading.Tasks; +using Volo.Abp.Authorization.Permissions; +using Volo.Abp.DependencyInjection; + +namespace Unity.GrantManager.Web.Identity.Authorization; + +public class RoleOrPermissionRequirement : IAuthorizationRequirement +{ + public string RoleName { get; } + public string PermissionName { get; } + + public RoleOrPermissionRequirement(string roleName, string permissionName) + { + RoleName = roleName; + PermissionName = permissionName; + } +} + +public class RoleOrPermissionAuthorizationHandler : AuthorizationHandler, ITransientDependency +{ + private readonly IPermissionChecker _permissionChecker; + + public RoleOrPermissionAuthorizationHandler(IPermissionChecker permissionChecker) + { + _permissionChecker = permissionChecker; + } + + protected override async Task HandleRequirementAsync( + AuthorizationHandlerContext context, + RoleOrPermissionRequirement requirement) + { + if (context.User.IsInRole(requirement.RoleName)) + { + context.Succeed(requirement); + return; + } + + if (await _permissionChecker.IsGrantedAsync(context.User, requirement.PermissionName)) + { + context.Succeed(requirement); + } + } +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Identity/CurrentUser.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Identity/CurrentUser.cs index ab17dea528..6037b9f5c3 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Identity/CurrentUser.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Identity/CurrentUser.cs @@ -75,7 +75,7 @@ public virtual bool IsInRole(string roleName) var userClaims = _principalAccessor.Principal?.Claims; if (userClaims != null && userClaims.Any()) { - var userId = userClaims.FirstOrDefault(s => s.Type == "UserId"); + var userId = userClaims.FirstOrDefault(s => s.Type == AbpClaimTypes.UserId); if (userId != null) { return Guid.Parse(userId.Value); diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Identity/IdentityExtensionMethods.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Identity/IdentityExtensionMethods.cs deleted file mode 100644 index 3eb02c46cb..0000000000 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Identity/IdentityExtensionMethods.cs +++ /dev/null @@ -1,19 +0,0 @@ -using OpenIddict.Abstractions; -using System.Collections.Immutable; -using System.Security.Claims; - -namespace Unity.GrantManager.Web.Identity -{ - public static class IdentityExtensionMethods - { - public static ClaimsPrincipal AddPermission(this ClaimsPrincipal principal, string value) - { - return principal.AddClaim(UnityClaimsTypes.Permission, value); - } - - public static ClaimsPrincipal AddPermissions(this ClaimsPrincipal principal, ImmutableArray values) - { - return principal.AddClaims(UnityClaimsTypes.Permission, values); - } - } -} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Identity/LoginHandlers/IdentityProfileLoginAdminHandler.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Identity/LoginHandlers/IdentityProfileLoginAdminHandler.cs index c80469dbd5..3949150b68 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Identity/LoginHandlers/IdentityProfileLoginAdminHandler.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Identity/LoginHandlers/IdentityProfileLoginAdminHandler.cs @@ -1,13 +1,10 @@ using Microsoft.AspNetCore.Authentication.OpenIdConnect; using System; using System.Collections.Generic; -using System.Collections.Immutable; using System.Linq; using System.Security.Claims; using System.Threading.Tasks; using Unity.GrantManager.Identity; -using Unity.Modules.Shared.Permissions; -using Unity.TenantManagement; using Volo.Abp; using Volo.Abp.Data; using Volo.Abp.Identity; @@ -16,18 +13,6 @@ namespace Unity.GrantManager.Web.Identity.LoginHandlers { internal class IdentityProfileLoginAdminHandler : IdentityProfileLoginBase { - internal readonly ImmutableArray _adminPermissions = ImmutableArray.Create( - TenantManagementPermissions.Tenants.Default, - TenantManagementPermissions.Tenants.Create, - TenantManagementPermissions.Tenants.Update, - TenantManagementPermissions.Tenants.Delete, - TenantManagementPermissions.Tenants.ManageFeatures, - TenantManagementPermissions.Tenants.ManageConnectionStrings, - IdentityPermissions.Users.Create, - IdentityPermissions.UserLookup.Default, - IdentityConsts.ITAdminPermissionName - ); - internal async Task Handle(TokenValidatedContext validatedTokenContext, IList userTenantAccounts, string? idp) @@ -43,7 +28,6 @@ internal async Task Handle(TokenValidatedContext validated userTenantAccount = userTenantAccounts.First(s => s.TenantId == null); } - AssignAdminHostPermissions(validatedTokenContext.Principal!); AssignDefaultClaims(validatedTokenContext.Principal!, userTenantAccount.DisplayName ?? string.Empty, userTenantAccount.Id); return userTenantAccount; } @@ -63,11 +47,6 @@ private static bool AdminHasUserAccount(IList? userTenantA return false; } - private void AssignAdminHostPermissions(ClaimsPrincipal claimsPrincipal) - { - claimsPrincipal.AddPermissions(_adminPermissions); - } - private async Task CreateAdminAccountAsync(TokenValidatedContext validatedTokenContext, string? idp) { var token = validatedTokenContext.SecurityToken; diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Identity/LoginHandlers/IdentityProfileLoginBase.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Identity/LoginHandlers/IdentityProfileLoginBase.cs index 0810de9d47..f152afb7f8 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Identity/LoginHandlers/IdentityProfileLoginBase.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Identity/LoginHandlers/IdentityProfileLoginBase.cs @@ -7,7 +7,7 @@ using Volo.Abp.MultiTenancy; using Volo.Abp.DependencyInjection; using Volo.Abp.Identity; -using Volo.Abp.PermissionManagement; +using Volo.Abp.Security.Claims; using Microsoft.Extensions.Configuration; using Volo.Abp.TenantManagement; @@ -19,7 +19,6 @@ internal abstract class IdentityProfileLoginBase : ITransientDependency protected ICurrentTenant CurrentTenant => LazyServiceProvider.LazyGetRequiredService(); protected IdentityUserManager IdentityUserManager => LazyServiceProvider.LazyGetRequiredService(); protected IdentityRoleManager IdentityRoleManager => LazyServiceProvider.LazyGetRequiredService(); - protected PermissionManager PermissionManager => LazyServiceProvider.LazyGetRequiredService(); protected IIdentityUserRepository IdentityUserRepository => LazyServiceProvider.LazyGetRequiredService(); protected IConfiguration Configuration => LazyServiceProvider.LazyGetRequiredService(); protected IUserImportAppService UserImportAppService => LazyServiceProvider.LazyGetRequiredService(); @@ -29,7 +28,7 @@ internal abstract class IdentityProfileLoginBase : ITransientDependency protected static void AssignDefaultClaims(ClaimsPrincipal claimsPrinicipal, string displayName, Guid userId) { claimsPrinicipal.AddClaim("DisplayName", displayName); - claimsPrinicipal.AddClaim("UserId", userId.ToString()); + claimsPrinicipal.AddClaim(AbpClaimTypes.UserId, userId.ToString()); claimsPrinicipal.AddClaim("Badge", Utils.CreateUserBadge(displayName)); } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Identity/LoginHandlers/IdentityProfileLoginUserHandler.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Identity/LoginHandlers/IdentityProfileLoginUserHandler.cs index a13314a84e..d04f1dc4aa 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Identity/LoginHandlers/IdentityProfileLoginUserHandler.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Identity/LoginHandlers/IdentityProfileLoginUserHandler.cs @@ -4,32 +4,18 @@ using OpenIddict.Abstractions; using System; using System.Collections.Generic; -using System.Collections.Immutable; using System.Linq; using System.Security.Claims; using System.Threading.Tasks; using Unity.GrantManager.Identity; -using Unity.GrantManager.Permissions; using Unity.GrantManager.Web.Exceptions; -using Unity.Modules.Shared.Permissions; using Volo.Abp.Identity; -using Volo.Abp.PermissionManagement; using Volo.Abp.Security.Claims; namespace Unity.GrantManager.Web.Identity.LoginHandlers { internal class IdentityProfileLoginUserHandler : IdentityProfileLoginBase { - internal readonly ImmutableArray _userPermissions = [ - GrantManagerPermissions.Default, - IdentityPermissions.UserLookup.Default - ]; - - internal readonly ImmutableArray _itOperationsPermissions = [ - GrantManagerPermissions.Endpoints.ManageEndpoints, - IdentityConsts.ITOperationsPermissionName - ]; - internal async Task Handle(TokenValidatedContext validatedTokenContext, IList? userTenantAccounts, string? idp) @@ -57,11 +43,6 @@ internal async Task Handle(TokenValidatedContext validated } } - if (validatedTokenContext.Principal != null && validatedTokenContext.Principal.IsInRole(IdentityConsts.ITOperationsRoleName)) - { - AssignITOperationsPermissions(validatedTokenContext.Principal); - } - UserTenantAccountDto? userTenantAccount = null; var setTenant = validatedTokenContext.Request.Cookies["set_tenant"]; if (setTenant != null && setTenant != Guid.Empty.ToString()) @@ -84,20 +65,11 @@ internal async Task Handle(TokenValidatedContext validated { var dbRole = await IdentityRoleManager.GetByIdAsync(role.Id); principal.AddClaim(UnityClaimsTypes.Role, dbRole.Name); + principal.AddClaim(AbpClaimTypes.Role, dbRole.Name); } } - - var userPermissions = (await PermissionManager.GetAllForUserAsync(userTenantAccount.Id)).Where(s => s.IsGranted); - - foreach (var permissionName in userPermissions - .Select(s => s.Name) - .Where(permissionName => !principal.HasClaim(UnityClaimsTypes.Permission, permissionName))) - { - principal.AddClaim(UnityClaimsTypes.Permission, permissionName); - } } - AssignDefaultPermissions(validatedTokenContext.Principal!); AssignDefaultClaims(validatedTokenContext.Principal!, userTenantAccount.DisplayName ?? string.Empty, userTenantAccount.Id); validatedTokenContext.Principal!.AddClaim(AbpClaimTypes.TenantId, userTenantAccount.TenantId?.ToString() ?? Guid.Empty.ToString()); @@ -105,11 +77,6 @@ internal async Task Handle(TokenValidatedContext validated return userTenantAccount; } - private void AssignITOperationsPermissions(ClaimsPrincipal claimsPrincipal) - { - claimsPrincipal.AddPermissions(_itOperationsPermissions); - } - private async Task> AutoRegisterUserWithDefaultAsync(string userIdentifier, string username, string firstName, @@ -151,10 +118,5 @@ private bool IsAutoRegisterFlagSet() { return Configuration.GetValue("IdentityProfileLogin:AutoCreateUser"); } - - private void AssignDefaultPermissions(ClaimsPrincipal claimsPrincipal) - { - claimsPrincipal.AddPermissions(_userPermissions); - } } } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Identity/PermissionChecker.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Identity/PermissionChecker.cs deleted file mode 100644 index 53ff40213a..0000000000 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Identity/PermissionChecker.cs +++ /dev/null @@ -1,118 +0,0 @@ -using System.Linq; -using System.Security.Claims; -using System.Security.Principal; -using System.Threading.Tasks; -using Volo.Abp; -using Volo.Abp.Authorization.Permissions; -using Volo.Abp.DependencyInjection; -using Volo.Abp.MultiTenancy; -using Volo.Abp.Security.Claims; -using Volo.Abp.SimpleStateChecking; - -namespace Unity.GrantManager.Web.Identity -{ - [Dependency(ReplaceServices = true)] - [ExposeServices(typeof(PermissionChecker), typeof(IPermissionChecker))] - public class PermissionChecker : IPermissionChecker, ITransientDependency - { - protected IPermissionDefinitionManager PermissionDefinitionManager { get; } - protected ICurrentPrincipalAccessor PrincipalAccessor { get; } - protected ICurrentTenant CurrentTenant { get; } - protected IPermissionValueProviderManager PermissionValueProviderManager { get; } - protected ISimpleStateCheckerManager StateCheckerManager { get; } - - public PermissionChecker( - ICurrentPrincipalAccessor principalAccessor, - IPermissionDefinitionManager permissionDefinitionManager, - ICurrentTenant currentTenant, - IPermissionValueProviderManager permissionValueProviderManager, - ISimpleStateCheckerManager stateCheckerManager) - { - PrincipalAccessor = principalAccessor; - PermissionDefinitionManager = permissionDefinitionManager; - CurrentTenant = currentTenant; - PermissionValueProviderManager = permissionValueProviderManager; - StateCheckerManager = stateCheckerManager; - } - - public virtual async Task IsGrantedAsync(string name) - { - return await IsGrantedAsync(PrincipalAccessor.Principal, name); - } - - public virtual async Task IsGrantedAsync( - ClaimsPrincipal? claimsPrincipal, - string name) - { - Check.NotNull(name, nameof(name)); - - var permission = await PermissionDefinitionManager.GetOrNullAsync(name); - if (permission == null) - { - return false; - } - - if (!permission.IsEnabled) - { - return false; - } - - if (!await StateCheckerManager.IsEnabledAsync(permission)) - { - return false; - } - - var multiTenancySide = claimsPrincipal?.GetMultiTenancySide() - ?? CurrentTenant.GetMultiTenancySide(); - - if (!permission.MultiTenancySide.HasFlag(multiTenancySide)) - { - return false; - } - - var isGranted = false; - - if (claimsPrincipal != null - && claimsPrincipal.Claims.Any(s => s.Type == "Permission" && s.Value == name)) - { - isGranted = true; - } - - return isGranted; - } - - public async Task IsGrantedAsync(string[] names) - { - return await IsGrantedAsync(PrincipalAccessor.Principal, names); - } - - public async Task IsGrantedAsync(ClaimsPrincipal? claimsPrincipal, string[] names) - { - Check.NotNull(names, nameof(names)); - - var result = new MultiplePermissionGrantResult(); - if (names.Length == 0) - { - return result; - } - - if (claimsPrincipal != null) - { - var permissions = claimsPrincipal.Claims.Where(s => s.Type == "Permission"); - foreach (var name in names) - { - if (permissions.Select(s => s.Value).Contains(name)) - { - result.Result.Add(name, PermissionGrantResult.Granted); - } - else - { - result.Result.Add(name, PermissionGrantResult.Prohibited); - } - } - } - - return await Task.FromResult(result); - } - } -} \ No newline at end of file diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Identity/PolicyRegistrant.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Identity/PolicyRegistrant.cs index 10e9446c70..10a51884d1 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Identity/PolicyRegistrant.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Identity/PolicyRegistrant.cs @@ -1,244 +1,49 @@ using Microsoft.Extensions.DependencyInjection; -using Unity.GrantManager.Permissions; +using Unity.GrantManager.Web.Identity.Authorization; using Unity.Modules.Shared; using Unity.Modules.Shared.Permissions; -using Unity.Reporting.Permissions; -using Unity.TenantManagement; -using Volo.Abp.Identity; using Volo.Abp.Modularity; namespace Unity.GrantManager.Web.Identity.Policy; internal static class PolicyRegistrant { - internal const string PermissionConstant = "Permission"; - internal static void Register(ServiceConfigurationContext context) { - // Using AddAuthorizationBuilder to register authorization services and construct policies - var authorizationBuilder = context.Services.AddAuthorizationBuilder(); - - // Identity Role Policies - authorizationBuilder.AddPolicy(IdentityPermissions.Roles.Default, - policy => policy.RequireClaim(PermissionConstant, IdentityPermissions.Roles.Default)); - authorizationBuilder.AddPolicy(IdentityPermissions.Roles.Create, - policy => policy.RequireClaim(PermissionConstant, IdentityPermissions.Roles.Create)); - authorizationBuilder.AddPolicy(IdentityPermissions.Roles.Update, - policy => policy.RequireClaim(PermissionConstant, IdentityPermissions.Roles.Update)); - authorizationBuilder.AddPolicy(IdentityPermissions.Roles.Delete, - policy => policy.RequireClaim(PermissionConstant, IdentityPermissions.Roles.Delete)); - authorizationBuilder.AddPolicy(IdentityPermissions.Roles.ManagePermissions, - policy => policy.RequireClaim(PermissionConstant, IdentityPermissions.Roles.ManagePermissions)); - - // Identity User Policies - authorizationBuilder.AddPolicy(IdentityPermissions.Users.Default, - policy => policy.RequireClaim(PermissionConstant, IdentityPermissions.Users.Default)); - authorizationBuilder.AddPolicy(IdentityPermissions.Users.Create, - policy => policy.RequireClaim(PermissionConstant, IdentityPermissions.Users.Create)); - authorizationBuilder.AddPolicy(IdentityPermissions.Users.Update, - policy => policy.RequireClaim(PermissionConstant, IdentityPermissions.Users.Update)); - authorizationBuilder.AddPolicy(IdentityPermissions.Users.Delete, - policy => policy.RequireClaim(PermissionConstant, IdentityPermissions.Users.Delete)); - authorizationBuilder.AddPolicy(IdentityPermissions.Users.ManagePermissions, - policy => policy.RequireClaim(PermissionConstant, IdentityPermissions.Users.ManagePermissions)); - - // User Lookup Policies - authorizationBuilder.AddPolicy(IdentityPermissions.UserLookup.Default, - policy => policy.RequireClaim(PermissionConstant, IdentityPermissions.UserLookup.Default)); - - // Grant Manager Policies - authorizationBuilder.AddPolicy(GrantManagerPermissions.Default, - policy => policy.RequireClaim(PermissionConstant, GrantManagerPermissions.Default)); - authorizationBuilder.AddPolicy(GrantManagerPermissions.Intakes.Default, - policy => policy.RequireClaim(PermissionConstant, GrantManagerPermissions.Intakes.Default)); - authorizationBuilder.AddPolicy(GrantManagerPermissions.ApplicationForms.Default, - policy => policy.RequireClaim(PermissionConstant, GrantManagerPermissions.ApplicationForms.Default)); - - // Grant Application Policies - authorizationBuilder.AddPolicy(GrantApplicationPermissions.Applications.Default, - policy => policy.RequireClaim(PermissionConstant, GrantApplicationPermissions.Applications.Default)); - authorizationBuilder.AddPolicy(GrantApplicationPermissions.Applicants.Default, - policy => policy.RequireClaim(PermissionConstant, GrantApplicationPermissions.Applicants.Default)); - authorizationBuilder.AddPolicy(GrantApplicationPermissions.Applicants.Edit, - policy => policy.RequireClaim(PermissionConstant, GrantApplicationPermissions.Applicants.Edit)); - authorizationBuilder.AddPolicy(GrantApplicationPermissions.Applicants.AssignApplicant, - policy => policy.RequireClaim(PermissionConstant, GrantApplicationPermissions.Applicants.AssignApplicant)); - authorizationBuilder.AddPolicy(GrantApplicationPermissions.Assignments.Default, - policy => policy.RequireClaim(PermissionConstant, GrantApplicationPermissions.Assignments.Default)); - authorizationBuilder.AddPolicy(GrantApplicationPermissions.Assignments.AssignInitial, - policy => policy.RequireClaim(PermissionConstant, GrantApplicationPermissions.Assignments.AssignInitial)); - authorizationBuilder.AddPolicy(GrantApplicationPermissions.Reviews.Default, - policy => policy.RequireClaim(PermissionConstant, GrantApplicationPermissions.Reviews.Default)); - authorizationBuilder.AddPolicy(GrantApplicationPermissions.Reviews.StartInitial, - policy => policy.RequireClaim(PermissionConstant, GrantApplicationPermissions.Reviews.StartInitial)); - authorizationBuilder.AddPolicy(GrantApplicationPermissions.Reviews.CompleteInitial, - policy => policy.RequireClaim(PermissionConstant, GrantApplicationPermissions.Reviews.CompleteInitial)); - authorizationBuilder.AddPolicy(GrantApplicationPermissions.Approvals.Default, - policy => policy.RequireClaim(PermissionConstant, GrantApplicationPermissions.Approvals.Default)); - authorizationBuilder.AddPolicy(GrantApplicationPermissions.Approvals.Complete, - policy => policy.RequireClaim(PermissionConstant, GrantApplicationPermissions.Approvals.Complete)); - authorizationBuilder.AddPolicy(GrantApplicationPermissions.Comments.Default, - policy => policy.RequireClaim(PermissionConstant, GrantApplicationPermissions.Comments.Default)); - authorizationBuilder.AddPolicy(GrantApplicationPermissions.Comments.Add, - policy => policy.RequireClaim(PermissionConstant, GrantApplicationPermissions.Comments.Add)); + // Simple permission-based policies (e.g., [Authorize("PermissionName")]) are handled + // automatically by ABP's AbpAuthorizationPolicyProvider + PermissionRequirementHandler. + // Only register custom composite policies here. - // R&A Policies - authorizationBuilder.AddPolicy(UnitySelector.Review.Default, - policy => policy.RequireClaim(PermissionConstant, UnitySelector.Review.Default)); - - // R&A - Approval Policies - authorizationBuilder.AddPolicy(UnitySelector.Review.Approval.Default, - policy => policy.RequireClaim(PermissionConstant, UnitySelector.Review.Approval.Default)); - authorizationBuilder.AddPolicy(UnitySelector.Review.Approval.Update.Default, - policy => policy.RequireClaim(PermissionConstant, UnitySelector.Review.Approval.Update.Default)); - authorizationBuilder.AddPolicy(UnitySelector.Review.Approval.Update.UpdateFinalStateFields, - policy => policy.RequireClaim(PermissionConstant, UnitySelector.Review.Approval.Update.UpdateFinalStateFields)); - - // R&A - Assessment Results Policies - authorizationBuilder.AddPolicy(UnitySelector.Review.AssessmentResults.Default, - policy => policy.RequireClaim(PermissionConstant, UnitySelector.Review.AssessmentResults.Default)); - authorizationBuilder.AddPolicy(UnitySelector.Review.AssessmentResults.Update.Default, - policy => policy.RequireClaim(PermissionConstant, UnitySelector.Review.AssessmentResults.Update.Default)); - authorizationBuilder.AddPolicy(UnitySelector.Review.AssessmentResults.Update.UpdateFinalStateFields, - policy => policy.RequireClaim(PermissionConstant, UnitySelector.Review.AssessmentResults.Update.UpdateFinalStateFields)); - - // R&A - Assessment Review List Policies - authorizationBuilder.AddPolicy(UnitySelector.Review.AssessmentReviewList.Default, - policy => policy.RequireClaim(PermissionConstant, UnitySelector.Review.AssessmentReviewList.Default)); - authorizationBuilder.AddPolicy(UnitySelector.Review.AssessmentReviewList.Create, - policy => policy.RequireClaim(PermissionConstant, UnitySelector.Review.AssessmentReviewList.Create)); - authorizationBuilder.AddPolicy(UnitySelector.Review.AssessmentReviewList.Update.SendBack, - policy => policy.RequireClaim(PermissionConstant, UnitySelector.Review.AssessmentReviewList.Update.SendBack)); - authorizationBuilder.AddPolicy(UnitySelector.Review.AssessmentReviewList.Update.Complete, - policy => policy.RequireClaim(PermissionConstant, UnitySelector.Review.AssessmentReviewList.Update.Complete)); - - //-- APPLICANT INFO - authorizationBuilder.AddPolicy(UnitySelector.Applicant.Default, - policy => policy.RequireClaim(PermissionConstant, UnitySelector.Applicant.Default)); - authorizationBuilder.AddPolicy(UnitySelector.Applicant.Authority.Default, - policy => policy.RequireClaim(PermissionConstant, UnitySelector.Applicant.Authority.Default)); - authorizationBuilder.AddPolicy(UnitySelector.Applicant.Authority.Update, - policy => policy.RequireClaim(PermissionConstant, UnitySelector.Applicant.Authority.Update)); - authorizationBuilder.AddPolicy(UnitySelector.Applicant.Contact.Default, - policy => policy.RequireClaim(PermissionConstant, UnitySelector.Applicant.Contact.Default)); - authorizationBuilder.AddPolicy(UnitySelector.Applicant.Contact.Update, - policy => policy.RequireClaim(PermissionConstant, UnitySelector.Applicant.Contact.Update)); - authorizationBuilder.AddPolicy(UnitySelector.Applicant.Location.Default, - policy => policy.RequireClaim(PermissionConstant, UnitySelector.Applicant.Location.Default)); - authorizationBuilder.AddPolicy(UnitySelector.Applicant.Location.Update, - policy => policy.RequireClaim(PermissionConstant, UnitySelector.Applicant.Location.Update)); - authorizationBuilder.AddPolicy(UnitySelector.Applicant.Summary.Default, - policy => policy.RequireClaim(PermissionConstant, UnitySelector.Applicant.Summary.Default)); - authorizationBuilder.AddPolicy(UnitySelector.Applicant.Summary.Update, - policy => policy.RequireClaim(PermissionConstant, UnitySelector.Applicant.Summary.Update)); - authorizationBuilder.AddPolicy(UnitySelector.Applicant.AdditionalContact.Default, - policy => policy.RequireClaim(PermissionConstant, UnitySelector.Applicant.AdditionalContact.Default)); - authorizationBuilder.AddPolicy(UnitySelector.Applicant.AdditionalContact.Create, - policy => policy.RequireClaim(PermissionConstant, UnitySelector.Applicant.AdditionalContact.Create)); - authorizationBuilder.AddPolicy(UnitySelector.Applicant.AdditionalContact.Update, - policy => policy.RequireClaim(PermissionConstant, UnitySelector.Applicant.AdditionalContact.Update)); - - // Applicant Info Logical OR policy - authorizationBuilder.AddPolicy(UnitySelector.Applicant.UpdatePolicy, - policy => policy.RequireAssertion(context => - context.User.HasClaim(PermissionConstant, UnitySelector.Applicant.Summary.Update) || - context.User.HasClaim(PermissionConstant, UnitySelector.Applicant.Contact.Update) || - context.User.HasClaim(PermissionConstant, UnitySelector.Applicant.Authority.Update) || - context.User.HasClaim(PermissionConstant, UnitySelector.Applicant.Location.Update) || - context.User.HasClaim(PermissionConstant, UnitySelector.Applicant.AdditionalContact.Update) || - - // NOTE: This will be replaced when Worksheets are normalized with UnitySelector.Applicant.Worksheet.Update - context.User.HasClaim(PermissionConstant, UnitySelector.Applicant.Default) - )); - - //-- PAYMENT INFO - authorizationBuilder.AddPolicy(UnitySelector.Payment.Default, - policy => policy.RequireClaim(PermissionConstant, UnitySelector.Payment.Default)); - authorizationBuilder.AddPolicy(UnitySelector.Payment.Supplier.Default, - policy => policy.RequireClaim(PermissionConstant, UnitySelector.Payment.Summary.Default)); - authorizationBuilder.AddPolicy(UnitySelector.Payment.Supplier.Default, - policy => policy.RequireClaim(PermissionConstant, UnitySelector.Payment.Supplier.Default)); - authorizationBuilder.AddPolicy(UnitySelector.Payment.Supplier.Update, - policy => policy.RequireClaim(PermissionConstant, UnitySelector.Payment.Supplier.Update)); - authorizationBuilder.AddPolicy(UnitySelector.Payment.Supplier.Default, - policy => policy.RequireClaim(PermissionConstant, UnitySelector.Payment.PaymentList.Default)); - - // Tenancy Policies - authorizationBuilder.AddPolicy(TenantManagementPermissions.Tenants.Default, - policy => policy.RequireClaim(PermissionConstant, TenantManagementPermissions.Tenants.Default)); - authorizationBuilder.AddPolicy(TenantManagementPermissions.Tenants.Create, - policy => policy.RequireClaim(PermissionConstant, TenantManagementPermissions.Tenants.Create)); - authorizationBuilder.AddPolicy(TenantManagementPermissions.Tenants.Update, - policy => policy.RequireClaim(PermissionConstant, TenantManagementPermissions.Tenants.Update)); - authorizationBuilder.AddPolicy(TenantManagementPermissions.Tenants.Delete, - policy => policy.RequireClaim(PermissionConstant, TenantManagementPermissions.Tenants.Delete)); - authorizationBuilder.AddPolicy(TenantManagementPermissions.Tenants.ManageFeatures, - policy => policy.RequireClaim(PermissionConstant, TenantManagementPermissions.Tenants.ManageFeatures)); - authorizationBuilder.AddPolicy(TenantManagementPermissions.Tenants.ManageConnectionStrings, - policy => policy.RequireClaim(PermissionConstant, TenantManagementPermissions.Tenants.ManageConnectionStrings)); - - // Setting Management - Tag Management - authorizationBuilder.AddPolicy(UnitySelector.SettingManagement.Tags.Default, - policy => policy.RequireClaim(PermissionConstant, UnitySelector.SettingManagement.Tags.Default)); - authorizationBuilder.AddPolicy(UnitySelector.SettingManagement.Tags.Create, - policy => policy.RequireClaim(PermissionConstant, UnitySelector.SettingManagement.Tags.Create)); - authorizationBuilder.AddPolicy(UnitySelector.SettingManagement.Tags.Update, - policy => policy.RequireClaim(PermissionConstant, UnitySelector.SettingManagement.Tags.Update)); - authorizationBuilder.AddPolicy(UnitySelector.SettingManagement.Tags.Delete, - policy => policy.RequireClaim(PermissionConstant, UnitySelector.SettingManagement.Tags.Delete)); + var authorizationBuilder = context.Services.AddAuthorizationBuilder(); - // IT Administrator Policies + // IT Administrator Policy: granted by role OR permission authorizationBuilder.AddPolicy(IdentityConsts.ITAdminPolicyName, - policy => policy.RequireAssertion(context => - context.User.IsInRole(IdentityConsts.ITAdminRoleName) || - context.User.HasClaim(c => c.Type == PermissionConstant && c.Value == IdentityConsts.ITAdminPermissionName) - )); + policy => policy.AddRequirements( + new RoleOrPermissionRequirement(IdentityConsts.ITAdminRoleName, IdentityConsts.ITAdminPermissionName))); - // IT Operations Policies + // IT Operations Policy: granted by role OR permission authorizationBuilder.AddPolicy(IdentityConsts.ITOperationsPolicyName, - policy => policy.RequireAssertion(context => - context.User.IsInRole(IdentityConsts.ITOperationsRoleName) || - context.User.HasClaim(c => c.Type == PermissionConstant && c.Value == IdentityConsts.ITOperationsPermissionName) - )); - - // Project Info Policies - authorizationBuilder.AddPolicy(UnitySelector.Project.Default, - policy => policy.RequireClaim(PermissionConstant, UnitySelector.Project.Default)); + policy => policy.AddRequirements( + new RoleOrPermissionRequirement(IdentityConsts.ITOperationsRoleName, IdentityConsts.ITOperationsPermissionName))); - // Project Info Logical OR policy + // Applicant Info Logical OR policy: any update sub-permission grants access + authorizationBuilder.AddPolicy(UnitySelector.Applicant.UpdatePolicy, + policy => policy.AddRequirements( + new PermissionOrRequirement( + UnitySelector.Applicant.Summary.Update, + UnitySelector.Applicant.Contact.Update, + UnitySelector.Applicant.Authority.Update, + UnitySelector.Applicant.Location.Update, + UnitySelector.Applicant.AdditionalContact.Update, + UnitySelector.Applicant.Default))); + + // Project Info Logical OR policy: any update sub-permission grants access authorizationBuilder.AddPolicy(UnitySelector.Project.UpdatePolicy, - policy => policy.RequireAssertion(context => - context.User.HasClaim(PermissionConstant, UnitySelector.Project.Location.Update.Default) || - context.User.HasClaim(PermissionConstant, UnitySelector.Project.Summary.Update.Default) || - - // NOTE: This will be replaced when Worksheets are normalized with UnitySelector.Project.Worksheet.Update - context.User.HasClaim(PermissionConstant, UnitySelector.Project.Default) - )); - - // Project Info - Summary Policies - authorizationBuilder.AddPolicy(UnitySelector.Project.Summary.Default, - policy => policy.RequireClaim(PermissionConstant, UnitySelector.Project.Summary.Default)); - authorizationBuilder.AddPolicy(UnitySelector.Project.Summary.Update.Default, - policy => policy.RequireClaim(PermissionConstant, UnitySelector.Project.Summary.Update.Default)); - authorizationBuilder.AddPolicy(UnitySelector.Project.Summary.Update.UpdateFinalStateFields, - policy => policy.RequireClaim(PermissionConstant, UnitySelector.Project.Summary.Update.UpdateFinalStateFields)); - - // Project Info - Location Policies - authorizationBuilder.AddPolicy(UnitySelector.Project.Location.Default, - policy => policy.RequireClaim(PermissionConstant, UnitySelector.Project.Location.Default)); - authorizationBuilder.AddPolicy(UnitySelector.Project.Location.Update.Default, - policy => policy.RequireClaim(PermissionConstant, UnitySelector.Project.Location.Update.Default)); - authorizationBuilder.AddPolicy(UnitySelector.Project.Location.Update.UpdateFinalStateFields, - policy => policy.RequireClaim(PermissionConstant, UnitySelector.Project.Location.Update.UpdateFinalStateFields)); - - - // Reporting Configuration - authorizationBuilder.AddPolicy(ReportingPermissions.Configuration.Default, - policy => policy.RequireClaim(PermissionConstant, ReportingPermissions.Configuration.Default)); - authorizationBuilder.AddPolicy(ReportingPermissions.Configuration.Update, - policy => policy.RequireClaim(PermissionConstant, ReportingPermissions.Configuration.Update)); - authorizationBuilder.AddPolicy(ReportingPermissions.Configuration.Delete, - policy => policy.RequireClaim(PermissionConstant, ReportingPermissions.Configuration.Delete)); + policy => policy.AddRequirements( + new PermissionOrRequirement( + UnitySelector.Project.Location.Update.Default, + UnitySelector.Project.Summary.Update.Default, + UnitySelector.Project.Default))); } } From 3c52090763ce785127071ac7fd59d308c245180a Mon Sep 17 00:00:00 2001 From: Patrick <135162612+plavoie-BC@users.noreply.github.com> Date: Thu, 30 Apr 2026 10:36:42 -0700 Subject: [PATCH 2/7] [AB#32738] Update claim handling for user ID compatibility --- .../Assessments/AssessmentAuthorizationHandler.cs | 4 +++- .../Identity/LoginHandlers/IdentityProfileLoginBase.cs | 1 + 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Assessments/AssessmentAuthorizationHandler.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Assessments/AssessmentAuthorizationHandler.cs index 3a8a4bbffd..0458eee55e 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Assessments/AssessmentAuthorizationHandler.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Assessments/AssessmentAuthorizationHandler.cs @@ -8,6 +8,7 @@ using Volo.Abp; using Volo.Abp.Authorization.Permissions; using Volo.Abp.DependencyInjection; +using Volo.Abp.Security.Claims; namespace Unity.GrantManager.Assessments; public class AssessmentAuthorizationHandler : AuthorizationHandler, ISingletonDependency @@ -81,7 +82,8 @@ protected virtual async Task CheckPolicyAsync(string permissionName, Autho { Check.NotNull(principal, nameof(principal)); - var userIdOrNull = principal.Claims?.FirstOrDefault(c => c.Type == "UserId"); + var userIdOrNull = principal.Claims?.FirstOrDefault(c => c.Type == AbpClaimTypes.UserId) + ?? principal.Claims?.FirstOrDefault(c => c.Type == "UserId"); if (userIdOrNull == null || userIdOrNull.Value.IsNullOrWhiteSpace()) { return null; diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Identity/LoginHandlers/IdentityProfileLoginBase.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Identity/LoginHandlers/IdentityProfileLoginBase.cs index f152afb7f8..4ca8271e8b 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Identity/LoginHandlers/IdentityProfileLoginBase.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Identity/LoginHandlers/IdentityProfileLoginBase.cs @@ -29,6 +29,7 @@ protected static void AssignDefaultClaims(ClaimsPrincipal claimsPrinicipal, stri { claimsPrinicipal.AddClaim("DisplayName", displayName); claimsPrinicipal.AddClaim(AbpClaimTypes.UserId, userId.ToString()); + claimsPrinicipal.AddClaim("UserId", userId.ToString()); // Legacy claim for backward compatibility claimsPrinicipal.AddClaim("Badge", Utils.CreateUserBadge(displayName)); } From 68192ca4e76dfd96bfe9e5e1c4774ef2de70f826 Mon Sep 17 00:00:00 2001 From: Patrick <135162612+plavoie-BC@users.noreply.github.com> Date: Thu, 30 Apr 2026 10:36:54 -0700 Subject: [PATCH 3/7] [AB#32738] Simplify permission check logic in handler Co-authored-by: Copilot --- .../Authorization/PermissionOrAuthorizationHandler.cs | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Identity/Authorization/PermissionOrAuthorizationHandler.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Identity/Authorization/PermissionOrAuthorizationHandler.cs index 7adb9b86e1..90f1034f54 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Identity/Authorization/PermissionOrAuthorizationHandler.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Identity/Authorization/PermissionOrAuthorizationHandler.cs @@ -1,3 +1,4 @@ +using System.Linq; using Microsoft.AspNetCore.Authorization; using System.Threading.Tasks; using Volo.Abp.Authorization.Permissions; @@ -28,13 +29,11 @@ protected override async Task HandleRequirementAsync( AuthorizationHandlerContext context, PermissionOrRequirement requirement) { - foreach (var permission in requirement.Permissions) + var result = await _permissionChecker.IsGrantedAsync(context.User, requirement.Permissions); + + if (result.Result.Any(r => r.Value == PermissionGrantResult.Granted)) { - if (await _permissionChecker.IsGrantedAsync(context.User, permission)) - { - context.Succeed(requirement); - return; - } + context.Succeed(requirement); } } } From cad2330125bacf5f7e356e7f51b52c74ccb99cc3 Mon Sep 17 00:00:00 2001 From: Patrick <135162612+plavoie-BC@users.noreply.github.com> Date: Thu, 30 Apr 2026 10:54:35 -0700 Subject: [PATCH 4/7] [AB#32738] Seed host-level ITAdmin/ITOps permissions Co-authored-by: Copilot --- .../Permissions/PermissionGrantsDataSeeder.cs | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/Permissions/PermissionGrantsDataSeeder.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/Permissions/PermissionGrantsDataSeeder.cs index d92d21a3dd..d2678d8078 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/Permissions/PermissionGrantsDataSeeder.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/Permissions/PermissionGrantsDataSeeder.cs @@ -4,6 +4,7 @@ using Unity.Flex.Permissions; using Unity.GrantManager.Identity; using Unity.Modules.Shared; +using Unity.Modules.Shared.Permissions; using Unity.Notifications.Permissions; using Unity.Payments.Permissions; using Volo.Abp.Authorization.Permissions; @@ -99,6 +100,12 @@ public PermissionGrantsDataSeeder(IPermissionDataSeeder permissionDataSeeder) public async Task SeedAsync(DataSeedContext context) { + if (context.TenantId == null) + { + await SeedHostPermissionsAsync(); + return; + } + // Default permission grants based on role // - Program Manager @@ -332,6 +339,30 @@ await _permissionDataSeeder.SeedAsync(RolePermissionValueProvider.ProviderName, ], context.TenantId); } + + private async Task SeedHostPermissionsAsync() + { + // ITAdministrator host-level permissions (previously stamped at login) + await _permissionDataSeeder.SeedAsync(RolePermissionValueProvider.ProviderName, IdentityConsts.ITAdminRoleName, + [ + "UnityTenantManagement.Tenants", + "UnityTenantManagement.Tenants.Create", + "UnityTenantManagement.Tenants.Update", + "UnityTenantManagement.Tenants.Delete", + "AbpTenantManagement.Tenants.ManageFeatures", + "UnityTenantManagement.Tenants.ManageConnectionStrings", + IdentitySeedPermissions.Users.Create, + IdentitySeedPermissions.UserLookup.Default, + IdentityConsts.ITAdminPermissionName + ], tenantId: null); + + // ITOperations host-level permissions (previously stamped at login) + await _permissionDataSeeder.SeedAsync(RolePermissionValueProvider.ProviderName, IdentityConsts.ITOperationsRoleName, + [ + GrantManagerPermissions.Endpoints.ManageEndpoints, + IdentityConsts.ITOperationsPermissionName + ], tenantId: null); + } } } From 3c404b8dc01d99127c2f2085f5dccf7b2ee7f94e Mon Sep 17 00:00:00 2001 From: Patrick <135162612+plavoie-BC@users.noreply.github.com> Date: Thu, 30 Apr 2026 11:07:59 -0700 Subject: [PATCH 5/7] [AB#32738] Add role claim for ITAdmin in login handler Co-authored-by: Copilot --- .../Identity/LoginHandlers/IdentityProfileLoginAdminHandler.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Identity/LoginHandlers/IdentityProfileLoginAdminHandler.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Identity/LoginHandlers/IdentityProfileLoginAdminHandler.cs index 3949150b68..8b0ff2641d 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Identity/LoginHandlers/IdentityProfileLoginAdminHandler.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Identity/LoginHandlers/IdentityProfileLoginAdminHandler.cs @@ -5,9 +5,11 @@ using System.Security.Claims; using System.Threading.Tasks; using Unity.GrantManager.Identity; +using Unity.Modules.Shared.Permissions; using Volo.Abp; using Volo.Abp.Data; using Volo.Abp.Identity; +using Volo.Abp.Security.Claims; namespace Unity.GrantManager.Web.Identity.LoginHandlers { @@ -29,6 +31,7 @@ internal async Task Handle(TokenValidatedContext validated } AssignDefaultClaims(validatedTokenContext.Principal!, userTenantAccount.DisplayName ?? string.Empty, userTenantAccount.Id); + (validatedTokenContext.Principal!.Identity as ClaimsIdentity)?.AddClaim(new Claim(AbpClaimTypes.Role, IdentityConsts.ITAdminRoleName)); return userTenantAccount; } From bf8761c83b0092a3f6802977680682ab37d77095 Mon Sep 17 00:00:00 2001 From: Patrick <135162612+plavoie-BC@users.noreply.github.com> Date: Thu, 30 Apr 2026 11:37:59 -0700 Subject: [PATCH 6/7] [AB#32738] Add unit tests for user ID claim handling --- .../Assessments/FindUserIdirIdTests.cs | 131 ++++++++++++++ .../PermissionOrAuthorizationHandlerTests.cs | 166 ++++++++++++++++++ ...leOrPermissionAuthorizationHandlerTests.cs | 145 +++++++++++++++ 3 files changed, 442 insertions(+) create mode 100644 applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/Assessments/FindUserIdirIdTests.cs create mode 100644 applications/Unity.GrantManager/test/Unity.GrantManager.Web.Tests/Identity/Authorization/PermissionOrAuthorizationHandlerTests.cs create mode 100644 applications/Unity.GrantManager/test/Unity.GrantManager.Web.Tests/Identity/Authorization/RoleOrPermissionAuthorizationHandlerTests.cs diff --git a/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/Assessments/FindUserIdirIdTests.cs b/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/Assessments/FindUserIdirIdTests.cs new file mode 100644 index 0000000000..8c0caf808e --- /dev/null +++ b/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/Assessments/FindUserIdirIdTests.cs @@ -0,0 +1,131 @@ +using System; +using System.Security.Claims; +using Shouldly; +using Unity.GrantManager.Assessments; +using Volo.Abp.Security.Claims; +using Xunit; + +namespace Unity.GrantManager.Assessments; + +public class FindUserIdirIdTests +{ + private static ClaimsPrincipal CreatePrincipal(params Claim[] claims) + { + var identity = new ClaimsIdentity("TestAuth"); + identity.AddClaims(claims); + return new ClaimsPrincipal(identity); + } + + [Fact] + public void ShouldReturnGuid_WhenAbpUserIdClaimPresent() + { + // Arrange + var userId = Guid.NewGuid(); + var principal = CreatePrincipal( + new Claim(AbpClaimTypes.UserId, userId.ToString())); + + // Act + var result = AssessmentAuthorizationHandler.FindUserIdirId(principal); + + // Assert + result.ShouldBe(userId); + } + + [Fact] + public void ShouldReturnGuid_WhenLegacyUserIdClaimPresent() + { + // Arrange + var userId = Guid.NewGuid(); + var principal = CreatePrincipal( + new Claim("UserId", userId.ToString())); + + // Act + var result = AssessmentAuthorizationHandler.FindUserIdirId(principal); + + // Assert + result.ShouldBe(userId); + } + + [Fact] + public void ShouldPreferAbpClaim_WhenBothPresent() + { + // Arrange + var abpUserId = Guid.NewGuid(); + var legacyUserId = Guid.NewGuid(); + var principal = CreatePrincipal( + new Claim(AbpClaimTypes.UserId, abpUserId.ToString()), + new Claim("UserId", legacyUserId.ToString())); + + // Act + var result = AssessmentAuthorizationHandler.FindUserIdirId(principal); + + // Assert + result.ShouldBe(abpUserId); + } + + [Fact] + public void ShouldFallbackToLegacy_WhenAbpClaimMissing() + { + // Arrange + var legacyUserId = Guid.NewGuid(); + var principal = CreatePrincipal( + new Claim("DisplayName", "Test User"), + new Claim("UserId", legacyUserId.ToString())); + + // Act + var result = AssessmentAuthorizationHandler.FindUserIdirId(principal); + + // Assert + result.ShouldBe(legacyUserId); + } + + [Fact] + public void ShouldReturnNull_WhenNeitherClaimPresent() + { + // Arrange + var principal = CreatePrincipal( + new Claim("DisplayName", "Test User")); + + // Act + var result = AssessmentAuthorizationHandler.FindUserIdirId(principal); + + // Assert + result.ShouldBeNull(); + } + + [Fact] + public void ShouldReturnNull_WhenClaimValueIsEmpty() + { + // Arrange + var principal = CreatePrincipal( + new Claim(AbpClaimTypes.UserId, ""), + new Claim("UserId", "")); + + // Act + var result = AssessmentAuthorizationHandler.FindUserIdirId(principal); + + // Assert + result.ShouldBeNull(); + } + + [Fact] + public void ShouldReturnNull_WhenClaimValueIsNotValidGuid() + { + // Arrange + var principal = CreatePrincipal( + new Claim(AbpClaimTypes.UserId, "not-a-guid")); + + // Act + var result = AssessmentAuthorizationHandler.FindUserIdirId(principal); + + // Assert + result.ShouldBeNull(); + } + + [Fact] + public void ShouldThrow_WhenPrincipalIsNull() + { + Should.Throw(() => + AssessmentAuthorizationHandler.FindUserIdirId(null!)); + } +} diff --git a/applications/Unity.GrantManager/test/Unity.GrantManager.Web.Tests/Identity/Authorization/PermissionOrAuthorizationHandlerTests.cs b/applications/Unity.GrantManager/test/Unity.GrantManager.Web.Tests/Identity/Authorization/PermissionOrAuthorizationHandlerTests.cs new file mode 100644 index 0000000000..afe984730c --- /dev/null +++ b/applications/Unity.GrantManager/test/Unity.GrantManager.Web.Tests/Identity/Authorization/PermissionOrAuthorizationHandlerTests.cs @@ -0,0 +1,166 @@ +using System.Security.Claims; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authorization; +using NSubstitute; +using Shouldly; +using Unity.GrantManager.Web.Identity.Authorization; +using Volo.Abp.Authorization.Permissions; +using Xunit; + +namespace Unity.GrantManager.Identity.Authorization; + +public class PermissionOrAuthorizationHandlerTests +{ + private readonly IPermissionChecker _permissionChecker; + private readonly PermissionOrAuthorizationHandler _handler; + + public PermissionOrAuthorizationHandlerTests() + { + _permissionChecker = Substitute.For(); + _handler = new PermissionOrAuthorizationHandler(_permissionChecker); + } + + private static AuthorizationHandlerContext CreateContext( + ClaimsPrincipal user, + PermissionOrRequirement requirement) + { + return new AuthorizationHandlerContext( + [requirement], + user, + resource: null); + } + + private static ClaimsPrincipal CreateUser() + { + var identity = new ClaimsIdentity("TestAuth"); + return new ClaimsPrincipal(identity); + } + + [Fact] + public async Task HandleAsync_ShouldSucceed_WhenAnyPermissionIsGranted() + { + // Arrange + var user = CreateUser(); + var permissions = new[] { "Perm.A", "Perm.B", "Perm.C" }; + var requirement = new PermissionOrRequirement(permissions); + + var grantResult = new MultiplePermissionGrantResult(); + grantResult.Result.Add("Perm.A", PermissionGrantResult.Prohibited); + grantResult.Result.Add("Perm.B", PermissionGrantResult.Granted); + grantResult.Result.Add("Perm.C", PermissionGrantResult.Prohibited); + + _permissionChecker.IsGrantedAsync(user, permissions) + .Returns(grantResult); + + var context = CreateContext(user, requirement); + + // Act + await _handler.HandleAsync(context); + + // Assert + context.HasSucceeded.ShouldBeTrue(); + } + + [Fact] + public async Task HandleAsync_ShouldNotSucceed_WhenNoPermissionsGranted() + { + // Arrange + var user = CreateUser(); + var permissions = new[] { "Perm.A", "Perm.B" }; + var requirement = new PermissionOrRequirement(permissions); + + var grantResult = new MultiplePermissionGrantResult(); + grantResult.Result.Add("Perm.A", PermissionGrantResult.Prohibited); + grantResult.Result.Add("Perm.B", PermissionGrantResult.Prohibited); + + _permissionChecker.IsGrantedAsync(user, permissions) + .Returns(grantResult); + + var context = CreateContext(user, requirement); + + // Act + await _handler.HandleAsync(context); + + // Assert + context.HasSucceeded.ShouldBeFalse(); + } + + [Fact] + public async Task HandleAsync_ShouldSucceed_WhenAllPermissionsGranted() + { + // Arrange + var user = CreateUser(); + var permissions = new[] { "Perm.A", "Perm.B" }; + var requirement = new PermissionOrRequirement(permissions); + + var grantResult = new MultiplePermissionGrantResult(); + grantResult.Result.Add("Perm.A", PermissionGrantResult.Granted); + grantResult.Result.Add("Perm.B", PermissionGrantResult.Granted); + + _permissionChecker.IsGrantedAsync(user, permissions) + .Returns(grantResult); + + var context = CreateContext(user, requirement); + + // Act + await _handler.HandleAsync(context); + + // Assert + context.HasSucceeded.ShouldBeTrue(); + } + + [Fact] + public async Task HandleAsync_ShouldUseBatchApi_NotIndividualCalls() + { + // Arrange + var user = CreateUser(); + var permissions = new[] { "Perm.X", "Perm.Y", "Perm.Z" }; + var requirement = new PermissionOrRequirement(permissions); + + var grantResult = new MultiplePermissionGrantResult(); + grantResult.Result.Add("Perm.X", PermissionGrantResult.Prohibited); + grantResult.Result.Add("Perm.Y", PermissionGrantResult.Prohibited); + grantResult.Result.Add("Perm.Z", PermissionGrantResult.Prohibited); + + _permissionChecker.IsGrantedAsync(user, permissions) + .Returns(grantResult); + + var context = CreateContext(user, requirement); + + // Act + await _handler.HandleAsync(context); + + // Assert - single batch call, not individual calls + await _permissionChecker.Received(1).IsGrantedAsync(user, permissions); + await _permissionChecker.DidNotReceive().IsGrantedAsync(user, Arg.Any()); + } + + [Fact] + public async Task HandleAsync_ShouldNotSucceed_WhenResultIsEmpty() + { + // Arrange + var user = CreateUser(); + var permissions = new[] { "Perm.A" }; + var requirement = new PermissionOrRequirement(permissions); + + var grantResult = new MultiplePermissionGrantResult(); + + _permissionChecker.IsGrantedAsync(user, permissions) + .Returns(grantResult); + + var context = CreateContext(user, requirement); + + // Act + await _handler.HandleAsync(context); + + // Assert + context.HasSucceeded.ShouldBeFalse(); + } + + [Fact] + public void Requirement_ShouldStorePermissions() + { + var requirement = new PermissionOrRequirement("A", "B", "C"); + requirement.Permissions.ShouldBe(new[] { "A", "B", "C" }); + } +} diff --git a/applications/Unity.GrantManager/test/Unity.GrantManager.Web.Tests/Identity/Authorization/RoleOrPermissionAuthorizationHandlerTests.cs b/applications/Unity.GrantManager/test/Unity.GrantManager.Web.Tests/Identity/Authorization/RoleOrPermissionAuthorizationHandlerTests.cs new file mode 100644 index 0000000000..399c8146df --- /dev/null +++ b/applications/Unity.GrantManager/test/Unity.GrantManager.Web.Tests/Identity/Authorization/RoleOrPermissionAuthorizationHandlerTests.cs @@ -0,0 +1,145 @@ +using System.Security.Claims; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authorization; +using NSubstitute; +using Shouldly; +using Unity.GrantManager.Web.Identity.Authorization; +using Volo.Abp.Authorization.Permissions; +using Xunit; + +namespace Unity.GrantManager.Identity.Authorization; + +public class RoleOrPermissionAuthorizationHandlerTests +{ + private readonly IPermissionChecker _permissionChecker; + private readonly RoleOrPermissionAuthorizationHandler _handler; + + public RoleOrPermissionAuthorizationHandlerTests() + { + _permissionChecker = Substitute.For(); + _handler = new RoleOrPermissionAuthorizationHandler(_permissionChecker); + } + + private static AuthorizationHandlerContext CreateContext( + ClaimsPrincipal user, + RoleOrPermissionRequirement requirement) + { + return new AuthorizationHandlerContext( + [requirement], + user, + resource: null); + } + + private static ClaimsPrincipal CreateUserWithRole(string roleName) + { + var identity = new ClaimsIdentity("TestAuth"); + identity.AddClaim(new Claim(ClaimTypes.Role, roleName)); + return new ClaimsPrincipal(identity); + } + + private static ClaimsPrincipal CreateUserWithoutRole() + { + var identity = new ClaimsIdentity("TestAuth"); + return new ClaimsPrincipal(identity); + } + + [Fact] + public async Task HandleAsync_ShouldSucceed_WhenUserHasRole() + { + // Arrange + var user = CreateUserWithRole("ITAdministrator"); + var requirement = new RoleOrPermissionRequirement("ITAdministrator", "Unity.ITAdmin"); + var context = CreateContext(user, requirement); + + // Act + await _handler.HandleAsync(context); + + // Assert + context.HasSucceeded.ShouldBeTrue(); + await _permissionChecker.DidNotReceive().IsGrantedAsync( + Arg.Any(), Arg.Any()); + } + + [Fact] + public async Task HandleAsync_ShouldSucceed_WhenUserHasPermission() + { + // Arrange + var user = CreateUserWithoutRole(); + var requirement = new RoleOrPermissionRequirement("ITAdministrator", "Unity.ITAdmin"); + + _permissionChecker.IsGrantedAsync(user, "Unity.ITAdmin") + .Returns(true); + + var context = CreateContext(user, requirement); + + // Act + await _handler.HandleAsync(context); + + // Assert + context.HasSucceeded.ShouldBeTrue(); + } + + [Fact] + public async Task HandleAsync_ShouldNotSucceed_WhenNeitherRoleNorPermission() + { + // Arrange + var user = CreateUserWithoutRole(); + var requirement = new RoleOrPermissionRequirement("ITAdministrator", "Unity.ITAdmin"); + + _permissionChecker.IsGrantedAsync(user, "Unity.ITAdmin") + .Returns(false); + + var context = CreateContext(user, requirement); + + // Act + await _handler.HandleAsync(context); + + // Assert + context.HasSucceeded.ShouldBeFalse(); + } + + [Fact] + public async Task HandleAsync_ShouldShortCircuit_WhenRoleMatches() + { + // Arrange + var user = CreateUserWithRole("ITOperations"); + var requirement = new RoleOrPermissionRequirement("ITOperations", "Unity.ITOperations"); + var context = CreateContext(user, requirement); + + // Act + await _handler.HandleAsync(context); + + // Assert - should not call permission checker at all + context.HasSucceeded.ShouldBeTrue(); + await _permissionChecker.DidNotReceive().IsGrantedAsync( + Arg.Any(), Arg.Any()); + } + + [Fact] + public async Task HandleAsync_ShouldCheckPermission_WhenRoleDoesNotMatch() + { + // Arrange + var user = CreateUserWithRole("SomeOtherRole"); + var requirement = new RoleOrPermissionRequirement("ITAdministrator", "Unity.ITAdmin"); + + _permissionChecker.IsGrantedAsync(user, "Unity.ITAdmin") + .Returns(false); + + var context = CreateContext(user, requirement); + + // Act + await _handler.HandleAsync(context); + + // Assert + context.HasSucceeded.ShouldBeFalse(); + await _permissionChecker.Received(1).IsGrantedAsync(user, "Unity.ITAdmin"); + } + + [Fact] + public void Requirement_ShouldStoreRoleAndPermission() + { + var requirement = new RoleOrPermissionRequirement("MyRole", "MyPermission"); + requirement.RoleName.ShouldBe("MyRole"); + requirement.PermissionName.ShouldBe("MyPermission"); + } +} From f1115c4644b8f535e086d5952c1cba7f5cfabe70 Mon Sep 17 00:00:00 2001 From: Patrick <135162612+plavoie-BC@users.noreply.github.com> Date: Thu, 30 Apr 2026 13:45:20 -0700 Subject: [PATCH 7/7] [AB#32738] Enhance user ID claim handling for GUID parsing --- .../Unity.GrantManager.Web/Identity/CurrentUser.cs | 14 +++++++++++++- .../Identity/IdentityProfileLoginHandler.cs | 1 - 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Identity/CurrentUser.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Identity/CurrentUser.cs index 6037b9f5c3..8bf2c7fb2b 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Identity/CurrentUser.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Identity/CurrentUser.cs @@ -75,10 +75,22 @@ public virtual bool IsInRole(string roleName) var userClaims = _principalAccessor.Principal?.Claims; if (userClaims != null && userClaims.Any()) { + // First try the IDIR-specific GUID claim + var idirGuid = userClaims.FirstOrDefault(s => s.Type == UnityClaimsTypes.IDirUserGuid); + if (idirGuid != null && Guid.TryParse(idirGuid.Value, out var guid)) + { + return guid; + } + + // Fallback to UserId claim (strip @azureidir suffix if present) var userId = userClaims.FirstOrDefault(s => s.Type == AbpClaimTypes.UserId); if (userId != null) { - return Guid.Parse(userId.Value); + var value = userId.Value.Split('@')[0]; // Remove @azureidir suffix + if (Guid.TryParse(value, out guid)) + { + return guid; + } } } return null; diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Identity/IdentityProfileLoginHandler.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Identity/IdentityProfileLoginHandler.cs index bf5cf48de0..d94f32e958 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Identity/IdentityProfileLoginHandler.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Identity/IdentityProfileLoginHandler.cs @@ -1,7 +1,6 @@ using Microsoft.AspNetCore.Authentication.OpenIdConnect; using Microsoft.Extensions.DependencyInjection; using OpenIddict.Abstractions; -using System; using System.Collections.Generic; using System.Linq; using System.Security.Claims;