From 2aa9d2b6aa5860fb7d4cc52d1c58c184c30f4dd5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 31 Oct 2025 16:50:25 +0000 Subject: [PATCH 1/4] Initial plan From 95143b6f74013ce6780383c6c6a4f7313336ec16 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 31 Oct 2025 17:04:31 +0000 Subject: [PATCH 2/4] Add LoadIncrementalFilteredPolicy functionality Co-authored-by: hsluoyz <3787410+hsluoyz@users.noreply.github.com> --- .../IncrementalFilteredAdapterTest.cs | 171 ++++++++++++++++++ Casbin/Abstractions/Persist/BaseAdapter.cs | 30 +++ .../Abstractions/Persist/IFilteredAdapter.cs | 4 + .../Extensions/Enforcer/EnforcerExtension.cs | 48 +++++ Casbin/Extensions/Model/ModelExtension.cs | 34 ++++ 5 files changed, 287 insertions(+) create mode 100644 Casbin.UnitTests/PersistTests/IncrementalFilteredAdapterTest.cs diff --git a/Casbin.UnitTests/PersistTests/IncrementalFilteredAdapterTest.cs b/Casbin.UnitTests/PersistTests/IncrementalFilteredAdapterTest.cs new file mode 100644 index 0000000..eeb72f0 --- /dev/null +++ b/Casbin.UnitTests/PersistTests/IncrementalFilteredAdapterTest.cs @@ -0,0 +1,171 @@ +using System.IO; +using System.Threading.Tasks; +using Casbin.Model; +using Casbin.Persist; +using Casbin.Persist.Adapter.File; +using Xunit; + +namespace Casbin.UnitTests.PersistTests; + +public class IncrementalFilteredAdapterTest +{ + [Fact] + public void TestLoadIncrementalFilteredPolicy() + { + Enforcer e = new("Examples/rbac_model.conf"); + Assert.False(e.Enforce("alice", "data1", "read")); + + FileAdapter a = new("Examples/rbac_policy.csv"); + e.SetAdapter(a); + + // Load only p policies for alice + e.LoadFilteredPolicy(new PolicyFilter(PermConstants.DefaultPolicyType, 0, Policy.ValuesFrom(["alice"]))); + + // alice can read data1 (from p policy) + Assert.True(e.Enforce("alice", "data1", "read")); + // bob cannot write data2 (not loaded yet) + Assert.False(e.Enforce("bob", "data2", "write")); + + // Incrementally load p policies for data2_admin role + e.LoadIncrementalFilteredPolicy(new PolicyFilter(PermConstants.DefaultPolicyType, 0, Policy.ValuesFrom(["data2_admin"]))); + + // alice still can only read data1 (no role link yet) + Assert.True(e.Enforce("alice", "data1", "read")); + Assert.False(e.Enforce("alice", "data2", "read")); + + // Incrementally load g policies for alice + e.LoadIncrementalFilteredPolicy(new PolicyFilter(PermConstants.DefaultRoleType, 0, Policy.ValuesFrom(["alice"]))); + + // Now alice can read data2 through role inheritance + Assert.True(e.Enforce("alice", "data2", "read")); + Assert.True(e.Enforce("alice", "data2", "write")); + // bob still cannot write data2 (not loaded) + Assert.False(e.Enforce("bob", "data2", "write")); + } + + [Fact] + public async Task TestLoadIncrementalFilteredPolicyAsync() + { + Enforcer e = new("Examples/rbac_model.conf"); + Assert.False(await e.EnforceAsync("alice", "data1", "read")); + + FileAdapter a = new("Examples/rbac_policy.csv"); + e.SetAdapter(a); + + // Load only p policies for alice + await e.LoadFilteredPolicyAsync(new PolicyFilter(PermConstants.DefaultPolicyType, 0, Policy.ValuesFrom(["alice"]))); + + // alice can read data1 (from p policy) + Assert.True(await e.EnforceAsync("alice", "data1", "read")); + // bob cannot write data2 (not loaded yet) + Assert.False(await e.EnforceAsync("bob", "data2", "write")); + + // Incrementally load p policies for data2_admin role + await e.LoadIncrementalFilteredPolicyAsync(new PolicyFilter(PermConstants.DefaultPolicyType, 0, Policy.ValuesFrom(["data2_admin"]))); + + // alice still can only read data1 (no role link yet) + Assert.True(await e.EnforceAsync("alice", "data1", "read")); + Assert.False(await e.EnforceAsync("alice", "data2", "read")); + + // Incrementally load g policies for alice + await e.LoadIncrementalFilteredPolicyAsync(new PolicyFilter(PermConstants.DefaultRoleType, 0, Policy.ValuesFrom(["alice"]))); + + // Now alice can read data2 through role inheritance + Assert.True(await e.EnforceAsync("alice", "data2", "read")); + Assert.True(await e.EnforceAsync("alice", "data2", "write")); + // bob still cannot write data2 (not loaded) + Assert.False(await e.EnforceAsync("bob", "data2", "write")); + } + + [Fact] + public void TestLoadIncrementalFilteredPolicyMultiplePTypes() + { + Enforcer e = new("Examples/rbac_model.conf"); + Assert.False(e.Enforce("alice", "data1", "read")); + + FileAdapter a = new("Examples/rbac_policy.csv"); + e.SetAdapter(a); + + // Load only p policies for alice + e.LoadFilteredPolicy(new PolicyFilter(PermConstants.DefaultPolicyType, 0, Policy.ValuesFrom(["alice"]))); + + // alice can read data1 + Assert.True(e.Enforce("alice", "data1", "read")); + // bob cannot write data2 + Assert.False(e.Enforce("bob", "data2", "write")); + + // Incrementally load p policies for bob + e.LoadIncrementalFilteredPolicy(new PolicyFilter(PermConstants.DefaultPolicyType, 0, Policy.ValuesFrom(["bob"]))); + + // alice still can read data1 + Assert.True(e.Enforce("alice", "data1", "read")); + // Now bob can write data2 + Assert.True(e.Enforce("bob", "data2", "write")); + } + + [Fact] + public async Task TestLoadIncrementalFilteredPolicyMultiplePTypesAsync() + { + Enforcer e = new("Examples/rbac_model.conf"); + Assert.False(await e.EnforceAsync("alice", "data1", "read")); + + FileAdapter a = new("Examples/rbac_policy.csv"); + e.SetAdapter(a); + + // Load only p policies for alice + await e.LoadFilteredPolicyAsync(new PolicyFilter(PermConstants.DefaultPolicyType, 0, Policy.ValuesFrom(["alice"]))); + + // alice can read data1 + Assert.True(await e.EnforceAsync("alice", "data1", "read")); + // bob cannot write data2 + Assert.False(await e.EnforceAsync("bob", "data2", "write")); + + // Incrementally load p policies for bob + await e.LoadIncrementalFilteredPolicyAsync(new PolicyFilter(PermConstants.DefaultPolicyType, 0, Policy.ValuesFrom(["bob"]))); + + // alice still can read data1 + Assert.True(await e.EnforceAsync("alice", "data1", "read")); + // Now bob can write data2 + Assert.True(await e.EnforceAsync("bob", "data2", "write")); + } + + [Fact] + public void TestLoadFilteredPolicyClearsExistingPolicies() + { + Enforcer e = new("Examples/rbac_model.conf"); + FileAdapter a = new("Examples/rbac_policy.csv"); + e.SetAdapter(a); + + // Load only p policies for alice + e.LoadFilteredPolicy(new PolicyFilter(PermConstants.DefaultPolicyType, 0, Policy.ValuesFrom(["alice"]))); + Assert.True(e.Enforce("alice", "data1", "read")); + + // Load filtered policy again (should clear previous policies) + e.LoadFilteredPolicy(new PolicyFilter(PermConstants.DefaultPolicyType, 0, Policy.ValuesFrom(["bob"]))); + + // alice can no longer read data1 (previous policies cleared) + Assert.False(e.Enforce("alice", "data1", "read")); + // bob can write data2 (new filtered policies loaded) + Assert.True(e.Enforce("bob", "data2", "write")); + } + + [Fact] + public async Task TestLoadFilteredPolicyClearsExistingPoliciesAsync() + { + Enforcer e = new("Examples/rbac_model.conf"); + FileAdapter a = new("Examples/rbac_policy.csv"); + e.SetAdapter(a); + + // Load only p policies for alice + await e.LoadFilteredPolicyAsync(new PolicyFilter(PermConstants.DefaultPolicyType, 0, Policy.ValuesFrom(["alice"]))); + Assert.True(await e.EnforceAsync("alice", "data1", "read")); + + // Load filtered policy again (should clear previous policies) + await e.LoadFilteredPolicyAsync(new PolicyFilter(PermConstants.DefaultPolicyType, 0, Policy.ValuesFrom(["bob"]))); + + // alice can no longer read data1 (previous policies cleared) + Assert.False(await e.EnforceAsync("alice", "data1", "read")); + // bob can write data2 (new filtered policies loaded) + Assert.True(await e.EnforceAsync("bob", "data2", "write")); + } +} diff --git a/Casbin/Abstractions/Persist/BaseAdapter.cs b/Casbin/Abstractions/Persist/BaseAdapter.cs index a93e4ae..518ef68 100644 --- a/Casbin/Abstractions/Persist/BaseAdapter.cs +++ b/Casbin/Abstractions/Persist/BaseAdapter.cs @@ -221,6 +221,36 @@ public Task LoadFilteredPolicyAsync(IPolicyStore store, Filter filter) #endif } + public void LoadIncrementalFilteredPolicy(IPolicyStore store, IPolicyFilter filter) + { + // ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract + if (filter is null) + { + LoadPolicy(store); + return; + } + + IEnumerable policies = ReadPersistPolicy(); + policies = filter.Apply(policies.AsQueryable()); + foreach (IPersistPolicy policy in policies) + { + int requiredCount = store.GetRequiredValuesCount(policy.Section, policy.Type); + IPolicyValues values = Policy.ValuesFrom(policy, requiredCount); + store.AddPolicy(policy.Section, policy.Type, values); + } + IsFiltered = true; + } + + public Task LoadIncrementalFilteredPolicyAsync(IPolicyStore store, IPolicyFilter filter) + { + LoadIncrementalFilteredPolicy(store, filter); +#if !NET452 + return Task.CompletedTask; +#else + return Task.FromResult(true); +#endif + } + #endregion protected IEnumerable ReadPersistPolicy() diff --git a/Casbin/Abstractions/Persist/IFilteredAdapter.cs b/Casbin/Abstractions/Persist/IFilteredAdapter.cs index 7d4f301..73f740f 100644 --- a/Casbin/Abstractions/Persist/IFilteredAdapter.cs +++ b/Casbin/Abstractions/Persist/IFilteredAdapter.cs @@ -10,5 +10,9 @@ public interface IFilteredAdapter void LoadFilteredPolicy(IPolicyStore store, IPolicyFilter filter); Task LoadFilteredPolicyAsync(IPolicyStore store, IPolicyFilter filter); + + void LoadIncrementalFilteredPolicy(IPolicyStore store, IPolicyFilter filter); + + Task LoadIncrementalFilteredPolicyAsync(IPolicyStore store, IPolicyFilter filter); } } diff --git a/Casbin/Extensions/Enforcer/EnforcerExtension.cs b/Casbin/Extensions/Enforcer/EnforcerExtension.cs index b9f31f8..2889435 100644 --- a/Casbin/Extensions/Enforcer/EnforcerExtension.cs +++ b/Casbin/Extensions/Enforcer/EnforcerExtension.cs @@ -229,6 +229,7 @@ public static bool LoadFilteredPolicy(this IEnforcer enforcer, IPolicyFilter fil return false; } + enforcer.ClearCache(); if (enforcer.AutoBuildRoleLinks) { enforcer.BuildRoleLinks(); @@ -251,6 +252,7 @@ public static async Task LoadFilteredPolicyAsync(this IEnforcer enforcer, return false; } + enforcer.ClearCache(); if (enforcer.AutoBuildRoleLinks) { enforcer.BuildRoleLinks(); @@ -269,6 +271,52 @@ public static async Task LoadFilteredPolicyAsync(this IEnforcer enforcer, public static Task LoadFilteredPolicyAsync(this IEnforcer enforcer, Filter filter) => LoadFilteredPolicyAsync(enforcer, filter as IPolicyFilter); + /// + /// Appends a filtered policy from file/database without clearing the existing policies. + /// + /// + /// The filter used to specify which type of policy should be loaded. + /// + public static bool LoadIncrementalFilteredPolicy(this IEnforcer enforcer, IPolicyFilter filter) + { + bool result = enforcer.Model.LoadIncrementalFilteredPolicy(filter); + if (result is false) + { + return false; + } + + enforcer.ClearCache(); + if (enforcer.AutoBuildRoleLinks) + { + enforcer.BuildRoleLinks(); + } + + return true; + } + + /// + /// Appends a filtered policy from file/database without clearing the existing policies. + /// + /// + /// The filter used to specify which type of policy should be loaded. + /// + public static async Task LoadIncrementalFilteredPolicyAsync(this IEnforcer enforcer, IPolicyFilter filter) + { + bool result = await enforcer.Model.LoadIncrementalFilteredPolicyAsync(filter); + if (result is false) + { + return false; + } + + enforcer.ClearCache(); + if (enforcer.AutoBuildRoleLinks) + { + enforcer.BuildRoleLinks(); + } + + return true; + } + /// /// Saves the current policy (usually after changed with Casbin API) /// back to file/database. diff --git a/Casbin/Extensions/Model/ModelExtension.cs b/Casbin/Extensions/Model/ModelExtension.cs index e36b5a0..324f68e 100644 --- a/Casbin/Extensions/Model/ModelExtension.cs +++ b/Casbin/Extensions/Model/ModelExtension.cs @@ -155,6 +155,40 @@ await model.AdapterHolder.FilteredAdapter.LoadFilteredPolicyAsync(policyStore, return true; } + public static bool LoadIncrementalFilteredPolicy(this IModel model, IPolicyFilter filter) + { + if (model.AdapterHolder.Adapter is null) + { + return false; + } + + if (model.AdapterHolder.FilteredAdapter is null) + { + return false; + } + + model.AdapterHolder.FilteredAdapter.LoadIncrementalFilteredPolicy( + model.PolicyStoreHolder.PolicyStore, filter); + return true; + } + + public static async Task LoadIncrementalFilteredPolicyAsync(this IModel model, IPolicyFilter filter) + { + if (model.AdapterHolder.Adapter is null) + { + return false; + } + + if (model.AdapterHolder.FilteredAdapter is null) + { + return false; + } + + await model.AdapterHolder.FilteredAdapter.LoadIncrementalFilteredPolicyAsync( + model.PolicyStoreHolder.PolicyStore, filter); + return true; + } + public static bool SavePolicy(this IModel model) { if (model.AdapterHolder.Adapter is null) From 9ff0bc031baa0d701e53739305a4240d9df366c4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 31 Oct 2025 17:15:15 +0000 Subject: [PATCH 3/4] Address code review feedback and add documentation Co-authored-by: hsluoyz <3787410+hsluoyz@users.noreply.github.com> --- .../IncrementalFilteredAdapterTest.cs | 52 ++++---- Casbin/Abstractions/Persist/BaseAdapter.cs | 5 +- Casbin/Extensions/Model/ModelExtension.cs | 14 ++ INCREMENTAL_FILTERED_POLICY.md | 122 ++++++++++++++++++ 4 files changed, 165 insertions(+), 28 deletions(-) create mode 100644 INCREMENTAL_FILTERED_POLICY.md diff --git a/Casbin.UnitTests/PersistTests/IncrementalFilteredAdapterTest.cs b/Casbin.UnitTests/PersistTests/IncrementalFilteredAdapterTest.cs index eeb72f0..30fa2d2 100644 --- a/Casbin.UnitTests/PersistTests/IncrementalFilteredAdapterTest.cs +++ b/Casbin.UnitTests/PersistTests/IncrementalFilteredAdapterTest.cs @@ -17,25 +17,25 @@ public void TestLoadIncrementalFilteredPolicy() FileAdapter a = new("Examples/rbac_policy.csv"); e.SetAdapter(a); - + // Load only p policies for alice e.LoadFilteredPolicy(new PolicyFilter(PermConstants.DefaultPolicyType, 0, Policy.ValuesFrom(["alice"]))); - + // alice can read data1 (from p policy) Assert.True(e.Enforce("alice", "data1", "read")); // bob cannot write data2 (not loaded yet) Assert.False(e.Enforce("bob", "data2", "write")); - + // Incrementally load p policies for data2_admin role e.LoadIncrementalFilteredPolicy(new PolicyFilter(PermConstants.DefaultPolicyType, 0, Policy.ValuesFrom(["data2_admin"]))); - + // alice still can only read data1 (no role link yet) Assert.True(e.Enforce("alice", "data1", "read")); Assert.False(e.Enforce("alice", "data2", "read")); - + // Incrementally load g policies for alice e.LoadIncrementalFilteredPolicy(new PolicyFilter(PermConstants.DefaultRoleType, 0, Policy.ValuesFrom(["alice"]))); - + // Now alice can read data2 through role inheritance Assert.True(e.Enforce("alice", "data2", "read")); Assert.True(e.Enforce("alice", "data2", "write")); @@ -51,25 +51,25 @@ public async Task TestLoadIncrementalFilteredPolicyAsync() FileAdapter a = new("Examples/rbac_policy.csv"); e.SetAdapter(a); - + // Load only p policies for alice await e.LoadFilteredPolicyAsync(new PolicyFilter(PermConstants.DefaultPolicyType, 0, Policy.ValuesFrom(["alice"]))); - + // alice can read data1 (from p policy) Assert.True(await e.EnforceAsync("alice", "data1", "read")); // bob cannot write data2 (not loaded yet) Assert.False(await e.EnforceAsync("bob", "data2", "write")); - + // Incrementally load p policies for data2_admin role await e.LoadIncrementalFilteredPolicyAsync(new PolicyFilter(PermConstants.DefaultPolicyType, 0, Policy.ValuesFrom(["data2_admin"]))); - + // alice still can only read data1 (no role link yet) Assert.True(await e.EnforceAsync("alice", "data1", "read")); Assert.False(await e.EnforceAsync("alice", "data2", "read")); - + // Incrementally load g policies for alice await e.LoadIncrementalFilteredPolicyAsync(new PolicyFilter(PermConstants.DefaultRoleType, 0, Policy.ValuesFrom(["alice"]))); - + // Now alice can read data2 through role inheritance Assert.True(await e.EnforceAsync("alice", "data2", "read")); Assert.True(await e.EnforceAsync("alice", "data2", "write")); @@ -85,18 +85,18 @@ public void TestLoadIncrementalFilteredPolicyMultiplePTypes() FileAdapter a = new("Examples/rbac_policy.csv"); e.SetAdapter(a); - + // Load only p policies for alice e.LoadFilteredPolicy(new PolicyFilter(PermConstants.DefaultPolicyType, 0, Policy.ValuesFrom(["alice"]))); - + // alice can read data1 Assert.True(e.Enforce("alice", "data1", "read")); // bob cannot write data2 Assert.False(e.Enforce("bob", "data2", "write")); - + // Incrementally load p policies for bob e.LoadIncrementalFilteredPolicy(new PolicyFilter(PermConstants.DefaultPolicyType, 0, Policy.ValuesFrom(["bob"]))); - + // alice still can read data1 Assert.True(e.Enforce("alice", "data1", "read")); // Now bob can write data2 @@ -111,18 +111,18 @@ public async Task TestLoadIncrementalFilteredPolicyMultiplePTypesAsync() FileAdapter a = new("Examples/rbac_policy.csv"); e.SetAdapter(a); - + // Load only p policies for alice await e.LoadFilteredPolicyAsync(new PolicyFilter(PermConstants.DefaultPolicyType, 0, Policy.ValuesFrom(["alice"]))); - + // alice can read data1 Assert.True(await e.EnforceAsync("alice", "data1", "read")); // bob cannot write data2 Assert.False(await e.EnforceAsync("bob", "data2", "write")); - + // Incrementally load p policies for bob await e.LoadIncrementalFilteredPolicyAsync(new PolicyFilter(PermConstants.DefaultPolicyType, 0, Policy.ValuesFrom(["bob"]))); - + // alice still can read data1 Assert.True(await e.EnforceAsync("alice", "data1", "read")); // Now bob can write data2 @@ -135,14 +135,14 @@ public void TestLoadFilteredPolicyClearsExistingPolicies() Enforcer e = new("Examples/rbac_model.conf"); FileAdapter a = new("Examples/rbac_policy.csv"); e.SetAdapter(a); - + // Load only p policies for alice e.LoadFilteredPolicy(new PolicyFilter(PermConstants.DefaultPolicyType, 0, Policy.ValuesFrom(["alice"]))); Assert.True(e.Enforce("alice", "data1", "read")); - + // Load filtered policy again (should clear previous policies) e.LoadFilteredPolicy(new PolicyFilter(PermConstants.DefaultPolicyType, 0, Policy.ValuesFrom(["bob"]))); - + // alice can no longer read data1 (previous policies cleared) Assert.False(e.Enforce("alice", "data1", "read")); // bob can write data2 (new filtered policies loaded) @@ -155,14 +155,14 @@ public async Task TestLoadFilteredPolicyClearsExistingPoliciesAsync() Enforcer e = new("Examples/rbac_model.conf"); FileAdapter a = new("Examples/rbac_policy.csv"); e.SetAdapter(a); - + // Load only p policies for alice await e.LoadFilteredPolicyAsync(new PolicyFilter(PermConstants.DefaultPolicyType, 0, Policy.ValuesFrom(["alice"]))); Assert.True(await e.EnforceAsync("alice", "data1", "read")); - + // Load filtered policy again (should clear previous policies) await e.LoadFilteredPolicyAsync(new PolicyFilter(PermConstants.DefaultPolicyType, 0, Policy.ValuesFrom(["bob"]))); - + // alice can no longer read data1 (previous policies cleared) Assert.False(await e.EnforceAsync("alice", "data1", "read")); // bob can write data2 (new filtered policies loaded) diff --git a/Casbin/Abstractions/Persist/BaseAdapter.cs b/Casbin/Abstractions/Persist/BaseAdapter.cs index 518ef68..bed4479 100644 --- a/Casbin/Abstractions/Persist/BaseAdapter.cs +++ b/Casbin/Abstractions/Persist/BaseAdapter.cs @@ -226,8 +226,9 @@ public void LoadIncrementalFilteredPolicy(IPolicyStore store, IPolicyFilter filt // ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract if (filter is null) { - LoadPolicy(store); - return; + throw new ArgumentNullException(nameof(filter), + "Filter cannot be null for incremental filtered policy loading. " + + "Use LoadPolicy() to load all policies instead."); } IEnumerable policies = ReadPersistPolicy(); diff --git a/Casbin/Extensions/Model/ModelExtension.cs b/Casbin/Extensions/Model/ModelExtension.cs index 324f68e..c5a08de 100644 --- a/Casbin/Extensions/Model/ModelExtension.cs +++ b/Casbin/Extensions/Model/ModelExtension.cs @@ -167,6 +167,13 @@ public static bool LoadIncrementalFilteredPolicy(this IModel model, IPolicyFilte return false; } + if (model.PolicyStoreHolder.PolicyStore is null) + { + throw new InvalidOperationException( + "Cannot load incremental filtered policy when policy store is not initialized. " + + "Call LoadPolicy() or LoadFilteredPolicy() first."); + } + model.AdapterHolder.FilteredAdapter.LoadIncrementalFilteredPolicy( model.PolicyStoreHolder.PolicyStore, filter); return true; @@ -184,6 +191,13 @@ public static async Task LoadIncrementalFilteredPolicyAsync(this IModel mo return false; } + if (model.PolicyStoreHolder.PolicyStore is null) + { + throw new InvalidOperationException( + "Cannot load incremental filtered policy when policy store is not initialized. " + + "Call LoadPolicy() or LoadFilteredPolicy() first."); + } + await model.AdapterHolder.FilteredAdapter.LoadIncrementalFilteredPolicyAsync( model.PolicyStoreHolder.PolicyStore, filter); return true; diff --git a/INCREMENTAL_FILTERED_POLICY.md b/INCREMENTAL_FILTERED_POLICY.md new file mode 100644 index 0000000..8743710 --- /dev/null +++ b/INCREMENTAL_FILTERED_POLICY.md @@ -0,0 +1,122 @@ +# Incremental Filtered Policy Loading + +## Overview + +Casbin.NET now supports incremental filtered policy loading, which allows you to load multiple filtered policy sets without overwriting previously loaded policies. This is useful when you need to load both filtered `p` policies and `g` policies, or different sets of `p` policies from your custom adapter. + +## Methods + +### LoadIncrementalFilteredPolicy + +Appends a filtered policy from file/database without clearing the existing policies. + +```csharp +public static bool LoadIncrementalFilteredPolicy(this IEnforcer enforcer, IPolicyFilter filter) +public static Task LoadIncrementalFilteredPolicyAsync(this IEnforcer enforcer, IPolicyFilter filter) +``` + +## Usage Examples + +### Example 1: Loading Multiple Policy Types + +```csharp +using Casbin; +using Casbin.Model; +using Casbin.Persist; +using Casbin.Persist.Adapter.File; + +// Setup +var enforcer = new Enforcer("path/to/model.conf"); +var adapter = new FileAdapter("path/to/policy.csv"); +enforcer.SetAdapter(adapter); + +// Load only alice's p policies +enforcer.LoadFilteredPolicy( + new PolicyFilter(PermConstants.DefaultPolicyType, 0, Policy.ValuesFrom(new[] { "alice" })) +); + +// Incrementally load the data2_admin role's p policies +enforcer.LoadIncrementalFilteredPolicy( + new PolicyFilter(PermConstants.DefaultPolicyType, 0, Policy.ValuesFrom(new[] { "data2_admin" })) +); + +// Incrementally load alice's g policies (role assignments) +enforcer.LoadIncrementalFilteredPolicy( + new PolicyFilter(PermConstants.DefaultRoleType, 0, Policy.ValuesFrom(new[] { "alice" })) +); + +// Now alice can access resources through both direct policies and role inheritance +``` + +### Example 2: Loading Policies for Multiple Users + +```csharp +// Load alice's policies +enforcer.LoadFilteredPolicy( + new PolicyFilter(PermConstants.DefaultPolicyType, 0, Policy.ValuesFrom(new[] { "alice" })) +); + +// Add bob's policies incrementally (alice's policies are preserved) +enforcer.LoadIncrementalFilteredPolicy( + new PolicyFilter(PermConstants.DefaultPolicyType, 0, Policy.ValuesFrom(new[] { "bob" })) +); + +// Both alice and bob's policies are now active +``` + +## Comparison with LoadFilteredPolicy + +### LoadFilteredPolicy +- **Behavior**: Clears all existing policies and loads only the filtered policies +- **Use case**: When you want to replace the entire policy set with a filtered subset + +```csharp +// Load alice's policies +enforcer.LoadFilteredPolicy(filter1); + +// Load bob's policies (alice's policies are cleared) +enforcer.LoadFilteredPolicy(filter2); +// Result: Only bob's policies are active +``` + +### LoadIncrementalFilteredPolicy +- **Behavior**: Keeps existing policies and adds filtered policies +- **Use case**: When you want to build up a policy set from multiple filtered subsets + +```csharp +// Load alice's policies +enforcer.LoadFilteredPolicy(filter1); + +// Add bob's policies (alice's policies are kept) +enforcer.LoadIncrementalFilteredPolicy(filter2); +// Result: Both alice and bob's policies are active +``` + +## PolicyFilter Usage + +The `PolicyFilter` class accepts three parameters: +1. **policyType**: The type of policy (e.g., `PermConstants.DefaultPolicyType` for "p", `PermConstants.DefaultRoleType` for "g") +2. **fieldIndex**: The starting field index to filter on (0-based) +3. **values**: The values to filter by + +```csharp +// Filter p policies where the first field (subject) is "alice" +new PolicyFilter(PermConstants.DefaultPolicyType, 0, Policy.ValuesFrom(new[] { "alice" })) + +// Filter g policies where the first field (user) is "alice" +new PolicyFilter(PermConstants.DefaultRoleType, 0, Policy.ValuesFrom(new[] { "alice" })) + +// Filter p policies where the second field (object) is "data1" +new PolicyFilter(PermConstants.DefaultPolicyType, 1, Policy.ValuesFrom(new[] { "data1" })) +``` + +## Additional Notes + +- Both `LoadFilteredPolicy` and `LoadIncrementalFilteredPolicy` will clear the enforcer cache and rebuild role links if `AutoBuildRoleLinks` is enabled +- The `IsFiltered` flag on the adapter will be set to `true` when using either method +- Async versions of both methods are available for asynchronous operations + +## Related Documentation + +- [Casbin Policy Subset Loading](https://casbin.org/docs/policy-subset-loading/) +- [Casbin Adapters](https://casbin.org/docs/adapters) From e7e798929cb8b8a9384a096456012f299589ea08 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 1 Nov 2025 02:17:10 +0000 Subject: [PATCH 4/4] Refactor to match Golang Casbin architecture pattern Co-authored-by: hsluoyz <3787410+hsluoyz@users.noreply.github.com> --- Casbin/Abstractions/Persist/BaseAdapter.cs | 31 ----- .../Abstractions/Persist/IFilteredAdapter.cs | 4 - Casbin/Extensions/Model/ModelExtension.cs | 111 ++++++++---------- 3 files changed, 46 insertions(+), 100 deletions(-) diff --git a/Casbin/Abstractions/Persist/BaseAdapter.cs b/Casbin/Abstractions/Persist/BaseAdapter.cs index bed4479..a93e4ae 100644 --- a/Casbin/Abstractions/Persist/BaseAdapter.cs +++ b/Casbin/Abstractions/Persist/BaseAdapter.cs @@ -221,37 +221,6 @@ public Task LoadFilteredPolicyAsync(IPolicyStore store, Filter filter) #endif } - public void LoadIncrementalFilteredPolicy(IPolicyStore store, IPolicyFilter filter) - { - // ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract - if (filter is null) - { - throw new ArgumentNullException(nameof(filter), - "Filter cannot be null for incremental filtered policy loading. " + - "Use LoadPolicy() to load all policies instead."); - } - - IEnumerable policies = ReadPersistPolicy(); - policies = filter.Apply(policies.AsQueryable()); - foreach (IPersistPolicy policy in policies) - { - int requiredCount = store.GetRequiredValuesCount(policy.Section, policy.Type); - IPolicyValues values = Policy.ValuesFrom(policy, requiredCount); - store.AddPolicy(policy.Section, policy.Type, values); - } - IsFiltered = true; - } - - public Task LoadIncrementalFilteredPolicyAsync(IPolicyStore store, IPolicyFilter filter) - { - LoadIncrementalFilteredPolicy(store, filter); -#if !NET452 - return Task.CompletedTask; -#else - return Task.FromResult(true); -#endif - } - #endregion protected IEnumerable ReadPersistPolicy() diff --git a/Casbin/Abstractions/Persist/IFilteredAdapter.cs b/Casbin/Abstractions/Persist/IFilteredAdapter.cs index 73f740f..7d4f301 100644 --- a/Casbin/Abstractions/Persist/IFilteredAdapter.cs +++ b/Casbin/Abstractions/Persist/IFilteredAdapter.cs @@ -10,9 +10,5 @@ public interface IFilteredAdapter void LoadFilteredPolicy(IPolicyStore store, IPolicyFilter filter); Task LoadFilteredPolicyAsync(IPolicyStore store, IPolicyFilter filter); - - void LoadIncrementalFilteredPolicy(IPolicyStore store, IPolicyFilter filter); - - Task LoadIncrementalFilteredPolicyAsync(IPolicyStore store, IPolicyFilter filter); } } diff --git a/Casbin/Extensions/Model/ModelExtension.cs b/Casbin/Extensions/Model/ModelExtension.cs index c5a08de..e933be5 100644 --- a/Casbin/Extensions/Model/ModelExtension.cs +++ b/Casbin/Extensions/Model/ModelExtension.cs @@ -94,68 +94,27 @@ public static async Task LoadPolicyAsync(this IModel model) public static bool LoadFilteredPolicy(this IModel model, IPolicyFilter filter) { - if (model.AdapterHolder.Adapter is null) - { - return false; - } - - if (model.AdapterHolder.FilteredAdapter is null) - { - return false; - } - - DefaultPolicyStore policyStore = new(); - foreach (KeyValuePair pair in model.Sections.GetPolicyAssertions()) - { - policyStore.AddNode(PermConstants.Section.PolicySection, pair.Key, pair.Value); - } - - if (model.Sections.ContainsSection(PermConstants.Section.RoleSection)) - { - foreach (KeyValuePair pair in model.Sections.GetRoleAssertions()) - { - policyStore.AddNode(PermConstants.Section.RoleSection, pair.Key, pair.Value); - } - } - - model.AdapterHolder.FilteredAdapter.LoadFilteredPolicy(policyStore, filter); - model.PolicyStoreHolder.PolicyStore = policyStore; - return true; + model.PolicyStoreHolder.PolicyStore?.ClearPolicy(); + return LoadFilteredPolicyInternal(model, filter); } public static async Task LoadFilteredPolicyAsync(this IModel model, IPolicyFilter filter) { - if (model.AdapterHolder.Adapter is null) - { - return false; - } - - if (model.AdapterHolder.FilteredAdapter is null) - { - return false; - } - - DefaultPolicyStore policyStore = new(); - foreach (KeyValuePair pair in model.Sections.GetPolicyAssertions()) - { - policyStore.AddNode(PermConstants.Section.PolicySection, pair.Key, pair.Value); - } + model.PolicyStoreHolder.PolicyStore?.ClearPolicy(); + return await LoadFilteredPolicyInternalAsync(model, filter); + } - if (model.Sections.ContainsSection(PermConstants.Section.RoleSection)) - { - foreach (KeyValuePair pair in model.Sections.GetRoleAssertions()) - { - policyStore.AddNode(PermConstants.Section.RoleSection, pair.Key, pair.Value); - } - } + public static bool LoadIncrementalFilteredPolicy(this IModel model, IPolicyFilter filter) + { + return LoadFilteredPolicyInternal(model, filter); + } - await model.AdapterHolder.FilteredAdapter.LoadFilteredPolicyAsync(policyStore, - filter); - model.PolicyStoreHolder.PolicyStore = policyStore; - return true; + public static async Task LoadIncrementalFilteredPolicyAsync(this IModel model, IPolicyFilter filter) + { + return await LoadFilteredPolicyInternalAsync(model, filter); } - public static bool LoadIncrementalFilteredPolicy(this IModel model, IPolicyFilter filter) + private static bool LoadFilteredPolicyInternal(IModel model, IPolicyFilter filter) { if (model.AdapterHolder.Adapter is null) { @@ -167,19 +126,30 @@ public static bool LoadIncrementalFilteredPolicy(this IModel model, IPolicyFilte return false; } + // Initialize policy store if it doesn't exist if (model.PolicyStoreHolder.PolicyStore is null) { - throw new InvalidOperationException( - "Cannot load incremental filtered policy when policy store is not initialized. " + - "Call LoadPolicy() or LoadFilteredPolicy() first."); + DefaultPolicyStore policyStore = new(); + foreach (KeyValuePair pair in model.Sections.GetPolicyAssertions()) + { + policyStore.AddNode(PermConstants.Section.PolicySection, pair.Key, pair.Value); + } + + if (model.Sections.ContainsSection(PermConstants.Section.RoleSection)) + { + foreach (KeyValuePair pair in model.Sections.GetRoleAssertions()) + { + policyStore.AddNode(PermConstants.Section.RoleSection, pair.Key, pair.Value); + } + } + model.PolicyStoreHolder.PolicyStore = policyStore; } - model.AdapterHolder.FilteredAdapter.LoadIncrementalFilteredPolicy( - model.PolicyStoreHolder.PolicyStore, filter); + model.AdapterHolder.FilteredAdapter.LoadFilteredPolicy(model.PolicyStoreHolder.PolicyStore, filter); return true; } - public static async Task LoadIncrementalFilteredPolicyAsync(this IModel model, IPolicyFilter filter) + private static async Task LoadFilteredPolicyInternalAsync(IModel model, IPolicyFilter filter) { if (model.AdapterHolder.Adapter is null) { @@ -191,15 +161,26 @@ public static async Task LoadIncrementalFilteredPolicyAsync(this IModel mo return false; } + // Initialize policy store if it doesn't exist if (model.PolicyStoreHolder.PolicyStore is null) { - throw new InvalidOperationException( - "Cannot load incremental filtered policy when policy store is not initialized. " + - "Call LoadPolicy() or LoadFilteredPolicy() first."); + DefaultPolicyStore policyStore = new(); + foreach (KeyValuePair pair in model.Sections.GetPolicyAssertions()) + { + policyStore.AddNode(PermConstants.Section.PolicySection, pair.Key, pair.Value); + } + + if (model.Sections.ContainsSection(PermConstants.Section.RoleSection)) + { + foreach (KeyValuePair pair in model.Sections.GetRoleAssertions()) + { + policyStore.AddNode(PermConstants.Section.RoleSection, pair.Key, pair.Value); + } + } + model.PolicyStoreHolder.PolicyStore = policyStore; } - await model.AdapterHolder.FilteredAdapter.LoadIncrementalFilteredPolicyAsync( - model.PolicyStoreHolder.PolicyStore, filter); + await model.AdapterHolder.FilteredAdapter.LoadFilteredPolicyAsync(model.PolicyStoreHolder.PolicyStore, filter); return true; }