This is an automated email from the ASF dual-hosted git repository. tisonkun pushed a commit to branch fix/domain-role-authorization in repository https://gitbox.apache.org/repos/asf/casbin-jcasbin.git
commit eaa158d0d1662f29ff6f3a7041426d2bf4782040 Author: tison <[email protected]> AuthorDate: Fri Oct 23 11:24:39 2026 +0800 fix: preserve domain isolation and conditional role restrictions --- .../java/org/casbin/jcasbin/main/CoreEnforcer.java | 44 ++++++++- .../jcasbin/rbac/ConditionalDomainManager.java | 20 +++-- .../org/casbin/jcasbin/main/ModelUnitTest.java | 100 +++++++++++++++++++++ 3 files changed, 153 insertions(+), 11 deletions(-) diff --git a/src/main/java/org/casbin/jcasbin/main/CoreEnforcer.java b/src/main/java/org/casbin/jcasbin/main/CoreEnforcer.java index e49b1e1..b1b16f5 100644 --- a/src/main/java/org/casbin/jcasbin/main/CoreEnforcer.java +++ b/src/main/java/org/casbin/jcasbin/main/CoreEnforcer.java @@ -46,11 +46,16 @@ import java.util.*; import java.util.concurrent.ConcurrentHashMap; import java.util.function.BiPredicate; import java.util.function.Function; +import java.util.regex.Matcher; +import java.util.regex.Pattern; /** * CoreEnforcer defines the core functionality of an enforcer. */ public class CoreEnforcer { + private static final Pattern STRING_LITERAL = Pattern.compile("'(?:\\\\.|[^'\\\\])*'|\"(?:\\\\.|[^\"\\\\])*\""); + private static final Pattern MATCHER_FUNCTION_CALL = Pattern.compile("(?<![\\w.])(\\w+)\\s*\\("); + String modelPath; Model model; FunctionMap fm; @@ -409,15 +414,48 @@ public class CoreEnforcer { private void registerDomainMatchingFunc(String ptype) { Assertion r = model.model.getOrDefault("r", Collections.emptyMap()).get("r"); Assertion p = model.model.getOrDefault("p", Collections.emptyMap()).get("p"); - if (r == null || p == null || r.tokens.length < 2 || p.tokens.length < 2) { + if (r == null || p == null) { return; } - String matchFun = "keyMatch(" + r.tokens[1] + ", " + p.tokens[1] + ")"; - if (model.model.get("m").get("m").value.contains(matchFun)) { + String matcher = STRING_LITERAL.matcher(model.model.get("m").get("m").value).replaceAll("\"\""); + Matcher calls = MATCHER_FUNCTION_CALL.matcher(matcher); + Set<String> domainTokens = new HashSet<>(); + Set<String> matchedTokens = new HashSet<>(); + while (calls.find()) { + List<String> args = getMatcherFunctionArguments(matcher, calls.end()); + if (ptype.equals(calls.group(1)) && args.size() == 3 && Arrays.asList(r.tokens).contains(args.get(2))) { + domainTokens.add(args.get(2)); + } else if ("keyMatch".equals(calls.group(1)) && args.size() == 2 + && Arrays.asList(p.tokens).contains(args.get(1))) { + matchedTokens.add(args.get(0)); + } + } + // Resource matching must not change how the role function's domain is resolved. + if (!Collections.disjoint(domainTokens, matchedTokens)) { addNamedDomainMatchingFunc(ptype, "g", BuiltInFunctions::keyMatch); } } + private static List<String> getMatcherFunctionArguments(String matcher, int start) { + List<String> args = new ArrayList<>(); + int depth = 1; + for (int i = start; i < matcher.length(); i++) { + char c = matcher.charAt(i); + if (c == '(' || c == '[' || c == '{') { + depth++; + } else if (c == ')' || c == ']' || c == '}') { + if (--depth == 0) { + args.add(matcher.substring(start, i).trim()); + return args; + } + } else if (c == ',' && depth == 1) { + args.add(matcher.substring(start, i).trim()); + start = i + 1; + } + } + return Collections.emptyList(); + } + /** * add or update the DomainManager object in rmMap and associate it with a specific domain matching function */ diff --git a/src/main/java/org/casbin/jcasbin/rbac/ConditionalDomainManager.java b/src/main/java/org/casbin/jcasbin/rbac/ConditionalDomainManager.java index 7d09a0b..30e3b4f 100644 --- a/src/main/java/org/casbin/jcasbin/rbac/ConditionalDomainManager.java +++ b/src/main/java/org/casbin/jcasbin/rbac/ConditionalDomainManager.java @@ -52,16 +52,20 @@ public class ConditionalDomainManager extends ConditionalRoleManager { Map<String, ConditionalRoleManager> rmMap = new HashMap<>(this.rmMap); clear(); rmMap.forEach((domain, rm) -> { - rm.allRoles.values().forEach(user -> { - user.roles.keySet().forEach(roleName -> addLink(user.getName(), roleName, domain)); - }); + ConditionalRoleManager newRm = new ConditionalRoleManager(this.maxHierarchyLevel, this.matchingFunc, this.domainMatchingFunc); + newRm.copyFrom(rm); + this.rmMap.put(domain, newRm); }); - rmMap.forEach((domain, rm) -> { - ConditionalRoleManager newRm = getRoleManager(domain, true); - rm.allRoles.values().forEach(user -> { - user.roles.keySet().forEach(roleName -> newRm.getRole(user.getName()).copyLinkConditions(user, roleName)); + // Copy complete links from the snapshot so propagated links retain their conditions. + if (this.domainMatchingFunc != null) { + this.rmMap.forEach((domain, rm) -> { + rmMap.forEach((pattern, source) -> { + if (!domain.equals(pattern) && matchDomain(domain, pattern)) { + rm.copyFrom(source); + } + }); }); - }); + } } private String domainName(String... domain) { diff --git a/src/test/java/org/casbin/jcasbin/main/ModelUnitTest.java b/src/test/java/org/casbin/jcasbin/main/ModelUnitTest.java index d031fb1..919a96b 100644 --- a/src/test/java/org/casbin/jcasbin/main/ModelUnitTest.java +++ b/src/test/java/org/casbin/jcasbin/main/ModelUnitTest.java @@ -20,11 +20,13 @@ package org.casbin.jcasbin.main; import com.googlecode.aviator.runtime.function.FunctionUtils; import com.googlecode.aviator.runtime.type.AviatorBoolean; import com.googlecode.aviator.runtime.type.AviatorObject; +import org.casbin.jcasbin.model.Model; import org.casbin.jcasbin.persist.file_adapter.AdapterMock; import org.casbin.jcasbin.rbac.RoleManager; import org.casbin.jcasbin.util.BuiltInFunctions; import org.casbin.jcasbin.util.Util; import org.casbin.jcasbin.util.function.CustomFunction; +import org.testng.annotations.DataProvider; import org.testng.annotations.Test; import java.util.*; @@ -36,6 +38,8 @@ import static org.casbin.jcasbin.main.TestUtil.testDomainEnforce; import static org.casbin.jcasbin.main.TestUtil.testEnforce; import static org.casbin.jcasbin.main.TestUtil.testEnforceWithoutUsers; import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertFalse; +import static org.testng.Assert.assertTrue; public class ModelUnitTest { @Test @@ -178,6 +182,102 @@ public class ModelUnitTest { testDomainEnforce(e, "bob", "domain2", "data2", "write", true); } + @Test + public void testResourceMatchingDoesNotEnableDomainMatching() { + Model m = new Model(); + m.addDef("r", "r", "sub, obj, act, dom"); + m.addDef("p", "p", "sub, obj, act, dom"); + m.addDef("g", "g", "_, _, _"); + m.addDef("e", "e", "some(where (p.eft == allow))"); + m.addDef("m", "m", "g(r.sub, p.sub, r.dom) && r.dom == p.dom && keyMatch(r.obj, p.obj) && r.act == p.act"); + Enforcer e = new Enforcer(m); + e.addPolicy("admin", "/data/*", "read", "tenantA"); + e.addGroupingPolicy("alice", "admin", "tenant*"); + e.addGroupingPolicy("bob", "admin", "tenantA"); + + assertFalse(e.enforce("alice", "/data/1", "read", "tenantA")); + assertTrue(e.enforce("bob", "/data/1", "read", "tenantA")); + + e.addNamedDomainMatchingFunc("g", "keyMatch", BuiltInFunctions::keyMatch); + assertTrue(e.enforce("alice", "/data/1", "read", "tenantA")); + } + + @DataProvider(name = "domainRoleCalls") + public Object[][] domainRoleCalls() { + return new Object[][] { + {"g2(r.sub,p.sub,r.tenant)"}, + {"g2((r.sub),'admin',r.tenant)"} + }; + } + + @Test(dataProvider = "domainRoleCalls") + public void testDomainMatchingUsesRoleDomainArgument(String roleCall) { + Model m = new Model(); + m.addDef("r", "r", "sub, obj, act, tenant"); + m.addDef("p", "p", "sub, obj, act, tenant"); + m.addDef("g", "g2", "_, _, _"); + m.addDef("e", "e", "some(where (p.eft == allow))"); + m.addDef("m", "m", roleCall + " && keyMatch(r.tenant,p.tenant) && r.obj == p.obj && r.act == p.act"); + Enforcer e = new Enforcer(m); + e.addPolicy("admin", "data", "read", "tenant*"); + e.addNamedGroupingPolicy("g2", "alice", "admin", "tenant*"); + + assertTrue(e.enforce("alice", "data", "read", "tenantA")); + assertFalse(e.enforce("alice", "data", "read", "other")); + assertFalse(e.enforce("bob", "data", "read", "tenantA")); + } + + @Test + public void testQuotedDomainMatcherDoesNotEnableDomainMatching() { + Model m = new Model(); + m.addDef("r", "r", "sub, dom, obj, act"); + m.addDef("p", "p", "sub, dom, obj, act"); + m.addDef("g", "g", "_, _, _"); + m.addDef("e", "e", "some(where (p.eft == allow))"); + m.addDef("m", "m", "g(r.sub, p.sub, r.dom) && r.dom == p.dom && r.obj == p.obj && r.act == p.act && r.obj != 'keyMatch(r_dom, p_dom)'"); + Enforcer e = new Enforcer(m); + e.addPolicy("admin", "tenantA", "data", "read"); + e.addGroupingPolicy("alice", "admin", "tenant*"); + e.addGroupingPolicy("bob", "admin", "tenantA"); + + assertFalse(e.enforce("alice", "tenantA", "data", "read")); + assertTrue(e.enforce("bob", "tenantA", "data", "read")); + } + + @Test + public void testConditionalDomainMatchingPreservesConditions() { + Model m = new Model(); + m.addDef("r", "r", "sub, dom, obj, act"); + m.addDef("p", "p", "sub, dom, obj, act"); + m.addDef("g", "g", "_, _, _, (_, _)"); + m.addDef("e", "e", "some(where (p.eft == allow))"); + m.addDef("m", "m", "g(r.sub, p.sub, r.dom) && r.dom == p.dom && r.obj == p.obj && r.act == p.act"); + Enforcer e = new Enforcer(m); + e.addPolicy("admin", "tenantA", "data", "read"); + e.addPolicy("admin", "tenantB", "data", "read"); + e.addGroupingPolicy("alice", "admin", "tenant*", "deny", "_"); + e.addGroupingPolicy("bob", "reader", "tenantA", "_", "_"); + e.addNamedDomainLinkConditionFunc("g", "alice", "admin", "tenant*", params -> "allow".equals(params[0])); + e.setNamedDomainLinkConditionFuncParams("g", "alice", "admin", "tenant*", "deny", "_"); + + assertFalse(e.enforce("alice", "tenantA", "data", "read")); + e.addNamedDomainMatchingFunc("g", "keyMatch", BuiltInFunctions::keyMatch); + assertFalse(e.enforce("alice", "tenantA", "data", "read")); + assertFalse(e.enforce("alice", "tenantB", "data", "read")); + + e.setNamedDomainLinkConditionFuncParams("g", "alice", "admin", "tenant*", "allow", "_"); + assertTrue(e.enforce("alice", "tenantA", "data", "read")); + assertTrue(e.enforce("alice", "tenantB", "data", "read")); + + e.addNamedDomainMatchingFunc("g", "keyMatch", BuiltInFunctions::keyMatch); + assertTrue(e.enforce("alice", "tenantA", "data", "read")); + assertTrue(e.enforce("alice", "tenantB", "data", "read")); + + e.setNamedDomainLinkConditionFuncParams("g", "alice", "admin", "tenant*", "deny", "_"); + assertFalse(e.enforce("alice", "tenantA", "data", "read")); + assertFalse(e.enforce("alice", "tenantB", "data", "read")); + } + @Test public void testRBACModelWithDomainsAtRuntime() { Enforcer e = new Enforcer("examples/rbac_with_domains_model.conf"); --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
