use std::collections::BTreeSet;
use serde::{Deserialize, Serialize};
use super::TeamScope;
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum TeamRole {
Viewer,
Member,
Admin,
Owner,
}
impl TeamRole {
#[must_use]
pub fn all() -> &'static [TeamRole] {
&[
TeamRole::Viewer,
TeamRole::Member,
TeamRole::Admin,
TeamRole::Owner,
]
}
#[must_use]
pub fn as_str(self) -> &'static str {
match self {
TeamRole::Viewer => "viewer",
TeamRole::Member => "member",
TeamRole::Admin => "admin",
TeamRole::Owner => "owner",
}
}
#[must_use]
pub fn parse(s: &str) -> Option<TeamRole> {
match s.trim().to_ascii_lowercase().as_str() {
"viewer" | "read" | "readonly" => Some(TeamRole::Viewer),
"member" | "contributor" | "write" => Some(TeamRole::Member),
"admin" => Some(TeamRole::Admin),
"owner" => Some(TeamRole::Owner),
_ => None,
}
}
#[must_use]
pub fn scopes(self) -> BTreeSet<TeamScope> {
let mut s = BTreeSet::new();
match self {
TeamRole::Viewer => {
s.insert(TeamScope::Search);
}
TeamRole::Member => {
s.insert(TeamScope::Search);
s.insert(TeamScope::Graph);
s.insert(TeamScope::Index);
s.insert(TeamScope::Knowledge);
s.insert(TeamScope::Events);
}
TeamRole::Admin | TeamRole::Owner => {
for scope in TeamScope::all() {
s.insert(*scope);
}
}
}
s
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn role_roundtrips_through_wire_id() {
for r in TeamRole::all() {
assert_eq!(TeamRole::parse(r.as_str()), Some(*r));
}
assert_eq!(TeamRole::parse("ADMIN"), Some(TeamRole::Admin));
assert_eq!(TeamRole::parse("nope"), None);
}
#[test]
fn roles_are_monotonic_in_privilege() {
let viewer = TeamRole::Viewer.scopes();
let member = TeamRole::Member.scopes();
let admin = TeamRole::Admin.scopes();
let owner = TeamRole::Owner.scopes();
assert!(viewer.is_subset(&member), "viewer ⊄ member");
assert!(member.is_subset(&admin), "member ⊄ admin");
assert_eq!(admin, owner, "owner shares admin's server scopes");
}
#[test]
fn viewer_is_read_only() {
let v = TeamRole::Viewer.scopes();
assert!(v.contains(&TeamScope::Search));
assert!(!v.contains(&TeamScope::SessionMutations));
assert!(!v.contains(&TeamScope::Audit));
}
#[test]
fn admin_grants_every_scope() {
let admin = TeamRole::Admin.scopes();
for scope in TeamScope::all() {
assert!(admin.contains(scope), "admin missing scope {scope:?}");
}
}
#[test]
fn token_effective_scopes_union_role_and_explicit() {
use super::super::TeamTokenConfig;
let role_only = TeamTokenConfig {
id: "t".into(),
sha256_hex: "x".into(),
scopes: vec![],
role: Some(TeamRole::Viewer),
};
assert_eq!(role_only.effective_scopes(), TeamRole::Viewer.scopes());
let mixed = TeamTokenConfig {
id: "t".into(),
sha256_hex: "x".into(),
scopes: vec![TeamScope::Audit],
role: Some(TeamRole::Viewer),
};
let eff = mixed.effective_scopes();
assert!(eff.contains(&TeamScope::Search)); assert!(eff.contains(&TeamScope::Audit)); }
}