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]

Reply via email to