From 850dda91cefa7815b1ac36014cf6d742c795735f Mon Sep 17 00:00:00 2001 From: Victor Yang Date: Fri, 24 Apr 2026 10:41:37 +0800 Subject: [PATCH 1/3] feat(management): add getUsers API to retrieve non-role subjects - add getUsers() method to ManagementEnforcer that returns subjects excluding roles - add getValuesForFieldInPolicyAllTypes() and getFieldIndex() to Model for subject extraction - update test imports and organization in ManagementAPIUnitTest --- .../jcasbin/main/ManagementEnforcer.java | 38 ++++++++++++++ .../casbin/jcasbin/main/SyncedEnforcer.java | 23 +++++++++ .../java/org/casbin/jcasbin/model/Model.java | 50 +++++++++++++++++++ .../java/org/casbin/jcasbin/util/Util.java | 25 ++++++++++ .../jcasbin/main/ManagementAPIUnitTest.java | 47 +++++++++++++++++ 5 files changed, 183 insertions(+) diff --git a/src/main/java/org/casbin/jcasbin/main/ManagementEnforcer.java b/src/main/java/org/casbin/jcasbin/main/ManagementEnforcer.java index 1a21dded..bae18455 100644 --- a/src/main/java/org/casbin/jcasbin/main/ManagementEnforcer.java +++ b/src/main/java/org/casbin/jcasbin/main/ManagementEnforcer.java @@ -125,6 +125,44 @@ public List getAllNamedRoles(String ptype) { return model.getValuesForFieldInPolicy("g", ptype, 1); } + /** + * getUsers gets the list of users that show up in the current policy. + * Users are subjects that are not roles. + * + * @return all users in policy and grouping rules, excluding roles. + */ + public List getUsers() { + List pSubjects = getAllSubjects(); + List gSubjects = model.getValuesForFieldInPolicyAllTypes("g", 0); + List roles = getAllRoles(); + + List allSubjects = new ArrayList<>(pSubjects); + allSubjects.addAll(gSubjects); + allSubjects = Util.arrayRemoveDuplicates(allSubjects); + + return Util.setSubtract(allSubjects, roles); + } + + /** + * getNamedUsers gets the list of users that show up in the current named policy. + * Users are subjects that are not roles. + * + * @param ptype the policy type, can be "p", "p2", "p3", .. + * @return all users in the specified policy type, excluding roles. + */ + public List getNamedUsers(String ptype) { + int subjectIndex = model.getFieldIndex(ptype, "sub"); + List pSubjects = model.getValuesForFieldInPolicy("p", ptype, subjectIndex); + List gSubjects = model.getValuesForFieldInPolicyAllTypes("g", 0); + List roles = getAllRoles(); + + List allSubjects = new ArrayList<>(pSubjects); + allSubjects.addAll(gSubjects); + allSubjects = Util.arrayRemoveDuplicates(allSubjects); + + return Util.setSubtract(allSubjects, roles); + } + /** * getPolicy gets all the authorization rules in the policy. * diff --git a/src/main/java/org/casbin/jcasbin/main/SyncedEnforcer.java b/src/main/java/org/casbin/jcasbin/main/SyncedEnforcer.java index c59871de..0ef62c2f 100644 --- a/src/main/java/org/casbin/jcasbin/main/SyncedEnforcer.java +++ b/src/main/java/org/casbin/jcasbin/main/SyncedEnforcer.java @@ -405,6 +405,29 @@ public List getAllNamedRoles(String ptype) { return runSynchronized(() -> super.getAllNamedRoles(ptype), getReadWriteLock().readLock()); } + /** + * getUsers gets the list of users that show up in the current policy. + * Users are subjects that are not roles. + * + * @return all users in policy and grouping rules, excluding roles. + */ + @Override + public List getUsers() { + return runSynchronized(super::getUsers, getReadWriteLock().readLock()); + } + + /** + * getNamedUsers gets the list of users that show up in the current named policy. + * Users are subjects that are not roles. + * + * @param ptype the policy type, can be "p", "p2", "p3", .. + * @return all users in the specified policy type, excluding roles. + */ + @Override + public List getNamedUsers(String ptype) { + return runSynchronized(() -> super.getNamedUsers(ptype), getReadWriteLock().readLock()); + } + /** * getPolicy gets all the authorization rules in the policy. * diff --git a/src/main/java/org/casbin/jcasbin/model/Model.java b/src/main/java/org/casbin/jcasbin/model/Model.java index 69187db7..9b44dce5 100644 --- a/src/main/java/org/casbin/jcasbin/model/Model.java +++ b/src/main/java/org/casbin/jcasbin/model/Model.java @@ -470,4 +470,54 @@ private void writeString(StringBuilder s, String sec, Map tokenP s.append(String.format("%s = %s\n", sec, value)); } } + + /** + * getValuesForFieldInPolicyAllTypes gets all values for a field for all rules + * across all policy types in a section. Duplicated values are removed. + * + * @param sec the section, "p" or "g". + * @param fieldIndex the policy rule's index. + * @return all field values across all ptypes. + */ + public List getValuesForFieldInPolicyAllTypes(String sec, int fieldIndex) { + List values = new ArrayList<>(); + + Map section = model.get(sec); + if (section == null) { + return values; + } + + for (Assertion assertion : section.values()) { + for (List rule : assertion.policy) { + if (fieldIndex < rule.size()) { + values.add(rule.get(fieldIndex)); + } + } + } + + return Util.arrayRemoveDuplicates(values); + } + + /** + * getFieldIndex returns the index of a field in a policy type. + * For example, given ptype="p" and field="sub", returns the index + * where "p_sub" appears in the tokens array. + * + * @param ptype the policy type, e.g., "p", "p2" + * @param field the field name, e.g., "sub", "obj", "act" + * @return the index of the field in the policy rule, or 0 if not found + */ + public int getFieldIndex(String ptype, String field) { + String pattern = ptype + "_" + field; + Assertion ast = model.get("p").get(ptype); + if (ast == null || ast.tokens == null) { + return 0; + } + for (int i = 0; i < ast.tokens.length; i++) { + if (pattern.equals(ast.tokens[i])) { + return i; + } + } + return 0; + } } diff --git a/src/main/java/org/casbin/jcasbin/util/Util.java b/src/main/java/org/casbin/jcasbin/util/Util.java index 4912993e..6df49c9c 100644 --- a/src/main/java/org/casbin/jcasbin/util/Util.java +++ b/src/main/java/org/casbin/jcasbin/util/Util.java @@ -388,4 +388,29 @@ public static boolean isJsonString(String str) { return false; } } + + /** + * setSubtract returns elements in list A that are not in list B. + * Preserves order from list A. + * + * @param a the first list (base) + * @param b the second list (subtrahend) + * @return a - b (elements in A but not in B) + */ + public static List setSubtract(List a, List b) { + if (a == null) { + a = new ArrayList<>(); + } + if (b == null) { + b = new ArrayList<>(); + } + Set bSet = new HashSet<>(b); + List result = new ArrayList<>(); + for (String item : a) { + if (!bSet.contains(item)) { + result.add(item); + } + } + return result; + } } diff --git a/src/test/java/org/casbin/jcasbin/main/ManagementAPIUnitTest.java b/src/test/java/org/casbin/jcasbin/main/ManagementAPIUnitTest.java index 54c0aadb..7006f7eb 100644 --- a/src/test/java/org/casbin/jcasbin/main/ManagementAPIUnitTest.java +++ b/src/test/java/org/casbin/jcasbin/main/ManagementAPIUnitTest.java @@ -16,6 +16,7 @@ import com.googlecode.aviator.AviatorEvaluator; import com.googlecode.aviator.AviatorEvaluatorInstance; +import org.casbin.jcasbin.util.Util; import org.testng.Assert; import org.testng.annotations.Test; @@ -298,4 +299,50 @@ public void should_true_when_setAviatorEvaluator_given_customInstance() { assertEquals(enforcer.getAviatorEval(), instance); } + @Test + public void testGetUsersAPI() { + Enforcer e = new Enforcer("examples/rbac_model.conf", "examples/rbac_policy.csv"); + + testGetAllSubjectsUtil(e, asList("alice", "bob", "data2_admin")); + testGetAllRolesUtil(e, asList("data2_admin")); + testGetUsersUtil(e, asList("alice", "bob")); + testGetNamedUsersUtil(e, "p", asList("alice", "bob")); + } + + private void testGetAllSubjectsUtil(Enforcer enforcer, List res) { + List myRes = enforcer.getAllSubjects(); + Util.logPrint("All subjects: " + myRes); + + if (!Util.setEquals(res, myRes)) { + Assert.fail("All subjects: " + myRes + ", supposed to be " + res); + } + } + + private void testGetAllRolesUtil(Enforcer enforcer, List res) { + List myRes = enforcer.getAllRoles(); + Util.logPrint("All roles: " + myRes); + + if (!Util.setEquals(res, myRes)) { + Assert.fail("All roles: " + myRes + ", supposed to be " + res); + } + } + + private void testGetUsersUtil(Enforcer enforcer, List res) { + List myRes = enforcer.getUsers(); + Util.logPrint("Users: " + myRes); + + if (!Util.setEquals(res, myRes)) { + Assert.fail("Users: " + myRes + ", supposed to be " + res); + } + } + + private void testGetNamedUsersUtil(Enforcer enforcer, String ptype, List res) { + List myRes = enforcer.getNamedUsers(ptype); + Util.logPrint("Named users (" + ptype + "): " + myRes); + + if (!Util.setEquals(res, myRes)) { + Assert.fail("Named users (" + ptype + "): " + myRes + ", supposed to be " + res); + } + } + } From 53a546e46edfb82279586975ed8b027677751e36 Mon Sep 17 00:00:00 2001 From: Victor Yang Date: Fri, 24 Apr 2026 20:13:57 +0800 Subject: [PATCH 2/3] fix(management): use final Go logic for getUsers/getNamedUsers Users = subjects from "p" - roles from "g" (replacing early-branch logic that included g column 0) --- .../jcasbin/main/ManagementEnforcer.java | 25 ++++++------------- 1 file changed, 7 insertions(+), 18 deletions(-) diff --git a/src/main/java/org/casbin/jcasbin/main/ManagementEnforcer.java b/src/main/java/org/casbin/jcasbin/main/ManagementEnforcer.java index bae18455..1c545257 100644 --- a/src/main/java/org/casbin/jcasbin/main/ManagementEnforcer.java +++ b/src/main/java/org/casbin/jcasbin/main/ManagementEnforcer.java @@ -127,20 +127,15 @@ public List getAllNamedRoles(String ptype) { /** * getUsers gets the list of users that show up in the current policy. - * Users are subjects that are not roles. + * Users are subjects that are not roles (i.e., subjects that do not appear + * as the second element in any grouping policy). * - * @return all users in policy and grouping rules, excluding roles. + * @return all users in policy, excluding roles. */ public List getUsers() { - List pSubjects = getAllSubjects(); - List gSubjects = model.getValuesForFieldInPolicyAllTypes("g", 0); + List subjects = getAllSubjects(); List roles = getAllRoles(); - - List allSubjects = new ArrayList<>(pSubjects); - allSubjects.addAll(gSubjects); - allSubjects = Util.arrayRemoveDuplicates(allSubjects); - - return Util.setSubtract(allSubjects, roles); + return Util.setSubtract(subjects, roles); } /** @@ -152,15 +147,9 @@ public List getUsers() { */ public List getNamedUsers(String ptype) { int subjectIndex = model.getFieldIndex(ptype, "sub"); - List pSubjects = model.getValuesForFieldInPolicy("p", ptype, subjectIndex); - List gSubjects = model.getValuesForFieldInPolicyAllTypes("g", 0); + List subjects = model.getValuesForFieldInPolicy("p", ptype, subjectIndex); List roles = getAllRoles(); - - List allSubjects = new ArrayList<>(pSubjects); - allSubjects.addAll(gSubjects); - allSubjects = Util.arrayRemoveDuplicates(allSubjects); - - return Util.setSubtract(allSubjects, roles); + return Util.setSubtract(subjects, roles); } /** From ef2901ce71e9c57d367fea3e8847ed42e3e71d64 Mon Sep 17 00:00:00 2001 From: Victor Yang Date: Sat, 25 Apr 2026 04:34:59 +0800 Subject: [PATCH 3/3] fix: sync GetAllUsers API from Go Casbin (#1621) - Rename getUsers() to getAllUsers() to align with Go API - Remove getNamedUsers() as Go final version doesn't have it - Fix Model.getFieldIndex() to return -1 when not found (instead of 0) - Add null check in Model.getFieldIndex() to prevent NPE - Update Javadoc to accurately describe behavior - Update tests accordingly --- .../jcasbin/main/ManagementEnforcer.java | 18 +----- .../casbin/jcasbin/main/SyncedEnforcer.java | 20 ++----- .../java/org/casbin/jcasbin/model/Model.java | 12 ++-- .../jcasbin/main/ManagementAPIUnitTest.java | 56 +++++++++++++------ 4 files changed, 54 insertions(+), 52 deletions(-) diff --git a/src/main/java/org/casbin/jcasbin/main/ManagementEnforcer.java b/src/main/java/org/casbin/jcasbin/main/ManagementEnforcer.java index 1c545257..c6efc465 100644 --- a/src/main/java/org/casbin/jcasbin/main/ManagementEnforcer.java +++ b/src/main/java/org/casbin/jcasbin/main/ManagementEnforcer.java @@ -126,32 +126,18 @@ public List getAllNamedRoles(String ptype) { } /** - * getUsers gets the list of users that show up in the current policy. + * getAllUsers gets the list of users that show up in the current policy. * Users are subjects that are not roles (i.e., subjects that do not appear * as the second element in any grouping policy). * * @return all users in policy, excluding roles. */ - public List getUsers() { + public List getAllUsers() { List subjects = getAllSubjects(); List roles = getAllRoles(); return Util.setSubtract(subjects, roles); } - /** - * getNamedUsers gets the list of users that show up in the current named policy. - * Users are subjects that are not roles. - * - * @param ptype the policy type, can be "p", "p2", "p3", .. - * @return all users in the specified policy type, excluding roles. - */ - public List getNamedUsers(String ptype) { - int subjectIndex = model.getFieldIndex(ptype, "sub"); - List subjects = model.getValuesForFieldInPolicy("p", ptype, subjectIndex); - List roles = getAllRoles(); - return Util.setSubtract(subjects, roles); - } - /** * getPolicy gets all the authorization rules in the policy. * diff --git a/src/main/java/org/casbin/jcasbin/main/SyncedEnforcer.java b/src/main/java/org/casbin/jcasbin/main/SyncedEnforcer.java index 0ef62c2f..9e0f9c25 100644 --- a/src/main/java/org/casbin/jcasbin/main/SyncedEnforcer.java +++ b/src/main/java/org/casbin/jcasbin/main/SyncedEnforcer.java @@ -406,26 +406,14 @@ public List getAllNamedRoles(String ptype) { } /** - * getUsers gets the list of users that show up in the current policy. + * getAllUsers gets the list of users that show up in the current policy. * Users are subjects that are not roles. * - * @return all users in policy and grouping rules, excluding roles. + * @return all users in policy rules, excluding roles. */ @Override - public List getUsers() { - return runSynchronized(super::getUsers, getReadWriteLock().readLock()); - } - - /** - * getNamedUsers gets the list of users that show up in the current named policy. - * Users are subjects that are not roles. - * - * @param ptype the policy type, can be "p", "p2", "p3", .. - * @return all users in the specified policy type, excluding roles. - */ - @Override - public List getNamedUsers(String ptype) { - return runSynchronized(() -> super.getNamedUsers(ptype), getReadWriteLock().readLock()); + public List getAllUsers() { + return runSynchronized(super::getAllUsers, getReadWriteLock().readLock()); } /** diff --git a/src/main/java/org/casbin/jcasbin/model/Model.java b/src/main/java/org/casbin/jcasbin/model/Model.java index 9b44dce5..6825d873 100644 --- a/src/main/java/org/casbin/jcasbin/model/Model.java +++ b/src/main/java/org/casbin/jcasbin/model/Model.java @@ -505,19 +505,23 @@ public List getValuesForFieldInPolicyAllTypes(String sec, int fieldIndex * * @param ptype the policy type, e.g., "p", "p2" * @param field the field name, e.g., "sub", "obj", "act" - * @return the index of the field in the policy rule, or 0 if not found + * @return the index of the field in the policy rule, or -1 if not found */ public int getFieldIndex(String ptype, String field) { String pattern = ptype + "_" + field; - Assertion ast = model.get("p").get(ptype); + Map pSection = model.get("p"); + if (pSection == null) { + return -1; + } + Assertion ast = pSection.get(ptype); if (ast == null || ast.tokens == null) { - return 0; + return -1; } for (int i = 0; i < ast.tokens.length; i++) { if (pattern.equals(ast.tokens[i])) { return i; } } - return 0; + return -1; } } diff --git a/src/test/java/org/casbin/jcasbin/main/ManagementAPIUnitTest.java b/src/test/java/org/casbin/jcasbin/main/ManagementAPIUnitTest.java index 7006f7eb..f3df5726 100644 --- a/src/test/java/org/casbin/jcasbin/main/ManagementAPIUnitTest.java +++ b/src/test/java/org/casbin/jcasbin/main/ManagementAPIUnitTest.java @@ -301,12 +301,45 @@ public void should_true_when_setAviatorEvaluator_given_customInstance() { @Test public void testGetUsersAPI() { + // 1. Basic RBAC: alice and bob are users, data2_admin is a role Enforcer e = new Enforcer("examples/rbac_model.conf", "examples/rbac_policy.csv"); - testGetAllSubjectsUtil(e, asList("alice", "bob", "data2_admin")); testGetAllRolesUtil(e, asList("data2_admin")); - testGetUsersUtil(e, asList("alice", "bob")); - testGetNamedUsersUtil(e, "p", asList("alice", "bob")); + testGetAllUsersUtil(e, asList("alice", "bob")); + + // 2. Add user "admin" that appears in both policy (as subject) and grouping (as role target) + // getAllUsers computes: subjects (from p) - roles (from g, column 1) + // admin is added as a policy subject and also assigned to role "root" + e.addPolicy("admin", "data1", "read"); + e.addGroupingPolicy("admin", "root"); + testGetAllSubjectsUtil(e, asList("alice", "bob", "data2_admin", "admin")); + testGetAllRolesUtil(e, asList("data2_admin", "root")); + testGetAllUsersUtil(e, asList("alice", "bob", "admin")); // admin is user since it's in p, not in g column 1 + e.removePolicy("admin", "data1", "read"); + e.removeGroupingPolicy("admin", "root"); + + // 3. Add regular user "eve" who is only in policy (not a role in any grouping) + e.addPolicy("eve", "data3", "read"); + testGetAllSubjectsUtil(e, asList("alice", "bob", "data2_admin", "eve")); + testGetAllRolesUtil(e, asList("data2_admin")); + testGetAllUsersUtil(e, asList("alice", "bob", "eve")); // eve is user since she's not in g column 1 + e.removePolicy("eve", "data3", "read"); + + // 4. Clear all policies - verify empty results + e.clearPolicy(); + testGetAllSubjectsUtil(e, asList()); + testGetAllRolesUtil(e, asList()); + testGetAllUsersUtil(e, asList()); + + // 5. Add new user and role relationship: user1 is a member of role "member" + // getAllSubjects: ["user1"] - from p column 0 + // getAllRoles: ["member"] - from g column 1 (the second element in grouping) + // getAllUsers: ["user1"] = subjects - roles + e.addPolicy("user1", "data1", "read"); + e.addGroupingPolicy("user1", "member"); + testGetAllSubjectsUtil(e, asList("user1")); + testGetAllRolesUtil(e, asList("member")); + testGetAllUsersUtil(e, asList("user1")); } private void testGetAllSubjectsUtil(Enforcer enforcer, List res) { @@ -327,21 +360,12 @@ private void testGetAllRolesUtil(Enforcer enforcer, List res) { } } - private void testGetUsersUtil(Enforcer enforcer, List res) { - List myRes = enforcer.getUsers(); - Util.logPrint("Users: " + myRes); - - if (!Util.setEquals(res, myRes)) { - Assert.fail("Users: " + myRes + ", supposed to be " + res); - } - } - - private void testGetNamedUsersUtil(Enforcer enforcer, String ptype, List res) { - List myRes = enforcer.getNamedUsers(ptype); - Util.logPrint("Named users (" + ptype + "): " + myRes); + private void testGetAllUsersUtil(Enforcer enforcer, List res) { + List myRes = enforcer.getAllUsers(); + Util.logPrint("All users: " + myRes); if (!Util.setEquals(res, myRes)) { - Assert.fail("Named users (" + ptype + "): " + myRes + ", supposed to be " + res); + Assert.fail("All users: " + myRes + ", supposed to be " + res); } }