From 4fd5287d371d3335e20165c62ce4b8735afb9cca Mon Sep 17 00:00:00 2001 From: Sagilio Date: Thu, 18 Mar 2021 23:55:37 +0800 Subject: [PATCH 1/8] perf: Improve performance in GetImplicitUsersForPermission Signed-off-by: Sagilio --- NetCasbin/Enforcer.cs | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/NetCasbin/Enforcer.cs b/NetCasbin/Enforcer.cs index 062be63a..75d7ab4f 100644 --- a/NetCasbin/Enforcer.cs +++ b/NetCasbin/Enforcer.cs @@ -564,12 +564,11 @@ public IEnumerable GetImplicitUsersForPermission(params string[] permiss public IEnumerable GetImplicitUsersForPermission(IEnumerable permissions) { - var policySubjects = GetAllSubjects(); - var groupInherit = model.GetValuesForFieldInPolicyAllTypes("g", 1); - var groupSubjects = model.GetValuesForFieldInPolicyAllTypes("g", 0); - return policySubjects.Concat(groupSubjects).Distinct() - .Where(subject => Enforce(new[]{ subject }.Concat(permissions).Cast().ToArray())) - .Except(groupInherit); + List policySubjects = GetAllSubjects(); + List groupInherit = model.GetValuesForFieldInPolicyAllTypes("g", 1); + List groupSubjects = model.GetValuesForFieldInPolicyAllTypes("g", 0); + return policySubjects.Concat(groupSubjects).Distinct().Except(groupInherit) + .Where(subject => Enforce(new[]{ subject }.Concat(permissions).Cast().ToArray())); } #endregion From 9aa78ed7ce02038e8cb3a74759d49197faed22e7 Mon Sep 17 00:00:00 2001 From: Sagilio Date: Sun, 21 Mar 2021 03:43:33 +0800 Subject: [PATCH 2/8] chore: Remove unused class Signed-off-by: Sagilio --- NetCasbin/Abstractions/IExpresstionCreator.cs | 23 --- NetCasbin/Evaluation/IExpressionProvider.cs | 165 ------------------ 2 files changed, 188 deletions(-) delete mode 100644 NetCasbin/Abstractions/IExpresstionCreator.cs delete mode 100644 NetCasbin/Evaluation/IExpressionProvider.cs diff --git a/NetCasbin/Abstractions/IExpresstionCreator.cs b/NetCasbin/Abstractions/IExpresstionCreator.cs deleted file mode 100644 index 6b2bba6b..00000000 --- a/NetCasbin/Abstractions/IExpresstionCreator.cs +++ /dev/null @@ -1,23 +0,0 @@ -using System.Collections.Generic; -using DynamicExpresso; -using NetCasbin.Model; - -namespace NetCasbin.Abstractions -{ - internal interface IExpressionProvider - { - public Assertion RequestAssertion { get; } - - public Assertion PolicyAssertion { get; } - - public void SetFunction(string name, AbstractFunction function); - - public void SetGFunctions(); - - public Lambda GetExpression(string expressionString, IReadOnlyList requestValues); - - public IDictionary AddOrUpdateRequestParameters(IReadOnlyList requestValues); - - public IDictionary AddOrUpdatePolicyParameters(IReadOnlyList policyValues); - } -} diff --git a/NetCasbin/Evaluation/IExpressionProvider.cs b/NetCasbin/Evaluation/IExpressionProvider.cs deleted file mode 100644 index f050cbba..00000000 --- a/NetCasbin/Evaluation/IExpressionProvider.cs +++ /dev/null @@ -1,165 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using DynamicExpresso; -using NetCasbin.Abstractions; -using NetCasbin.Model; -using NetCasbin.Util; - -namespace NetCasbin.Evaluation -{ - public class ExpressionProvider : IExpressionProvider - { - private readonly IDictionary _expressionCache - = new Dictionary(); - private readonly FunctionMap _functionMap = FunctionMap.LoadFunctionMap(); - private readonly Model.Model _model; - private Interpreter _interpreter; - private readonly IDictionary _parameters = new Dictionary(); - - public ExpressionProvider(Model.Model model, - string requestType = PermConstants.DefaultRequestType, - string policyType = PermConstants.DefaultPolicyType) - { - _model = model; - if (model.Model.ContainsKey(PermConstants.Section.RequestSection)) - { - RequestAssertion = model.Model[PermConstants.Section.RequestSection][requestType]; - } - if (model.Model.ContainsKey(PermConstants.Section.PolicySection)) - { - PolicyAssertion = model.Model[PermConstants.Section.PolicySection][policyType]; - } - } - - public Assertion RequestAssertion { get; } - public Assertion PolicyAssertion { get; } - - private IDictionary RequestTokens => RequestAssertion.Tokens; - private IDictionary PolicyTokens => PolicyAssertion.Tokens; - - public void SetFunction(string name, AbstractFunction function) - { - _expressionCache.Clear(); - var interpreter = GetInterpreter(); - interpreter.SetFunction(name, function); - } - - public void SetGFunctions() - { - _expressionCache.Clear(); - var interpreter = GetInterpreter(); - SetGFunctions(interpreter); - } - - public Lambda GetExpression(string expressionString, IReadOnlyList requestValues) - { - if (_expressionCache.ContainsKey(expressionString)) - { - return _expressionCache[expressionString]; - } - - Lambda expression = CreateExpression(expressionString, requestValues); - _expressionCache[expressionString] = expression; - return expression; - } - - public IDictionary AddOrUpdateRequestParameters(IReadOnlyList requestValues = null) - { - foreach (string token in RequestTokens.Keys) - { - object requestValue = requestValues?[RequestTokens[token]]; - - if (_parameters.ContainsKey(token)) - { - if (requestValue is not null) - { - _parameters[token] = new Parameter(token, requestValue); - } - } - else - { - _parameters.Add(token, new Parameter(token, requestValue ?? string.Empty)); - } - } - return _parameters; - } - - public IDictionary AddOrUpdatePolicyParameters(IReadOnlyList policyValues = null) - { - foreach (string token in PolicyTokens.Keys) - { - string policyValue = policyValues?[PolicyTokens[token]]; - - if (_parameters.ContainsKey(token)) - { - if (policyValue is not null) - { - _parameters[token] = new Parameter(token, policyValue); - } - } - else - { - _parameters.Add(token, new Parameter(token, policyValue ?? string.Empty)); - } - } - return _parameters; - } - - private Lambda CreateExpression(string expressionString, IReadOnlyList requestValues) - { - Parameter[] parameterArray = GetParameters(requestValues).Values.ToArray(); - Interpreter interpreter = GetInterpreter(); - return interpreter.Parse(expressionString, parameterArray);; - } - - private IDictionary GetParameters(IReadOnlyList requestValues = null) - { - AddOrUpdateRequestParameters(requestValues); - AddOrUpdatePolicyParameters(); - return _parameters; - } - - private Interpreter GetInterpreter() - { - if (_interpreter is not null) - { - return _interpreter; - } - - _interpreter = CreateInterpreter(); - return _interpreter; - } - - private Interpreter CreateInterpreter() - { - var interpreter = new Interpreter(); - SetFunctions(interpreter); - SetGFunctions(interpreter); - return interpreter; - } - - private void SetFunctions(Interpreter interpreter) - { - foreach (KeyValuePair functionKeyValue in _functionMap.FunctionDict) - { - interpreter.SetFunction(functionKeyValue.Key, functionKeyValue.Value); - } - } - - private void SetGFunctions(Interpreter interpreter) - { - if (_model.Model.ContainsKey(PermConstants.Section.RoleSection) is false) - { - return; - } - - foreach (KeyValuePair assertionKeyValue in _model.Model[PermConstants.Section.RoleSection]) - { - string key = assertionKeyValue.Key; - Assertion assertion = assertionKeyValue.Value; - interpreter.SetFunction(key, BuiltInFunctions.GenerateGFunction(key, assertion.RoleManager)); - } - } - } -} From 19884d4a6020e0668e0df3e10a48219d089fb506 Mon Sep 17 00:00:00 2001 From: Sagilio Date: Tue, 23 Mar 2021 16:31:26 +0800 Subject: [PATCH 3/8] fix(ci): Fix error assembly version Signed-off-by: Sagilio --- .github/workflows/build-and-release.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build-and-release.yml b/.github/workflows/build-and-release.yml index 49997648..8e5821c7 100644 --- a/.github/workflows/build-and-release.yml +++ b/.github/workflows/build-and-release.yml @@ -176,7 +176,7 @@ jobs: echo "Now branch name: $NowBranchName"; $PackageVersion = ($LastTag).TrimStart('v') + "-" + $env:BUILD_RUN_NUMBER + "." + $NowBranchName + "." + $env:SHA.SubString(0, 7); echo "Publishing package version: ${PackageVersion}"; - dotnet pack -c Release -o packages /p:PackageVersion=$PackageVersion /p:Version=$Version --no-build; + dotnet pack -c Release -o packages /p:PackageVersion=$PackageVersion /p:Version=$Version; - name: Upload packages artefacts uses: actions/upload-artifact@v1.0.0 @@ -232,7 +232,7 @@ jobs: echo "Last tag is: $LastTag"; $Version = ($LastTag).TrimStart('v'); echo "Publishing version: $Version"; - dotnet pack -c Release -o packages /p:PackageVersion=$Version /p:Version=$Version --no-build; + dotnet pack -c Release -o packages /p:PackageVersion=$Version /p:Version=$Version; - name: Upload packages artefacts uses: actions/upload-artifact@v1.0.0 From db7f40b0febb981619c8407e8378e5600ed4b90b Mon Sep 17 00:00:00 2001 From: Sagilio Date: Mon, 29 Mar 2021 22:18:08 +0800 Subject: [PATCH 4/8] test: Add GetRolesFromUserWithDomains test Signed-off-by: Sagilio --- NetCasbin.UnitTest/RbacApiWithDomainsTest.cs | 17 +++++++++++++++++ NetCasbin.UnitTest/Util/TestUtil.cs | 7 +++++++ NetCasbin/Rbac/Role.cs | 4 ++-- 3 files changed, 26 insertions(+), 2 deletions(-) diff --git a/NetCasbin.UnitTest/RbacApiWithDomainsTest.cs b/NetCasbin.UnitTest/RbacApiWithDomainsTest.cs index 18d4ce48..56fc737a 100644 --- a/NetCasbin.UnitTest/RbacApiWithDomainsTest.cs +++ b/NetCasbin.UnitTest/RbacApiWithDomainsTest.cs @@ -3,6 +3,7 @@ using Xunit; using static NetCasbin.UnitTest.Util.TestUtil; + namespace NetCasbin.UnitTest { [Collection("Model collection")] @@ -15,6 +16,22 @@ public RbacApiWithDomainsTest(TestModelFixture testModelFixture) _testModelFixture = testModelFixture; } + [Fact] + public void TestGetRolesFromUserWithDomains() + { + var e = new Enforcer(TestModelFixture.GetNewTestModel( + _testModelFixture._rbacWithDomainsModelText, + _testModelFixture._rbacWithHierarchyWithDomainsPolicyText)); + + e.BuildRoleLinks(); + + // This is only able to retrieve the first level of roles. + TestGetRolesInDomain(e, "alice", "domain1", AsList("role:global_admin")); + + // Retrieve all inherit roles. It supports domains as well. + TestGetImplicitRolesInDomain(e, "alice", "domain1", AsList("role:global_admin", "role:reader", "role:writer")); + } + [Fact] public void TestRoleApiWithDomains() { diff --git a/NetCasbin.UnitTest/Util/TestUtil.cs b/NetCasbin.UnitTest/Util/TestUtil.cs index 796aa1bb..64cebecd 100644 --- a/NetCasbin.UnitTest/Util/TestUtil.cs +++ b/NetCasbin.UnitTest/Util/TestUtil.cs @@ -149,6 +149,13 @@ internal static void TestGetRolesInDomain(Enforcer e, string name, string domain Assert.True(Utility.SetEquals(res, myRes), message); } + internal static void TestGetImplicitRolesInDomain(Enforcer e, string name, string domain, List res) + { + List myRes = e.GetImplicitRolesForUser(name, domain); + string message = "Implicit roles in domain " + name + " under " + domain + ": " + myRes + ", supposed to be " + res; + Assert.True(Utility.SetEquals(res, myRes), message); + } + internal static void TestGetPermissionsInDomain(Enforcer e, string name, string domain, List> res) { List> myRes = e.GetPermissionsForUserInDomain(name, domain); diff --git a/NetCasbin/Rbac/Role.cs b/NetCasbin/Rbac/Role.cs index aaf6d661..ba0e40f0 100644 --- a/NetCasbin/Rbac/Role.cs +++ b/NetCasbin/Rbac/Role.cs @@ -71,9 +71,9 @@ public bool HasDirectRole(string roleName) return _roles.IsValueCreated is not false && _roles.Value.ContainsKey(roleName); } - public List GetRoles() + public IEnumerable GetRoles() { - return _roles.Value.Select(x => x.Key).ToList(); + return _roles.Value.Keys; } public override string ToString() From 0ed2aceedd383b5ba0e84bf001bc28c3c2000a67 Mon Sep 17 00:00:00 2001 From: Sagilio Date: Mon, 22 Mar 2021 17:11:56 +0800 Subject: [PATCH 5/8] feat: Support caching feature Signed-off-by: Sagilio --- .../EnforcerWithCacheBenchmark.cs | 255 ++++++++++++++++++ NetCasbin.UnitTest/EnforcerCacheTest.cs | 57 ++++ NetCasbin/Abstractions/IEnforceCache.cs | 26 ++ NetCasbin/Caching/ConcurrentEnforceCache.cs | 50 ++++ NetCasbin/Caching/ReaderWriterEnforceCache.cs | 96 +++++++ .../ReaderWriterEnforceCacheOptions.cs | 9 + NetCasbin/CoreEnforcer.cs | 91 ++++++- NetCasbin/Extensions/LoggerExtension.cs | 6 + 8 files changed, 581 insertions(+), 9 deletions(-) create mode 100644 Casbin.Benchmark/EnforcerWithCacheBenchmark.cs create mode 100644 NetCasbin.UnitTest/EnforcerCacheTest.cs create mode 100644 NetCasbin/Abstractions/IEnforceCache.cs create mode 100644 NetCasbin/Caching/ConcurrentEnforceCache.cs create mode 100644 NetCasbin/Caching/ReaderWriterEnforceCache.cs create mode 100644 NetCasbin/Caching/ReaderWriterEnforceCacheOptions.cs diff --git a/Casbin.Benchmark/EnforcerWithCacheBenchmark.cs b/Casbin.Benchmark/EnforcerWithCacheBenchmark.cs new file mode 100644 index 00000000..bd5d9382 --- /dev/null +++ b/Casbin.Benchmark/EnforcerWithCacheBenchmark.cs @@ -0,0 +1,255 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Engines; +using BenchmarkDotNet.Jobs; +using Microsoft.Extensions.Options; +using NetCasbin; +using NetCasbin.Caching; +using static Casbin.Benchmark.TestHelper; + +namespace Casbin.Benchmark +{ + [MemoryDiagnoser] + [BenchmarkCategory("EnforcerWithCache")] + [SimpleJob(RunStrategy.Throughput, targetCount: 10, runtimeMoniker: RuntimeMoniker.Net48)] + [SimpleJob(RunStrategy.Throughput, targetCount: 10, runtimeMoniker: RuntimeMoniker.NetCoreApp31, baseline: true)] + [SimpleJob(RunStrategy.Throughput, targetCount: 10, runtimeMoniker: RuntimeMoniker.NetCoreApp50)] + public class EnforcerWithCacheBenchmark + { + private Enforcer NowEnforcer { get; set; } + private string NowTestUserName { get; set; } + private string NowTestDataName { get; set; } + private TestResource NowTestResource { get; set; } + private int[][] RbacScale { get; } = + { + new[] {100, 1000}, // Small + new[] {1000, 10000}, // Medium + new[] {10000, 100000} // Large + }; + + #region GlobalSetup + [GlobalSetup(Targets = new[] {nameof(BasicModel)})] + public void GlobalSetupForBasicModel() + { + GlobalSetupFromFile("basic_model.conf", "basic_policy.csv"); + Console.WriteLine("// Set the Basic enforcer"); + } + + [GlobalSetup(Targets = new[] {nameof(RbacModel)})] + public void GlobalSetupForRbacModel() + { + GlobalSetupFromFile("rbac_model.conf", "rbac_policy.csv"); + Console.WriteLine("// Set the Rbac Model enforcer"); + } + + [GlobalSetup(Targets = new[] {nameof(RbacModelWithSmallScale)})] + public void GlobalSetupForRbacModelWithSmallScale() + { + int groupCount = RbacScale[0][0]; + int userCount = RbacScale[0][1]; + GlobalSetupForRbacModelWithScale(groupCount, userCount); + Console.WriteLine($"// Set the Rbac Model with small scale ({groupCount} groups and {userCount} users) enforcer."); + } + + [GlobalSetup(Targets = new[] {nameof(RbacModelWithMediumScale)})] + public void GlobalSetupForRbacModelWithMediumScale() + { + int groupCount = RbacScale[1][0]; + int userCount = RbacScale[1][1]; + GlobalSetupForRbacModelWithScale(groupCount, userCount); + Console.WriteLine($"// Set the Rbac Model with medium scale ({groupCount} groups and {userCount} users) enforcer."); + } + + [GlobalSetup(Targets = new[] { nameof(RbacModelWithLargeScale) })] + public void GlobalSetupForRbacModelWithLargeScale() + { + int groupCount = RbacScale[2][0]; + int userCount = RbacScale[2][1]; + GlobalSetupForRbacModelWithScale(groupCount, userCount); + Console.WriteLine($"// Set the RBAC with large scale ({groupCount} groups and {userCount} users) enforcer."); + } + + [GlobalSetup(Targets = new[] {nameof(RbacModelWithResourceRoles)})] + public void GlobalSetupForRbacModelWithResourceRoles() + { + GlobalSetupFromFile("rbac_with_resource_roles_model.conf", "rbac_with_resource_roles_policy.csv"); + Console.WriteLine("// Set the RbacModel With Resource Roles enforcer"); + } + + [GlobalSetup(Targets = new[] {nameof(RbacModelWithDomains)})] + public void GlobalSetupForRbacModelWithDomains() + { + GlobalSetupFromFile("rbac_with_domains_model.conf", "rbac_with_domains_policy.csv"); + Console.WriteLine("// Set the Rbac Model With Domains enforcer"); + } + + [GlobalSetup(Targets = new[] {nameof(RbacModelWithDeny)})] + public void GlobalSetupForRbacModelWithDeny() + { + GlobalSetupFromFile("rbac_with_deny_model.conf", "rbac_with_deny_policy.csv"); + Console.WriteLine("// Set the Rbac Model With Deny enforcer"); + } + + [GlobalSetup(Targets = new[] {nameof(AbacModel)})] + public void GlobalSetupForAbacModel() + { + GlobalSetupFromFile("abac_model.conf"); + NowTestResource = new TestResource("data1", "alice"); + Console.WriteLine("// Set the Abac Model enforcer"); + } + + [GlobalSetup(Targets = new[] {nameof(KeyMatchModel)})] + public void GlobalSetupForKeyMatchModel() + { + GlobalSetupFromFile("keymatch_model.conf", "keymatch_policy.csv"); + Console.WriteLine("// Set the Key Match Model enforcer"); + } + + [GlobalSetup(Targets = new[] {nameof(PriorityModel)})] + public void GlobalSetupForPriorityModel() + { + GlobalSetupFromFile("priority_model.conf", "priority_policy.csv"); + Console.WriteLine("// Set the Priority Model enforcer"); + } + #endregion + + #region private help method + private void GlobalSetupForRbacModelWithScale(int groupCount, int userCount) + { + GlobalSetupForRbacModel(); + var policyList = new List>(); + for (int i = 0; i < groupCount; i++) + { + policyList.Add( new[] {$"group{i}", $"data{i / 10}", "read"}.ToList()); + } + NowEnforcer.AddPolicies(policyList); + + policyList.Clear(); + for (int i = 0; i < userCount; i++) + { + policyList.Add( new[] {$"user{i}", $"group{i / 10}"}.ToList()); + } + NowEnforcer.EnableAutoBuildRoleLinks(false); + NowEnforcer.AddGroupingPolicies(policyList); + NowEnforcer.BuildRoleLinks(); + + NowTestUserName = $"user{userCount / 2 + 1}"; // if 1000 => 501... + NowTestDataName = $"data{groupCount / 10 - 1}"; // if 100 => 9... + Console.WriteLine($"// Already set user name to {NowTestUserName}."); + Console.WriteLine($"// Already set data name to {NowTestDataName}."); + } + + private void GlobalSetupFromFile(string modelFileName, string policyFileName = null) + { + if (policyFileName is null) + { + NowEnforcer = new Enforcer(GetTestFilePath(modelFileName)); + NowEnforcer.EnableCache(true); + return; + } + + NowEnforcer = new Enforcer(GetTestFilePath(modelFileName), GetTestFilePath(policyFileName)); + NowEnforcer.EnableCache(true); + } + #endregion + + [GlobalCleanup] + public void GlobalCleanup() + { + NowEnforcer = null; + Console.WriteLine("// Cleaned the enforcer"); + } + + [Benchmark] + //[Benchmark(Description = "ACL, 2 rules (2 users)")] + [BenchmarkCategory("BasicModel")] + public void BasicModel() + { + _ = NowEnforcer.Enforce("alice", "data1", "read"); + } + + [Benchmark] + //[Benchmark(Description = "RBAC, 5 rules (2 users, 1 role)")] + [BenchmarkCategory("RbacModel")] + public void RbacModel() + { + _ = NowEnforcer.Enforce("alice", "data2", "read"); + } + + [Benchmark] + //[Benchmark(Description = "RBAC (small), 1100 rules (1000 users, 100 roles)")] + [BenchmarkCategory("RbacModel")] + public void RbacModelWithSmallScale() + { + _ = NowEnforcer.Enforce(NowTestUserName, NowTestDataName, "read"); + } + + [Benchmark] + //[Benchmark(Description = "RBAC (medium), 11000 rules (10000 users, 1000 roles)")] + [BenchmarkCategory("RbacModel")] + public void RbacModelWithMediumScale() + { + _ = NowEnforcer.Enforce(NowTestUserName, NowTestDataName, "read"); + } + + [Benchmark] + //[Benchmark(Description = "RBAC (large), 110000 rules (100000 users, 10000 roles)")] + [BenchmarkCategory("RbacModel")] + public void RbacModelWithLargeScale() + { + _ = NowEnforcer.Enforce(NowTestUserName, NowTestDataName, "read"); + } + + [Benchmark] + //[Benchmark(Description = "RBAC with resource roles, 6 rules (2 users, 2 roles)")] + [BenchmarkCategory("RbacModel")] + public void RbacModelWithResourceRoles() + { + _ = NowEnforcer.Enforce("alice", "data1", "read"); + } + + [Benchmark] + //[Benchmark(Description = "RBAC with domains/tenants, 6 rules (2 users, 1 role, 2 domains)")] + [BenchmarkCategory("RbacModel")] + public void RbacModelWithDomains() + { + _ = NowEnforcer.Enforce("alice", "domain1", "data1", "read"); + } + + [Benchmark] + //[Benchmark(Description = "Deny-override, 6 rules (2 users, 1 role)")] + [BenchmarkCategory("RbacModel")] + public void RbacModelWithDeny() + { + _ = NowEnforcer.Enforce("alice", "data1", "read"); + } + + [Benchmark] + //[Benchmark(Description = "ABAC, 0 rule (0 user)")] + [BenchmarkCategory("AbacModel")] + public void AbacModel() + { + var data1 = NowTestResource; + _ = NowEnforcer.Enforce("alice", data1, "read"); + } + + [Benchmark] + //[Benchmark(Description = "RESTful, 5 rules (3 users)")] + [BenchmarkCategory("KeyMatchModel")] + public void KeyMatchModel() + { + _ = NowEnforcer.Enforce("alice", "/alice_data/resource1", "GET"); + } + + [Benchmark] + //[Benchmark(Description = "Priority, 9 rules (2 users, 2 roles)")] + [BenchmarkCategory("PriorityModel")] + public void PriorityModel() + { + _ = NowEnforcer.Enforce("alice", "data1", "read"); + } + } +} diff --git a/NetCasbin.UnitTest/EnforcerCacheTest.cs b/NetCasbin.UnitTest/EnforcerCacheTest.cs new file mode 100644 index 00000000..449186d0 --- /dev/null +++ b/NetCasbin.UnitTest/EnforcerCacheTest.cs @@ -0,0 +1,57 @@ +using System.Collections.Generic; +using NetCasbin.UnitTest.Fixtures; +using NetCasbin.UnitTest.Mock; +using Xunit; +using Xunit.Abstractions; +using static NetCasbin.UnitTest.Util.TestUtil; + +namespace NetCasbin.UnitTest +{ + [Collection("Model collection")] + public class EnforcerCacheTest + { + private readonly ITestOutputHelper _testOutputHelper; + private readonly TestModelFixture _testModelFixture; + + public EnforcerCacheTest(ITestOutputHelper testOutputHelper, TestModelFixture testModelFixture) + { + _testOutputHelper = testOutputHelper; + _testModelFixture = testModelFixture; + } + + [Fact] + public void TestEnforceWithCache() + { + var e = new Enforcer(_testModelFixture.GetBasicTestModel()) + { + Logger = new MockLogger(_testOutputHelper) + }; + e.EnableCache(true); + + TestEnforce(e, "alice", "data1", "read", true); + TestEnforce(e, "alice", "data1", "write", false); + TestEnforce(e, "alice", "data2", "read", false); + TestEnforce(e, "alice", "data2", "write", false); + + // The cache is enabled, so even if we remove a policy rule, the decision + // for ("alice", "data1", "read") will still be true, as it uses the cached result. + _ = e.RemovePolicy("alice", "data1", "read"); + + TestEnforce(e, "alice", "data1", "read", true); + TestEnforce(e, "alice", "data1", "write", false); + TestEnforce(e, "alice", "data2", "read", false); + TestEnforce(e, "alice", "data2", "write", false); + + // Now we invalidate the cache, then all first-coming Enforce() has to be evaluated in real-time. + // The decision for ("alice", "data1", "read") will be false now. + e.EnforceCache.Clear(); + + TestEnforce(e, "alice", "data1", "read", false); + TestEnforce(e, "alice", "data1", "write", false); + TestEnforce(e, "alice", "data2", "read", false); + TestEnforce(e, "alice", "data2", "write", false); + + e.Logger = null; + } + } +} diff --git a/NetCasbin/Abstractions/IEnforceCache.cs b/NetCasbin/Abstractions/IEnforceCache.cs new file mode 100644 index 00000000..c02e96e3 --- /dev/null +++ b/NetCasbin/Abstractions/IEnforceCache.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace NetCasbin.Abstractions +{ + public interface IEnforceCache : IEnforceCache + { + public TOptions CacheOptions { get; } + } + + public interface IEnforceCache + { + public bool TryGetResult(IReadOnlyList requestValues, string key, out bool result); + + public Task TryGetResultAsync(IReadOnlyList requestValues, string key); + + public bool TrySetResult(IReadOnlyList requestValues, string key, bool result); + + public Task TrySetResultAsync(IReadOnlyList requestValues, string key, bool result); + + public void Clear(); + + public Task ClearAsync(); + } +} diff --git a/NetCasbin/Caching/ConcurrentEnforceCache.cs b/NetCasbin/Caching/ConcurrentEnforceCache.cs new file mode 100644 index 00000000..12dd84a0 --- /dev/null +++ b/NetCasbin/Caching/ConcurrentEnforceCache.cs @@ -0,0 +1,50 @@ +#if !NET45 +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Options; +using NetCasbin.Abstractions; + +namespace NetCasbin.Caching +{ + public class ConcurrentEnforceCache : IEnforceCache + { + private readonly ConcurrentDictionary _memoryCache = new(); + + public bool TryGetResult(IReadOnlyList requestValues, string key, out bool result) + { + return _memoryCache.TryGetValue(key, out result); + } + + public Task TryGetResultAsync(IReadOnlyList requestValues, string key) + { + return TryGetResult(requestValues, key, out bool result) + ? Task.FromResult((bool?) result) : null; + } + + public bool TrySetResult(IReadOnlyList requestValues, string key, bool result) + { + _memoryCache[key] = result; + return true; + } + + public Task TrySetResultAsync(IReadOnlyList requestValues, string key, bool result) + { + return Task.FromResult(TrySetResult(requestValues, key, result)); + } + + public void Clear() + { + _memoryCache.Clear(); + } + + public Task ClearAsync() + { + Clear(); + return Task.CompletedTask; + } + } +} +#endif diff --git a/NetCasbin/Caching/ReaderWriterEnforceCache.cs b/NetCasbin/Caching/ReaderWriterEnforceCache.cs new file mode 100644 index 00000000..604fe41a --- /dev/null +++ b/NetCasbin/Caching/ReaderWriterEnforceCache.cs @@ -0,0 +1,96 @@ +#if !NET45 +using System; +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Options; +using NetCasbin.Abstractions; + +namespace NetCasbin.Caching +{ + public class ReaderWriterEnforceCache : IEnforceCache + { + private readonly ReaderWriterLockSlim _lockSlim = new(); + private Dictionary _memoryCache = new(); + + public ReaderWriterEnforceCache(IOptions options) + { + if (options?.Value is not null) + { + CacheOptions = options.Value; + } + } + + public ReaderWriterEnforceCacheOptions CacheOptions { get; } = new(); + + public bool TryGetResult(IReadOnlyList requestValues, string key, out bool result) + { + if (requestValues is null) + { + throw new ArgumentNullException(nameof(requestValues)); + } + + if (_lockSlim.TryEnterReadLock(CacheOptions.WaitTimeOut) is false) + { + result = false; + return false; + } + + try + { + return _memoryCache.TryGetValue(key, out result); + } + finally + { + _lockSlim.ExitReadLock(); + } + } + + public Task TryGetResultAsync(IReadOnlyList requestValues, string key) + { + return TryGetResult(requestValues, key, out bool result) + ? Task.FromResult((bool?) result) : null; + } + + public bool TrySetResult(IReadOnlyList requestValues, string key, bool result) + { + if (requestValues is null) + { + throw new ArgumentNullException(nameof(requestValues)); + } + + if (_lockSlim.TryEnterWriteLock(CacheOptions.WaitTimeOut) is false) + { + return false; + } + + try + { + _memoryCache[key] = result; + return true; + } + finally + { + _lockSlim.ExitWriteLock(); + } + } + + public Task TrySetResultAsync(IReadOnlyList requestValues, string key, bool result) + { + return Task.FromResult(TrySetResult(requestValues, key, result)); + } + + public void Clear() + { + _memoryCache = new Dictionary(); + } + + public Task ClearAsync() + { + Clear(); + return Task.CompletedTask; + } + } +} +#endif diff --git a/NetCasbin/Caching/ReaderWriterEnforceCacheOptions.cs b/NetCasbin/Caching/ReaderWriterEnforceCacheOptions.cs new file mode 100644 index 00000000..ed11b3c8 --- /dev/null +++ b/NetCasbin/Caching/ReaderWriterEnforceCacheOptions.cs @@ -0,0 +1,9 @@ +using System; + +namespace NetCasbin.Caching +{ + public class ReaderWriterEnforceCacheOptions + { + public TimeSpan WaitTimeOut { get; set; } = TimeSpan.FromMilliseconds(50); + } +} diff --git a/NetCasbin/CoreEnforcer.cs b/NetCasbin/CoreEnforcer.cs index 08ee911b..6b666c57 100644 --- a/NetCasbin/CoreEnforcer.cs +++ b/NetCasbin/CoreEnforcer.cs @@ -5,6 +5,7 @@ using System.Threading.Tasks; #if !NET45 using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; #endif using DynamicExpresso; using NetCasbin.Abstractions; @@ -15,6 +16,8 @@ using NetCasbin.Rbac; using NetCasbin.Util; using NetCasbin.Extensions; +using NetCasbin.Caching; +using System.Text; namespace NetCasbin { @@ -37,7 +40,10 @@ public class CoreEnforcer : ICoreEnforcer protected bool autoNotifyWatcher; internal IExpressionHandler ExpressionHandler { get; private set; } + #if !NET45 + private bool _enableCache; + public IEnforceCache EnforceCache { get; set; } public ILogger Logger { get; set; } #endif @@ -170,6 +176,17 @@ public void SetEffector(IEffector effector) _effector = effector; } +#if !NET45 + /// + /// Sets an enforce cache. + /// + /// + public void SetEnforceCache(IEnforceCache enforceCache) + { + EnforceCache = enforceCache; + } +#endif + /// /// Clears all policy. /// @@ -306,9 +323,9 @@ public async Task SavePolicyAsync() throw new InvalidOperationException("Cannot save a filtered policy"); } await adapter.SavePolicyAsync(model); - if (!(watcher is null)) + if (watcher is not null) { - await watcher?.UpdateAsync(); + await watcher.UpdateAsync(); } } @@ -352,6 +369,13 @@ public void EnableAutoNotifyWatcher(bool autoNotifyWatcher) this.autoNotifyWatcher = autoNotifyWatcher; } +#if !NET45 + public void EnableCache(bool enableCache) + { + _enableCache = enableCache; + } +#endif + /// /// Manually rebuilds the role inheritance relations. /// @@ -368,9 +392,35 @@ public void BuildRoleLinks() /// The request needs to be mediated, usually an array of strings, /// can be class instances if ABAC is used. /// Whether to allow the request. - public Task EnforceAsync(params object[] requestValues) + public async Task EnforceAsync(params object[] requestValues) { - return Task.FromResult(Enforce(requestValues)); +#if !NET45 + if (requestValues.Any(requestValue => requestValue is not string)) + { + return InternalEnforce(requestValues); + } + + if (_enableCache) + { + string key = string.Join("$$", requestValues); + EnforceCache ??= new ReaderWriterEnforceCache(Options.Create(new ReaderWriterEnforceCacheOptions())); + bool? tryGetCachedResult = await EnforceCache.TryGetResultAsync(requestValues, key); + if (tryGetCachedResult.HasValue) + { + bool cachedResult = tryGetCachedResult.Value; + Logger?.LogEnforceCachedResult(requestValues, cachedResult); + return cachedResult; + } + + bool result = InternalEnforce(requestValues); + + EnforceCache ??= new ReaderWriterEnforceCache(Options.Create(new ReaderWriterEnforceCacheOptions())); + await EnforceCache.TrySetResultAsync(requestValues, key, result); + return result; + } +#endif + + return InternalEnforce(requestValues); } /// @@ -382,7 +432,30 @@ public Task EnforceAsync(params object[] requestValues) /// Whether to allow the request. public bool Enforce(params object[] requestValues) { - return Enforce((IReadOnlyList) requestValues); +#if !NET45 + if (requestValues.Any(requestValue => requestValue is not string)) + { + return InternalEnforce(requestValues); + } + + if (_enableCache) + { + string key = string.Join("$$", requestValues); + EnforceCache ??= new ReaderWriterEnforceCache(Options.Create(new ReaderWriterEnforceCacheOptions())); + if (EnforceCache.TryGetResult(requestValues, key, out bool cachedResult)) + { + Logger?.LogEnforceCachedResult(requestValues, cachedResult); + return cachedResult; + } + + bool result = InternalEnforce(requestValues); + EnforceCache ??= new ReaderWriterEnforceCache(Options.Create(new ReaderWriterEnforceCacheOptions())); + EnforceCache.TrySetResult(requestValues, key, result); + return result; + } +#endif + + return InternalEnforce(requestValues); } #if !NET45 @@ -396,7 +469,7 @@ public bool Enforce(params object[] requestValues) EnforceEx(params object[] requestValues) { var explains = new List>(); - bool result = Enforce(requestValues, explains); + bool result = InternalEnforce(requestValues, explains); return (result, explains); } @@ -424,7 +497,7 @@ public bool Enforce(params object[] requestValues) EnforceEx(params object[] requestValues) { var explains = new List>(); - bool result = Enforce(requestValues, explains); + bool result = InternalEnforce(requestValues, explains); return new Tuple>>(result, explains); } @@ -453,7 +526,7 @@ public bool Enforce(params object[] requestValues) /// Whether to allow the request. private Task EnforceAsync(IReadOnlyList requestValues, ICollection> explains = null) { - return Task.FromResult(Enforce(requestValues, explains)); + return Task.FromResult(InternalEnforce(requestValues, explains)); } /// @@ -464,7 +537,7 @@ private Task EnforceAsync(IReadOnlyList requestValues, ICollection /// The request needs to be mediated, usually an array of strings, /// can be class instances if ABAC is used. /// Whether to allow the request. - private bool Enforce(IReadOnlyList requestValues, ICollection> explains = null) + private bool InternalEnforce(IReadOnlyList requestValues, ICollection> explains = null) { if (_enabled is false) { diff --git a/NetCasbin/Extensions/LoggerExtension.cs b/NetCasbin/Extensions/LoggerExtension.cs index 9deeaa70..d95fcc1d 100644 --- a/NetCasbin/Extensions/LoggerExtension.cs +++ b/NetCasbin/Extensions/LoggerExtension.cs @@ -9,6 +9,12 @@ namespace NetCasbin.Extensions { public static class LoggerExtension { + public static void LogEnforceCachedResult(this ILogger logger, IEnumerable requestValues, bool result) + { + logger.LogInformation("Request: {1} ---> {0} (cached)", result, + string.Join(", ", requestValues)); + } + public static void LogEnforceResult(this ILogger logger, IEnumerable requestValues, bool result) { logger.LogInformation("Request: {1} ---> {0}", result, From 2fc9db6111557ab290fb3ceff3a8c9d5580031a9 Mon Sep 17 00:00:00 2001 From: Sagilio Date: Thu, 25 Mar 2021 10:40:47 +0800 Subject: [PATCH 6/8] test: Add .NET45/461 targets unit tests Signed-off-by: Sagilio --- NetCasbin.UnitTest/EnforcerCacheTest.cs | 3 +- NetCasbin.UnitTest/EnforcerTest.cs | 5 +- NetCasbin.UnitTest/ManagementApiTest.cs | 2 +- NetCasbin.UnitTest/Mock/MockLogger.cs | 4 +- NetCasbin.UnitTest/NetCasbin.UnitTest.csproj | 10 +- NetCasbin.UnitTest/RbacApiTest.cs | 2 +- NetCasbin.UnitTest/Util/TestUtil.cs | 14 ++- NetCasbin/CoreEnforcer.cs | 104 ++++++++++--------- 8 files changed, 87 insertions(+), 57 deletions(-) diff --git a/NetCasbin.UnitTest/EnforcerCacheTest.cs b/NetCasbin.UnitTest/EnforcerCacheTest.cs index 449186d0..330abfec 100644 --- a/NetCasbin.UnitTest/EnforcerCacheTest.cs +++ b/NetCasbin.UnitTest/EnforcerCacheTest.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +#if !NET452 using NetCasbin.UnitTest.Fixtures; using NetCasbin.UnitTest.Mock; using Xunit; @@ -55,3 +55,4 @@ public void TestEnforceWithCache() } } } +#endif diff --git a/NetCasbin.UnitTest/EnforcerTest.cs b/NetCasbin.UnitTest/EnforcerTest.cs index b0f2c24f..37ea2ccb 100644 --- a/NetCasbin.UnitTest/EnforcerTest.cs +++ b/NetCasbin.UnitTest/EnforcerTest.cs @@ -1,7 +1,6 @@ using System.Collections.Generic; using System.IO; using System.Threading.Tasks; -using Microsoft.Extensions.Logging; using NetCasbin.Persist; using NetCasbin.Persist.FileAdapter; using NetCasbin.UnitTest.Fixtures; @@ -464,6 +463,7 @@ public void TestEnableEnforce() TestEnforce(e, "bob", "data2", "write", true); } +#if !NET452 [Fact] public void TestEnableLog() { @@ -491,6 +491,7 @@ public void TestEnableLog() TestEnforce(e, "bob", "data2", "read", false); TestEnforce(e, "bob", "data2", "write", true); } +#endif [Fact] public void TestEnableAutoSave() @@ -832,6 +833,7 @@ public async Task TestEnforceExApiAsync() await TestEnforceExAsync(e, "bob", "data2", "write", new List {"bob", "data2", "write", "deny"}); } +#if !NET452 [Fact] public void TestEnforceExApiLog() { @@ -851,5 +853,6 @@ public void TestEnforceExApiLog() e.Logger = null; } +#endif } } diff --git a/NetCasbin.UnitTest/ManagementApiTest.cs b/NetCasbin.UnitTest/ManagementApiTest.cs index 12e498cb..c54d419e 100644 --- a/NetCasbin.UnitTest/ManagementApiTest.cs +++ b/NetCasbin.UnitTest/ManagementApiTest.cs @@ -118,7 +118,7 @@ public void TestModifyPolicy() e.RemoveFilteredPolicy(1, "data2"); TestGetPolicy(e, AsList(AsList("eve", "data3", "read"))); - e.RemoveFilteredPolicy(1, Array.Empty()); + e.RemoveFilteredPolicy(1); TestGetPolicy(e, AsList(AsList("eve", "data3", "read"))); e.RemoveFilteredPolicy(1, ""); diff --git a/NetCasbin.UnitTest/Mock/MockLogger.cs b/NetCasbin.UnitTest/Mock/MockLogger.cs index 6df639d2..61eb49a2 100644 --- a/NetCasbin.UnitTest/Mock/MockLogger.cs +++ b/NetCasbin.UnitTest/Mock/MockLogger.cs @@ -1,4 +1,5 @@ -using System; +#if !NET452 +using System; using Microsoft.Extensions.Logging; using Xunit.Abstractions; @@ -30,3 +31,4 @@ public IDisposable BeginScope(TState state) } } } +#endif diff --git a/NetCasbin.UnitTest/NetCasbin.UnitTest.csproj b/NetCasbin.UnitTest/NetCasbin.UnitTest.csproj index 187818d8..850a1be5 100644 --- a/NetCasbin.UnitTest/NetCasbin.UnitTest.csproj +++ b/NetCasbin.UnitTest/NetCasbin.UnitTest.csproj @@ -1,14 +1,14 @@  - net5 + net452;net461;net5 full false + 9.0 - - + runtime; build; native; contentfiles; analyzers; buildtransitive @@ -20,6 +20,10 @@ + + + + diff --git a/NetCasbin.UnitTest/RbacApiTest.cs b/NetCasbin.UnitTest/RbacApiTest.cs index b9734fbc..08eb9b88 100644 --- a/NetCasbin.UnitTest/RbacApiTest.cs +++ b/NetCasbin.UnitTest/RbacApiTest.cs @@ -449,7 +449,7 @@ public void GetImplicitRolesForUser() AsList("bob", "data2", "write"))); Assert.Equal(new[] { "admin", "data1_admin", "data2_admin" }, e.GetImplicitRolesForUser("alice")); - Assert.Equal(Array.Empty(), + Assert.Equal(new string[0], e.GetImplicitRolesForUser("bob")); } diff --git a/NetCasbin.UnitTest/Util/TestUtil.cs b/NetCasbin.UnitTest/Util/TestUtil.cs index 64cebecd..1519a75c 100644 --- a/NetCasbin.UnitTest/Util/TestUtil.cs +++ b/NetCasbin.UnitTest/Util/TestUtil.cs @@ -25,6 +25,7 @@ internal static void TestEnforce(Enforcer e, object sub, object obj, string act, Assert.Equal(res, e.Enforce(sub, obj, act)); } +#if !NET452 internal static void TestEnforceEx(Enforcer e, object sub, object obj, string act, List res) { var myRes = e.EnforceEx(sub, obj, act).Explains.ToList(); @@ -34,10 +35,21 @@ internal static void TestEnforceEx(Enforcer e, object sub, object obj, string ac Assert.True(Utility.SetEquals(res, myRes[0].ToList()), message); } } +#else + internal static void TestEnforceEx(Enforcer e, object sub, object obj, string act, List res) + { + var myRes = e.EnforceEx(sub, obj, act).Item2.ToList(); + string message = "Key: " + myRes + ", supposed to be " + res; + if (myRes.Count > 0) + { + Assert.True(Utility.SetEquals(res, myRes[0].ToList()), message); + } + } +#endif internal static async Task TestEnforceExAsync(Enforcer e, object sub, object obj, string act, List res) { - var myRes = (await e.EnforceExAsync(sub, obj, act)).Explains.ToList(); + var myRes = (await e.EnforceExAsync(sub, obj, act)).Item2.ToList(); string message = "Key: " + myRes + ", supposed to be " + res; if (myRes.Count > 0) { diff --git a/NetCasbin/CoreEnforcer.cs b/NetCasbin/CoreEnforcer.cs index 6b666c57..451a72b4 100644 --- a/NetCasbin/CoreEnforcer.cs +++ b/NetCasbin/CoreEnforcer.cs @@ -392,36 +392,42 @@ public void BuildRoleLinks() /// The request needs to be mediated, usually an array of strings, /// can be class instances if ABAC is used. /// Whether to allow the request. +#if !NET45 public async Task EnforceAsync(params object[] requestValues) { -#if !NET45 + if (_enableCache is false) + { + return InternalEnforce(requestValues); + } + if (requestValues.Any(requestValue => requestValue is not string)) { return InternalEnforce(requestValues); } - if (_enableCache) + string key = string.Join("$$", requestValues); + EnforceCache ??= new ReaderWriterEnforceCache(Options.Create(new ReaderWriterEnforceCacheOptions())); + bool? tryGetCachedResult = await EnforceCache.TryGetResultAsync(requestValues, key); + if (tryGetCachedResult.HasValue) { - string key = string.Join("$$", requestValues); - EnforceCache ??= new ReaderWriterEnforceCache(Options.Create(new ReaderWriterEnforceCacheOptions())); - bool? tryGetCachedResult = await EnforceCache.TryGetResultAsync(requestValues, key); - if (tryGetCachedResult.HasValue) - { - bool cachedResult = tryGetCachedResult.Value; - Logger?.LogEnforceCachedResult(requestValues, cachedResult); - return cachedResult; - } + bool cachedResult = tryGetCachedResult.Value; + Logger?.LogEnforceCachedResult(requestValues, cachedResult); + return cachedResult; + } - bool result = InternalEnforce(requestValues); + bool result = InternalEnforce(requestValues); - EnforceCache ??= new ReaderWriterEnforceCache(Options.Create(new ReaderWriterEnforceCacheOptions())); - await EnforceCache.TrySetResultAsync(requestValues, key, result); - return result; - } -#endif + EnforceCache ??= new ReaderWriterEnforceCache(Options.Create(new ReaderWriterEnforceCacheOptions())); + await EnforceCache.TrySetResultAsync(requestValues, key, result); + return result; - return InternalEnforce(requestValues); } +#else + public Task EnforceAsync(params object[] requestValues) + { + return Task.FromResult(InternalEnforce(requestValues)); + } +#endif /// /// Decides whether a "subject" can access a "object" with the operation @@ -430,41 +436,47 @@ public async Task EnforceAsync(params object[] requestValues) /// The request needs to be mediated, usually an array of strings, /// can be class instances if ABAC is used. /// Whether to allow the request. +#if !NET45 public bool Enforce(params object[] requestValues) { -#if !NET45 - if (requestValues.Any(requestValue => requestValue is not string)) + if (_enableCache is false) { return InternalEnforce(requestValues); } - if (_enableCache) + if (requestValues.Any(requestValue => requestValue is not string)) { - string key = string.Join("$$", requestValues); - EnforceCache ??= new ReaderWriterEnforceCache(Options.Create(new ReaderWriterEnforceCacheOptions())); - if (EnforceCache.TryGetResult(requestValues, key, out bool cachedResult)) - { - Logger?.LogEnforceCachedResult(requestValues, cachedResult); - return cachedResult; - } + return InternalEnforce(requestValues); + } - bool result = InternalEnforce(requestValues); - EnforceCache ??= new ReaderWriterEnforceCache(Options.Create(new ReaderWriterEnforceCacheOptions())); - EnforceCache.TrySetResult(requestValues, key, result); - return result; + string key = string.Join("$$", requestValues); + EnforceCache ??= new ReaderWriterEnforceCache(Options.Create(new ReaderWriterEnforceCacheOptions())); + if (EnforceCache.TryGetResult(requestValues, key, out bool cachedResult)) + { + Logger?.LogEnforceCachedResult(requestValues, cachedResult); + return cachedResult; } -#endif + bool result = InternalEnforce(requestValues); + EnforceCache ??= new ReaderWriterEnforceCache(Options.Create(new ReaderWriterEnforceCacheOptions())); + EnforceCache.TrySetResult(requestValues, key, result); + return result; + } +#else + public bool Enforce(params object[] requestValues) + { return InternalEnforce(requestValues); } +#endif + -#if !NET45 /// /// Explains enforcement by informing matched rules /// /// The request needs to be mediated, usually an array of strings, /// can be class instances if ABAC is used. /// Whether to allow the request and explains. +#if !NET45 public (bool Result, IEnumerable> Explains) EnforceEx(params object[] requestValues) { @@ -472,20 +484,6 @@ public bool Enforce(params object[] requestValues) bool result = InternalEnforce(requestValues, explains); return (result, explains); } - - /// - /// Explains enforcement by informing matched rules - /// - /// The request needs to be mediated, usually an array of strings, - /// can be class instances if ABAC is used. - /// Whether to allow the request and explains. - public async Task<(bool Result, IEnumerable> Explains)> - EnforceExAsync(params object[] requestValues) - { - var explains = new List>(); - bool result = await EnforceAsync(requestValues, explains); - return (result, explains); - } #else /// /// Explains enforcement by informing matched rules @@ -500,6 +498,7 @@ public bool Enforce(params object[] requestValues) bool result = InternalEnforce(requestValues, explains); return new Tuple>>(result, explains); } +#endif /// /// Explains enforcement by informing matched rules @@ -507,6 +506,15 @@ public bool Enforce(params object[] requestValues) /// The request needs to be mediated, usually an array of strings, /// can be class instances if ABAC is used. /// Whether to allow the request and explains. +#if !NET45 + public async Task<(bool Result, IEnumerable> Explains)> + EnforceExAsync(params object[] requestValues) + { + var explains = new List>(); + bool result = await EnforceAsync(requestValues, explains); + return (result, explains); + } +#else public async Task>>> EnforceExAsync(params object[] requestValues) { From edeeed6375ca99a4803b63cb95a3ea3be44e7b08 Mon Sep 17 00:00:00 2001 From: Sagilio Date: Thu, 25 Mar 2021 15:12:35 +0800 Subject: [PATCH 7/8] feat: Make caching feature support .NET45 Signed-off-by: Sagilio --- NetCasbin.UnitTest/EnforcerCacheTest.cs | 10 +++--- NetCasbin/Caching/ConcurrentEnforceCache.cs | 15 ++++---- NetCasbin/Caching/ReaderWriterEnforceCache.cs | 27 +++++++++++--- NetCasbin/CoreEnforcer.cs | 35 ++++++------------- NetCasbin/Extensions/LoggerExtension.cs | 4 +-- NetCasbin/InternalEnforcer.cs | 2 ++ 6 files changed, 49 insertions(+), 44 deletions(-) diff --git a/NetCasbin.UnitTest/EnforcerCacheTest.cs b/NetCasbin.UnitTest/EnforcerCacheTest.cs index 330abfec..2e76d2a0 100644 --- a/NetCasbin.UnitTest/EnforcerCacheTest.cs +++ b/NetCasbin.UnitTest/EnforcerCacheTest.cs @@ -1,5 +1,4 @@ -#if !NET452 -using NetCasbin.UnitTest.Fixtures; +using NetCasbin.UnitTest.Fixtures; using NetCasbin.UnitTest.Mock; using Xunit; using Xunit.Abstractions; @@ -22,10 +21,14 @@ public EnforcerCacheTest(ITestOutputHelper testOutputHelper, TestModelFixture te [Fact] public void TestEnforceWithCache() { +#if !NET452 var e = new Enforcer(_testModelFixture.GetBasicTestModel()) { Logger = new MockLogger(_testOutputHelper) }; +#else + var e = new Enforcer(_testModelFixture.GetBasicTestModel()); +#endif e.EnableCache(true); TestEnforce(e, "alice", "data1", "read", true); @@ -50,9 +53,6 @@ public void TestEnforceWithCache() TestEnforce(e, "alice", "data1", "write", false); TestEnforce(e, "alice", "data2", "read", false); TestEnforce(e, "alice", "data2", "write", false); - - e.Logger = null; } } } -#endif diff --git a/NetCasbin/Caching/ConcurrentEnforceCache.cs b/NetCasbin/Caching/ConcurrentEnforceCache.cs index 12dd84a0..cea4ef0f 100644 --- a/NetCasbin/Caching/ConcurrentEnforceCache.cs +++ b/NetCasbin/Caching/ConcurrentEnforceCache.cs @@ -1,10 +1,6 @@ -#if !NET45 -using System; -using System.Collections.Concurrent; +using System.Collections.Concurrent; using System.Collections.Generic; -using System.Threading; using System.Threading.Tasks; -using Microsoft.Extensions.Options; using NetCasbin.Abstractions; namespace NetCasbin.Caching @@ -40,11 +36,18 @@ public void Clear() _memoryCache.Clear(); } +#if !NET45 public Task ClearAsync() { Clear(); return Task.CompletedTask; } +#else + public Task ClearAsync() + { + Clear(); + return Task.FromResult(false); + } +#endif } } -#endif diff --git a/NetCasbin/Caching/ReaderWriterEnforceCache.cs b/NetCasbin/Caching/ReaderWriterEnforceCache.cs index 604fe41a..4a32de08 100644 --- a/NetCasbin/Caching/ReaderWriterEnforceCache.cs +++ b/NetCasbin/Caching/ReaderWriterEnforceCache.cs @@ -1,11 +1,11 @@ -#if !NET45 -using System; +using System; using System.Collections.Generic; -using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; -using Microsoft.Extensions.Options; using NetCasbin.Abstractions; +#if !NET45 +using Microsoft.Extensions.Options; +#endif namespace NetCasbin.Caching { @@ -14,6 +14,7 @@ public class ReaderWriterEnforceCache : IEnforceCache _memoryCache = new(); +#if !NET45 public ReaderWriterEnforceCache(IOptions options) { if (options?.Value is not null) @@ -21,6 +22,15 @@ public ReaderWriterEnforceCache(IOptions option CacheOptions = options.Value; } } +#endif + + public ReaderWriterEnforceCache(ReaderWriterEnforceCacheOptions options) + { + if (options is not null) + { + CacheOptions = options; + } + } public ReaderWriterEnforceCacheOptions CacheOptions { get; } = new(); @@ -86,11 +96,18 @@ public void Clear() _memoryCache = new Dictionary(); } +#if !NET45 public Task ClearAsync() { Clear(); return Task.CompletedTask; } +#else + public Task ClearAsync() + { + Clear(); + return Task.FromResult(false); + } +#endif } } -#endif diff --git a/NetCasbin/CoreEnforcer.cs b/NetCasbin/CoreEnforcer.cs index 451a72b4..3535d68b 100644 --- a/NetCasbin/CoreEnforcer.cs +++ b/NetCasbin/CoreEnforcer.cs @@ -1,5 +1,4 @@ using System; -using System.Collections; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; @@ -17,7 +16,6 @@ using NetCasbin.Util; using NetCasbin.Extensions; using NetCasbin.Caching; -using System.Text; namespace NetCasbin { @@ -41,9 +39,10 @@ public class CoreEnforcer : ICoreEnforcer internal IExpressionHandler ExpressionHandler { get; private set; } -#if !NET45 private bool _enableCache; public IEnforceCache EnforceCache { get; set; } + +#if !NET45 public ILogger Logger { get; set; } #endif @@ -369,12 +368,10 @@ public void EnableAutoNotifyWatcher(bool autoNotifyWatcher) this.autoNotifyWatcher = autoNotifyWatcher; } -#if !NET45 public void EnableCache(bool enableCache) { _enableCache = enableCache; } -#endif /// /// Manually rebuilds the role inheritance relations. @@ -392,7 +389,6 @@ public void BuildRoleLinks() /// The request needs to be mediated, usually an array of strings, /// can be class instances if ABAC is used. /// Whether to allow the request. -#if !NET45 public async Task EnforceAsync(params object[] requestValues) { if (_enableCache is false) @@ -406,28 +402,23 @@ public async Task EnforceAsync(params object[] requestValues) } string key = string.Join("$$", requestValues); - EnforceCache ??= new ReaderWriterEnforceCache(Options.Create(new ReaderWriterEnforceCacheOptions())); + EnforceCache ??= new ReaderWriterEnforceCache(new ReaderWriterEnforceCacheOptions()); bool? tryGetCachedResult = await EnforceCache.TryGetResultAsync(requestValues, key); if (tryGetCachedResult.HasValue) { bool cachedResult = tryGetCachedResult.Value; +#if !NET45 Logger?.LogEnforceCachedResult(requestValues, cachedResult); +#endif return cachedResult; } bool result = InternalEnforce(requestValues); - EnforceCache ??= new ReaderWriterEnforceCache(Options.Create(new ReaderWriterEnforceCacheOptions())); + EnforceCache ??= new ReaderWriterEnforceCache(new ReaderWriterEnforceCacheOptions()); await EnforceCache.TrySetResultAsync(requestValues, key, result); return result; - - } -#else - public Task EnforceAsync(params object[] requestValues) - { - return Task.FromResult(InternalEnforce(requestValues)); } -#endif /// /// Decides whether a "subject" can access a "object" with the operation @@ -436,7 +427,6 @@ public Task EnforceAsync(params object[] requestValues) /// The request needs to be mediated, usually an array of strings, /// can be class instances if ABAC is used. /// Whether to allow the request. -#if !NET45 public bool Enforce(params object[] requestValues) { if (_enableCache is false) @@ -450,25 +440,20 @@ public bool Enforce(params object[] requestValues) } string key = string.Join("$$", requestValues); - EnforceCache ??= new ReaderWriterEnforceCache(Options.Create(new ReaderWriterEnforceCacheOptions())); + EnforceCache ??= new ReaderWriterEnforceCache(new ReaderWriterEnforceCacheOptions()); if (EnforceCache.TryGetResult(requestValues, key, out bool cachedResult)) { +#if !NET45 Logger?.LogEnforceCachedResult(requestValues, cachedResult); +#endif return cachedResult; } bool result = InternalEnforce(requestValues); - EnforceCache ??= new ReaderWriterEnforceCache(Options.Create(new ReaderWriterEnforceCacheOptions())); + EnforceCache ??= new ReaderWriterEnforceCache(new ReaderWriterEnforceCacheOptions()); EnforceCache.TrySetResult(requestValues, key, result); return result; } -#else - public bool Enforce(params object[] requestValues) - { - return InternalEnforce(requestValues); - } -#endif - /// /// Explains enforcement by informing matched rules diff --git a/NetCasbin/Extensions/LoggerExtension.cs b/NetCasbin/Extensions/LoggerExtension.cs index d95fcc1d..72ba9273 100644 --- a/NetCasbin/Extensions/LoggerExtension.cs +++ b/NetCasbin/Extensions/LoggerExtension.cs @@ -1,7 +1,5 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; -using System.Text; #if !NET45 using Microsoft.Extensions.Logging; diff --git a/NetCasbin/InternalEnforcer.cs b/NetCasbin/InternalEnforcer.cs index added60e..e0462a05 100644 --- a/NetCasbin/InternalEnforcer.cs +++ b/NetCasbin/InternalEnforcer.cs @@ -2,7 +2,9 @@ using System.Collections.Generic; using System.Linq; using System.Security; +using System.Threading; using System.Threading.Tasks; +using System.Xml.Schema; using NetCasbin.Model; namespace NetCasbin From ed292e6c0eec3e71575057e3ae681f3369af280f Mon Sep 17 00:00:00 2001 From: Sagilio Date: Thu, 25 Mar 2021 18:00:56 +0800 Subject: [PATCH 8/8] feat: Add auto clear cache option Signed-off-by: Sagilio --- NetCasbin.UnitTest/EnforcerCacheTest.cs | 29 ++++++++ NetCasbin/CoreEnforcer.cs | 99 +++++++++++++++++++------ NetCasbin/InternalEnforcer.cs | 22 +++++- 3 files changed, 123 insertions(+), 27 deletions(-) diff --git a/NetCasbin.UnitTest/EnforcerCacheTest.cs b/NetCasbin.UnitTest/EnforcerCacheTest.cs index 2e76d2a0..d6512ff2 100644 --- a/NetCasbin.UnitTest/EnforcerCacheTest.cs +++ b/NetCasbin.UnitTest/EnforcerCacheTest.cs @@ -30,6 +30,7 @@ public void TestEnforceWithCache() var e = new Enforcer(_testModelFixture.GetBasicTestModel()); #endif e.EnableCache(true); + e.EnableAutoCleanEnforceCache(false); TestEnforce(e, "alice", "data1", "read", true); TestEnforce(e, "alice", "data1", "write", false); @@ -54,5 +55,33 @@ public void TestEnforceWithCache() TestEnforce(e, "alice", "data2", "read", false); TestEnforce(e, "alice", "data2", "write", false); } + + [Fact] + public void TestAutoCleanCache() + { +#if !NET452 + var e = new Enforcer(_testModelFixture.GetBasicTestModel()) + { + Logger = new MockLogger(_testOutputHelper) + }; +#else + var e = new Enforcer(_testModelFixture.GetBasicTestModel()); +#endif + e.EnableCache(true); + + TestEnforce(e, "alice", "data1", "read", true); + TestEnforce(e, "alice", "data1", "write", false); + TestEnforce(e, "alice", "data2", "read", false); + TestEnforce(e, "alice", "data2", "write", false); + + // The cache is enabled, so even if we remove a policy rule, the decision + // for ("alice", "data1", "read") will still be true, as it uses the cached result. + _ = e.RemovePolicy("alice", "data1", "read"); + + TestEnforce(e, "alice", "data1", "read", false); + TestEnforce(e, "alice", "data1", "write", false); + TestEnforce(e, "alice", "data2", "read", false); + TestEnforce(e, "alice", "data2", "write", false); + } } } diff --git a/NetCasbin/CoreEnforcer.cs b/NetCasbin/CoreEnforcer.cs index 3535d68b..5bbb9fd5 100644 --- a/NetCasbin/CoreEnforcer.cs +++ b/NetCasbin/CoreEnforcer.cs @@ -36,12 +36,12 @@ public class CoreEnforcer : ICoreEnforcer protected bool autoSave; protected bool autoBuildRoleLinks; protected bool autoNotifyWatcher; + protected bool autoCleanEnforceCache = true; internal IExpressionHandler ExpressionHandler { get; private set; } private bool _enableCache; - public IEnforceCache EnforceCache { get; set; } - + public IEnforceCache EnforceCache { get; private set; } #if !NET45 public ILogger Logger { get; set; } #endif @@ -192,7 +192,13 @@ public void SetEnforceCache(IEnforceCache enforceCache) public void ClearPolicy() { model.ClearPolicy(); - + if (autoCleanEnforceCache) + { + EnforceCache?.Clear(); +#if !NET45 + Logger?.LogInformation("Enforcer Cache, Cleared all enforce cache."); +#endif + } #if !NET45 Logger?.LogInformation("Policy Management, Cleared all policy."); #endif @@ -208,7 +214,7 @@ public void LoadPolicy() return; } - model.ClearPolicy(); + ClearPolicy(); adapter.LoadPolicy(model); model.RefreshPolicyStringSet(); if (autoBuildRoleLinks) @@ -227,7 +233,7 @@ public async Task LoadPolicyAsync() return; } - model.ClearPolicy(); + ClearPolicy(); await adapter.LoadPolicyAsync(model); if (autoBuildRoleLinks) { @@ -242,7 +248,7 @@ public async Task LoadPolicyAsync() /// public bool LoadFilteredPolicy(Filter filter) { - model.ClearPolicy(); + ClearPolicy(); if (adapter is not IFilteredAdapter filteredAdapter) { throw new NotSupportedException("Filtered policies are not supported by this adapter."); @@ -264,7 +270,7 @@ public bool LoadFilteredPolicy(Filter filter) /// public async Task LoadFilteredPolicyAsync(Filter filter) { - model.ClearPolicy(); + ClearPolicy(); if (adapter is not IFilteredAdapter filteredAdapter) { throw new NotSupportedException("Filtered policies are not supported by this adapter."); @@ -373,6 +379,11 @@ public void EnableCache(bool enableCache) _enableCache = enableCache; } + public void EnableAutoCleanEnforceCache(bool autoCleanEnforceCache) + { + this.autoCleanEnforceCache = autoCleanEnforceCache; + } + /// /// Manually rebuilds the role inheritance relations. /// @@ -389,7 +400,7 @@ public void BuildRoleLinks() /// The request needs to be mediated, usually an array of strings, /// can be class instances if ABAC is used. /// Whether to allow the request. - public async Task EnforceAsync(params object[] requestValues) + public bool Enforce(params object[] requestValues) { if (_enableCache is false) { @@ -403,20 +414,17 @@ public async Task EnforceAsync(params object[] requestValues) string key = string.Join("$$", requestValues); EnforceCache ??= new ReaderWriterEnforceCache(new ReaderWriterEnforceCacheOptions()); - bool? tryGetCachedResult = await EnforceCache.TryGetResultAsync(requestValues, key); - if (tryGetCachedResult.HasValue) + if (EnforceCache.TryGetResult(requestValues, key, out bool cachedResult)) { - bool cachedResult = tryGetCachedResult.Value; #if !NET45 Logger?.LogEnforceCachedResult(requestValues, cachedResult); #endif return cachedResult; } - bool result = InternalEnforce(requestValues); - + bool result = InternalEnforce(requestValues); EnforceCache ??= new ReaderWriterEnforceCache(new ReaderWriterEnforceCacheOptions()); - await EnforceCache.TrySetResultAsync(requestValues, key, result); + EnforceCache.TrySetResult(requestValues, key, result); return result; } @@ -427,31 +435,34 @@ public async Task EnforceAsync(params object[] requestValues) /// The request needs to be mediated, usually an array of strings, /// can be class instances if ABAC is used. /// Whether to allow the request. - public bool Enforce(params object[] requestValues) + public async Task EnforceAsync(params object[] requestValues) { if (_enableCache is false) { - return InternalEnforce(requestValues); + return await InternalEnforceAsync(requestValues); } if (requestValues.Any(requestValue => requestValue is not string)) { - return InternalEnforce(requestValues); + return await InternalEnforceAsync(requestValues); } string key = string.Join("$$", requestValues); EnforceCache ??= new ReaderWriterEnforceCache(new ReaderWriterEnforceCacheOptions()); - if (EnforceCache.TryGetResult(requestValues, key, out bool cachedResult)) + bool? tryGetCachedResult = await EnforceCache.TryGetResultAsync(requestValues, key); + if (tryGetCachedResult.HasValue) { + bool cachedResult = tryGetCachedResult.Value; #if !NET45 Logger?.LogEnforceCachedResult(requestValues, cachedResult); #endif return cachedResult; } - bool result = InternalEnforce(requestValues); + bool result = await InternalEnforceAsync(requestValues); + EnforceCache ??= new ReaderWriterEnforceCache(new ReaderWriterEnforceCacheOptions()); - EnforceCache.TrySetResult(requestValues, key, result); + await EnforceCache.TrySetResultAsync(requestValues, key, result); return result; } @@ -466,7 +477,27 @@ public bool Enforce(params object[] requestValues) EnforceEx(params object[] requestValues) { var explains = new List>(); - bool result = InternalEnforce(requestValues, explains); + if (_enableCache is false) + { + return (InternalEnforce(requestValues, explains), explains); + } + + if (requestValues.Any(requestValue => requestValue is not string)) + { + return (InternalEnforce(requestValues, explains), explains); + } + + string key = string.Join("$$", requestValues); + EnforceCache ??= new ReaderWriterEnforceCache(new ReaderWriterEnforceCacheOptions()); + if (EnforceCache.TryGetResult(requestValues, key, out bool cachedResult)) + { + Logger?.LogEnforceCachedResult(requestValues, cachedResult); + return (cachedResult, explains); + } + + bool result = InternalEnforce(requestValues, explains); + EnforceCache ??= new ReaderWriterEnforceCache(new ReaderWriterEnforceCacheOptions()); + EnforceCache.TrySetResult(requestValues, key, result); return (result, explains); } #else @@ -496,7 +527,27 @@ public bool Enforce(params object[] requestValues) EnforceExAsync(params object[] requestValues) { var explains = new List>(); - bool result = await EnforceAsync(requestValues, explains); + if (_enableCache is false) + { + return (await InternalEnforceAsync(requestValues, explains), explains); + } + + if (requestValues.Any(requestValue => requestValue is not string)) + { + return (await InternalEnforceAsync(requestValues, explains), explains); + } + + string key = string.Join("$$", requestValues); + EnforceCache ??= new ReaderWriterEnforceCache(new ReaderWriterEnforceCacheOptions()); + if (EnforceCache.TryGetResult(requestValues, key, out bool cachedResult)) + { + Logger?.LogEnforceCachedResult(requestValues, cachedResult); + return (cachedResult, explains); + } + + bool result = await InternalEnforceAsync(requestValues, explains); + EnforceCache ??= new ReaderWriterEnforceCache(new ReaderWriterEnforceCacheOptions()); + await EnforceCache.TrySetResultAsync(requestValues, key, result); return (result, explains); } #else @@ -504,7 +555,7 @@ public bool Enforce(params object[] requestValues) EnforceExAsync(params object[] requestValues) { var explains = new List>(); - bool result = await EnforceAsync(requestValues, explains); + bool result = await InternalEnforceAsync(requestValues, explains); return new Tuple>>(result, explains); } #endif @@ -517,7 +568,7 @@ public bool Enforce(params object[] requestValues) /// can be class instances if ABAC is used. /// /// Whether to allow the request. - private Task EnforceAsync(IReadOnlyList requestValues, ICollection> explains = null) + private Task InternalEnforceAsync(IReadOnlyList requestValues, ICollection> explains = null) { return Task.FromResult(InternalEnforce(requestValues, explains)); } diff --git a/NetCasbin/InternalEnforcer.cs b/NetCasbin/InternalEnforcer.cs index e0462a05..a97a2687 100644 --- a/NetCasbin/InternalEnforcer.cs +++ b/NetCasbin/InternalEnforcer.cs @@ -1,10 +1,10 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Security; -using System.Threading; using System.Threading.Tasks; -using System.Xml.Schema; +#if !NET45 +using Microsoft.Extensions.Logging; +#endif using NetCasbin.Model; namespace NetCasbin @@ -455,6 +455,14 @@ protected async Task InternalRemoveFilteredPolicyAsync(string sec, string private void NotifyPolicyChanged() { + if (autoCleanEnforceCache) + { + EnforceCache?.Clear(); +#if !NET45 + Logger?.LogInformation("Enforcer Cache, Cleared all enforce cache."); +#endif + } + if (autoNotifyWatcher) { watcher?.Update(); @@ -463,6 +471,14 @@ private void NotifyPolicyChanged() private async Task NotifyPolicyChangedAsync() { + if (autoCleanEnforceCache && EnforceCache is not null) + { + await EnforceCache.ClearAsync(); +#if !NET45 + Logger?.LogInformation("Enforcer Cache, Cleared all enforce cache."); +#endif + } + if (autoNotifyWatcher && watcher is not null) { await watcher.UpdateAsync();