Browse Source

Implement role-based permission management and admin role checks

pull/24775/head
maliming 8 months 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.Authorization.Permissions;
using Volo.Abp.Data; using Volo.Abp.Data;
using Volo.Abp.ObjectExtending; using Volo.Abp.ObjectExtending;
using Volo.Abp.Roles;
using Volo.Abp.Users; using Volo.Abp.Users;
namespace Volo.Abp.Identity; namespace Volo.Abp.Identity;
@ -70,8 +71,18 @@ public class IdentityUserAppService : IdentityAppServiceBase, IIdentityUserAppSe
[Authorize(IdentityPermissions.Users.Default)] [Authorize(IdentityPermissions.Users.Default)]
public virtual async Task<ListResultDto<IdentityRoleDto>> GetAssignableRolesAsync() public virtual async Task<ListResultDto<IdentityRoleDto>> GetAssignableRolesAsync()
{ {
var currentUserRoles = await UserManager.GetRolesAsync(await UserManager.GetByIdAsync(CurrentUser.GetId())); List<IdentityRole> list;
var list = (await RoleRepository.GetListAsync(currentUserRoles)).OrderBy(x => x.Name).ToList();
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)); 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) 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 targetCurrentRoleSet = (await UserManager.GetRolesAsync(user)).ToHashSet(StringComparer.OrdinalIgnoreCase);
var operatorUser = await UserManager.GetByIdAsync(CurrentUser.GetId()); var operatorUser = await UserManager.GetByIdAsync(CurrentUser.GetId());
@ -215,4 +233,9 @@ public class IdentityUserAppService : IdentityAppServiceBase, IIdentityUserAppSe
.Distinct(StringComparer.OrdinalIgnoreCase) .Distinct(StringComparer.OrdinalIgnoreCase)
.ToArray(); .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"> <TabPanel Name="Roles">
@if (NewUserRoles != null) @if (NewUserRoles != null)
{ {
@foreach (var role in NewUserRoles) @foreach (var role in NewUserRoles)
{ {
<Field> <Field>
<input type="hidden" @bind-value="@role.Name" /> <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> </Field>
} }
} }
</TabPanel> </TabPanel>
</Content> </Content>
@ -277,7 +277,7 @@
{ {
<Field> <Field>
<input type="hidden" @bind-value="@role.Name" /> <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> </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 NewUserRoles = Roles.Select(x => new AssignedRoleViewModel
{ {
Name = x.Name, Name = x.Name,
IsAssigned = x.IsDefault IsAssigned = x.IsDefault,
IsAssignable = true
}).ToArray(); }).ToArray();
ChangePasswordTextRole(TextRole.Password); ChangePasswordTextRole(TextRole.Password);
@ -130,12 +131,23 @@ public partial class UserManagement
if (await PermissionChecker.IsGrantedAsync(IdentityPermissions.Users.ManageRoles)) 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, Name = x.Name,
IsAssigned = userRoleIds.Contains(x.Id) IsAssigned = currentRoleIds.Contains(x.Id),
IsAssignable = assignableRoleIds.Contains(x.Id)
}).ToArray(); }).ToArray();
ChangePasswordTextRole(TextRole.Password); ChangePasswordTextRole(TextRole.Password);
@ -262,4 +274,6 @@ public class AssignedRoleViewModel
public string Name { get; set; } public string Name { get; set; }
public bool IsAssigned { 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.DependencyInjection;
using Volo.Abp.Guids; using Volo.Abp.Guids;
using Volo.Abp.MultiTenancy; using Volo.Abp.MultiTenancy;
using Volo.Abp.Roles;
using Volo.Abp.Uow; using Volo.Abp.Uow;
namespace Volo.Abp.Identity; namespace Volo.Abp.Identity;
@ -83,7 +84,7 @@ public class IdentityDataSeeder : ITransientDependency, IIdentityDataSeeder
result.CreatedAdminUser = true; result.CreatedAdminUser = true;
//"admin" role //"admin" role
const string adminRoleName = "admin"; const string adminRoleName = AbpRoleConsts.AdminRoleName;
var adminRole = var adminRole =
await RoleRepository.FindByNormalizedNameAsync(LookupNormalizer.NormalizeName(adminRoleName)); await RoleRepository.FindByNormalizedNameAsync(LookupNormalizer.NormalizeName(adminRoleName));
if (adminRole == null) 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"); 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] [Fact]
public async Task UpdateRolesAsync_Self_Cannot_Add_New_Roles() 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.MultiTenancy;
using Volo.Abp.SimpleStateChecking; using Volo.Abp.SimpleStateChecking;
using Volo.Abp.PermissionManagement.Localization; using Volo.Abp.PermissionManagement.Localization;
using Volo.Abp.Roles;
using Volo.Abp.Users;
namespace Volo.Abp.PermissionManagement; namespace Volo.Abp.PermissionManagement;
@ -136,6 +138,11 @@ public class PermissionAppService : ApplicationService, IPermissionAppService
protected virtual async Task FilterOutputPermissionsByCurrentUserAsync(GetPermissionListResultDto result) protected virtual async Task FilterOutputPermissionsByCurrentUserAsync(GetPermissionListResultDto result)
{ {
if (await HasAdminRoleAsync())
{
return;
}
// Collect all permission names // Collect all permission names
var allPermissionNames = result.Groups var allPermissionNames = result.Groups
.SelectMany(g => g.Permissions) .SelectMany(g => g.Permissions)
@ -417,6 +424,11 @@ public class PermissionAppService : ApplicationService, IPermissionAppService
protected virtual async Task FilterInputPermissionsByCurrentUserAsync(UpdatePermissionsDto input) protected virtual async Task FilterInputPermissionsByCurrentUserAsync(UpdatePermissionsDto input)
{ {
if (await HasAdminRoleAsync())
{
return;
}
if (input.Permissions.IsNullOrEmpty()) if (input.Permissions.IsNullOrEmpty())
{ {
input.Permissions = Array.Empty<UpdatePermissionDto>(); 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. // Filters the input DTO in-place to only include manageable permissions.
input.Permissions = input.Permissions.Where(x => grantedPermissions.Contains(x.Name)).ToArray(); 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> _allGroups;
protected List<PermissionGroupDto> _groups; protected List<PermissionGroupDto> _groups;
protected List<PermissionGrantInfoDto> _disabledPermissions = new List<PermissionGrantInfoDto>();
protected string _selectedTabName; protected string _selectedTabName;
protected bool _selectAllDisabled; protected bool _selectAllDisabled;
@ -102,19 +100,6 @@ public partial class PermissionManagementModal
{ {
_selectAllDisabled = _groups.All(IsPermissionGroupDisabled); _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) foreach (var group in _groups)
{ {
SetPermissionDepths(group.Permissions, null, 0); SetPermissionDepths(group.Permissions, null, 0);
@ -285,12 +270,28 @@ public partial class PermissionManagementModal
protected virtual bool IsDisabledPermission(PermissionGrantInfoDto permissionGrantInfo) 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) 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; return permissionGrantInfo.DisplayName;
} }
@ -298,10 +299,7 @@ public partial class PermissionManagementModal
return string.Format( return string.Format(
"{0} ({1})", "{0} ({1})",
permissionGrantInfo.DisplayName, permissionGrantInfo.DisplayName,
permissionGrantInfo.GrantedProviders grantedByOtherProviders.JoinAsString(", ")
.Where(p => p.ProviderName != _providerName)
.Select(p => p.ProviderName)
.JoinAsString(", ")
); );
} }
@ -313,10 +311,7 @@ public partial class PermissionManagementModal
protected virtual bool IsPermissionGroupDisabled(PermissionGroupDto group) protected virtual bool IsPermissionGroupDisabled(PermissionGroupDto group)
{ {
var permissions = group.Permissions; return group.Permissions.All(IsDisabledPermission);
var grantedProviders = permissions.SelectMany(x => x.GrantedProviders);
return permissions.All(x => x.IsGranted) && grantedProviders.Any(p => p.ProviderName != _providerName);
} }
protected virtual async Task ResetSearchTextAsync() 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.Data;
using Volo.Abp.DependencyInjection; using Volo.Abp.DependencyInjection;
using Volo.Abp.MultiTenancy; using Volo.Abp.MultiTenancy;
using Volo.Abp.Roles;
namespace Volo.Abp.PermissionManagement; namespace Volo.Abp.PermissionManagement;
@ -34,7 +35,7 @@ public class PermissionDataSeedContributor : IDataSeedContributor, ITransientDep
await PermissionDataSeeder.SeedAsync( await PermissionDataSeeder.SeedAsync(
RolePermissionValueProvider.ProviderName, RolePermissionValueProvider.ProviderName,
"admin", AbpRoleConsts.AdminRoleName,
permissionNames, permissionNames,
context?.TenantId 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;
using System.Collections.Generic;
using System.Text;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.DependencyInjection.Extensions;
using NSubstitute;
using Volo.Abp.Authorization.Permissions; using Volo.Abp.Authorization.Permissions;
using Volo.Abp.Users;
namespace Volo.Abp.PermissionManagement; namespace Volo.Abp.PermissionManagement;
@ -19,12 +15,6 @@ public class AbpPermissionManagementApplicationTestBase : PermissionManagementTe
} }
protected override void AfterAddApplication(IServiceCollection services) 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(); var fakePermissionChecker = new FakePermissionChecker();
services.AddSingleton(fakePermissionChecker); services.AddSingleton(fakePermissionChecker);
services.Replace(ServiceDescriptor.Singleton<IPermissionChecker>(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(); 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] [Fact]
public async Task Update_Should_Not_Grant_Permission_That_Current_User_Does_Not_Have() 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) // MyPermission2 should still be granted (current user doesn't have it, revoke filtered out)
(await _permissionGrantRepository.FindAsync("MyPermission2", "Test", "Test")).ShouldNotBeNull(); (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