This is an automated email from the ASF dual-hosted git repository.

hsluoyz pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/casbin-jcasbin.git


The following commit(s) were added to refs/heads/master by this push:
     new 949c228  fix: match Go in regexMatch, in lists and conditional domain 
roles
949c228 is described below

commit 949c228c96d4a15306553ca48c0cfc55314ffde0
Author: Yang Luo <[email protected]>
AuthorDate: Wed Oct 21 01:49:29 2026 +0800

    fix: match Go in regexMatch, in lists and conditional domain roles
---
 .github/workflows/maven-ci.yml                     |   9 +
 .../java/org/casbin/jcasbin/main/CoreEnforcer.java |  39 +++--
 .../java/org/casbin/jcasbin/model/Assertion.java   |   2 +-
 .../jcasbin/rbac/ConditionalDomainManager.java     | 194 +++++++++++++++++++++
 .../jcasbin/rbac/ConditionalRoleManager.java       |  36 ++++
 src/main/java/org/casbin/jcasbin/rbac/Role.java    |  17 ++
 .../org/casbin/jcasbin/util/BuiltInFunctions.java  |   2 +-
 src/main/java/org/casbin/jcasbin/util/Util.java    |   4 +-
 .../org/casbin/jcasbin/main/ConformanceTest.java   |  83 +++++++++
 9 files changed, 368 insertions(+), 18 deletions(-)

diff --git a/.github/workflows/maven-ci.yml b/.github/workflows/maven-ci.yml
index ea00f58..55369ad 100644
--- a/.github/workflows/maven-ci.yml
+++ b/.github/workflows/maven-ci.yml
@@ -35,6 +35,13 @@ jobs:
         with:
           fetch-depth: '0'
 
+      - name: Checkout conformance cases
+        uses: actions/checkout@v4
+        with:
+          repository: apache/casbin
+          path: casbin-conformance
+          sparse-checkout: conformance
+
       - name: Set up JDK 8
         uses: actions/setup-java@v4
         with:
@@ -46,6 +53,8 @@ jobs:
 
       - name: Build with Maven
         run: mvn -B -ntp clean verify cobertura:cobertura -Dgpg.skip=true
+        env:
+          CASBIN_CONFORMANCE_DIR: ${{ github.workspace 
}}/casbin-conformance/conformance
 
       - name: Test release verification
         run: python3 -m unittest discover -s .github/scripts -p 'test_*.py'
diff --git a/src/main/java/org/casbin/jcasbin/main/CoreEnforcer.java 
b/src/main/java/org/casbin/jcasbin/main/CoreEnforcer.java
index d6079f3..e49b1e1 100644
--- a/src/main/java/org/casbin/jcasbin/main/CoreEnforcer.java
+++ b/src/main/java/org/casbin/jcasbin/main/CoreEnforcer.java
@@ -397,27 +397,33 @@ public class CoreEnforcer {
                     assertion.rm = new DomainManager(10);
                     rmMap.put(ptype, assertion.rm);
                 } else {
-                    assertion.condRM = new ConditionalRoleManager(10);
+                    assertion.condRM = new ConditionalDomainManager(10);
                     condRmMap.put(ptype, assertion.condRM);
                     rmMap.put(ptype, assertion.condRM);
                 }
-                String matchFun = "keyMatch(r_dom, p_dom)";
-                if (model.model.get("m").get("m").value.contains(matchFun)) {
-                    addNamedDomainMatchingFunc(ptype, "g", 
BuiltInFunctions::keyMatch);
-                }
+                registerDomainMatchingFunc(ptype);
             }
         }
     }
 
