Skip to main content

cloudillo_core/
roles.rs

1// SPDX-FileCopyrightText: Szilárd Hajba
2// SPDX-License-Identifier: LGPL-3.0-or-later
3
4//! Role hierarchy and expansion logic
5//!
6//! This module defines the built-in role hierarchy and provides utilities
7//! for expanding hierarchical roles.
8
9/// The single parser for comma-separated role strings, re-exported for core-side
10/// callers. Empty segments must be dropped — see the definition for why.
11pub use cloudillo_types::utils::parse_roles;
12
13/// The hierarchy itself and its expansion live in `cloudillo-types` so the auth adapters can mint
14/// role strings through the same code the token-refresh path uses — see that module for why.
15pub use cloudillo_types::roles::{
16	ROLE_HIERARCHY, expand_roles, expand_roles_preserving_extras, role_level,
17};
18
19/// Highest hierarchy level among the given roles; unknown roles are ignored.
20/// Empty / all-unknown ⇒ 0 (public).
21pub fn highest_role_level(roles: &[Box<str>]) -> usize {
22	roles.iter().filter_map(|r| role_level(r)).max().unwrap_or(0)
23}
24
25/// True iff `roles` reaches the `leader` level — the bar for managing tenant-owned
26/// resources. The tenant owner carries the full hierarchy (`build_tenant_owner_roles`
27/// in auth-adapter-sqlite); federated visitors and share-link tokens carry none.
28pub fn is_leader(roles: &[Box<str>]) -> bool {
29	highest_role_level(roles) >= LEADER_LEVEL
30}
31
32/// Lowest hierarchy level permitted to manage (remove / re-role) other members.
33pub const MODERATOR_LEVEL: usize = 4;
34/// Hierarchy level of the "leader" role.
35pub const LEADER_LEVEL: usize = 5;
36
37/// Whether an actor at `actor_level` may manage (remove or re-role) a member at
38/// `target_level`. Rule: the actor must be moderator+ and strictly outrank the
39/// target — except leaders, who may also manage peer leaders.
40pub fn can_manage_member(actor_level: usize, target_level: usize) -> bool {
41	actor_level >= MODERATOR_LEVEL && (actor_level > target_level || actor_level == LEADER_LEVEL)
42}
43
44/// Whether an actor with `actor_roles` may manage (remove / re-role) a member
45/// with `target_roles`. Convenience over `can_manage_member` + `highest_role_level`.
46pub fn can_manage_member_by_roles(actor_roles: &[Box<str>], target_roles: &[Box<str>]) -> bool {
47	can_manage_member(highest_role_level(actor_roles), highest_role_level(target_roles))
48}
49
50/// Whether an actor at `actor_level` may *assign* `role`. Leaders may assign any
51/// known role; everyone else is capped strictly below their own level. Unknown
52/// roles are never assignable.
53pub fn can_assign_role(role: &str, actor_level: usize) -> bool {
54	match role_level(role) {
55		Some(new_level) => actor_level >= LEADER_LEVEL || new_level < actor_level,
56		None => false,
57	}
58}
59
60#[cfg(test)]
61mod tests {
62	// These pure helpers are the security-critical decision points the auth guards
63	// route through: `can_manage_member_by_roles` (manage authority) and
64	// `can_assign_role` (assignment cap). The handlers compose them with
65	// field-level rules that remain in `update.rs` (name/status leader-only, and the
66	// self-role-change block), which are not exercised here.
67	use super::*;
68
69	#[test]
70	fn test_level_consts_match_hierarchy() {
71		assert_eq!(role_level("moderator"), Some(MODERATOR_LEVEL));
72		assert_eq!(role_level("leader"), Some(LEADER_LEVEL));
73	}
74
75	#[test]
76	fn test_can_manage_member() {
77		// moderator (4) outranks contributor (3)
78		assert!(can_manage_member(4, 3));
79		// moderator cannot manage a peer moderator
80		assert!(!can_manage_member(4, 4));
81		// leader (5) may manage a peer leader
82		assert!(can_manage_member(5, 5));
83		// contributor (3) is below moderator → cannot manage anyone
84		assert!(!can_manage_member(3, 0));
85	}
86
87	#[test]
88	fn test_highest_role_level() {
89		// Empty / all-unknown ⇒ 0 (public)
90		assert_eq!(highest_role_level(&[]), 0);
91		assert_eq!(highest_role_level(&["unknown".into()]), 0);
92		// Takes the highest known role, ignoring unknowns
93		assert_eq!(highest_role_level(&["follower".into()]), 1);
94		assert_eq!(highest_role_level(&["moderator".into()]), 4);
95		assert_eq!(highest_role_level(&["leader".into()]), 5);
96		assert_eq!(highest_role_level(&["contributor".into(), "moderator".into()]), 4);
97		assert_eq!(highest_role_level(&["unknown".into(), "leader".into()]), 5);
98	}
99
100	#[test]
101	fn test_is_leader() {
102		// The full tenant-owner role set.
103		assert!(is_leader(&[
104			"public".into(),
105			"follower".into(),
106			"supporter".into(),
107			"contributor".into(),
108			"moderator".into(),
109			"leader".into(),
110		]));
111		assert!(is_leader(&["leader".into()]));
112		// Federated visitors and share-link tokens carry no roles.
113		assert!(!is_leader(&[]));
114		assert!(!is_leader(&["contributor".into()]));
115	}
116
117	#[test]
118	fn test_can_manage_member_by_roles() {
119		// moderator outranks contributor
120		assert!(can_manage_member_by_roles(&["moderator".into()], &["contributor".into()]));
121		// moderator cannot manage a peer moderator
122		assert!(!can_manage_member_by_roles(&["moderator".into()], &["moderator".into()]));
123		// leader may manage a peer leader
124		assert!(can_manage_member_by_roles(&["leader".into()], &["leader".into()]));
125		// contributor is below moderator → cannot manage anyone
126		assert!(!can_manage_member_by_roles(&["contributor".into()], &["public".into()]));
127		// empty actor roles (level 0) cannot manage anyone
128		assert!(!can_manage_member_by_roles(&[], &["public".into()]));
129		// unknown roles are ignored when computing levels
130		assert!(can_manage_member_by_roles(
131			&["unknown".into(), "moderator".into()],
132			&["contributor".into()]
133		));
134	}
135
136	#[test]
137	fn test_can_assign_role() {
138		// leader (5) may assign any known role, including peer leader
139		assert!(can_assign_role("leader", LEADER_LEVEL));
140		assert!(can_assign_role("moderator", LEADER_LEVEL));
141		assert!(can_assign_role("contributor", LEADER_LEVEL));
142		// moderator (4) may assign strictly-below roles only
143		assert!(can_assign_role("contributor", MODERATOR_LEVEL));
144		assert!(!can_assign_role("moderator", MODERATOR_LEVEL));
145		assert!(!can_assign_role("leader", MODERATOR_LEVEL));
146		// unknown roles are never assignable, even by a leader
147		assert!(!can_assign_role("unknown", LEADER_LEVEL));
148	}
149}
150
151// vim: ts=4