154 lines
4.8 KiB
Go
154 lines
4.8 KiB
Go
// 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
|
|
}
|