+    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) {
+            return;
+        }
+        String matchFun = "keyMatch(" + r.tokens[1] + ", " + p.tokens[1] + ")";
+        if (model.model.get("m").get("m").value.contains(matchFun)) {
+            addNamedDomainMatchingFunc(ptype, "g", BuiltInFunctions::keyMatch);
+        }
+    }
+
     /**
      * add or update the DomainManager object in rmMap and associate it with a 
specific domain matching function
      */
     private void addOrUpdateDomainManagerMatching(String ptype) {
         rmMap.put(ptype, new DomainManager(10));
-        String matchFun = "keyMatch(r_dom, p_dom)";
-        if (model.model.get("m").get("m").value.contains(matchFun)) {
-            addNamedDomainMatchingFunc(ptype, "g", BuiltInFunctions::keyMatch);
-        }
+        registerDomainMatchingFunc(ptype);
     }
 
     private void initBuiltInFunction() {
@@ -812,16 +818,19 @@ public class CoreEnforcer {
      * @return whether the matching function was successfully added.
      */
     public boolean addNamedDomainMatchingFunc(String ptype, String name, 
BiPredicate<String, String> fn) {
-        if (rmMap.containsKey(ptype)) {
-            DomainManager rm = (DomainManager) rmMap.get(ptype);
-            rm.addDomainMatchingFunc(name, fn);
+        RoleManager rm = rmMap.get(ptype);
+        if (rm instanceof DomainManager) {
+            ((DomainManager) rm).addDomainMatchingFunc(name, fn);
+        } else if (rm instanceof DefaultRoleManager) {
+            ((DefaultRoleManager) rm).addDomainMatchingFunc(name, fn);
+        } else {
+            return false;
+        }
 //            clearRmMap();
 //            if (autoBuildRoleLinks) {
 //                buildRoleLinks();
 //            }
-            return true;
-        }
-        return false;
+        return true;
     }
 
     /**
diff --git a/src/main/java/org/casbin/jcasbin/model/Assertion.java 
b/src/main/java/org/casbin/jcasbin/model/Assertion.java
index 194c347..ccef6b3 100644
--- a/src/main/java/org/casbin/jcasbin/model/Assertion.java
+++ b/src/main/java/org/casbin/jcasbin/model/Assertion.java
@@ -181,7 +181,7 @@ public class Assertion {
             rm = condRM;
         }else {
             String domain = domainRule.get(0);
-            condRM.addLink(rule.get(0), rule.get(1));
+            condRM.addLink(rule.get(0), rule.get(1), domain);
             condRM.setDomainLinkConditionFuncParams(rule.get(0), rule.get(1), 
domain, rule.subList(tokens.length, rule.size()).toArray(new String[0]));
             rm = condRM;
         }
diff --git 
a/src/main/java/org/casbin/jcasbin/rbac/ConditionalDomainManager.java 
b/src/main/java/org/casbin/jcasbin/rbac/ConditionalDomainManager.java
new file mode 100644
index 0000000..7d09a0b
--- /dev/null
+++ b/src/main/java/org/casbin/jcasbin/rbac/ConditionalDomainManager.java
@@ -0,0 +1,194 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you 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
+//
+//     https://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.rbac;
+
+import org.casbin.jcasbin.util.SyncedLRUCache;
+import org.casbin.jcasbin.util.Util;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.function.BiPredicate;
+import java.util.function.Function;
+
+public class ConditionalDomainManager extends ConditionalRoleManager {
+    private static final String DEFAULT_DOMAIN = "casbin::default";
+    private Map<String, ConditionalRoleManager> rmMap;
+    private SyncedLRUCache<String, Boolean> domainMatchingFuncCache;
+
+    public ConditionalDomainManager(int maxHierarchyLevel) {
+        super(maxHierarchyLevel);
+    }
+
+    @Override
+    public void addMatchingFunc(String name, BiPredicate<String, String> 
matchingFunc) {
+        this.matchingFunc = matchingFunc;
+        this.rmMap.values().forEach(rm -> rm.addMatchingFunc(name, 
matchingFunc));
+    }
+
+    @Override
+    public void addDomainMatchingFunc(String name, BiPredicate<String, String> 
domainMatchingFunc) {
+        this.domainMatchingFunc = domainMatchingFunc;
+        rebuild();
+    }
+
+    private void rebuild() {
+        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));
+            });
+        });
+        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));
+            });
+        });
+    }
+
+    private String domainName(String... domain) {
+        return domain.length == 0 ? DEFAULT_DOMAIN : domain[0];
+    }
+
+    private ConditionalRoleManager getRoleManager(String domain, boolean 
store) {
+        ConditionalRoleManager rm = this.rmMap.get(domain);
+        if (rm == null) {
+            rm = new ConditionalRoleManager(this.maxHierarchyLevel, 
this.matchingFunc, this.domainMatchingFunc);
+            if (store) {
+                this.rmMap.put(domain, rm);
+            }
+            if (this.domainMatchingFunc != null) {
+                for (Map.Entry<String, ConditionalRoleManager> entry : 
this.rmMap.entrySet()) {
+                    String domain2 = entry.getKey();
+                    if (!domain.equals(domain2) && matchDomain(domain, 
domain2)) {
+                        rm.copyFrom(entry.getValue());
+                    }
+                }
+            }
+        }
+        return rm;
+    }
+
+    private boolean matchDomain(String str, String pattern) {
+        String cacheKey = String.join("$$", str, pattern);
+        Boolean matched = this.domainMatchingFuncCache.get(cacheKey);
+        if (matched == null) {
+            if (this.domainMatchingFunc != null) {
+                matched = this.domainMatchingFunc.test(str, pattern);
+            } else {
+                matched = str.equals(pattern);
+            }
+            this.domainMatchingFuncCache.put(cacheKey, matched);
+        }
+        return matched;
+    }
+
+    @Override
+    public void clear() {
+        super.clear();
+        this.rmMap = new HashMap<>();
+        this.domainMatchingFuncCache = new SyncedLRUCache<>(100);
+    }
+
+    @Override
+    public void addLink(String name1, String name2, String... domain) {
+        String domainName = domainName(domain);
+        getRoleManager(domainName, true).addLink(name1, name2, domain);
+
+        if (this.domainMatchingFunc != null) {
+            this.rmMap.forEach((domain2, rm) -> {
+                if (!domainName.equals(domain2) && matchDomain(domain2, 
domainName)) {
+                    rm.addLink(name1, name2, domain);
+                }
+            });
+        }
+    }
+
+    @Override
+    public void deleteLink(String name1, String name2, String... domain) {
+        String domainName = domainName(domain);
+        getRoleManager(domainName, true).deleteLink(name1, name2, domain);
+
+        if (this.domainMatchingFunc != null) {
+            this.rmMap.forEach((domain2, rm) -> {
+                if (!domainName.equals(domain2) && matchDomain(domain2, 
domainName)) {
+                    rm.deleteLink(name1, name2, domain);
+                }
+            });
+        }
+    }
+
+    @Override
+    public boolean hasLink(String name1, String name2, String... domain) {
+        return getRoleManager(domainName(domain), false).hasLink(name1, name2, 
domain);
+    }
+
+    @Override
+    public List<String> getRoles(String name, String... domain) {
+        return getRoleManager(domainName(domain), false).getRoles(name, 
domain);
+    }
+
+    @Override
+    public List<String> getUsers(String name, String... domain) {
+        return getRoleManager(domainName(domain), false).getUsers(name, 
domain);
+    }
+
+    @Override
+    public void addLinkConditionFunc(String userName, String roleName, 
Function<String[], Boolean> fn) {
+        this.rmMap.values().forEach(rm -> rm.addLinkConditionFunc(userName, 
roleName, fn));
+    }
+
+    @Override
+    public void addDomainLinkConditionFunc(String userName, String roleName, 
String domain, Function<String[], Boolean> fn) {
+        getRoleManager(domain, true);
+        this.rmMap.values().forEach(rm -> 
rm.addDomainLinkConditionFunc(userName, roleName, domain, fn));
+    }
+
+    @Override
+    public void setLinkConditionFuncParams(String userName, String roleName, 
String... params) {
+        this.rmMap.values().forEach(rm -> 
rm.setLinkConditionFuncParams(userName, roleName, params));
+    }
+
+    @Override
+    public void setDomainLinkConditionFuncParams(String userName, String 
roleName, String domain, String... params) {
+        this.rmMap.values().forEach(rm -> 
rm.setDomainLinkConditionFuncParams(userName, roleName, domain, params));
+    }
+
+    @Override
+    public String toString() {
+        List<String> roles = new ArrayList<>();
+        this.rmMap.forEach((domain, rm) -> {
+            List<String> domainRoles = new ArrayList<>();
+            rm.allRoles.values().forEach(role -> {
+                if (!"".equals(role.toString())) {
+                    domainRoles.add(role.toString());
+                }
+            });
+            roles.add(domain + ": " + String.join(", ", domainRoles));
+        });
+        return String.join("\n", roles);
+    }
+
+    @Override
+    public void printRoles() {
+        Util.logPrintfInfo("{}", this);
+    }
+}
diff --git a/src/main/java/org/casbin/jcasbin/rbac/ConditionalRoleManager.java 
b/src/main/java/org/casbin/jcasbin/rbac/ConditionalRoleManager.java
index 5f616ef..6ab5dc3 100644
--- a/src/main/java/org/casbin/jcasbin/rbac/ConditionalRoleManager.java
+++ b/src/main/java/org/casbin/jcasbin/rbac/ConditionalRoleManager.java
@@ -23,12 +23,24 @@ import java.util.function.Consumer;
 import java.util.function.Function;
 
 public class ConditionalRoleManager extends DefaultRoleManager{
+    BiPredicate<String, String> domainMatchingFunc;
+
     public ConditionalRoleManager(int maxHierarchyLevel) {
         super(maxHierarchyLevel);
     }
 
     public ConditionalRoleManager(int maxHierarchyLevel, BiPredicate<String, 
String> matchingFunc, BiPredicate<String, String> domainMatchingFunc) {
         super(maxHierarchyLevel, matchingFunc, domainMatchingFunc);
+        this.domainMatchingFunc = domainMatchingFunc;
+    }
+
+    void copyFrom(ConditionalRoleManager other) {
+        other.allRoles.values().forEach(user -> {
+            user.roles.keySet().forEach(roleName -> {
+                addLink(user.getName(), roleName);
+                getRole(user.getName()).copyLinkConditions(user, roleName);
+            });
+        });
     }
 
     public synchronized boolean hasLink(String name1, String name2, String... 
domains) {
@@ -99,6 +111,12 @@ public class ConditionalRoleManager extends 
DefaultRoleManager{
                 } catch (Exception e) {
                     err = e;
                 }
+            } else if (domainMatchingFunc != null) {
+                try {
+                    passLinkConditionFunc = 
checkPatternLinkCondition(currentRole.getName(), nextRole.getName(), 
domains[0]);
+                } catch (Exception e) {
+                    err = e;
+                }
             }
         }
 
@@ -115,6 +133,24 @@ public class ConditionalRoleManager extends 
DefaultRoleManager{
         return true;
     }
 
+    private boolean checkPatternLinkCondition(String name1, String name2, 
String domain) {
+        Role user = this.allRoles.get(name1);
+        if (user == null) {
+            return true;
+        }
+        for (Map.Entry<LinkConditionFuncKey, Function<String[], Boolean>> 
entry : user.getLinkConditionFuncMap().entrySet()) {
+            LinkConditionFuncKey key = entry.getKey();
+            if (!key.getRoleName().equals(name2) || 
key.getDomainName().equals(domain) || !domainMatchingFunc.test(domain, 
key.getDomainName())) {
+                continue;
+            }
+            List<String> params = getLinkConditionFuncParams(name1, name2, new 
String[]{key.getDomainName()});
+            if (!entry.getValue().apply(params.toArray(new String[0]))) {
+                return false;
+            }
+        }
+        return true;
+    }
+
     /**
      * getLinkConditionFunc get LinkConditionFunc based on userName, roleName
      *
diff --git a/src/main/java/org/casbin/jcasbin/rbac/Role.java 
b/src/main/java/org/casbin/jcasbin/rbac/Role.java
index 03d4d2c..9353dc2 100644
--- a/src/main/java/org/casbin/jcasbin/rbac/Role.java
+++ b/src/main/java/org/casbin/jcasbin/rbac/Role.java
@@ -169,6 +169,23 @@ class Role {
         return linkConditionFuncMap.get(new LinkConditionFuncKey(role.name, 
domain));
     }
 
+    Map<LinkConditionFuncKey, Function<String[], Boolean>> 
getLinkConditionFuncMap() {
+        return linkConditionFuncMap;
+    }
+
+    void copyLinkConditions(Role other, String roleName) {
+        other.linkConditionFuncMap.forEach((key, fn) -> {
+            if (key.getRoleName().equals(roleName)) {
+                linkConditionFuncMap.put(key, fn);
+            }
+        });
+        other.linkConditionFuncParamsMap.forEach((key, params) -> {
+            if (key.getRoleName().equals(roleName)) {
+                linkConditionFuncParamsMap.put(key, params);
+            }
+        });
+    }
+
     void setLinkConditionFuncParams(Role role, String domain, String... 
params){
         linkConditionFuncParamsMap.put(new LinkConditionFuncKey(role.name, 
domain), params);
     }
diff --git a/src/main/java/org/casbin/jcasbin/util/BuiltInFunctions.java 
b/src/main/java/org/casbin/jcasbin/util/BuiltInFunctions.java
index 7cb39b0..318d5b5 100644
--- a/src/main/java/org/casbin/jcasbin/util/BuiltInFunctions.java
+++ b/src/main/java/org/casbin/jcasbin/util/BuiltInFunctions.java
@@ -321,7 +321,7 @@ public class BuiltInFunctions {
      * @return whether key1 matches key2.
      */
     public static boolean regexMatch(String key1, String key2) {
-        return Pattern.compile(key2).matcher(key1).lookingAt();
+        return Pattern.compile(key2).matcher(key1).find();
     }
 
     /**
diff --git a/src/main/java/org/casbin/jcasbin/util/Util.java 
b/src/main/java/org/casbin/jcasbin/util/Util.java
index 0d70645..fb52d0f 100644
--- a/src/main/java/org/casbin/jcasbin/util/Util.java
+++ b/src/main/java/org/casbin/jcasbin/util/Util.java
@@ -38,7 +38,7 @@ public class Util {
     public static boolean enableLog = true;
     private static Pattern evalReg = Pattern.compile("\\beval\\(([^),]*)\\)");
 
-    private static final Pattern IN_SYNTAX_PATTERN = 
Pattern.compile("([^\\s]+?) +in +(\\([^)]*\\)|\\S+)");
+    private static final Pattern IN_SYNTAX_PATTERN = 
Pattern.compile("([^\\s]+?) +in +(\\([^)]*\\)|\\[[^\\]]*\\]|\\S+)");
 
     private static Pattern escapeAssertionRegex = 
Pattern.compile("\\b(r|p)[0-9]*\\.");
 
@@ -217,6 +217,8 @@ public class Util {
             // convert to tuple() function. Otherwise, keep parentheses as-is 
for grouping.
             if (rightOperand.startsWith("(") && rightOperand.endsWith(")") && 
rightOperand.contains(",")) {
                 rightOperand = "tuple" + rightOperand;
+            } else if (rightOperand.startsWith("[") && 
rightOperand.endsWith("]")) {
+                rightOperand = "tuple(" + rightOperand.substring(1, 
rightOperand.length() - 1) + ")";
             }
             
             matcher.appendReplacement(sb, Matcher.quoteReplacement("include(" 
+ rightOperand + ", " + leftOperand + ")"));
diff --git a/src/test/java/org/casbin/jcasbin/main/ConformanceTest.java 
b/src/test/java/org/casbin/jcasbin/main/ConformanceTest.java
new file mode 100644
index 0000000..9246132
--- /dev/null
+++ b/src/test/java/org/casbin/jcasbin/main/ConformanceTest.java
@@ -0,0 +1,83 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you 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
+//
+//     https://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.main;
+
+import com.google.gson.Gson;
+import org.testng.SkipException;
+import org.testng.annotations.Test;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+import static org.testng.Assert.assertTrue;
+
+public class ConformanceTest {
+    static class ConformanceCase {
+        String model;
+        String policy;
+        List<ConformanceRequest> tests;
+    }
+
+    static class ConformanceRequest {
+        List<String> request;
+        boolean allow;
+        List<String> explain;
+    }
+
+    @Test
+    public void testConformance() throws IOException {
+        String dir = System.getenv("CASBIN_CONFORMANCE_DIR");
+        if (dir == null || dir.isEmpty()) {
+            throw new SkipException("CASBIN_CONFORMANCE_DIR is not set");
+        }
+
+        File[] files = new File(dir, "cases").listFiles((d, name) -> 
name.endsWith(".json"));
+        assertTrue(files != null && files.length > 0, "no conformance cases 
found in " + dir);
+        Arrays.sort(files);
+
+        List<String> failures = new ArrayList<>();
+        for (File file : files) {
+            ConformanceCase c = new Gson().fromJson(new 
String(Files.readAllBytes(file.toPath()), StandardCharsets.UTF_8), 
ConformanceCase.class);
+            Enforcer e;
+            try {
+                e = new Enforcer(new File(dir, "data/" + c.model).getPath(), 
new File(dir, "data/" + c.policy).getPath());
+            } catch (Exception ex) {
+                failures.add(file.getName() + ": " + ex);
+                continue;
+            }
+            for (ConformanceRequest test : c.tests) {
+                try {
+                    EnforceResult result = e.enforceEx(test.request.toArray());
+                    List<String> explain = result.getExplain() == null ? 
Collections.emptyList() : result.getExplain();
+                    if (result.isAllow() != test.allow || 
!explain.equals(test.explain)) {
+                        failures.add(file.getName() + " " + test.request + ": 
got (" + result.isAllow() + ", " + explain + "), want (" + test.allow + ", " + 
test.explain + ")");
+                    }
+                } catch (Exception ex) {
+                    failures.add(file.getName() + " " + test.request + ": " + 
ex);
+                }
+            }
+        }
+        assertTrue(failures.isEmpty(), failures.size() + " conformance 
failures:\n" + String.join("\n", failures));
+    }
+}


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to