diff --git a/examples/basic_model_without_spaces.conf b/examples/basic_model_without_spaces.conf new file mode 100644 index 00000000..a5b49fef --- /dev/null +++ b/examples/basic_model_without_spaces.conf @@ -0,0 +1,11 @@ +[request_definition] +r = sub,obj,act + +[policy_definition] +p = sub,obj,act + +[policy_effect] +e = some(where (p.eft == allow)) + +[matchers] +m = r.sub == p.sub && r.obj == p.obj && r.act == p.act diff --git a/examples/rbac_with_pattern_policy.csv b/examples/rbac_with_pattern_policy.csv index 3505daba..a7a9b7f3 100644 --- a/examples/rbac_with_pattern_policy.csv +++ b/examples/rbac_with_pattern_policy.csv @@ -2,6 +2,13 @@ p, alice, /pen/1, GET p, alice, /pen2/1, GET p, book_admin, book_group, GET p, pen_admin, pen_group, GET +p, *, pen3_group, GET + +p, /book/admin/:id, pen4_group, GET +g, /book/user/:id, /book/admin/1 + +p, /book/leader/2, pen4_group, POST +g, /book/user/:id, /book/leader/2 g, alice, book_admin g, bob, pen_admin @@ -16,3 +23,6 @@ g2, /pen/:id, pen_group g2, /book2/{id}, book_group g2, /pen2/{id}, pen_group + +g2, /pen3/:id, pen3_group +g2, /pen4/:id, pen4_group diff --git a/src/main/java/org/casbin/jcasbin/persist/file_adapter/AdapterMock.java b/src/main/java/org/casbin/jcasbin/persist/file_adapter/AdapterMock.java new file mode 100644 index 00000000..cb378a7c --- /dev/null +++ b/src/main/java/org/casbin/jcasbin/persist/file_adapter/AdapterMock.java @@ -0,0 +1,80 @@ +// Copyright 2024 The casbin Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package org.casbin.jcasbin.persist.file_adapter; + +import java.io.BufferedReader; +import java.io.FileReader; +import java.io.IOException; +import java.util.List; + +import org.casbin.jcasbin.model.Model; +import org.casbin.jcasbin.persist.Adapter; +import org.casbin.jcasbin.persist.Helper; + +public class AdapterMock implements Adapter { + private String filePath; + private String errorValue; + + public AdapterMock(String filePath) { + this.filePath = filePath; + } + + public void setMockErr(String errorToSet) { + this.errorValue = errorToSet; + } + + public Exception getMockErr() { + if (errorValue != null && !errorValue.isEmpty()) { + return new Exception(errorValue); + } + return null; + } + + @Override + public void loadPolicy(Model model) { + try { + loadPolicyFile(model, Helper::loadPolicyLine); + } catch (IOException e) { + e.printStackTrace(); + } + } + + @Override + public void savePolicy(Model model) { + } + + @Override + public void addPolicy(String sec, String ptype, List rule) { + + } + + @Override + public void removePolicy(String sec, String ptype, List rule) { + } + + @Override + public void removeFilteredPolicy(String sec, String ptype, int fieldIndex, String... fieldValues) { + } + + private void loadPolicyFile(Model model, Helper.loadPolicyLineHandler handler) throws IOException { + try (BufferedReader br = new BufferedReader(new FileReader(filePath))) { + String line; + while ((line = br.readLine()) != null) { + line = line.trim(); + handler.accept(line, model); + } + } + } +} diff --git a/src/main/java/org/casbin/jcasbin/rbac/Role.java b/src/main/java/org/casbin/jcasbin/rbac/Role.java index dbff29f8..de8e6440 100644 --- a/src/main/java/org/casbin/jcasbin/rbac/Role.java +++ b/src/main/java/org/casbin/jcasbin/rbac/Role.java @@ -67,10 +67,10 @@ void removeMatch(Role role) { } void removeMatches() { - this.matched.values().forEach(this::removeMatch); - // https://stackoverflow.com/a/223929/10206831 - for (Iterator iterator = this.matchedBy.values().iterator(); iterator.hasNext();) { - Role role = iterator.next(); + Iterator> iterator = this.matchedBy.entrySet().iterator(); + while (iterator.hasNext()) { + Map.Entry entry = iterator.next(); + Role role = entry.getValue(); role.matched.remove(this.name); iterator.remove(); } diff --git a/src/main/java/org/casbin/jcasbin/util/BuiltInFunctions.java b/src/main/java/org/casbin/jcasbin/util/BuiltInFunctions.java index de4d7fda..72b87c69 100644 --- a/src/main/java/org/casbin/jcasbin/util/BuiltInFunctions.java +++ b/src/main/java/org/casbin/jcasbin/util/BuiltInFunctions.java @@ -74,6 +74,7 @@ public static boolean keyMatch(String key1, String key2) { public static boolean keyMatch2(String key1, String key2) { key2 = key2.replace("/*", "/.*"); key2 = KEY_MATCH2_PATTERN.matcher(key2).replaceAll("[^/]+"); + key2 = key2.replaceAll("\\{([^/]+)\\}", "([^/]+)"); if(Objects.equals(key2, "*")) { key2 = "(.*)"; } diff --git a/src/test/java/org/casbin/jcasbin/main/ModelUnitTest.java b/src/test/java/org/casbin/jcasbin/main/ModelUnitTest.java index ad9991a2..e68df326 100644 --- a/src/test/java/org/casbin/jcasbin/main/ModelUnitTest.java +++ b/src/test/java/org/casbin/jcasbin/main/ModelUnitTest.java @@ -14,10 +14,11 @@ package org.casbin.jcasbin.main; +import org.casbin.jcasbin.persist.file_adapter.AdapterMock; import org.casbin.jcasbin.rbac.RoleManager; +import org.casbin.jcasbin.util.BuiltInFunctions; import org.junit.Test; -import java.util.Arrays; import java.util.List; import static org.casbin.jcasbin.main.TestUtil.testDomainEnforce; @@ -27,6 +28,20 @@ public class ModelUnitTest { @Test public void testBasicModel() { + Enforcer e = new Enforcer("examples/basic_model_without_spaces.conf", "examples/basic_policy.csv"); + + testEnforce(e, "alice", "data1", "read", true); + testEnforce(e, "alice", "data1", "write", false); + testEnforce(e, "alice", "data2", "read", false); + testEnforce(e, "alice", "data2", "write", false); + testEnforce(e, "bob", "data1", "read", false); + testEnforce(e, "bob", "data1", "write", false); + testEnforce(e, "bob", "data2", "read", false); + testEnforce(e, "bob", "data2", "write", true); + } + + @Test + public void testBasicModelWithoutSpaces() { Enforcer e = new Enforcer("examples/basic_model.conf", "examples/basic_policy.csv"); testEnforce(e, "alice", "data1", "read", true); @@ -197,6 +212,25 @@ public void testRBACModelWithDomainsAtRuntime() { testDomainEnforce(e, "bob", "domain2", "data2", "write", true); } + @Test + public void testRBACModelWithDomainsAtRuntimeMockAdapter(){ + AdapterMock adapter = new AdapterMock("examples/rbac_with_domains_policy.csv"); + Enforcer e = new Enforcer("examples/rbac_with_domains_model.conf", adapter); + + e.addPolicy("admin", "domain3", "data1", "read"); + e.addGroupingPolicy("alice", "admin", "domain3"); + + testDomainEnforce(e, "alice", "domain3", "data1", "read", true); + + testDomainEnforce(e, "alice", "domain1", "data1", "read", true); + e.removeFilteredPolicy(1, "domain1", "data1"); + testDomainEnforce(e, "alice", "domain1", "data1", "read", false); + + testDomainEnforce(e, "bob", "domain2", "data2", "read", true); + e.removePolicy("admin", "domain2", "data2", "read"); + testDomainEnforce(e, "bob", "domain2", "data2", "read", false); + } + @Test public void testRBACModelWithDeny() { Enforcer e = new Enforcer("examples/rbac_with_deny_model.conf", "examples/rbac_with_deny_policy.csv"); @@ -253,6 +287,43 @@ public void testRBACModelWithCustomData() { testEnforce(e, "bob", "data2", "write", true); } + @Test + public void testRBACModelWithPattern(){ + Enforcer e = new Enforcer("examples/rbac_with_pattern_model.conf", "examples/rbac_with_pattern_policy.csv"); + + // Here's a little confusing: the matching function here is not the custom function used in matcher. + // It is the matching function used by "g" (and "g2", "g3" if any..) + // You can see in policy that: "g2, /book/:id, book_group", so in "g2()" function in the matcher, instead + // of checking whether "/book/:id" equals the obj: "/book/1", it checks whether the pattern matches. + // You can see it as normal RBAC: "/book/:id" == "/book/1" becomes KeyMatch2("/book/:id", "/book/1") + e.addNamedMatchingFunc("g2", "KeyMatch2", BuiltInFunctions::keyMatch2); + e.addNamedMatchingFunc("g", "KeyMatch2", BuiltInFunctions::keyMatch2); + testEnforce(e, "any_user", "/pen3/1", "GET", true); + testEnforce(e, "/book/user/1", "/pen4/1", "GET", true); + + testEnforce(e, "/book/user/1", "/pen4/1", "POST", true); + + testEnforce(e, "alice", "/book/1", "GET", true); + testEnforce(e, "alice", "/book/2", "GET", true); + testEnforce(e, "alice", "/pen/1", "GET", true); + testEnforce(e, "alice", "/pen/2", "GET", false); + testEnforce(e, "bob", "/book/1", "GET", false); + testEnforce(e, "bob", "/pen/1", "GET", true); + testEnforce(e, "bob", "/pen/2", "GET", true); + + // AddMatchingFunc() is actually setting a function because only one function is allowed, + // so when we set "KeyMatch3", we are actually replacing "KeyMatch2" with "KeyMatch3". + e.addNamedMatchingFunc("g2", "KeyMatch2", BuiltInFunctions::keyMatch3); + testEnforce(e, "alice", "/book2/1", "GET", true); + testEnforce(e, "alice", "/book2/2", "GET", true); + testEnforce(e, "alice", "/pen2/1", "GET", true); + testEnforce(e, "alice", "/pen2/2", "GET", false); + testEnforce(e, "bob", "/book2/1", "GET", false); + testEnforce(e, "bob", "/book2/2", "GET", false); + testEnforce(e, "bob", "/pen2/1", "GET", true); + testEnforce(e, "bob", "/pen2/2", "GET", true); + } + class CustomRoleManager implements RoleManager { @Override public void clear() {