diff --git a/Casbin.UnitTests/PersistTests/IncrementalFilteredAdapterTest.cs b/Casbin.UnitTests/PersistTests/IncrementalFilteredAdapterTest.cs new file mode 100644 index 0000000..30fa2d2 --- /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/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..e933be5 100644 --- a/Casbin/Extensions/Model/ModelExtension.cs +++ b/Casbin/Extensions/Model/ModelExtension.cs @@ -93,6 +93,28 @@ public static async Task LoadPolicyAsync(this IModel model) } public static bool LoadFilteredPolicy(this IModel model, IPolicyFilter filter) + { + model.PolicyStoreHolder.PolicyStore?.ClearPolicy(); + return LoadFilteredPolicyInternal(model, filter); + } + + public static async Task LoadFilteredPolicyAsync(this IModel model, IPolicyFilter filter) + { + model.PolicyStoreHolder.PolicyStore?.ClearPolicy(); + return await LoadFilteredPolicyInternalAsync(model, filter); + } + + public static bool LoadIncrementalFilteredPolicy(this IModel model, IPolicyFilter filter) + { + return LoadFilteredPolicyInternal(model, filter); + } + + public static async Task LoadIncrementalFilteredPolicyAsync(this IModel model, IPolicyFilter filter) + { + return await LoadFilteredPolicyInternalAsync(model, filter); + } + + private static bool LoadFilteredPolicyInternal(IModel model, IPolicyFilter filter) { if (model.AdapterHolder.Adapter is null) { @@ -104,26 +126,30 @@ public static bool LoadFilteredPolicy(this IModel model, IPolicyFilter filter) return false; } - DefaultPolicyStore policyStore = new(); - foreach (KeyValuePair pair in model.Sections.GetPolicyAssertions()) + // Initialize policy store if it doesn't exist + if (model.PolicyStoreHolder.PolicyStore is null) { - policyStore.AddNode(PermConstants.Section.PolicySection, pair.Key, pair.Value); - } + 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()) + if (model.Sections.ContainsSection(PermConstants.Section.RoleSection)) { - policyStore.AddNode(PermConstants.Section.RoleSection, pair.Key, pair.Value); + foreach (KeyValuePair pair in model.Sections.GetRoleAssertions()) + { + policyStore.AddNode(PermConstants.Section.RoleSection, pair.Key, pair.Value); + } } + model.PolicyStoreHolder.PolicyStore = policyStore; } - model.AdapterHolder.FilteredAdapter.LoadFilteredPolicy(policyStore, filter); - model.PolicyStoreHolder.PolicyStore = policyStore; + model.AdapterHolder.FilteredAdapter.LoadFilteredPolicy(model.PolicyStoreHolder.PolicyStore, filter); return true; } - public static async Task LoadFilteredPolicyAsync(this IModel model, IPolicyFilter filter) + private static async Task LoadFilteredPolicyInternalAsync(IModel model, IPolicyFilter filter) { if (model.AdapterHolder.Adapter is null) { @@ -135,23 +161,26 @@ public static async Task LoadFilteredPolicyAsync(this IModel model, IPolic return false; } - DefaultPolicyStore policyStore = new(); - foreach (KeyValuePair pair in model.Sections.GetPolicyAssertions()) + // Initialize policy store if it doesn't exist + if (model.PolicyStoreHolder.PolicyStore is null) { - policyStore.AddNode(PermConstants.Section.PolicySection, pair.Key, pair.Value); - } + 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()) + if (model.Sections.ContainsSection(PermConstants.Section.RoleSection)) { - policyStore.AddNode(PermConstants.Section.RoleSection, pair.Key, pair.Value); + foreach (KeyValuePair pair in model.Sections.GetRoleAssertions()) + { + policyStore.AddNode(PermConstants.Section.RoleSection, pair.Key, pair.Value); + } } + model.PolicyStoreHolder.PolicyStore = policyStore; } - await model.AdapterHolder.FilteredAdapter.LoadFilteredPolicyAsync(policyStore, - filter); - model.PolicyStoreHolder.PolicyStore = policyStore; + await model.AdapterHolder.FilteredAdapter.LoadFilteredPolicyAsync(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)