Browse Source

Implement role-based permission management and admin role checks

pull/24775/head
maliming 14 hours ago
parent
commit
a475272eb5
No known key found for this signature in database GPG Key ID: A646B9CB645ECEA4
  1. 10
      framework/src/Volo.Abp.Security/Volo/Abp/Roles/AbpRoleConsts.cs
  2. 27
      modules/identity/src/Volo.Abp.Identity.Application/Volo/Abp/Identity/IdentityUserAppService.cs
  3. 16
      modules/identity/src/Volo.Abp.Identity.Blazor/Pages/Identity/UserManagement.razor
  4. 22
      modules/identity/src/Volo.Abp.Identity.Blazor/Pages/Identity/UserManagement.razor.cs
  5. 3
      modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IdentityDataSeeder.cs
  6. 23
      modules/identity/test/Volo.Abp.Identity.Application.Tests/Volo/Abp/Identity/IdentityUserAppService_Tests.cs
  7. 17
      modules/permission-management/src/Volo.Abp.PermissionManagement.Application/Volo/Abp/PermissionManagement/PermissionAppService.cs
  8. 45
      modules/permission-management/src/Volo.Abp.PermissionManagement.Blazor/Components/PermissionManagementModal.razor.cs
  9. 3
      modules/permission-management/src/Volo.Abp.PermissionManagement.Domain/Volo/Abp/PermissionManagement/PermissionDataSeedContributor.cs
  10. 10
      modules/permission-management/test/Volo.Abp.PermissionManagement.Application.Tests/Volo/Abp/PermissionManagement/AbpPermissionManagementApplicationTestBase.cs
  11. 41
      modules/permission-management/test/Volo.Abp.PermissionManagement.Application.Tests/Volo/Abp/PermissionManagement/PermissionAppService_Tests.cs

10
framework/src/Volo.Abp.Security/Volo/Abp/Roles/AbpRoleConsts.cs

@ -0,0 +1,10 @@
namespace Volo.Abp.Roles;
public static class AbpRoleConsts
{
/// <summary>
/// The static name of the admin role.
/// Default value: "admin"
/// </summary>
public const string AdminRoleName = "admin";
}

27
modules/identity/src/Volo.Abp.Identity.Application/Volo/Abp/Identity/IdentityUserAppService.cs

@ -9,6 +9,7 @@ using Volo.Abp.Application.Dtos;
using Volo.Abp.Authorization.Permissions;
using Volo.Abp.Data;
using Volo.Abp.ObjectExtending;
using Volo.Abp.Roles;
using Volo.Abp.Users;
namespace Volo.Abp.Identity;
@ -70,8 +71,18 @@ public class IdentityUserAppService : IdentityAppServiceBase, IIdentityUserAppSe
[Authorize(IdentityPermissions.Users.Default)]
public virtual async Task<ListResultDto<IdentityRoleDto>> GetAssignableRolesAsync()
{
var currentUserRoles = await UserManager.GetRolesAsync(await UserManager.GetByIdAsync(CurrentUser.GetId()));
var list = (await RoleRepository.GetListAsync(currentUserRoles)).OrderBy(x => x.Name).ToList();
List<IdentityRole> list;
if (await HasAdminRoleAsync())
{
list = (await RoleRepository.GetListAsync()).OrderBy(x => x.Name).ToList();
}
else
{
var currentUserRoles = await UserManager.GetRolesAsync(await UserManager.GetByIdAsync(CurrentUser.GetId()));
list = (await RoleRepository.GetListAsync(currentUserRoles)).OrderBy(x => x.Name).ToList();
}
return new ListResultDto<IdentityRoleDto>(ObjectMapper.Map<List<IdentityRole>, List<IdentityRoleDto>>(list));
}
@ -200,6 +211,13 @@ public class IdentityUserAppService : IdentityAppServiceBase, IIdentityUserAppSe
protected virtual async Task<string[]> FilterRolesByCurrentUserAsync(IdentityUser user, string[] inputRoleNames)
{
if (await HasAdminRoleAsync())
{
return (inputRoleNames ?? Array.Empty<string>())
.Distinct(StringComparer.OrdinalIgnoreCase)
.ToArray();
}
var targetCurrentRoleSet = (await UserManager.GetRolesAsync(user)).ToHashSet(StringComparer.OrdinalIgnoreCase);
var operatorUser = await UserManager.GetByIdAsync(CurrentUser.GetId());
@ -215,4 +233,9 @@ public class IdentityUserAppService : IdentityAppServiceBase, IIdentityUserAppSe
.Distinct(StringComparer.OrdinalIgnoreCase)
.ToArray();
}
protected virtual Task<bool> HasAdminRoleAsync()
{
return Task.FromResult(CurrentUser.IsInRole(AbpRoleConsts.AdminRoleName));
}
}

