Skip to content

Commit 8ccf1b8

Browse files
authored
feat: add DefaultDetector which supports RBAC role loop detection (#483)
1 parent feedfc9 commit 8ccf1b8

File tree

2 files changed

+463
-0
lines changed

2 files changed

+463
-0
lines changed
Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
// Copyright 2025 The casbin Authors. All Rights Reserved.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package org.casbin.jcasbin.detector;
16+
17+
import org.casbin.jcasbin.rbac.DefaultRoleManager;
18+
import org.casbin.jcasbin.rbac.RoleManager;
19+
20+
import java.util.*;
21+
22+
/**
23+
* DefaultDetector is the default implementation of Detector interface.
24+
* It uses depth-first search to detect cycles in RBAC role inheritance graph.
25+
*/
26+
public class DefaultDetector implements Detector {
27+
28+
/**
29+
* Checks whether the current status of the passed-in RoleManager contains logical errors (e.g., cycles in role inheritance).
30+
* @param rm RoleManager instance
31+
* @return If a cycle is found, return a description message in the form "Cycle detected: A -> B -> C -> A"; otherwise return null
32+
*/
33+
@Override
34+
public String check(RoleManager rm) {
35+
if (!(rm instanceof DefaultRoleManager)) {
36+
throw new IllegalArgumentException("DefaultDetector only supports DefaultRoleManager");
37+
}
38+
39+
DefaultRoleManager drm = (DefaultRoleManager) rm;
40+
41+
// Build adjacency list from the role manager
42+
// Using local data structures to avoid sharing references with RoleManager's internal state
43+
Map<String, List<String>> graph = buildGraph(drm);
44+
45+
// Perform DFS to detect cycles
46+
Set<String> visited = new HashSet<>();
47+
Set<String> recursionStack = new HashSet<>();
48+
Map<String, String> parent = new HashMap<>();
49+
50+
for (String node : graph.keySet()) {
51+
if (!visited.contains(node)) {
52+
String cycle = dfs(node, graph, visited, recursionStack, parent);
53+
if (cycle != null) {
54+
return cycle;
55+
}
56+
}
57+
}
58+
59+
return null;
60+
}
61+
62+
/**
63+
* Builds a directed graph (adjacency list) from the DefaultRoleManager.
64+
* Each role points to the roles it inherits (its parent roles).
65+
*/
66+
private Map<String, List<String>> buildGraph(DefaultRoleManager drm) {
67+
Map<String, List<String>> graph = new HashMap<>();
68+
69+
try {
70+
// Use reflection to access the package-private allRoles field
71+
java.lang.reflect.Field allRolesField = DefaultRoleManager.class.getDeclaredField("allRoles");
72+
allRolesField.setAccessible(true);
73+
@SuppressWarnings("unchecked")
74+
Map<String, ?> allRoles = (Map<String, ?>) allRolesField.get(drm);
75+
76+
// Iterate through all roles and get their parent roles
77+
for (String roleName : allRoles.keySet()) {
78+
List<String> parentRoles = drm.getRoles(roleName);
79+
graph.put(roleName, new ArrayList<>(parentRoles));
80+
}
81+
} catch (NoSuchFieldException e) {
82+
throw new RuntimeException("Failed to access 'allRoles' field in DefaultRoleManager via reflection. " +
83+
"The field may have been renamed or removed.", e);
84+
} catch (IllegalAccessException e) {
85+
throw new RuntimeException("Failed to access 'allRoles' field in DefaultRoleManager via reflection. " +
86+
"Permission denied to access the field.", e);
87+
}
88+
89+
return graph;
90+
}
91+
92+
/**
93+
* Performs depth-first search to detect cycles in the graph using an iterative approach.
94+
*
95+
* @param startNode Starting node for DFS
96+
* @param graph The adjacency list representation of the role inheritance graph
97+
* @param visited Set of all visited nodes
98+
* @param recursionStack Set of nodes in current DFS path (used to detect back edges)
99+
* @param parent Map to track parent of each node for cycle path reconstruction
100+
* @return Cycle description if found, null otherwise
101+
*/
102+
private String dfs(String startNode, Map<String, List<String>> graph, Set<String> visited,
103+
Set<String> recursionStack, Map<String, String> parent) {
104+
// Use iterative DFS with explicit stack to avoid stack overflow on large graphs
105+
Stack<DFSState> stack = new Stack<>();
106+
stack.push(new DFSState(startNode, 0));
107+
visited.add(startNode);
108+
recursionStack.add(startNode);
109+
110+
while (!stack.isEmpty()) {
111+
DFSState state = stack.peek();
112+
String node = state.node;
113+
List<String> neighbors = graph.get(node);
114+
115+
if (neighbors == null || state.index >= neighbors.size()) {
116+
// All neighbors processed, backtrack
117+
stack.pop();
118+
recursionStack.remove(node);
119+
continue;
120+
}
121+
122+
String neighbor = neighbors.get(state.index);
123+
state.index++;
124+
125+
if (!visited.contains(neighbor)) {
126+
parent.put(neighbor, node);
127+
visited.add(neighbor);
128+
recursionStack.add(neighbor);
129+
stack.push(new DFSState(neighbor, 0));
130+
} else if (recursionStack.contains(neighbor)) {
131+
// Cycle detected! Build the cycle path
132+
parent.put(neighbor, node);
133+
return buildCyclePath(neighbor, node, parent);
134+
}
135+
}
136+
137+
return null;
138+
}
139+
140+
/**
141+
* Helper class to maintain DFS state for iterative traversal.
142+
*/
143+
private static class DFSState {
144+
String node;
145+
int index; // Index of next neighbor to process
146+
147+
DFSState(String node, int index) {
148+
this.node = node;
149+
this.index = index;
150+
}
151+
}
152+
153+
/**
154+
* Builds a human-readable cycle path description.
155+
*
156+
* @param cycleStart The node where the cycle was detected (the node being revisited)
157+
* @param cycleEnd The current node that creates the back edge to cycleStart
158+
* @param parent Map of parent relationships used to reconstruct the path
159+
* @return Cycle description in the form "Cycle detected: A -> B -> C -> A"
160+
*/
161+
private String buildCyclePath(String cycleStart, String cycleEnd, Map<String, String> parent) {
162+
List<String> path = new ArrayList<>();
163+
164+
// Build path from cycleEnd back to cycleStart
165+
String current = cycleEnd;
166+
while (current != null && !current.equals(cycleStart)) {
167+
path.add(0, current);
168+
current = parent.get(current);
169+
}
170+
171+
// Add cycleStart at the beginning and end to show the complete cycle
172+
path.add(0, cycleStart);
173+
path.add(cycleStart);
174+
175+
return "Cycle detected: " + String.join(" -> ", path);
176+
}
177+
}

0 commit comments

Comments
 (0)