snapshot of https://github.com/casbin/casbin.git (master, 2026-09-15; blobs > 131072B omitted)
This commit is contained in:
153
detector/default_detector.go
Normal file
153
detector/default_detector.go
Normal file
@@ -0,0 +1,153 @@
|
||||
// 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
|
||||
//
|
||||
// http://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 detector
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/casbin/casbin/v3/rbac"
|
||||
)
|
||||
|
||||
// rangeableRM is an interface for role managers that support iterating over all role links.
|
||||
// This is used to build the adjacency graph for cycle detection.
|
||||
type rangeableRM interface {
|
||||
Range(func(name1, name2 string, domain ...string) bool)
|
||||
}
|
||||
|
||||
// DefaultDetector is the default implementation of the Detector interface.
|
||||
// It uses depth-first search (DFS) to detect cycles in role inheritance.
|
||||
type DefaultDetector struct{}
|
||||
|
||||
// NewDefaultDetector creates a new instance of DefaultDetector.
|
||||
func NewDefaultDetector() *DefaultDetector {
|
||||
return &DefaultDetector{}
|
||||
}
|
||||
|
||||
// Check checks whether the current status of the passed-in RoleManager contains logical errors (e.g., cycles in role inheritance).
|
||||
// It uses DFS to traverse the role graph and detect cycles.
|
||||
// Returns nil if no cycle is found, otherwise returns an error with a description of the cycle.
|
||||
func (d *DefaultDetector) Check(rm rbac.RoleManager) error {
|
||||
// Defensive nil check to prevent runtime panics
|
||||
if rm == nil {
|
||||
return fmt.Errorf("role manager cannot be nil")
|
||||
}
|
||||
|
||||
// Build the adjacency graph by exploring all roles
|
||||
graph, err := d.buildGraph(rm)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Run DFS to detect cycles
|
||||
visited := make(map[string]bool)
|
||||
recursionStack := make(map[string]bool)
|
||||
|
||||
for role := range graph {
|
||||
if !visited[role] {
|
||||
if cycle := d.detectCycle(role, graph, visited, recursionStack, []string{}); cycle != nil {
|
||||
return fmt.Errorf("cycle detected: %s", strings.Join(cycle, " -> "))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// buildGraph builds an adjacency list representation of the role inheritance graph.
|
||||
// It uses the Range method (via type assertion) to iterate through all role links.
|
||||
func (d *DefaultDetector) buildGraph(rm rbac.RoleManager) (graph map[string][]string, err error) {
|
||||
graph = make(map[string][]string)
|
||||
|
||||
// Recover from any panics during Range iteration (e.g., nil pointer dereferences)
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
err = fmt.Errorf("RoleManager is not properly initialized: %v", r)
|
||||
}
|
||||
}()
|
||||
|
||||
// Try to cast to a RoleManager implementation that supports Range
|
||||
// This works with RoleManagerImpl and similar implementations
|
||||
rrm, ok := rm.(rangeableRM)
|
||||
if !ok {
|
||||
// Return an error if the RoleManager doesn't support Range iteration
|
||||
return nil, fmt.Errorf("RoleManager does not support Range iteration, cannot detect cycles")
|
||||
}
|
||||
|
||||
// Use Range method to build the graph directly
|
||||
rrm.Range(func(name1, name2 string, domain ...string) bool {
|
||||
// Initialize empty slice for name1 if it doesn't exist
|
||||
if graph[name1] == nil {
|
||||
graph[name1] = []string{}
|
||||
}
|
||||
// Add the link: name1 -> name2
|
||||
graph[name1] = append(graph[name1], name2)
|
||||
|
||||
// Ensure name2 exists in graph even if it has no outgoing edges
|
||||
if graph[name2] == nil {
|
||||
graph[name2] = []string{}
|
||||
}
|
||||
return true
|
||||
})
|
||||
return graph, nil
|
||||
}
|
||||
|
||||
// detectCycle performs DFS to detect cycles in the role graph.
|
||||
// Returns a slice representing the cycle path if found, nil otherwise.
|
||||
func (d *DefaultDetector) detectCycle(
|
||||
role string,
|
||||
graph map[string][]string,
|
||||
visited map[string]bool,
|
||||
recursionStack map[string]bool,
|
||||
path []string,
|
||||
) []string {
|
||||
// Mark the current role as visited and add to recursion stack
|
||||
visited[role] = true
|
||||
recursionStack[role] = true
|
||||
path = append(path, role)
|
||||
|
||||
// Visit all neighbors (parent roles)
|
||||
for _, neighbor := range graph[role] {
|
||||
if !visited[neighbor] {
|
||||
// Recursively visit unvisited neighbor
|
||||
if cycle := d.detectCycle(neighbor, graph, visited, recursionStack, path); cycle != nil {
|
||||
return cycle
|
||||
}
|
||||
} else if recursionStack[neighbor] {
|
||||
// Back edge found - cycle detected
|
||||
// Find where the cycle starts in the path
|
||||
cycleStart := -1
|
||||
for i, p := range path {
|
||||
if p == neighbor {
|
||||
cycleStart = i
|
||||
break
|
||||
}
|
||||
}
|
||||
if cycleStart >= 0 {
|
||||
// Build the cycle path
|
||||
cyclePath := append([]string{}, path[cycleStart:]...)
|
||||
cyclePath = append(cyclePath, neighbor)
|
||||
return cyclePath
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Remove from recursion stack before returning
|
||||
recursionStack[role] = false
|
||||
return nil
|
||||
}
|
||||
341
detector/default_detector_test.go
Normal file
341
detector/default_detector_test.go
Normal file
@@ -0,0 +1,341 @@
|
||||
// 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
|
||||
//
|
||||
// http://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 detector
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
defaultrolemanager "github.com/casbin/casbin/v3/rbac/default-role-manager"
|
||||
)
|
||||
|
||||
func TestDefaultDetector_NilRoleManager(t *testing.T) {
|
||||
detector := NewDefaultDetector()
|
||||
err := detector.Check(nil)
|
||||
|
||||
if err == nil {
|
||||
t.Error("Expected error for nil role manager, but got nil")
|
||||
} else {
|
||||
errMsg := err.Error()
|
||||
if !strings.Contains(errMsg, "role manager cannot be nil") {
|
||||
t.Errorf("Expected error message to contain 'role manager cannot be nil', got: %s", errMsg)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefaultDetector_NoCycle(t *testing.T) {
|
||||
rm := defaultrolemanager.NewRoleManagerImpl(10)
|
||||
_ = rm.AddLink("alice", "admin")
|
||||
_ = rm.AddLink("bob", "user")
|
||||
_ = rm.AddLink("admin", "superuser")
|
||||
|
||||
detector := NewDefaultDetector()
|
||||
err := detector.Check(rm)
|
||||
if err != nil {
|
||||
t.Errorf("Expected no cycle, but got error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefaultDetector_SimpleCycle(t *testing.T) {
|
||||
rm := defaultrolemanager.NewRoleManagerImpl(10)
|
||||
_ = rm.AddLink("A", "B")
|
||||
_ = rm.AddLink("B", "A")
|
||||
|
||||
detector := NewDefaultDetector()
|
||||
err := detector.Check(rm)
|
||||
|
||||
if err == nil {
|
||||
t.Error("Expected cycle detection error, but got nil")
|
||||
} else {
|
||||
errMsg := err.Error()
|
||||
if !strings.Contains(errMsg, "cycle detected") {
|
||||
t.Errorf("Expected error message to contain 'Cycle detected', got: %s", errMsg)
|
||||
}
|
||||
// Should contain both A and B in the cycle
|
||||
if !strings.Contains(errMsg, "A") || !strings.Contains(errMsg, "B") {
|
||||
t.Errorf("Expected error message to contain both A and B, got: %s", errMsg)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefaultDetector_ComplexCycle(t *testing.T) {
|
||||
rm := defaultrolemanager.NewRoleManagerImpl(10)
|
||||
_ = rm.AddLink("A", "B")
|
||||
_ = rm.AddLink("B", "C")
|
||||
_ = rm.AddLink("C", "A")
|
||||
|
||||
detector := NewDefaultDetector()
|
||||
err := detector.Check(rm)
|
||||
|
||||
if err == nil {
|
||||
t.Error("Expected cycle detection error, but got nil")
|
||||
} else {
|
||||
errMsg := err.Error()
|
||||
if !strings.Contains(errMsg, "cycle detected") {
|
||||
t.Errorf("Expected error message to contain 'Cycle detected', got: %s", errMsg)
|
||||
}
|
||||
// Should contain A, B, and C in the cycle
|
||||
if !strings.Contains(errMsg, "A") || !strings.Contains(errMsg, "B") || !strings.Contains(errMsg, "C") {
|
||||
t.Errorf("Expected error message to contain A, B, and C, got: %s", errMsg)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefaultDetector_SelfLoop(t *testing.T) {
|
||||
rm := defaultrolemanager.NewRoleManagerImpl(10)
|
||||
_ = rm.AddLink("A", "A")
|
||||
|
||||
detector := NewDefaultDetector()
|
||||
err := detector.Check(rm)
|
||||
|
||||
if err == nil {
|
||||
t.Error("Expected cycle detection error for self-loop, but got nil")
|
||||
} else {
|
||||
errMsg := err.Error()
|
||||
if !strings.Contains(errMsg, "cycle detected") {
|
||||
t.Errorf("Expected error message to contain 'Cycle detected', got: %s", errMsg)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefaultDetector_MultipleCycles(t *testing.T) {
|
||||
rm := defaultrolemanager.NewRoleManagerImpl(10)
|
||||
// First cycle: A -> B -> A
|
||||
_ = rm.AddLink("A", "B")
|
||||
_ = rm.AddLink("B", "A")
|
||||
// Second cycle: C -> D -> C
|
||||
_ = rm.AddLink("C", "D")
|
||||
_ = rm.AddLink("D", "C")
|
||||
|
||||
detector := NewDefaultDetector()
|
||||
err := detector.Check(rm)
|
||||
|
||||
if err == nil {
|
||||
t.Error("Expected cycle detection error, but got nil")
|
||||
} else {
|
||||
errMsg := err.Error()
|
||||
if !strings.Contains(errMsg, "cycle detected") {
|
||||
t.Errorf("Expected error message to contain 'Cycle detected', got: %s", errMsg)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefaultDetector_DisconnectedComponents(t *testing.T) {
|
||||
rm := defaultrolemanager.NewRoleManagerImpl(10)
|
||||
// Component 1: alice -> admin -> superuser
|
||||
_ = rm.AddLink("alice", "admin")
|
||||
_ = rm.AddLink("admin", "superuser")
|
||||
// Component 2: bob -> user
|
||||
_ = rm.AddLink("bob", "user")
|
||||
// Component 3: carol -> moderator
|
||||
_ = rm.AddLink("carol", "moderator")
|
||||
|
||||
detector := NewDefaultDetector()
|
||||
err := detector.Check(rm)
|
||||
if err != nil {
|
||||
t.Errorf("Expected no cycle in disconnected components, but got error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefaultDetector_ComplexGraphWithCycle(t *testing.T) {
|
||||
rm := defaultrolemanager.NewRoleManagerImpl(10)
|
||||
// Build a complex graph with one cycle
|
||||
_ = rm.AddLink("u1", "g1")
|
||||
_ = rm.AddLink("u2", "g1")
|
||||
_ = rm.AddLink("g1", "g2")
|
||||
_ = rm.AddLink("g2", "g3")
|
||||
_ = rm.AddLink("g3", "g1") // Creates cycle: g1 -> g2 -> g3 -> g1
|
||||
_ = rm.AddLink("u3", "g4")
|
||||
|
||||
detector := NewDefaultDetector()
|
||||
err := detector.Check(rm)
|
||||
|
||||
if err == nil {
|
||||
t.Error("Expected cycle detection error, but got nil")
|
||||
} else {
|
||||
errMsg := err.Error()
|
||||
if !strings.Contains(errMsg, "cycle detected") {
|
||||
t.Errorf("Expected error message to contain 'Cycle detected', got: %s", errMsg)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefaultDetector_LongCycle(t *testing.T) {
|
||||
rm := defaultrolemanager.NewRoleManagerImpl(20)
|
||||
// Create a long cycle: A -> B -> C -> D -> E -> A
|
||||
_ = rm.AddLink("A", "B")
|
||||
_ = rm.AddLink("B", "C")
|
||||
_ = rm.AddLink("C", "D")
|
||||
_ = rm.AddLink("D", "E")
|
||||
_ = rm.AddLink("E", "A")
|
||||
|
||||
detector := NewDefaultDetector()
|
||||
err := detector.Check(rm)
|
||||
|
||||
if err == nil {
|
||||
t.Error("Expected cycle detection error, but got nil")
|
||||
} else {
|
||||
errMsg := err.Error()
|
||||
if !strings.Contains(errMsg, "cycle detected") {
|
||||
t.Errorf("Expected error message to contain 'Cycle detected', got: %s", errMsg)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefaultDetector_EmptyRoleManager(t *testing.T) {
|
||||
rm := defaultrolemanager.NewRoleManagerImpl(10)
|
||||
|
||||
detector := NewDefaultDetector()
|
||||
err := detector.Check(rm)
|
||||
if err != nil {
|
||||
t.Errorf("Expected no error for empty role manager, but got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefaultDetector_LargeGraphNoCycle(t *testing.T) {
|
||||
rm := defaultrolemanager.NewRoleManagerImpl(100)
|
||||
|
||||
// Build a large graph with no cycles: a tree structure
|
||||
// Create 100 levels: u0 -> u1 -> u2 -> ... -> u99
|
||||
for i := 0; i < 99; i++ {
|
||||
user := fmt.Sprintf("u%d", i)
|
||||
role := fmt.Sprintf("u%d", i+1)
|
||||
_ = rm.AddLink(user, role)
|
||||
}
|
||||
|
||||
detector := NewDefaultDetector()
|
||||
err := detector.Check(rm)
|
||||
if err != nil {
|
||||
t.Errorf("Expected no cycle in large graph, but got error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefaultDetector_LargeGraphWithCycle(t *testing.T) {
|
||||
rm := defaultrolemanager.NewRoleManagerImpl(100)
|
||||
|
||||
// Build a large graph with a cycle at the end
|
||||
// Create a chain: u0 -> u1 -> u2 -> ... -> u99 -> u0
|
||||
for i := 0; i < 99; i++ {
|
||||
user := fmt.Sprintf("u%d", i)
|
||||
role := fmt.Sprintf("u%d", i+1)
|
||||
_ = rm.AddLink(user, role)
|
||||
}
|
||||
// Add the cycle
|
||||
_ = rm.AddLink("u99", "u0")
|
||||
|
||||
detector := NewDefaultDetector()
|
||||
err := detector.Check(rm)
|
||||
|
||||
if err == nil {
|
||||
t.Error("Expected cycle detection error in large graph, but got nil")
|
||||
} else {
|
||||
errMsg := err.Error()
|
||||
if !strings.Contains(errMsg, "cycle detected") {
|
||||
t.Errorf("Expected error message to contain 'Cycle detected', got: %s", errMsg)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Performance test with 10,000 roles.
|
||||
func TestDefaultDetector_PerformanceLargeGraph(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("Skipping performance test in short mode")
|
||||
}
|
||||
|
||||
// Use a higher maxHierarchyLevel to support deep hierarchies
|
||||
rm := defaultrolemanager.NewRoleManagerImpl(10000)
|
||||
|
||||
// Build a large tree structure with 10,000 roles
|
||||
// Each role has up to 3 children
|
||||
numRoles := 10000
|
||||
for i := 0; i < numRoles; i++ {
|
||||
role := fmt.Sprintf("r%d", i)
|
||||
// Add links to create a tree structure
|
||||
child1 := (i * 3) + 1
|
||||
child2 := (i * 3) + 2
|
||||
child3 := (i * 3) + 3
|
||||
if child1 < numRoles {
|
||||
_ = rm.AddLink(fmt.Sprintf("r%d", child1), role)
|
||||
}
|
||||
if child2 < numRoles {
|
||||
_ = rm.AddLink(fmt.Sprintf("r%d", child2), role)
|
||||
}
|
||||
if child3 < numRoles {
|
||||
_ = rm.AddLink(fmt.Sprintf("r%d", child3), role)
|
||||
}
|
||||
}
|
||||
|
||||
detector := NewDefaultDetector()
|
||||
err := detector.Check(rm)
|
||||
if err != nil {
|
||||
t.Errorf("Expected no cycle in large performance test, but got error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefaultDetector_MultipleInheritance(t *testing.T) {
|
||||
rm := defaultrolemanager.NewRoleManagerImpl(10)
|
||||
// User inherits from multiple roles
|
||||
_ = rm.AddLink("alice", "admin")
|
||||
_ = rm.AddLink("alice", "moderator")
|
||||
_ = rm.AddLink("admin", "superuser")
|
||||
_ = rm.AddLink("moderator", "user")
|
||||
|
||||
detector := NewDefaultDetector()
|
||||
err := detector.Check(rm)
|
||||
if err != nil {
|
||||
t.Errorf("Expected no cycle with multiple inheritance, but got error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefaultDetector_DiamondPattern(t *testing.T) {
|
||||
rm := defaultrolemanager.NewRoleManagerImpl(10)
|
||||
// Diamond pattern: alice -> admin, alice -> moderator, admin -> superuser, moderator -> superuser
|
||||
_ = rm.AddLink("alice", "admin")
|
||||
_ = rm.AddLink("alice", "moderator")
|
||||
_ = rm.AddLink("admin", "superuser")
|
||||
_ = rm.AddLink("moderator", "superuser")
|
||||
|
||||
detector := NewDefaultDetector()
|
||||
err := detector.Check(rm)
|
||||
if err != nil {
|
||||
t.Errorf("Expected no cycle in diamond pattern, but got error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefaultDetector_DiamondPatternWithCycle(t *testing.T) {
|
||||
rm := defaultrolemanager.NewRoleManagerImpl(10)
|
||||
// Diamond pattern with cycle: alice -> admin, alice -> moderator, admin -> superuser, moderator -> superuser, superuser -> alice
|
||||
_ = rm.AddLink("alice", "admin")
|
||||
_ = rm.AddLink("alice", "moderator")
|
||||
_ = rm.AddLink("admin", "superuser")
|
||||
_ = rm.AddLink("moderator", "superuser")
|
||||
_ = rm.AddLink("superuser", "alice") // Creates cycle
|
||||
|
||||
detector := NewDefaultDetector()
|
||||
err := detector.Check(rm)
|
||||
|
||||
if err == nil {
|
||||
t.Error("Expected cycle detection error in diamond pattern with cycle, but got nil")
|
||||
} else {
|
||||
errMsg := err.Error()
|
||||
if !strings.Contains(errMsg, "cycle detected") {
|
||||
t.Errorf("Expected error message to contain 'Cycle detected', got: %s", errMsg)
|
||||
}
|
||||
}
|
||||
}
|
||||
28
detector/detector.go
Normal file
28
detector/detector.go
Normal file
@@ -0,0 +1,28 @@
|
||||
// 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
|
||||
//
|
||||
// http://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 detector
|
||||
|
||||
import "github.com/casbin/casbin/v3/rbac"
|
||||
|
||||
// Detector defines the interface of a policy consistency checker, currently used to detect RBAC inheritance cycles.
|
||||
type Detector interface {
|
||||
// Check checks whether the current status of the passed-in RoleManager contains logical errors (e.g., cycles in role inheritance).
|
||||
// param: rm RoleManager instance
|
||||
// return: If an error is found, return a descriptive error; otherwise return nil.
|
||||
Check(rm rbac.RoleManager) error
|
||||
}
|
||||
Reference in New Issue
Block a user