diff --git a/.github/workflows/build-and-release.yml b/.github/workflows/build-and-release.yml index b5ea1d92..6cf0cbe7 100644 --- a/.github/workflows/build-and-release.yml +++ b/.github/workflows/build-and-release.yml @@ -37,7 +37,7 @@ jobs: run: dotnet --info - name: Install dependencies - run: nuget restore + run: dotnet restore - name: Build solution run: dotnet build -c Release --no-restore @@ -96,7 +96,7 @@ jobs: run: dotnet --info - name: Install dependencies - run: nuget restore + run: dotnet restore - name: Build solution run: dotnet build -c Release --no-restore @@ -161,7 +161,7 @@ jobs: run: dotnet --info - name: Install dependencies - run: nuget restore + run: dotnet restore - name: Build solution run: dotnet build -c Release --no-restore @@ -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 @@ -221,7 +221,7 @@ jobs: run: dotnet --info - name: Install dependencies - run: nuget restore + run: dotnet restore - name: Build solution run: dotnet build -c Release --no-restore @@ -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 diff --git a/.releaserc.json b/.releaserc.json index 64a3ac5e..b38acbbe 100644 --- a/.releaserc.json +++ b/.releaserc.json @@ -4,8 +4,12 @@ "+([0-9])?(.{+([0-9]),x}).x", "master", { - "name": "develop", - "prerelease": true + "name": "alpha", + "prerelease": true + }, + { + "name": "preview", + "prerelease": true } ], "plugins": [ diff --git a/Casbin.Benchmark/EnforcerBenchmark.cs b/Casbin.Benchmark/EnforcerBenchmark.cs index 86c659b7..a7e8cfdb 100644 --- a/Casbin.Benchmark/EnforcerBenchmark.cs +++ b/Casbin.Benchmark/EnforcerBenchmark.cs @@ -145,12 +145,14 @@ private void GlobalSetupFromFile(string modelFileName, string policyFileName = n { NowEnforcer = new Enforcer( GetTestFilePath(modelFileName)); + NowEnforcer.EnableCache(false); return; } NowEnforcer = new Enforcer( GetTestFilePath(modelFileName), GetTestFilePath(policyFileName)); + NowEnforcer.EnableCache(false); } #endregion diff --git a/Casbin.Benchmark/EnforcerWithCacheBenchmark.cs b/Casbin.Benchmark/EnforcerWithCacheBenchmark.cs new file mode 100644 index 00000000..ad72976b --- /dev/null +++ b/Casbin.Benchmark/EnforcerWithCacheBenchmark.cs @@ -0,0 +1,252 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Engines; +using BenchmarkDotNet.Jobs; +using Casbin.Extensions; +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/Casbin.NET.sln b/Casbin.NET.sln index 62882c4d..42a8c03e 100644 --- a/Casbin.NET.sln +++ b/Casbin.NET.sln @@ -12,6 +12,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution .editorconfig = .editorconfig .gitattributes = .gitattributes .gitignore = .gitignore + .releaserc.json = .releaserc.json azure-pipelines.yml = azure-pipelines.yml .github\workflows\build-and-release.yml = .github\workflows\build-and-release.yml LICENSE = LICENSE diff --git a/NetCasbin.UnitTest/Casbin.UnitTests.csproj b/NetCasbin.UnitTest/Casbin.UnitTests.csproj index 45490cc2..e112c27d 100644 --- a/NetCasbin.UnitTest/Casbin.UnitTests.csproj +++ b/NetCasbin.UnitTest/Casbin.UnitTests.csproj @@ -1,25 +1,29 @@ - + - net5 + net5;net461;net452 full false + 9.0 - - + runtime; build; native; contentfiles; analyzers; buildtransitive all - + runtime; build; native; contentfiles; analyzers; buildtransitive all + + + + diff --git a/NetCasbin.UnitTest/EnforcerCacheTest.cs b/NetCasbin.UnitTest/EnforcerCacheTest.cs new file mode 100644 index 00000000..8eb19002 --- /dev/null +++ b/NetCasbin.UnitTest/EnforcerCacheTest.cs @@ -0,0 +1,88 @@ +using Casbin.Extensions; +using Casbin.UnitTests.Fixtures; +using Casbin.UnitTests.Mock; +using Xunit; +using Xunit.Abstractions; +using static Casbin.UnitTests.Util.TestUtil; + +namespace Casbin.UnitTests +{ + [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() + { +#if !NET452 + var e = new Enforcer(_testModelFixture.GetBasicTestModel()) + { + Logger = new MockLogger(_testOutputHelper) + }; +#else + 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); + 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); + } + + [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.UnitTest/EnforcerTest.cs b/NetCasbin.UnitTest/EnforcerTest.cs index a8d8d511..d9e9fa79 100644 --- a/NetCasbin.UnitTest/EnforcerTest.cs +++ b/NetCasbin.UnitTest/EnforcerTest.cs @@ -5,8 +5,8 @@ using Casbin.Extensions; using Casbin.Model; using Casbin.Persist; -using Casbin.UnitTest.Mock; using Casbin.UnitTests.Fixtures; +using Casbin.UnitTests.Mock; using Xunit; using Xunit.Abstractions; using static Casbin.UnitTests.Util.TestUtil; @@ -465,6 +465,7 @@ public void TestEnableEnforce() TestEnforce(e, "bob", "data2", "write", true); } +#if !NET452 [Fact] public void TestEnableLog() { @@ -492,6 +493,7 @@ public void TestEnableLog() TestEnforce(e, "bob", "data2", "read", false); TestEnforce(e, "bob", "data2", "write", true); } +#endif [Fact] public void TestEnableAutoSave() @@ -833,6 +835,7 @@ await TestEnforceExAsync(e, "bob", "data2", "read", await TestEnforceExAsync(e, "bob", "data2", "write", new List {"bob", "data2", "write", "deny"}); } +#if !NET452 [Fact] public void TestEnforceExApiLog() { @@ -852,5 +855,6 @@ public void TestEnforceExApiLog() e.Logger = null; } +#endif } } diff --git a/NetCasbin.UnitTest/ManagementApiTest.cs b/NetCasbin.UnitTest/ManagementApiTest.cs index 099d39cf..ab229235 100644 --- a/NetCasbin.UnitTest/ManagementApiTest.cs +++ b/NetCasbin.UnitTest/ManagementApiTest.cs @@ -119,7 +119,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 3c461816..a3ec9af4 100644 --- a/NetCasbin.UnitTest/Mock/MockLogger.cs +++ b/NetCasbin.UnitTest/Mock/MockLogger.cs @@ -1,8 +1,9 @@ -using System; +#if !NET452 +using System; using Microsoft.Extensions.Logging; using Xunit.Abstractions; -namespace Casbin.UnitTest.Mock +namespace Casbin.UnitTests.Mock { public class MockLogger : ILogger { @@ -30,3 +31,4 @@ public IDisposable BeginScope(TState state) } } } +#endif diff --git a/NetCasbin.UnitTest/RbacApiTest.cs b/NetCasbin.UnitTest/RbacApiTest.cs index 48b07dda..82cab9b8 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/RbacApiWithDomainsTest.cs b/NetCasbin.UnitTest/RbacApiWithDomainsTest.cs index 91edb384..5bb5820f 100644 --- a/NetCasbin.UnitTest/RbacApiWithDomainsTest.cs +++ b/NetCasbin.UnitTest/RbacApiWithDomainsTest.cs @@ -16,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 01bf3550..2fa1ad38 100644 --- a/NetCasbin.UnitTest/Util/TestUtil.cs +++ b/NetCasbin.UnitTest/Util/TestUtil.cs @@ -26,6 +26,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(); @@ -35,10 +36,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) { @@ -150,6 +162,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) { var myRes = e.GetPermissionsForUserInDomain(name, domain); diff --git a/NetCasbin/Abstractions/IEnforceCache.cs b/NetCasbin/Abstractions/IEnforceCache.cs new file mode 100644 index 00000000..35b5622c --- /dev/null +++ b/NetCasbin/Abstractions/IEnforceCache.cs @@ -0,0 +1,25 @@ +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Casbin +{ + 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/Abstractions/IEnforcer.cs b/NetCasbin/Abstractions/IEnforcer.cs index 872e71f6..ac1d0a61 100644 --- a/NetCasbin/Abstractions/IEnforcer.cs +++ b/NetCasbin/Abstractions/IEnforcer.cs @@ -1,7 +1,9 @@ using System.Threading.Tasks; using Casbin.Persist; using Casbin.Rbac; - +#if !NET45 +using Microsoft.Extensions.Logging; +#endif namespace Casbin { /// @@ -11,9 +13,11 @@ public interface IEnforcer { #region Options public bool Enabled { get; } + public bool EnabledCache { get; } public bool AutoSave { get; } public bool AutoBuildRoleLinks { get; } public bool AutoNotifyWatcher { get; } + public bool AutoCleanEnforceCache { get; } #endregion #region Extensions @@ -22,6 +26,10 @@ public interface IEnforcer public IAdapter Adapter { get; } public IWatcher Watcher { get; } public IRoleManager RoleManager { get; } + public IEnforceCache EnforceCache { get; } +#if !NET45 + public ILogger Logger { get; set; } +#endif #endregion public string ModelPath { get; } @@ -50,6 +58,8 @@ public interface IEnforcer /// Whether to automatically build the role links. public void EnableAutoBuildRoleLinks(bool autoBuildRoleLinks); + + /// /// Sets the current model. /// @@ -87,6 +97,12 @@ public interface IEnforcer /// public void SetEffector(IEffector effector); + /// + /// Sets an enforce cache. + /// + /// + public void SetEnforceCache(IEnforceCache enforceCache); + /// /// LoadModel reloads the model from the model CONF file. Because the policy is /// Attached to a model, so the policy is invalidated and needs to be reloaded by diff --git a/NetCasbin/Caching/ConcurrentEnforceCache.cs b/NetCasbin/Caching/ConcurrentEnforceCache.cs new file mode 100644 index 00000000..633875a9 --- /dev/null +++ b/NetCasbin/Caching/ConcurrentEnforceCache.cs @@ -0,0 +1,52 @@ +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Casbin.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) : Task.FromResult((bool?) 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(); + } + +#if !NET45 + public Task ClearAsync() + { + Clear(); + return Task.CompletedTask; + } +#else + public Task ClearAsync() + { + Clear(); + return Task.FromResult(false); + } +#endif + } +} diff --git a/NetCasbin/Caching/ReaderWriterEnforceCache.cs b/NetCasbin/Caching/ReaderWriterEnforceCache.cs new file mode 100644 index 00000000..e3dacc06 --- /dev/null +++ b/NetCasbin/Caching/ReaderWriterEnforceCache.cs @@ -0,0 +1,112 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +#if !NET45 +using Microsoft.Extensions.Options; +#endif + +namespace Casbin.Caching +{ + public class ReaderWriterEnforceCache : IEnforceCache + { + private readonly ReaderWriterLockSlim _lockSlim = new(); + private Dictionary _memoryCache = new(); + +#if !NET45 + public ReaderWriterEnforceCache(IOptions options) + { + if (options?.Value is not null) + { + CacheOptions = options.Value; + } + } +#endif + + public ReaderWriterEnforceCache(ReaderWriterEnforceCacheOptions options) + { + if (options is not null) + { + CacheOptions = options; + } + } + + 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) : Task.FromResult((bool?) 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(); + } + +#if !NET45 + public Task ClearAsync() + { + Clear(); + return Task.CompletedTask; + } +#else + public Task ClearAsync() + { + Clear(); + return Task.FromResult(false); + } +#endif + } +} diff --git a/NetCasbin/Caching/ReaderWriterEnforceCacheOptions.cs b/NetCasbin/Caching/ReaderWriterEnforceCacheOptions.cs new file mode 100644 index 00000000..eeeaf52d --- /dev/null +++ b/NetCasbin/Caching/ReaderWriterEnforceCacheOptions.cs @@ -0,0 +1,9 @@ +using System; + +namespace Casbin.Caching +{ + public class ReaderWriterEnforceCacheOptions + { + public TimeSpan WaitTimeOut { get; set; } = TimeSpan.FromMilliseconds(50); + } +} diff --git a/NetCasbin/Casbin.csproj b/NetCasbin/Casbin.csproj index 88081378..3f4c3f68 100644 --- a/NetCasbin/Casbin.csproj +++ b/NetCasbin/Casbin.csproj @@ -25,7 +25,7 @@ - + diff --git a/NetCasbin/Effect/DefaultEffector.cs b/NetCasbin/Effect/DefaultEffector.cs index 4011034d..6b1f72ac 100644 --- a/NetCasbin/Effect/DefaultEffector.cs +++ b/NetCasbin/Effect/DefaultEffector.cs @@ -32,7 +32,7 @@ public bool MergeEffects(string effectExpression, PolicyEffect[] effects, float[ /// private bool MergeEffects(string effectExpression, Span effects, Span results, out int hitPolicyIndex) { - PolicyEffectType = ParsePolicyEffectType(effectExpression); + PolicyEffectType = ParseEffectExpressionType(effectExpression); return MergeEffects(PolicyEffectType, effects, results, out hitPolicyIndex); } @@ -64,7 +64,7 @@ private bool MergeEffects(EffectExpressionType effectExpressionType, Span effectExpression switch + public static EffectExpressionType ParseEffectExpressionType(string effectExpression) => effectExpression switch { PermConstants.PolicyEffect.AllowOverride => EffectExpressionType.AllowOverride, PermConstants.PolicyEffect.DenyOverride => EffectExpressionType.DenyOverride, @@ -88,7 +88,7 @@ private bool MergeEffects(EffectExpressionType effectExpressionType, Span @@ -181,6 +202,14 @@ public void SetRoleManager(IRoleManager roleManager) RoleManager = roleManager; } + /// + /// Sets an enforce cache. + /// + /// + public void SetEnforceCache(IEnforceCache enforceCache) + { + EnforceCache = enforceCache; + } #endregion /// @@ -218,7 +247,7 @@ public void LoadPolicy() return; } - Model.ClearPolicy(); + ClearPolicy(); Adapter.LoadPolicy(Model); Model.RefreshPolicyStringSet(); if (AutoBuildRoleLinks) @@ -237,7 +266,7 @@ public async Task LoadPolicyAsync() return; } - Model.ClearPolicy(); + ClearPolicy(); await Adapter.LoadPolicyAsync(Model); if (AutoBuildRoleLinks) { @@ -252,18 +281,19 @@ public async Task LoadPolicyAsync() /// public bool LoadFilteredPolicy(Filter filter) { - Model.ClearPolicy(); if (Adapter is not IFilteredAdapter filteredAdapter) { throw new NotSupportedException("Filtered policies are not supported by this adapter."); } + ClearPolicy(); filteredAdapter.LoadFilteredPolicy(Model, filter); if (AutoBuildRoleLinks) { BuildRoleLinks(); } + return true; } @@ -274,12 +304,12 @@ public bool LoadFilteredPolicy(Filter filter) /// public async Task LoadFilteredPolicyAsync(Filter filter) { - Model.ClearPolicy(); if (Adapter is not IFilteredAdapter filteredAdapter) { throw new NotSupportedException("Filtered policies are not supported by this adapter."); } + ClearPolicy(); await filteredAdapter.LoadFilteredPolicyAsync(Model, filter); if (AutoBuildRoleLinks) @@ -336,14 +366,19 @@ public async Task SavePolicyAsync() 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 } #endregion - /// /// Decides whether a "subject" can access a "object" with the operation /// "action", input parameters are usually: (sub, obj, act). @@ -351,9 +386,37 @@ public void ClearPolicy() /// 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 bool Enforce(params object[] requestValues) { - return Task.FromResult(Enforce(requestValues)); + if (Enabled is false) + { + return true; + } + + if (EnabledCache is false) + { + return InternalEnforce(requestValues); + } + + if (requestValues.Any(requestValue => requestValue is not string)) + { + return InternalEnforce(requestValues); + } + + string key = string.Join("$$", requestValues); + 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(new ReaderWriterEnforceCacheOptions()); + EnforceCache.TrySetResult(requestValues, key, result); + return result; } /// @@ -363,37 +426,79 @@ 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. - public bool Enforce(params object[] requestValues) + public async Task EnforceAsync(params object[] requestValues) { - return Enforce((IReadOnlyList) requestValues); - } + if (Enabled is false) + { + return true; + } + + if (EnabledCache is false) + { + return await InternalEnforceAsync(requestValues); + } + + if (requestValues.Any(requestValue => requestValue is not string)) + { + return await InternalEnforceAsync(requestValues); + } + string key = string.Join("$$", requestValues); + 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 = await InternalEnforceAsync(requestValues); + + EnforceCache ??= new ReaderWriterEnforceCache(new ReaderWriterEnforceCacheOptions()); + await EnforceCache.TrySetResultAsync(requestValues, key, result); + return result; + } + /// /// 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) { var explains = new List>(); - bool result = Enforce(requestValues, explains); - return (result, explains); - } + if (Enabled is false) + { + return (true, 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); + if (EnabledCache 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 @@ -407,9 +512,10 @@ public Tuple>> EnforceEx(params object[] requestValues) { var explains = new List>(); - bool result = Enforce(requestValues, explains); + bool result = InternalEnforce(requestValues, explains); return new Tuple>>(result, explains); } +#endif /// /// Explains enforcement by informing matched rules @@ -417,11 +523,45 @@ public Tuple>> /// 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>(); + if (Enabled is false) + { + return (true, explains); + } + + if (EnabledCache 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 public async Task>>> 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 @@ -434,9 +574,9 @@ public async Task>>> /// 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(Enforce(requestValues, explains)); + return Task.FromResult(InternalEnforce(requestValues, explains)); } /// @@ -447,13 +587,8 @@ 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) - { - return true; - } - bool explain = explains is not null; string effect = Model.Sections[PermConstants.Section.PolicyEffectSection][PermConstants.DefaultPolicyEffectType].Value; var policyList = Model.Sections[PermConstants.Section.PolicySection][PermConstants.DefaultPolicyType].Policy; diff --git a/NetCasbin/Extensions/InternalEnforcerExtension.cs b/NetCasbin/Extensions/InternalEnforcerExtension.cs index 7089fd2d..b29e3750 100644 --- a/NetCasbin/Extensions/InternalEnforcerExtension.cs +++ b/NetCasbin/Extensions/InternalEnforcerExtension.cs @@ -2,6 +2,9 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +#if !NET45 +using Microsoft.Extensions.Logging; +#endif namespace Casbin.Extensions { @@ -458,6 +461,14 @@ internal static async Task InternalRemoveFilteredPolicyAsync(this IEnforce private static void NotifyPolicyChanged(IEnforcer enforcer) { + if (enforcer.AutoCleanEnforceCache) + { + enforcer.EnforceCache?.Clear(); +#if !NET45 + enforcer.Logger?.LogInformation("Enforcer Cache, Cleared all enforce cache."); +#endif + } + if (enforcer.AutoNotifyWatcher) { enforcer.Watcher?.Update(); @@ -466,6 +477,14 @@ private static void NotifyPolicyChanged(IEnforcer enforcer) private static async Task NotifyPolicyChangedAsync(IEnforcer enforcer) { + if (enforcer.AutoCleanEnforceCache && enforcer.EnforceCache is not null) + { + await enforcer.EnforceCache.ClearAsync(); +#if !NET45 + enforcer.Logger?.LogInformation("Enforcer Cache, Cleared all enforce cache."); +#endif + } + if (enforcer.AutoNotifyWatcher && enforcer.Watcher is not null) { await enforcer.Watcher.UpdateAsync(); diff --git a/NetCasbin/Extensions/LoggerExtension.cs b/NetCasbin/Extensions/LoggerExtension.cs index 1ca62cec..dbc4e9dc 100644 --- a/NetCasbin/Extensions/LoggerExtension.cs +++ b/NetCasbin/Extensions/LoggerExtension.cs @@ -7,6 +7,12 @@ namespace Casbin.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, diff --git a/NetCasbin/Rbac/Role.cs b/NetCasbin/Rbac/Role.cs index b4a8020d..b93f1506 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()