Browse Source

Merge pull request #25231 from abpframework/perf/improve-application-configuration-with-large-permissions

Improve performance for application-configuration with large number of permissions
pull/25232/head
Engincan VESKE 4 weeks ago
committed by GitHub
parent
commit
d1c108b4cf
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 6
      framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/AbpApplicationConfigurationAppService.cs
  2. 31
      framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/Permissions/PermissionChecker.cs
  3. 11
      framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/Permissions/PermissionDefinitionManager.cs
  4. 32
      framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/Permissions/Resources/ResourcePermissionChecker.cs
  5. 11
      framework/src/Volo.Abp.Core/Volo/Abp/SimpleStateChecking/SimpleStateCheckerManager.cs
  6. 51
      framework/test/Volo.Abp.Authorization.Tests/Volo/Abp/Authorization/PermissionChecker_BulkWithStateChecker_Tests.cs
  7. 57
      framework/test/Volo.Abp.Authorization.Tests/Volo/Abp/Authorization/ResourcePermissionChecker_BulkWithStateChecker_Tests.cs
  8. 14
      framework/test/Volo.Abp.Authorization.Tests/Volo/Abp/Authorization/StaticPermissionDefinitionStore_Tests.cs
  9. 66
      framework/test/Volo.Abp.Core.Tests/Volo/Abp/SimpleStateChecking/SimpleStateChecker_NoCheckers_Test.cs

6
framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/AbpApplicationConfigurationAppService.cs

@ -171,10 +171,14 @@ public class AbpApplicationConfigurationAppService : ApplicationService, IAbpApp
var abpPolicyNames = new List<string>();
var otherPolicyNames = new List<string>();
var permissionNameSet = new HashSet<string>(
(await _permissionDefinitionManager.GetPermissionsAsync()).Select(p => p.Name),
StringComparer.Ordinal);
foreach (var policyName in policyNames)
{
if (await _defaultAuthorizationPolicyProvider.GetPolicyAsync(policyName) == null &&
await _permissionDefinitionManager.GetOrNullAsync(policyName) != null)
permissionNameSet.Contains(policyName))
{
abpPolicyNames.Add(policyName);
}

31
framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/Permissions/PermissionChecker.cs

