diff --git a/framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/Permissions/AuthenticatedSimpleStateCheckerSerializerContributor.cs b/framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/Permissions/AuthenticatedSimpleStateCheckerSerializerContributor.cs index d23273e7a2..13c6f92d94 100644 --- a/framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/Permissions/AuthenticatedSimpleStateCheckerSerializerContributor.cs +++ b/framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/Permissions/AuthenticatedSimpleStateCheckerSerializerContributor.cs @@ -10,7 +10,7 @@ public class AuthenticatedSimpleStateCheckerSerializerContributor : { public const string CheckerShortName = "A"; - public string? SerializeToJson(ISimpleStateChecker checker) + public string? SerializeToJson(ISimpleStateChecker checker) where TState : IHasSimpleStateCheckers { if (checker is not RequireAuthenticatedSimpleStateChecker) @@ -25,6 +25,10 @@ public class AuthenticatedSimpleStateCheckerSerializerContributor : return jsonObject.ToJsonString(); } + public string? SerializeToJson(ISimpleStateChecker checker, TState state) + where TState : IHasSimpleStateCheckers + => SerializeToJson(checker); + public ISimpleStateChecker? Deserialize(JsonObject jsonObject, TState state) where TState : IHasSimpleStateCheckers { diff --git a/framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/Permissions/PermissionsSimpleStateCheckerSerializerContributor.cs b/framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/Permissions/PermissionsSimpleStateCheckerSerializerContributor.cs index 1a06b83ed7..61978be6c4 100644 --- a/framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/Permissions/PermissionsSimpleStateCheckerSerializerContributor.cs +++ b/framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/Permissions/PermissionsSimpleStateCheckerSerializerContributor.cs @@ -20,13 +20,31 @@ public class PermissionsSimpleStateCheckerSerializerContributor : return null; } - var jsonObject = new JsonObject { + return BuildJson(permissionsSimpleStateChecker.RequiresAll, permissionsSimpleStateChecker.PermissionNames); + } + + public string? SerializeToJson(ISimpleStateChecker checker, TState state) + where TState : IHasSimpleStateCheckers + { + if (checker is RequirePermissionsSimpleBatchStateChecker batch) + { + var model = batch.GetModelOrNull(state); + return model == null ? null : BuildJson(model.RequiresAll, model.Permissions); + } + + return SerializeToJson(checker); + } + + private static string BuildJson(bool requiresAll, string[] permissionNames) + { + var jsonObject = new JsonObject + { ["T"] = CheckerShortName, - ["A"] = permissionsSimpleStateChecker.RequiresAll + ["A"] = requiresAll }; var nameArray = new JsonArray(); - foreach (var permissionName in permissionsSimpleStateChecker.PermissionNames) + foreach (var permissionName in permissionNames) { nameArray.Add(permissionName); } diff --git a/framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/Permissions/RequirePermissionsSimpleBatchStateChecker.cs b/framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/Permissions/RequirePermissionsSimpleBatchStateChecker.cs index 0957f51666..d78e0ceb6f 100644 --- a/framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/Permissions/RequirePermissionsSimpleBatchStateChecker.cs +++ b/framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/Permissions/RequirePermissionsSimpleBatchStateChecker.cs @@ -27,9 +27,12 @@ public class RequirePermissionsSimpleBatchStateChecker : SimpleBatchStat private readonly List> _models; + private readonly Dictionary> _modelsByState; + public RequirePermissionsSimpleBatchStateChecker() { _models = new List>(); + _modelsByState = new Dictionary>(); } public RequirePermissionsSimpleBatchStateChecker AddCheckModels(params RequirePermissionsSimpleBatchStateCheckerModel[] models) @@ -37,6 +40,13 @@ public class RequirePermissionsSimpleBatchStateChecker : SimpleBatchStat Check.NotNullOrEmpty(models, nameof(models)); _models.AddRange(models); + foreach (var model in models) + { + if (!_modelsByState.ContainsKey(model.State)) + { + _modelsByState[model.State] = model; + } + } return this; } @@ -47,6 +57,11 @@ public class RequirePermissionsSimpleBatchStateChecker : SimpleBatchStat return new DisposeAction(() => _current.Value = previousValue); } + public virtual RequirePermissionsSimpleBatchStateCheckerModel? GetModelOrNull(TState state) + { + return _modelsByState.TryGetValue(state, out var model) ? model : null; + } + public override async Task> IsEnabledAsync(SimpleBatchStateCheckerContext context) { var permissionChecker = context.ServiceProvider.GetRequiredService(); diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/SimpleStateChecking/ISimpleStateCheckerSerializer.cs b/framework/src/Volo.Abp.Core/Volo/Abp/SimpleStateChecking/ISimpleStateCheckerSerializer.cs index 96c0785d6e..0be80ff319 100644 --- a/framework/src/Volo.Abp.Core/Volo/Abp/SimpleStateChecking/ISimpleStateCheckerSerializer.cs +++ b/framework/src/Volo.Abp.Core/Volo/Abp/SimpleStateChecking/ISimpleStateCheckerSerializer.cs @@ -7,6 +7,10 @@ public interface ISimpleStateCheckerSerializer public string? Serialize(ISimpleStateChecker checker) where TState : IHasSimpleStateCheckers; + + public string? Serialize(ISimpleStateChecker checker, TState state) + where TState : IHasSimpleStateCheckers; + public ISimpleStateChecker? Deserialize(JsonObject jsonObject, TState state) where TState : IHasSimpleStateCheckers; } \ No newline at end of file diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/SimpleStateChecking/ISimpleStateCheckerSerializerContributor.cs b/framework/src/Volo.Abp.Core/Volo/Abp/SimpleStateChecking/ISimpleStateCheckerSerializerContributor.cs index 79510024b7..b8c3e4943f 100644 --- a/framework/src/Volo.Abp.Core/Volo/Abp/SimpleStateChecking/ISimpleStateCheckerSerializerContributor.cs +++ b/framework/src/Volo.Abp.Core/Volo/Abp/SimpleStateChecking/ISimpleStateCheckerSerializerContributor.cs @@ -7,6 +7,9 @@ public interface ISimpleStateCheckerSerializerContributor public string? SerializeToJson(ISimpleStateChecker checker) where TState : IHasSimpleStateCheckers; + public string? SerializeToJson(ISimpleStateChecker checker, TState state) + where TState : IHasSimpleStateCheckers; + public ISimpleStateChecker? Deserialize(JsonObject jsonObject, TState state) where TState : IHasSimpleStateCheckers; } \ No newline at end of file diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/SimpleStateChecking/SimpleStateCheckerSerializer.cs b/framework/src/Volo.Abp.Core/Volo/Abp/SimpleStateChecking/SimpleStateCheckerSerializer.cs index 8ae58d16be..bc4004ec89 100644 --- a/framework/src/Volo.Abp.Core/Volo/Abp/SimpleStateChecking/SimpleStateCheckerSerializer.cs +++ b/framework/src/Volo.Abp.Core/Volo/Abp/SimpleStateChecking/SimpleStateCheckerSerializer.cs @@ -15,7 +15,7 @@ public class SimpleStateCheckerSerializer : _contributors = contributors; } - public string? Serialize(ISimpleStateChecker checker) + public string? Serialize(ISimpleStateChecker checker) where TState : IHasSimpleStateCheckers { foreach (var contributor in _contributors) @@ -30,6 +30,21 @@ public class SimpleStateCheckerSerializer : return null; } + public string? Serialize(ISimpleStateChecker checker, TState state) + where TState : IHasSimpleStateCheckers + { + foreach (var contributor in _contributors) + { + var result = contributor.SerializeToJson(checker, state); + if (result != null) + { + return result; + } + } + + return null; + } + public ISimpleStateChecker? Deserialize(JsonObject jsonObject, TState state) where TState : IHasSimpleStateCheckers { diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/SimpleStateChecking/SimpleStateCheckerSerializerExtensions.cs b/framework/src/Volo.Abp.Core/Volo/Abp/SimpleStateChecking/SimpleStateCheckerSerializerExtensions.cs index 8d06c7cfc3..43e8ca0d6c 100644 --- a/framework/src/Volo.Abp.Core/Volo/Abp/SimpleStateChecking/SimpleStateCheckerSerializerExtensions.cs +++ b/framework/src/Volo.Abp.Core/Volo/Abp/SimpleStateChecking/SimpleStateCheckerSerializerExtensions.cs @@ -8,25 +8,42 @@ namespace Volo.Abp.SimpleStateChecking; public static class SimpleStateCheckerSerializerExtensions { public static string? Serialize( - this ISimpleStateCheckerSerializer serializer, + this ISimpleStateCheckerSerializer serializer, IList> stateCheckers) where TState : IHasSimpleStateCheckers + { + return SerializeCore(stateCheckers, serializer.Serialize); + } + + public static string? Serialize( + this ISimpleStateCheckerSerializer serializer, + IList> stateCheckers, + TState state) + where TState : IHasSimpleStateCheckers + { + return SerializeCore(stateCheckers, c => serializer.Serialize(c, state)); + } + + private static string? SerializeCore( + IList> stateCheckers, + Func, string?> serializeChecker) + where TState : IHasSimpleStateCheckers { switch (stateCheckers.Count) { case 0: return null; case 1: - var serializedChecker = serializer.Serialize(stateCheckers.Single()); + var serializedChecker = serializeChecker(stateCheckers.Single()); return serializedChecker != null ? $"[{serializedChecker}]" : null; default: var serializedCheckers = new List(stateCheckers.Count); - + foreach (var stateChecker in stateCheckers) { - var serialized = serializer.Serialize(stateChecker); + var serialized = serializeChecker(stateChecker); if (serialized != null) { serializedCheckers.Add(serialized); diff --git a/framework/src/Volo.Abp.Features/Volo/Abp/Features/FeaturesSimpleStateCheckerSerializerContributor.cs b/framework/src/Volo.Abp.Features/Volo/Abp/Features/FeaturesSimpleStateCheckerSerializerContributor.cs index 5f652269b5..555a9d32a5 100644 --- a/framework/src/Volo.Abp.Features/Volo/Abp/Features/FeaturesSimpleStateCheckerSerializerContributor.cs +++ b/framework/src/Volo.Abp.Features/Volo/Abp/Features/FeaturesSimpleStateCheckerSerializerContributor.cs @@ -19,13 +19,31 @@ public class FeaturesSimpleStateCheckerSerializerContributor : return null; } - var jsonObject = new JsonObject { + return BuildJson(featuresSimpleStateChecker.RequiresAll, featuresSimpleStateChecker.FeatureNames); + } + + public string? SerializeToJson(ISimpleStateChecker checker, TState state) + where TState : IHasSimpleStateCheckers + { + if (checker is RequireFeaturesSimpleBatchStateChecker batch) + { + var model = batch.GetModelOrNull(state); + return model == null ? null : BuildJson(model.RequiresAll, model.FeatureNames); + } + + return SerializeToJson(checker); + } + + private static string BuildJson(bool requiresAll, string[] featureNames) + { + var jsonObject = new JsonObject + { ["T"] = CheckerShortName, - ["A"] = featuresSimpleStateChecker.RequiresAll + ["A"] = requiresAll }; var nameArray = new JsonArray(); - foreach (var featureName in featuresSimpleStateChecker.FeatureNames) + foreach (var featureName in featureNames) { nameArray.Add(featureName); } diff --git a/framework/src/Volo.Abp.Features/Volo/Abp/Features/RequireFeaturesSimpleBatchStateChecker.cs b/framework/src/Volo.Abp.Features/Volo/Abp/Features/RequireFeaturesSimpleBatchStateChecker.cs index 66d484f7f9..5faf1b891a 100644 --- a/framework/src/Volo.Abp.Features/Volo/Abp/Features/RequireFeaturesSimpleBatchStateChecker.cs +++ b/framework/src/Volo.Abp.Features/Volo/Abp/Features/RequireFeaturesSimpleBatchStateChecker.cs @@ -27,9 +27,12 @@ public class RequireFeaturesSimpleBatchStateChecker : SimpleBatchStateCh private readonly List> _models; + private readonly Dictionary> _modelsByState; + public RequireFeaturesSimpleBatchStateChecker() { _models = new List>(); + _modelsByState = new Dictionary>(); } public RequireFeaturesSimpleBatchStateChecker AddCheckModels( @@ -38,6 +41,13 @@ public class RequireFeaturesSimpleBatchStateChecker : SimpleBatchStateCh Check.NotNullOrEmpty(models, nameof(models)); _models.AddRange(models); + foreach (var model in models) + { + if (!_modelsByState.ContainsKey(model.State)) + { + _modelsByState[model.State] = model; + } + } return this; } @@ -48,6 +58,11 @@ public class RequireFeaturesSimpleBatchStateChecker : SimpleBatchStateCh return new DisposeAction(() => _current.Value = previousValue); } + public virtual RequireFeaturesSimpleBatchStateCheckerModel? GetModelOrNull(TState state) + { + return _modelsByState.TryGetValue(state, out var model) ? model : null; + } + public override async Task> IsEnabledAsync( SimpleBatchStateCheckerContext context) { diff --git a/framework/src/Volo.Abp.GlobalFeatures/Volo/Abp/GlobalFeatures/GlobalFeaturesSimpleStateCheckerSerializerContributor.cs b/framework/src/Volo.Abp.GlobalFeatures/Volo/Abp/GlobalFeatures/GlobalFeaturesSimpleStateCheckerSerializerContributor.cs index fe8530435b..4063197cf1 100644 --- a/framework/src/Volo.Abp.GlobalFeatures/Volo/Abp/GlobalFeatures/GlobalFeaturesSimpleStateCheckerSerializerContributor.cs +++ b/framework/src/Volo.Abp.GlobalFeatures/Volo/Abp/GlobalFeatures/GlobalFeaturesSimpleStateCheckerSerializerContributor.cs @@ -34,6 +34,10 @@ public class GlobalFeaturesSimpleStateCheckerSerializerContributor : return jsonObject.ToJsonString(); } + public string? SerializeToJson(ISimpleStateChecker checker, TState state) + where TState : IHasSimpleStateCheckers + => SerializeToJson(checker); + public ISimpleStateChecker? Deserialize(JsonObject jsonObject, TState state) where TState : IHasSimpleStateCheckers { diff --git a/framework/test/Volo.Abp.Features.Tests/Volo/Abp/Features/RequireFeaturesSimpleBatchStateChecker_Tests.cs b/framework/test/Volo.Abp.Features.Tests/Volo/Abp/Features/RequireFeaturesSimpleBatchStateChecker_Tests.cs index b0775e70d2..acd961931e 100644 --- a/framework/test/Volo.Abp.Features.Tests/Volo/Abp/Features/RequireFeaturesSimpleBatchStateChecker_Tests.cs +++ b/framework/test/Volo.Abp.Features.Tests/Volo/Abp/Features/RequireFeaturesSimpleBatchStateChecker_Tests.cs @@ -85,6 +85,58 @@ public class RequireFeaturesSimpleBatchStateChecker_Tests : FeatureTestBase } } + [Fact] + public void GetModelOrNull_Returns_First_Win_When_Same_State_Registered_Twice() + { + // Mirrors IsEnabledAsync's modelLookup behaviour: when the same state is registered + // multiple times, the first registration wins. Backed by the dictionary index. + + var checker = new RequireFeaturesSimpleBatchStateChecker(); + var state = new NamedState("A"); + + checker.AddCheckModels( + new RequireFeaturesSimpleBatchStateCheckerModel(state, new[] { "First" }, true)); + checker.AddCheckModels( + new RequireFeaturesSimpleBatchStateCheckerModel(state, new[] { "Second" }, true)); + + checker.GetModelOrNull(state)!.FeatureNames.ShouldBe(new[] { "First" }); + } + + [Fact] + public void GetModelOrNull_Uses_Same_Equality_As_Runtime() + { + // The runtime path (IsEnabledAsync) looks up models via HashSet(context.States), + // i.e. EqualityComparer.Default. GetModelOrNull must use the same semantics or + // a custom TState.Equals would make the runtime gate and the serializer disagree. + + var checker = new RequireFeaturesSimpleBatchStateChecker(); + var stateA1 = new NamedState("A"); + var stateA2 = new NamedState("A"); // distinct instance, equal by Name + var stateB = new NamedState("B"); + + checker.AddCheckModels( + new RequireFeaturesSimpleBatchStateCheckerModel(stateA1, new[] { "F1" }, true), + new RequireFeaturesSimpleBatchStateCheckerModel(stateB, new[] { "F2" }, true)); + + // Same equality semantics as the runtime: A2 hits A1's model. + checker.GetModelOrNull(stateA1).ShouldNotBeNull(); + checker.GetModelOrNull(stateA2).ShouldNotBeNull(); + checker.GetModelOrNull(stateA2)!.FeatureNames.ShouldBe(new[] { "F1" }); + checker.GetModelOrNull(new NamedState("missing")).ShouldBeNull(); + } + + private sealed class NamedState : IHasSimpleStateCheckers, IEquatable + { + public string Name { get; } + public List> StateCheckers { get; } = new(); + + public NamedState(string name) => Name = name; + + public bool Equals(NamedState? other) => other is not null && other.Name == Name; + public override bool Equals(object? obj) => obj is NamedState other && Equals(other); + public override int GetHashCode() => Name.GetHashCode(); + } + [Fact] public async Task Current_Should_Not_Be_Null_In_Fresh_ExecutionContext() { diff --git a/modules/permission-management/src/Volo.Abp.PermissionManagement.Domain/Volo/Abp/PermissionManagement/PermissionDefinitionSerializer.cs b/modules/permission-management/src/Volo.Abp.PermissionManagement.Domain/Volo/Abp/PermissionManagement/PermissionDefinitionSerializer.cs index 707a3e89ea..22d0b0e179 100644 --- a/modules/permission-management/src/Volo.Abp.PermissionManagement.Domain/Volo/Abp/PermissionManagement/PermissionDefinitionSerializer.cs +++ b/modules/permission-management/src/Volo.Abp.PermissionManagement.Domain/Volo/Abp/PermissionManagement/PermissionDefinitionSerializer.cs @@ -93,7 +93,7 @@ public class PermissionDefinitionSerializer : IPermissionDefinitionSerializer, I permission.IsEnabled, permission.MultiTenancySide, SerializeProviders(permission.Providers), - SerializeStateCheckers(permission.StateCheckers) + SerializeStateCheckers(permission, permission.StateCheckers) ); foreach (var property in permission.Properties) @@ -112,8 +112,10 @@ public class PermissionDefinitionSerializer : IPermissionDefinitionSerializer, I : null; } - protected virtual string SerializeStateCheckers(List> stateCheckers) + protected virtual string SerializeStateCheckers( + PermissionDefinition permission, + List> stateCheckers) { - return StateCheckerSerializer.Serialize(stateCheckers); + return StateCheckerSerializer.Serialize(stateCheckers, permission); } } diff --git a/modules/permission-management/test/Volo.Abp.PermissionManagement.Domain.Tests/Volo/Abp/PermissionManagement/PermissionDefinitionSerializer_Tests.cs b/modules/permission-management/test/Volo.Abp.PermissionManagement.Domain.Tests/Volo/Abp/PermissionManagement/PermissionDefinitionSerializer_Tests.cs index a8bdc4b4ea..c395398bb7 100644 --- a/modules/permission-management/test/Volo.Abp.PermissionManagement.Domain.Tests/Volo/Abp/PermissionManagement/PermissionDefinitionSerializer_Tests.cs +++ b/modules/permission-management/test/Volo.Abp.PermissionManagement.Domain.Tests/Volo/Abp/PermissionManagement/PermissionDefinitionSerializer_Tests.cs @@ -79,6 +79,74 @@ public class PermissionDefinitionSerializer_Tests : PermissionTestBase } + [Fact] + public async Task Serialize_Permission_With_Default_Batch_RequireFeatures() + { + var context = new PermissionDefinitionContext(null); + var group = context.AddGroup("AbpAuditLogging"); + var permission = group.AddPermission( + "AbpAuditLogging.Settings", + new LocalizableString(typeof(AbpPermissionManagementResource), "Permission1")) + .RequireFeatures("AbpAuditLogging.SettingManagement"); + + var record = await _serializer.SerializeAsync(permission, group); + + record.StateCheckers.ShouldBe( + "[{\"T\":\"F\",\"A\":true,\"N\":[\"AbpAuditLogging.SettingManagement\"]}]"); + } + + [Fact] + public async Task Serialize_Permission_With_Default_Batch_RequireFeatures_Picks_Per_State_Model() + { + var context = new PermissionDefinitionContext(null); + var group = context.AddGroup("Group1"); + + var permA = group.AddPermission("PermA", new LocalizableString(typeof(AbpPermissionManagementResource), "Permission1")) + .RequireFeatures("FeatureForA"); + var permB = group.AddPermission("PermB", new LocalizableString(typeof(AbpPermissionManagementResource), "Permission1")) + .RequireFeatures("FeatureForB1", "FeatureForB2"); + + var recordA = await _serializer.SerializeAsync(permA, group); + var recordB = await _serializer.SerializeAsync(permB, group); + + recordA.StateCheckers.ShouldBe("[{\"T\":\"F\",\"A\":true,\"N\":[\"FeatureForA\"]}]"); + recordB.StateCheckers.ShouldBe("[{\"T\":\"F\",\"A\":true,\"N\":[\"FeatureForB1\",\"FeatureForB2\"]}]"); + } + + [Fact] + public async Task Serialize_Permission_With_Default_Batch_RequirePermissions() + { + var context = new PermissionDefinitionContext(null); + var group = context.AddGroup("Group1"); + var permission = group.AddPermission( + "Permission1", + new LocalizableString(typeof(AbpPermissionManagementResource), "Permission1")) + .RequirePermissions("OtherPermission1", "OtherPermission2"); + + var record = await _serializer.SerializeAsync(permission, group); + + record.StateCheckers.ShouldBe( + "[{\"T\":\"P\",\"A\":true,\"N\":[\"OtherPermission1\",\"OtherPermission2\"]}]"); + } + + [Fact] + public async Task Serialize_Permission_With_Default_Batch_RequirePermissions_Picks_Per_State_Model() + { + var context = new PermissionDefinitionContext(null); + var group = context.AddGroup("Group1"); + + var permA = group.AddPermission("PermA", new LocalizableString(typeof(AbpPermissionManagementResource), "Permission1")) + .RequirePermissions("OtherForA"); + var permB = group.AddPermission("PermB", new LocalizableString(typeof(AbpPermissionManagementResource), "Permission1")) + .RequirePermissions(requiresAll: false, "OtherForB1", "OtherForB2"); + + var recordA = await _serializer.SerializeAsync(permA, group); + var recordB = await _serializer.SerializeAsync(permB, group); + + recordA.StateCheckers.ShouldBe("[{\"T\":\"P\",\"A\":true,\"N\":[\"OtherForA\"]}]"); + recordB.StateCheckers.ShouldBe("[{\"T\":\"P\",\"A\":false,\"N\":[\"OtherForB1\",\"OtherForB2\"]}]"); + } + [Fact] public async Task Serialize_Complex_Resource_Permission_Definition() {