16
modules/identity/src/Volo.Abp.Identity.Blazor/Pages/Identity/UserManagement.razor

@ -142,13 +142,13 @@
<TabPanel Name="Roles">
@if (NewUserRoles != null)
{
@foreach (var role in NewUserRoles)
{
<Field>
<input type="hidden" @bind-value="@role.Name" />
<Check TValue="bool" @bind-Checked="@role.IsAssigned">@role.Name</Check>
</Field>
}
@foreach (var role in NewUserRoles)
{
<Field>
<input type="hidden" @bind-value="@role.Name" />
<Check TValue="bool" @bind-Checked="@role.IsAssigned" Disabled="@(!role.IsAssignable)">@role.Name</Check>
</Field>
}
}
</TabPanel>
</Content>
@ -277,7 +277,7 @@
{
<Field>
<input type="hidden" @bind-value="@role.Name" />
<Check TValue="bool" @bind-Checked="@role.IsAssigned">@role.Name</Check>
<Check TValue="bool" @bind-Checked="@role.IsAssigned" Disabled="@(!role.IsAssignable)">@role.Name</Check>
</Field>
}
}

22
modules/identity/src/Volo.Abp.Identity.Blazor/Pages/Identity/UserManagement.razor.cs

@ -103,7 +103,8 @@ public partial class UserManagement
NewUserRoles = Roles.Select(x => new AssignedRoleViewModel
{
Name = x.Name,
IsAssigned = x.IsDefault
IsAssigned = x.IsDefault,
IsAssignable = true
}).ToArray();
ChangePasswordTextRole(TextRole.Password);
@ -130,12 +131,23 @@ public partial class UserManagement
if (await PermissionChecker.IsGrantedAsync(IdentityPermissions.Users.ManageRoles))
{
var userRoleIds = (await AppService.GetRolesAsync(entity.Id)).Items.Select(r => r.Id).ToList();
var assignableRoles = Roles ?? (await AppService.GetAssignableRolesAsync()).Items;
var currentRoles = (await AppService.GetRolesAsync(entity.Id)).Items;
EditUserRoles = Roles.Select(x => new AssignedRoleViewModel
var combinedRoles = assignableRoles
.Concat(currentRoles)
.GroupBy(role => role.Id)
.Select(group => group.First())
.ToList();
var currentRoleIds = currentRoles.Select(r => r.Id).ToHashSet();
var assignableRoleIds = assignableRoles.Select(r => r.Id).ToHashSet();
EditUserRoles = combinedRoles.Select(x => new AssignedRoleViewModel
{
Name = x.Name,
IsAssigned = userRoleIds.Contains(x.Id)
IsAssigned = currentRoleIds.Contains(x.Id),
IsAssignable = assignableRoleIds.Contains(x.Id)
}).ToArray();
ChangePasswordTextRole(TextRole.Password);
@ -262,4 +274,6 @@ public class AssignedRoleViewModel
public string Name { get; set; }
public bool IsAssigned { get; set; }
public bool IsAssignable { get; set; }
}

3
modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IdentityDataSeeder.cs

@ -5,6 +5,7 @@ using Microsoft.Extensions.Options;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Guids;
using Volo.Abp.MultiTenancy;
using Volo.Abp.Roles;
using Volo.Abp.Uow;
namespace Volo.Abp.Identity;
@ -83,7 +84,7 @@ public class IdentityDataSeeder : ITransientDependency, IIdentityDataSeeder
result.CreatedAdminUser = true;
//"admin" role
const string adminRoleName = "admin";
const string adminRoleName = AbpRoleConsts.AdminRoleName;
var adminRole =
await RoleRepository.FindByNormalizedNameAsync(LookupNormalizer.NormalizeName(adminRoleName));
if (adminRole == null)

23
modules/identity/test/Volo.Abp.Identity.Application.Tests/Volo/Abp/Identity/IdentityUserAppService_Tests.cs

@ -311,6 +311,29 @@ public class IdentityUserAppService_Tests : AbpIdentityApplicationTestBase
roleNames.ShouldNotContain("supporter");
}
[Fact]
public async Task UpdateRolesAsync_Admin_Can_Assign_Any_Role()
{
// admin user can assign roles they do not have (e.g. "sale")
using (_currentPrincipalAccessor.Change(new[]
{
new Claim(AbpClaimTypes.UserId, _testData.UserAdminId.ToString()),
new Claim(AbpClaimTypes.Role, "admin")
}))
{
await _userAppService.UpdateRolesAsync(
_testData.UserDavidId,
new IdentityUserUpdateRolesDto
{
RoleNames = new[] { "sale" }
}
);
}
var roleNames = await _userRepository.GetRoleNamesAsync(_testData.UserDavidId);
roleNames.ShouldContain("sale");
}
[Fact]
public async Task UpdateRolesAsync_Self_Cannot_Add_New_Roles()
{

17
modules/permission-management/src/Volo.Abp.PermissionManagement.Application/Volo/Abp/PermissionManagement/PermissionAppService.cs

@ -10,6 +10,8 @@ using Volo.Abp.Localization;
using Volo.Abp.MultiTenancy;
using Volo.Abp.SimpleStateChecking;
using Volo.Abp.PermissionManagement.Localization;
using Volo.Abp.Roles;
using Volo.Abp.Users;
namespace Volo.Abp.PermissionManagement;
@ -136,6 +138,11 @@ public class PermissionAppService : ApplicationService, IPermissionAppService
protected virtual async Task FilterOutputPermissionsByCurrentUserAsync(GetPermissionListResultDto result)
{
if (await HasAdminRoleAsync())
{
return;
}
// Collect all permission names
var allPermissionNames = result.Groups
.SelectMany(g => g.Permissions)
@ -417,6 +424,11 @@ public class PermissionAppService : ApplicationService, IPermissionAppService
protected virtual async Task FilterInputPermissionsByCurrentUserAsync(UpdatePermissionsDto input)
{
if (await HasAdminRoleAsync())
{
return;
}
if (input.Permissions.IsNullOrEmpty())
{
input.Permissions = Array.Empty<UpdatePermissionDto>();
@ -432,4 +444,9 @@ public class PermissionAppService : ApplicationService, IPermissionAppService
// Filters the input DTO in-place to only include manageable permissions.
input.Permissions = input.Permissions.Where(x => grantedPermissions.Contains(x.Name)).ToArray();
}
protected virtual Task<bool> HasAdminRoleAsync()
{
return Task.FromResult(CurrentUser.IsInRole(AbpRoleConsts.AdminRoleName));
}
}

45
modules/permission-management/src/Volo.Abp.PermissionManagement.Blazor/Components/PermissionManagementModal.razor.cs

@ -28,8 +28,6 @@ public partial class PermissionManagementModal
protected List<PermissionGroupDto> _allGroups;
protected List<PermissionGroupDto> _groups;
protected List<PermissionGrantInfoDto> _disabledPermissions = new List<PermissionGrantInfoDto>();
protected string _selectedTabName;
protected bool _selectAllDisabled;
@ -102,19 +100,6 @@ public partial class PermissionManagementModal
{
_selectAllDisabled = _groups.All(IsPermissionGroupDisabled);
if (checkDisabledPermissions)
{
_disabledPermissions.Clear();
}
foreach (var permission in _groups.SelectMany(x => x.Permissions))
{
if (checkDisabledPermissions && permission.IsGranted && permission.GrantedProviders.All(x => x.ProviderName != _providerName))
{
_disabledPermissions.Add(permission);
}
}
foreach (var group in _groups)
{
SetPermissionDepths(group.Permissions, null, 0);
@ -285,12 +270,28 @@ public partial class PermissionManagementModal
protected virtual bool IsDisabledPermission(PermissionGrantInfoDto permissionGrantInfo)
{
return _disabledPermissions.Any(x => x == permissionGrantInfo);
if (!permissionGrantInfo.IsEditable)
{
return true;
}
return permissionGrantInfo.IsGranted &&
permissionGrantInfo.GrantedProviders.All(p => p.ProviderName != _providerName);
}
protected virtual string GetShownName(PermissionGrantInfoDto permissionGrantInfo)
{
if (!IsDisabledPermission(permissionGrantInfo))
if (permissionGrantInfo.GrantedProviders.All(p => p.ProviderName == _providerName))
{
return permissionGrantInfo.DisplayName;
}
var grantedByOtherProviders = permissionGrantInfo.GrantedProviders
.Where(p => p.ProviderName != _providerName)
.Select(p => p.ProviderName)
.ToList();
if (!grantedByOtherProviders.Any())
{
return permissionGrantInfo.DisplayName;
}
@ -298,10 +299,7 @@ public partial class PermissionManagementModal
return string.Format(
"{0} ({1})",
permissionGrantInfo.DisplayName,
permissionGrantInfo.GrantedProviders
.Where(p => p.ProviderName != _providerName)
.Select(p => p.ProviderName)
.JoinAsString(", ")
grantedByOtherProviders.JoinAsString(", ")
);
}
@ -313,10 +311,7 @@ public partial class PermissionManagementModal
protected virtual bool IsPermissionGroupDisabled(PermissionGroupDto group)
{
var permissions = group.Permissions;
var grantedProviders = permissions.SelectMany(x => x.GrantedProviders);
return permissions.All(x => x.IsGranted) && grantedProviders.Any(p => p.ProviderName != _providerName);
return group.Permissions.All(IsDisabledPermission);
}
protected virtual async Task ResetSearchTextAsync()

3
modules/permission-management/src/Volo.Abp.PermissionManagement.Domain/Volo/Abp/PermissionManagement/PermissionDataSeedContributor.cs

@ -4,6 +4,7 @@ using Volo.Abp.Authorization.Permissions;
using Volo.Abp.Data;
using Volo.Abp.DependencyInjection;
using Volo.Abp.MultiTenancy;
using Volo.Abp.Roles;
namespace Volo.Abp.PermissionManagement;
@ -34,7 +35,7 @@ public class PermissionDataSeedContributor : IDataSeedContributor, ITransientDep
await PermissionDataSeeder.SeedAsync(
RolePermissionValueProvider.ProviderName,
"admin",
AbpRoleConsts.AdminRoleName,
permissionNames,
context?.TenantId
);

10
modules/permission-management/test/Volo.Abp.PermissionManagement.Application.Tests/Volo/Abp/PermissionManagement/AbpPermissionManagementApplicationTestBase.cs

@ -1,11 +1,7 @@
using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using NSubstitute;
using Volo.Abp.Authorization.Permissions;
using Volo.Abp.Users;
namespace Volo.Abp.PermissionManagement;
@ -19,12 +15,6 @@ public class AbpPermissionManagementApplicationTestBase : PermissionManagementTe
}
protected override void AfterAddApplication(IServiceCollection services)
{
var currentUser = Substitute.For<ICurrentUser>();
currentUser.Roles.Returns(new[] { "admin" });
currentUser.IsAuthenticated.Returns(true);
services.AddSingleton(currentUser);
var fakePermissionChecker = new FakePermissionChecker();
services.AddSingleton(fakePermissionChecker);
services.Replace(ServiceDescriptor.Singleton<IPermissionChecker>(fakePermissionChecker));

41
modules/permission-management/test/Volo.Abp.PermissionManagement.Application.Tests/Volo/Abp/PermissionManagement/PermissionAppService_Tests.cs

@ -163,6 +163,26 @@ public class PermissionAppService_Tests : AbpPermissionManagementApplicationTest
testGroup.Permissions.First(p => p.Name == "MyPermission6.ChildPermission2").IsEditable.ShouldBeFalse();
}
[Fact]
public async Task Get_Should_Allow_Admin_To_Edit_All_Permissions()
{
// Current user does NOT have these permissions, but has admin role
_fakePermissionChecker.SetGrantedPermissions("MyPermission1");
using (_currentPrincipalAccessor.Change(new Claim(AbpClaimTypes.Role, "admin")))
{
var result = await _permissionAppService.GetAsync(
UserPermissionValueProvider.ProviderName,
PermissionTestDataBuilder.User1Id.ToString());
var testGroup = result.Groups.FirstOrDefault(g => g.Name == "TestGroup");
testGroup.ShouldNotBeNull();
testGroup.Permissions.First(p => p.Name == "MyPermission3").IsEditable.ShouldBeTrue();
testGroup.Permissions.First(p => p.Name == "MyPermission6").IsEditable.ShouldBeTrue();
}
}
[Fact]
public async Task Update_Should_Not_Grant_Permission_That_Current_User_Does_Not_Have()
{
@ -214,4 +234,25 @@ public class PermissionAppService_Tests : AbpPermissionManagementApplicationTest
// MyPermission2 should still be granted (current user doesn't have it, revoke filtered out)
(await _permissionGrantRepository.FindAsync("MyPermission2", "Test", "Test")).ShouldNotBeNull();
}
[Fact]
public async Task Update_Should_Allow_Admin_To_Grant_Permissions_Without_Having_Them()
{
(await _permissionGrantRepository.FindAsync("MyPermission2", "Test", "Test")).ShouldBeNull();
_fakePermissionChecker.SetGrantedPermissions();
using (_currentPrincipalAccessor.Change(new Claim(AbpClaimTypes.Role, "admin")))
{
await _permissionAppService.UpdateAsync("Test", "Test", new UpdatePermissionsDto()
{
Permissions = new UpdatePermissionDto[]
{
new UpdatePermissionDto() { IsGranted = true, Name = "MyPermission2" }
}
});
}
(await _permissionGrantRepository.FindAsync("MyPermission2", "Test", "Test")).ShouldNotBeNull();
}
}

Loading…
Cancel
Save