@ -1,4 +1,5 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
using System.Security.Principal;
@ -110,11 +111,15 @@ public class PermissionChecker : IPermissionChecker, ITransientDependency
var multiTenancySide = claimsPrincipal?.GetMultiTenancySide() ??
CurrentTenant.GetMultiTenancySide();
var allPermissions = (await PermissionDefinitionManager.GetPermissionsAsync())
.ToDictionary(p => p.Name, StringComparer.Ordinal);
var pendingStateCheck = new List<PermissionDefinition>();
var permissionDefinitions = new List<PermissionDefinition>();
foreach (var name in names)
{
var permission = await PermissionDefinitionManager.GetOrNullAsync(name);
if (permission == null)
if (!allPermissions.TryGetValue(name, out var permission))
{
result.Result.Add(name, PermissionGrantResult.Prohibited);
continue;
@ -122,11 +127,23 @@ public class PermissionChecker : IPermissionChecker, ITransientDependency
result.Result.Add(name, PermissionGrantResult.Undefined);
if (permission.IsEnabled &&
await StateCheckerManager.IsEnabledAsync(permission) &&
permission.MultiTenancySide.HasFlag(multiTenancySide))
if (!permission.IsEnabled || !permission.MultiTenancySide.HasFlag(multiTenancySide))
{
permissionDefinitions.Add(permission);
continue;
}
pendingStateCheck.Add(permission);
}
if (pendingStateCheck.Count > 0)
{
var stateCheckResult = await StateCheckerManager.IsEnabledAsync(pendingStateCheck.ToArray());
foreach (var item in stateCheckResult)
{
if (item.Value)
{
permissionDefinitions.Add(item.Key);
}
}
}

11
framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/Permissions/PermissionDefinitionManager.cs

@ -75,15 +75,18 @@ public class PermissionDefinitionManager : IPermissionDefinitionManager, ITransi
public virtual async Task<IReadOnlyList<PermissionDefinition>> GetResourcePermissionsAsync()
{
var staticResourcePermissions = await _staticStore.GetResourcePermissionsAsync();
var staticResourcePermissionNames = staticResourcePermissions
.Select(p => p.Name)
var staticResourcePermissionKeys = staticResourcePermissions
.Select(p => (p.ResourceName, p.Name))
.ToImmutableHashSet();
var dynamicResourcePermissions = await _dynamicStore.GetResourcePermissionsAsync();
/* We prefer static permissions over dynamics */
/* We prefer static permissions over dynamics.
* Resource permissions are unique by (ResourceName, Name), so we must deduplicate
* using both fields to avoid incorrectly excluding dynamic permissions whose name
* matches a static permission from a different resource. */
return staticResourcePermissions.Concat(
dynamicResourcePermissions.Where(d => !staticResourcePermissionNames.Contains(d.Name))
dynamicResourcePermissions.Where(d => !staticResourcePermissionKeys.Contains((d.ResourceName, d.Name)))
).ToImmutableList();
}

32
framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/Permissions/Resources/ResourcePermissionChecker.cs

@ -1,4 +1,5 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
using System.Security.Principal;
@ -115,11 +116,16 @@ public class ResourcePermissionChecker : IResourcePermissionChecker, ITransientD
var multiTenancySide = claimsPrincipal?.GetMultiTenancySide() ??
CurrentTenant.GetMultiTenancySide();
var allResourcePermissions = (await PermissionDefinitionManager.GetResourcePermissionsAsync())
.Where(p => p.ResourceName == resourceName)
.ToDictionary(p => p.Name, StringComparer.Ordinal);
var pendingStateCheck = new List<PermissionDefinition>();
var permissionDefinitions = new List<PermissionDefinition>();
foreach (var name in names)
{
var permission = await PermissionDefinitionManager.GetResourcePermissionOrNullAsync(resourceName, name);
if (permission == null)
if (!allResourcePermissions.TryGetValue(name, out var permission))
{
result.Result.Add(name, PermissionGrantResult.Prohibited);
continue;
@ -127,11 +133,23 @@ public class ResourcePermissionChecker : IResourcePermissionChecker, ITransientD
result.Result.Add(name, PermissionGrantResult.Undefined);
if (permission.IsEnabled &&
await StateCheckerManager.IsEnabledAsync(permission) &&
permission.MultiTenancySide.HasFlag(multiTenancySide))
if (!permission.IsEnabled || !permission.MultiTenancySide.HasFlag(multiTenancySide))
{
continue;
}
pendingStateCheck.Add(permission);
}
if (pendingStateCheck.Count > 0)
{
var stateCheckResult = await StateCheckerManager.IsEnabledAsync(pendingStateCheck.ToArray());
foreach (var item in stateCheckResult)
{
permissionDefinitions.Add(permission);
if (item.Value)
{
permissionDefinitions.Add(item.Key);
}
}
}

11
framework/src/Volo.Abp.Core/Volo/Abp/SimpleStateChecking/SimpleStateCheckerManager.cs

@ -83,6 +83,17 @@ public class SimpleStateCheckerManager<TState> : ISimpleStateCheckerManager<TSta
protected virtual async Task<bool> InternalIsEnabledAsync(TState state, bool useBatchChecker)
{
var hasStateCheckers = state.StateCheckers
.WhereIf(!useBatchChecker, x => x is not ISimpleBatchStateChecker<TState>)
.Any();
var hasGlobalCheckers = Options.GlobalStateCheckers
.WhereIf(!useBatchChecker, x => !typeof(ISimpleBatchStateChecker<TState>).IsAssignableFrom(x))
.Any();
if (!hasStateCheckers && !hasGlobalCheckers)
{
return true;
}
using (var scope = ServiceProvider.CreateScope())
{
var context = new SimpleStateCheckerContext<TState>(scope.ServiceProvider.GetRequiredService<ICachedServiceProvider>(), state);

51
framework/test/Volo.Abp.Authorization.Tests/Volo/Abp/Authorization/PermissionChecker_BulkWithStateChecker_Tests.cs

@ -0,0 +1,51 @@
using System;
using System.Threading.Tasks;
using Shouldly;
using Volo.Abp.Authorization.Permissions;
using Xunit;
namespace Volo.Abp.Authorization;
public class PermissionChecker_BulkWithStateChecker_Tests : AuthorizationTestBase
{
private readonly IPermissionChecker _permissionChecker;
public PermissionChecker_BulkWithStateChecker_Tests()
{
_permissionChecker = GetRequiredService<IPermissionChecker>();
}
[Fact]
public async Task IsGrantedAsync_With_Empty_Names_Should_Return_Empty_Result()
{
var result = await _permissionChecker.IsGrantedAsync(Array.Empty<string>());
result.Result.ShouldBeEmpty();
}
[Fact]
public async Task IsGrantedAsync_StateChecker_Permission_Is_Undefined_When_StateChecker_Fails()
{
// MyPermission1 has TestRequireEditionPermissionSimpleStateChecker.
// Current user (Douglas) has no EditionId claim → StateChecker returns false.
// The permission must stay Undefined (never reaches the value-provider pipeline).
var result = await _permissionChecker.IsGrantedAsync(new[] { "MyPermission1", "MyPermission3" });
result.Result["MyPermission1"].ShouldBe(PermissionGrantResult.Undefined);
result.Result["MyPermission3"].ShouldBe(PermissionGrantResult.Granted);
}
[Fact]
public async Task IsGrantedAsync_Mix_Of_Defined_And_Undefined_Permissions()
{
var result = await _permissionChecker.IsGrantedAsync(new[]
{
"MyPermission3",
"NonExistentPermission",
"MyPermission5"
});
result.Result["MyPermission3"].ShouldBe(PermissionGrantResult.Granted);
result.Result["NonExistentPermission"].ShouldBe(PermissionGrantResult.Prohibited);
result.Result["MyPermission5"].ShouldBe(PermissionGrantResult.Granted);
}
}

57
framework/test/Volo.Abp.Authorization.Tests/Volo/Abp/Authorization/ResourcePermissionChecker_BulkWithStateChecker_Tests.cs

@ -0,0 +1,57 @@
using System;
using System.Threading.Tasks;
using Shouldly;
using Volo.Abp.Authorization.Permissions;
using Volo.Abp.Authorization.Permissions.Resources;
using Volo.Abp.Authorization.TestServices.Resources;
using Xunit;
namespace Volo.Abp.Authorization;
public class ResourcePermissionChecker_BulkWithStateChecker_Tests : AuthorizationTestBase
{
private readonly IResourcePermissionChecker _resourcePermissionChecker;
public ResourcePermissionChecker_BulkWithStateChecker_Tests()
{
_resourcePermissionChecker = GetRequiredService<IResourcePermissionChecker>();
}
[Fact]
public async Task IsGrantedAsync_With_Empty_Names_Should_Return_Empty_Result()
{
var result = await _resourcePermissionChecker.IsGrantedAsync(
Array.Empty<string>(), TestEntityResource.ResourceName, TestEntityResource.ResourceKey5);
result.Result.ShouldBeEmpty();
}
[Fact]
public async Task IsGrantedAsync_StateChecker_Permission_Is_Undefined_When_StateChecker_Fails()
{
// MyResourcePermission1 has TestRequireEditionPermissionSimpleStateChecker.
// Current user (Douglas) has no EditionId claim → StateChecker returns false.
// The permission must stay Undefined (never reaches the value-provider pipeline).
var result = await _resourcePermissionChecker.IsGrantedAsync(
new[] { "MyResourcePermission1", "MyResourcePermission3" },
TestEntityResource.ResourceName,
TestEntityResource.ResourceKey3);
result.Result["MyResourcePermission1"].ShouldBe(PermissionGrantResult.Undefined);
result.Result["MyResourcePermission3"].ShouldBe(PermissionGrantResult.Granted);
}
[Fact]
public async Task IsGrantedAsync_Mix_Of_Defined_And_Undefined_Permissions()
{
var result = await _resourcePermissionChecker.IsGrantedAsync(
new[] { "MyResourcePermission3", "NonExistentPermission", "MyResourcePermission5" },
TestEntityResource.ResourceName,
TestEntityResource.ResourceKey5);
result.Result["MyResourcePermission3"].ShouldBe(PermissionGrantResult.Granted);
result.Result["NonExistentPermission"].ShouldBe(PermissionGrantResult.Prohibited);
result.Result["MyResourcePermission5"].ShouldBe(PermissionGrantResult.Granted);
}
}

14
framework/test/Volo.Abp.Authorization.Tests/Volo/Abp/Authorization/StaticPermissionDefinitionStore_Tests.cs

@ -70,4 +70,18 @@ public class StaticPermissionDefinitionStore_Tests : AuthorizationTestBase
permissions.ShouldContain(x => x.Name == "MyResourcePermission6");
permissions.ShouldContain(x => x.Name == "MyResourcePermission7");
}
[Fact]
public async Task GetResourcePermissionsAsync_Same_Name_In_Different_Resources_Should_Both_Be_Returned()
{
// MyResourcePermission7 is defined for both TestEntityResource and TestEntityResource2.
// GetResourcePermissionsAsync must return both entries because resource permissions are
// unique by (ResourceName, Name), not by Name alone.
var permissions = await _store.GetResourcePermissionsAsync();
permissions.ShouldContain(x =>
x.Name == "MyResourcePermission7" && x.ResourceName == TestEntityResource.ResourceName);
permissions.ShouldContain(x =>
x.Name == "MyResourcePermission7" && x.ResourceName == TestEntityResource2.ResourceName);
}
}

66
framework/test/Volo.Abp.Core.Tests/Volo/Abp/SimpleStateChecking/SimpleStateChecker_NoCheckers_Test.cs

@ -0,0 +1,66 @@
using System.Globalization;
using System;
using System.Threading.Tasks;
using Shouldly;
using Xunit;
namespace Volo.Abp.SimpleStateChecking;
public class SimpleStateChecker_NoCheckers_Test : SimpleStateCheckerTestBase
{
// No GlobalStateCheckers registered — verifies the fast path when both
// state.StateCheckers and Options.GlobalStateCheckers are empty.
[Fact]
public async Task Entity_With_No_State_Checkers_Should_Be_Enabled()
{
var entity = new MyStateEntity();
(await SimpleStateCheckerManager.IsEnabledAsync(entity)).ShouldBeTrue();
}
[Fact]
public async Task Entity_With_No_State_Checkers_Should_Not_Increment_Check_Counts()
{
var entity = new MyStateEntity();
await SimpleStateCheckerManager.IsEnabledAsync(entity);
entity.CheckCount.ShouldBe(0);
entity.GlobalCheckCount.ShouldBe(0);
entity.MultipleCheckCount.ShouldBe(0);
entity.MultipleGlobalCheckCount.ShouldBe(0);
}
[Fact]
public async Task Multiple_Entities_With_No_State_Checkers_Should_All_Be_Enabled()
{
var entities = new[]
{
new MyStateEntity { CreationTime = DateTime.Parse("2022-01-01", CultureInfo.InvariantCulture) },
new MyStateEntity { CreationTime = DateTime.Parse("2019-01-01", CultureInfo.InvariantCulture) }
};
var result = await SimpleStateCheckerManager.IsEnabledAsync(entities);
result.Values.ShouldAllBe(v => v);
}
[Fact]
public async Task Entity_With_Individual_Checker_Should_Still_Be_Checked()
{
var entity = new MyStateEntity
{
CreationTime = DateTime.Parse("2021-01-01", CultureInfo.InvariantCulture)
};
entity.AddSimpleStateChecker(new MySimpleStateChecker());
(await SimpleStateCheckerManager.IsEnabledAsync(entity)).ShouldBeTrue();
entity.CheckCount.ShouldBe(1);
entity.CreationTime = DateTime.Parse("2001-01-01", CultureInfo.InvariantCulture);
(await SimpleStateCheckerManager.IsEnabledAsync(entity)).ShouldBeFalse();
entity.CheckCount.ShouldBe(2);
}
}
Loading…
Cancel
Save