feat(spaces): add space roles service with enforcement and caching
Implement the roles service that manages space permission cascading: - In-memory cache populated from state events, rebuilt on startup - Join gating, power level sync (highest-wins across parent spaces), auto-join on role grant, auto-kick on role revocation - Per-space enable/disable via com.continuwuity.space.cascading event - Background enforcement tasks with semaphore-limited concurrency - Graceful shutdown support via server.running() checks
This commit is contained in:
parent
87ad6f156c
commit
f4ab456bbd
4 changed files with 1409 additions and 0 deletions
|
|
@ -7,6 +7,7 @@ pub mod metadata;
|
||||||
pub mod outlier;
|
pub mod outlier;
|
||||||
pub mod pdu_metadata;
|
pub mod pdu_metadata;
|
||||||
pub mod read_receipt;
|
pub mod read_receipt;
|
||||||
|
pub mod roles;
|
||||||
pub mod search;
|
pub mod search;
|
||||||
pub mod short;
|
pub mod short;
|
||||||
pub mod spaces;
|
pub mod spaces;
|
||||||
|
|
@ -31,6 +32,7 @@ pub struct Service {
|
||||||
pub outlier: Arc<outlier::Service>,
|
pub outlier: Arc<outlier::Service>,
|
||||||
pub pdu_metadata: Arc<pdu_metadata::Service>,
|
pub pdu_metadata: Arc<pdu_metadata::Service>,
|
||||||
pub read_receipt: Arc<read_receipt::Service>,
|
pub read_receipt: Arc<read_receipt::Service>,
|
||||||
|
pub roles: Arc<roles::Service>,
|
||||||
pub search: Arc<search::Service>,
|
pub search: Arc<search::Service>,
|
||||||
pub short: Arc<short::Service>,
|
pub short: Arc<short::Service>,
|
||||||
pub spaces: Arc<spaces::Service>,
|
pub spaces: Arc<spaces::Service>,
|
||||||
|
|
|
||||||
1202
src/service/rooms/roles/mod.rs
Normal file
1202
src/service/rooms/roles/mod.rs
Normal file
File diff suppressed because it is too large
Load diff
204
src/service/rooms/roles/tests.rs
Normal file
204
src/service/rooms/roles/tests.rs
Normal file
|
|
@ -0,0 +1,204 @@
|
||||||
|
use std::collections::{BTreeMap, HashSet};
|
||||||
|
|
||||||
|
use conduwuit_core::matrix::space_roles::RoleDefinition;
|
||||||
|
|
||||||
|
use super::{compute_user_power_level, roles_satisfy_requirements};
|
||||||
|
|
||||||
|
pub(super) fn make_roles(entries: &[(&str, Option<i64>)]) -> BTreeMap<String, RoleDefinition> {
|
||||||
|
entries
|
||||||
|
.iter()
|
||||||
|
.map(|(name, pl)| {
|
||||||
|
((*name).to_owned(), RoleDefinition {
|
||||||
|
description: format!("{name} role"),
|
||||||
|
power_level: *pl,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn make_set(items: &[&str]) -> HashSet<String> {
|
||||||
|
items.iter().map(|s| (*s).to_owned()).collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn power_level_single_role() {
|
||||||
|
let roles = make_roles(&[("admin", Some(100)), ("mod", Some(50))]);
|
||||||
|
assert_eq!(compute_user_power_level(&roles, &make_set(&["admin"])), Some(100));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn power_level_multiple_roles_takes_highest() {
|
||||||
|
let roles = make_roles(&[("admin", Some(100)), ("mod", Some(50)), ("helper", Some(25))]);
|
||||||
|
assert_eq!(compute_user_power_level(&roles, &make_set(&["mod", "helper"])), Some(50));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn power_level_no_power_roles() {
|
||||||
|
let roles = make_roles(&[("nsfw", None), ("vip", None)]);
|
||||||
|
assert_eq!(compute_user_power_level(&roles, &make_set(&["nsfw", "vip"])), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn power_level_mixed_roles() {
|
||||||
|
let roles = make_roles(&[("mod", Some(50)), ("nsfw", None)]);
|
||||||
|
assert_eq!(compute_user_power_level(&roles, &make_set(&["mod", "nsfw"])), Some(50));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn power_level_no_roles_assigned() {
|
||||||
|
let roles = make_roles(&[("admin", Some(100))]);
|
||||||
|
assert_eq!(compute_user_power_level(&roles, &HashSet::new()), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn power_level_unknown_role_ignored() {
|
||||||
|
let roles = make_roles(&[("admin", Some(100))]);
|
||||||
|
assert_eq!(compute_user_power_level(&roles, &make_set(&["nonexistent"])), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn qualifies_with_all_required_roles() {
|
||||||
|
assert!(roles_satisfy_requirements(
|
||||||
|
&make_set(&["nsfw", "vip"]),
|
||||||
|
&make_set(&["nsfw", "vip", "extra"]),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn does_not_qualify_missing_one_role() {
|
||||||
|
assert!(!roles_satisfy_requirements(&make_set(&["nsfw", "vip"]), &make_set(&["nsfw"]),));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn qualifies_with_no_requirements() {
|
||||||
|
assert!(roles_satisfy_requirements(&HashSet::new(), &make_set(&["nsfw"])));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn does_not_qualify_with_no_roles() {
|
||||||
|
assert!(!roles_satisfy_requirements(&make_set(&["nsfw"]), &HashSet::new()));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Multi-space scenarios
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn multi_space_highest_pl_wins() {
|
||||||
|
let space_a_roles = make_roles(&[("mod", Some(50))]);
|
||||||
|
let space_b_roles = make_roles(&[("admin", Some(100))]);
|
||||||
|
|
||||||
|
let user_roles_a = make_set(&["mod"]);
|
||||||
|
let user_roles_b = make_set(&["admin"]);
|
||||||
|
|
||||||
|
let pl_a = compute_user_power_level(&space_a_roles, &user_roles_a);
|
||||||
|
let pl_b = compute_user_power_level(&space_b_roles, &user_roles_b);
|
||||||
|
|
||||||
|
let effective = [pl_a, pl_b].into_iter().flatten().max();
|
||||||
|
assert_eq!(effective, Some(100));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn multi_space_one_space_has_no_pl() {
|
||||||
|
let space_a_roles = make_roles(&[("nsfw", None)]);
|
||||||
|
let space_b_roles = make_roles(&[("mod", Some(50))]);
|
||||||
|
|
||||||
|
let user_roles_a = make_set(&["nsfw"]);
|
||||||
|
let user_roles_b = make_set(&["mod"]);
|
||||||
|
|
||||||
|
let pl_a = compute_user_power_level(&space_a_roles, &user_roles_a);
|
||||||
|
let pl_b = compute_user_power_level(&space_b_roles, &user_roles_b);
|
||||||
|
|
||||||
|
let effective = [pl_a, pl_b].into_iter().flatten().max();
|
||||||
|
assert_eq!(effective, Some(50));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn multi_space_neither_has_pl() {
|
||||||
|
let space_a_roles = make_roles(&[("nsfw", None)]);
|
||||||
|
let space_b_roles = make_roles(&[("vip", None)]);
|
||||||
|
|
||||||
|
let user_roles_a = make_set(&["nsfw"]);
|
||||||
|
let user_roles_b = make_set(&["vip"]);
|
||||||
|
|
||||||
|
let pl_a = compute_user_power_level(&space_a_roles, &user_roles_a);
|
||||||
|
let pl_b = compute_user_power_level(&space_b_roles, &user_roles_b);
|
||||||
|
|
||||||
|
let effective = [pl_a, pl_b].into_iter().flatten().max();
|
||||||
|
assert_eq!(effective, None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn multi_space_user_only_in_one_space() {
|
||||||
|
let space_a_roles = make_roles(&[("admin", Some(100))]);
|
||||||
|
let space_b_roles = make_roles(&[("mod", Some(50))]);
|
||||||
|
|
||||||
|
let user_roles_a = make_set(&["admin"]);
|
||||||
|
let user_roles_b: HashSet<String> = HashSet::new();
|
||||||
|
|
||||||
|
let pl_a = compute_user_power_level(&space_a_roles, &user_roles_a);
|
||||||
|
let pl_b = compute_user_power_level(&space_b_roles, &user_roles_b);
|
||||||
|
|
||||||
|
let effective = [pl_a, pl_b].into_iter().flatten().max();
|
||||||
|
assert_eq!(effective, Some(100));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn multi_space_qualifies_in_one_not_other() {
|
||||||
|
let space_a_reqs = make_set(&["staff"]);
|
||||||
|
let space_b_reqs = make_set(&["nsfw"]);
|
||||||
|
|
||||||
|
let user_roles = make_set(&["nsfw"]);
|
||||||
|
|
||||||
|
assert!(!roles_satisfy_requirements(&space_a_reqs, &user_roles));
|
||||||
|
assert!(roles_satisfy_requirements(&space_b_reqs, &user_roles));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn multi_space_qualifies_after_role_revoke_via_other_space() {
|
||||||
|
let space_a_reqs = make_set(&["nsfw"]);
|
||||||
|
let space_b_reqs = make_set(&["vip"]);
|
||||||
|
|
||||||
|
let user_roles_after_revoke = make_set(&["vip"]);
|
||||||
|
|
||||||
|
assert!(!roles_satisfy_requirements(&space_a_reqs, &user_roles_after_revoke));
|
||||||
|
assert!(roles_satisfy_requirements(&space_b_reqs, &user_roles_after_revoke));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn multi_space_room_has_reqs_in_one_space_only() {
|
||||||
|
let space_a_reqs = make_set(&["admin"]);
|
||||||
|
let space_b_reqs: HashSet<String> = HashSet::new();
|
||||||
|
|
||||||
|
let user_roles = make_set(&["nsfw"]);
|
||||||
|
|
||||||
|
assert!(!roles_satisfy_requirements(&space_a_reqs, &user_roles));
|
||||||
|
assert!(roles_satisfy_requirements(&space_b_reqs, &user_roles));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn multi_space_no_qualification_anywhere() {
|
||||||
|
let space_a_reqs = make_set(&["staff"]);
|
||||||
|
let space_b_reqs = make_set(&["admin"]);
|
||||||
|
|
||||||
|
let user_roles = make_set(&["nsfw"]);
|
||||||
|
|
||||||
|
let qualifies_a = roles_satisfy_requirements(&space_a_reqs, &user_roles);
|
||||||
|
let qualifies_b = roles_satisfy_requirements(&space_b_reqs, &user_roles);
|
||||||
|
|
||||||
|
assert!(!qualifies_a);
|
||||||
|
assert!(!qualifies_b);
|
||||||
|
assert!(!(qualifies_a || qualifies_b));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn multi_space_same_role_different_pl() {
|
||||||
|
let space_a_roles = make_roles(&[("mod", Some(50))]);
|
||||||
|
let space_b_roles = make_roles(&[("mod", Some(75))]);
|
||||||
|
|
||||||
|
let user_roles = make_set(&["mod"]);
|
||||||
|
|
||||||
|
let pl_a = compute_user_power_level(&space_a_roles, &user_roles);
|
||||||
|
let pl_b = compute_user_power_level(&space_b_roles, &user_roles);
|
||||||
|
|
||||||
|
let effective = [pl_a, pl_b].into_iter().flatten().max();
|
||||||
|
assert_eq!(effective, Some(75));
|
||||||
|
}
|
||||||
|
|
@ -94,6 +94,7 @@ impl Services {
|
||||||
outlier: build!(rooms::outlier::Service),
|
outlier: build!(rooms::outlier::Service),
|
||||||
pdu_metadata: build!(rooms::pdu_metadata::Service),
|
pdu_metadata: build!(rooms::pdu_metadata::Service),
|
||||||
read_receipt: build!(rooms::read_receipt::Service),
|
read_receipt: build!(rooms::read_receipt::Service),
|
||||||
|
roles: build!(rooms::roles::Service),
|
||||||
search: build!(rooms::search::Service),
|
search: build!(rooms::search::Service),
|
||||||
short: build!(rooms::short::Service),
|
short: build!(rooms::short::Service),
|
||||||
spaces: build!(rooms::spaces::Service),
|
spaces: build!(rooms::spaces::Service),
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue