// role_manager.mbt — Role graphs and role managers (RBAC).
//
// Mirrors Casbin's default role manager semantics:
//
// - `add_link(user, role, domain?)` records that `user` inherits `role`.
// - `has_link(name1, name2, domain?)` is true when both names are equal or
// when `name2` is reachable from `name1` through at most
// `max_hierarchy_level` links.
// - `get_roles` / `get_users` return direct relations in insertion order.
// - `get_implicit_roles` / `get_implicit_users` walk the graph
// breadth-first with a visited set, up to `max_hierarchy_level` levels,
// excluding the start name, and deduplicated.
//
// Links are grouped by domain; the empty string is the default domain, so a
// two-token role definition (`g = _, _`) is simply a manager whose links all
// live in the default domain. Role-name pattern matching (Casbin's
// `AddMatchingFunc`) is not supported: names compare exactly.
//
// ponytail: node lookup is a linear scan; add a hash index if role graphs
// grow beyond a few thousand nodes.
///|
/// One node of a role graph: direct parents (roles inherited) and direct
/// users (nodes that inherit this one).
pub(all) struct RoleNode {
name : String
parents : Array[String]
users : Array[String]
}
///|
/// The role graph of one domain.
pub(all) struct RoleGraph {
nodes : Array[RoleNode]
}
///|
/// A role manager holding one graph per domain.
pub(all) struct RoleManager {
graphs : Array[(String, RoleGraph)]
max_hierarchy_level : Int
}
///|
/// The maximum hierarchy level used by Casbin's default role managers.
pub let default_max_hierarchy_level : Int = 10
///|
/// Creates an empty role manager.
pub fn RoleManager::new(
max_hierarchy_level? : Int = default_max_hierarchy_level,
) -> RoleManager {
{ graphs: [], max_hierarchy_level, }
}
///|
/// The configured maximum hierarchy level.
pub fn RoleManager::max_hierarchy_level(self : RoleManager) -> Int {
self.max_hierarchy_level
}
///|
/// Records that `user` inherits `role` in `domain` (the default domain when
/// omitted).
pub fn RoleManager::add_link(
self : RoleManager,
user : String,
role : String,
domain? : String = "",
) -> Unit {
let graph = self.ensure_graph(domain)
let user_node = graph.nodes[graph.ensure_node(user)]
if !contains_name(user_node.parents, role) {
user_node.parents.push(role)
}
let role_node = graph.nodes[graph.ensure_node(role)]
if !contains_name(role_node.users, user) {
role_node.users.push(user)
}
}
///|
/// Removes the link `user` -> `role`. Returns `false` when the link does
/// not exist.
pub fn RoleManager::delete_link(
self : RoleManager,
user : String,
role : String,
domain? : String = "",
) -> Bool {
let graph = match self.graph(domain) {
Some(graph) => graph
None => return false
}
let user_index = match graph.find(user) {
Some(index) => index
None => return false
}
let role_index = match graph.find(role) {
Some(index) => index
None => return false
}
let user_node = graph.nodes[user_index]
let had_link = contains_name(user_node.parents, role)
remove_name(user_node.parents, role)
remove_name(graph.nodes[role_index].users, user)
had_link
}
///|
/// Whether `name1` inherits `name2`, directly or transitively.
pub fn RoleManager::has_link(
self : RoleManager,
name1 : String,
name2 : String,
domain? : String = "",
) -> Bool {
if name1 == name2 {
return true
}
let graph = match self.graph(domain) {
Some(graph) => graph
None => return false
}
let mut current : Array[String] = [name1]
let mut level = self.max_hierarchy_level
while level >= 0 && current.length() != 0 {
let next : Array[String] = []
for node_name in current {
if node_name == name2 {
return true
}
match graph.find(node_name) {
Some(index) =>
for parent in graph.nodes[index].parents {
if !contains_name(next, parent) {
next.push(parent)
}
}
None => ()
}
}
current = next
level -= 1
}
false
}
///|
/// The roles `name` directly inherits, in insertion order.
pub fn RoleManager::get_roles(
self : RoleManager,
name : String,
domain? : String = "",
) -> Array[String] {
match self.graph(domain) {
Some(graph) =>
match graph.find(name) {
Some(index) => copy_names(graph.nodes[index].parents)
None => []
}
None => []
}
}
///|
/// The nodes directly inheriting `name`, in insertion order.
pub fn RoleManager::get_users(
self : RoleManager,
name : String,
domain? : String = "",
) -> Array[String] {
match self.graph(domain) {
Some(graph) =>
match graph.find(name) {
Some(index) => copy_names(graph.nodes[index].users)
None => []
}
None => []
}
}
///|
/// Every role `name` inherits, direct and indirect, deduplicated and in
/// breadth-first order; `name` itself is never included.
pub fn RoleManager::get_implicit_roles(
self : RoleManager,
name : String,
domain? : String = "",
) -> Array[String] {
self.collect_implicit(name, domain, true)
}
///|
/// Every node that inherits `name`, direct and indirect, deduplicated and
/// in breadth-first order; `name` itself is never included.
pub fn RoleManager::get_implicit_users(
self : RoleManager,
name : String,
domain? : String = "",
) -> Array[String] {
self.collect_implicit(name, domain, false)
}
///|
/// Removes every link of every domain.
pub fn RoleManager::clear(self : RoleManager) -> Unit {
self.graphs.clear()
}
///|
fn RoleManager::collect_implicit(
self : RoleManager,
name : String,
domain : String,
upwards : Bool,
) -> Array[String] {
let graph = match self.graph(domain) {
Some(graph) => graph
None => return []
}
let result : Array[String] = []
let visited : Array[String] = [name]
let mut current : Array[String] = [name]
let mut level = 0
while level < self.max_hierarchy_level && current.length() != 0 {
let next : Array[String] = []
for node_name in current {
let neighbours = match graph.find(node_name) {
Some(index) =>
if upwards {
graph.nodes[index].parents
} else {
graph.nodes[index].users
}
None => []
}
for neighbour in neighbours {
if !contains_name(visited, neighbour) {
visited.push(neighbour)
result.push(neighbour)
next.push(neighbour)
}
}
}
current = next
level += 1
}
result
}
///|
fn RoleManager::graph(self : RoleManager, domain : String) -> RoleGraph? {
for entry in self.graphs {
if entry.0 == domain {
return Some(entry.1)
}
}
None
}
///|
fn RoleManager::ensure_graph(self : RoleManager, domain : String) -> RoleGraph {
for entry in self.graphs {
if entry.0 == domain {
return entry.1
}
}
let graph : RoleGraph = { nodes: [], }
self.graphs.push((domain, graph))
graph
}
///|
fn RoleGraph::find(self : RoleGraph, name : String) -> Int? {
for i in 0.. Int {
match self.find(name) {
Some(index) => index
None => {
self.nodes.push({ name, parents: [], users: [], })
self.nodes.length() - 1
}
}
}
///|
fn contains_name(names : Array[String], name : String) -> Bool {
for existing in names {
if existing == name {
return true
}
}
false
}
///|
fn copy_names(names : Array[String]) -> Array[String] {
let copy : Array[String] = []
for name in names {
copy.push(name)
}
copy
}
///|
fn remove_name(names : Array[String], name : String) -> Unit {
let mut index = 0
while index < names.length() {
if names[index] == name {
for i in index..<(names.length() - 1) {
names[i] = names[i + 1]
}
ignore(names.pop())
return
}
index += 1
}
}