use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
#[serde(transparent)]
pub struct Permissions(pub u64);
impl Permissions {
pub const MANAGE_ROLES: u64 = 1 << 0;
pub const MANAGE_CHANNELS: u64 = 1 << 1;
pub const MANAGE_METADATA: u64 = 1 << 2;
pub const KICK: u64 = 1 << 3;
pub const BAN: u64 = 1 << 4;
pub const MANAGE_MESSAGES: u64 = 1 << 5;
pub const CREATE_INVITE: u64 = 1 << 6;
pub const VIEW_AUDIT_LOG: u64 = 1 << 8;
pub const MENTION_EVERYONE: u64 = 1 << 9;
pub const ADMIN_ALL: u64 = Self::MANAGE_ROLES
| Self::MANAGE_CHANNELS
| Self::MANAGE_METADATA
| Self::KICK
| Self::BAN
| Self::MANAGE_MESSAGES
| Self::CREATE_INVITE
| Self::VIEW_AUDIT_LOG
| Self::MENTION_EVERYONE;
pub const MANAGEMENT_MASK: u64 = Self::MANAGE_ROLES
| Self::MANAGE_CHANNELS
| Self::MANAGE_METADATA
| Self::KICK
| Self::BAN
| Self::MANAGE_MESSAGES
| Self::CREATE_INVITE
| Self::VIEW_AUDIT_LOG;
pub fn empty() -> Self {
Permissions(0)
}
pub fn admin() -> Self {
Permissions(Self::ADMIN_ALL)
}
pub fn is_management(self) -> bool {
self.0 & Self::MANAGEMENT_MASK != 0
}
pub fn contains(self, bits: u64) -> bool {
self.0 & bits == bits
}
pub fn union(self, other: Permissions) -> Permissions {
Permissions(self.0 | other.0)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case", tag = "kind", content = "channel_id")]
pub enum RoleScope {
Server,
Channel(String),
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Role {
pub role_id: String,
pub name: String,
pub position: u32,
pub permissions: Permissions,
pub scope: RoleScope,
#[serde(default)]
pub color: u32,
}
impl Role {
pub fn admin(role_id: String) -> Self {
Role {
role_id,
name: "Admin".to_string(),
position: 1,
permissions: Permissions::admin(),
scope: RoleScope::Server,
color: 0,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct MemberGrant {
pub member: String,
#[serde(default)]
pub role_ids: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
pub struct CommunityRoles {
#[serde(default)]
pub roles: Vec<Role>,
#[serde(default)]
pub grants: Vec<MemberGrant>,
}
impl CommunityRoles {
pub fn role(&self, role_id: &str) -> Option<&Role> {
self.roles.iter().find(|r| r.role_id == role_id)
}
pub fn roles_of<'a>(&'a self, member_hex: &'a str) -> impl Iterator<Item = &'a Role> + 'a {
self.grants
.iter()
.filter(move |g| g.member == member_hex)
.flat_map(move |g| g.role_ids.iter())
.filter_map(move |rid| self.role(rid))
}
pub fn effective_permissions(&self, member_hex: &str) -> Permissions {
self.roles_of(member_hex)
.fold(Permissions::empty(), |acc, r| acc.union(r.permissions))
}
pub fn has_permission(&self, member_hex: &str, bits: u64) -> bool {
self.effective_permissions(member_hex).contains(bits)
}
pub fn highest_position(&self, member_hex: &str) -> Option<u32> {
self.roles_of(member_hex).map(|r| r.position).min()
}
pub fn is_privileged(&self, member_hex: &str) -> bool {
self.roles_of(member_hex).next().is_some()
}
pub fn is_admin(&self, member_hex: &str) -> bool {
self.roles_of(member_hex).any(|r| r.permissions.is_management())
}
pub fn is_authorized(&self, actor_hex: &str, owner_hex: Option<&str>, permission: u64) -> bool {
if owner_hex == Some(actor_hex) {
return true;
}
self.has_permission(actor_hex, permission)
}
pub fn can_manage_position(&self, actor_hex: &str, owner_hex: Option<&str>, target_position: u32) -> bool {
self.can_act_on_position(actor_hex, owner_hex, target_position, Permissions::MANAGE_ROLES)
}
pub fn can_manage_member(&self, actor_hex: &str, owner_hex: Option<&str>, target_hex: &str) -> bool {
self.can_act_on_member(actor_hex, owner_hex, target_hex, Permissions::MANAGE_ROLES)
}
pub fn outranks(&self, actor_hex: &str, owner_hex: Option<&str>, target_position: u32) -> bool {
if owner_hex == Some(actor_hex) {
return true;
}
match self.highest_position(actor_hex) {
Some(p) => p < target_position,
None => false,
}
}
pub fn can_act_on_position(&self, actor_hex: &str, owner_hex: Option<&str>, target_position: u32, permission: u64) -> bool {
if owner_hex == Some(actor_hex) {
return true;
}
self.has_permission(actor_hex, permission) && self.outranks(actor_hex, owner_hex, target_position)
}
pub fn can_act_on_member(&self, actor_hex: &str, owner_hex: Option<&str>, target_hex: &str, permission: u64) -> bool {
if owner_hex == Some(target_hex) {
return false;
}
let target_position = self.highest_position(target_hex).unwrap_or(u32::MAX);
self.can_act_on_position(actor_hex, owner_hex, target_position, permission)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn admin_roster(member: &str) -> CommunityRoles {
let role = Role::admin("a".repeat(64));
CommunityRoles {
grants: vec![MemberGrant {
member: member.to_string(),
role_ids: vec![role.role_id.clone()],
}],
roles: vec![role],
}
}
#[test]
fn admin_role_holds_every_management_bit() {
let p = Permissions::admin();
for bit in [
Permissions::MANAGE_ROLES,
Permissions::MANAGE_CHANNELS,
Permissions::MANAGE_METADATA,
Permissions::KICK,
Permissions::BAN,
Permissions::MANAGE_MESSAGES,
Permissions::CREATE_INVITE,
Permissions::VIEW_AUDIT_LOG,
Permissions::MENTION_EVERYONE,
] {
assert!(p.contains(bit), "admin must hold bit {bit}");
}
}
#[test]
fn social_only_role_is_not_management_nor_admin() {
let social = Permissions(Permissions::MENTION_EVERYONE);
assert!(!social.is_management(), "a purely-social permission is not management");
let role = Role {
role_id: "c".repeat(64),
name: "Hype".into(),
position: 5,
permissions: social,
scope: RoleScope::Server,
color: 0,
};
let alice = "aa".repeat(32);
let r = CommunityRoles {
grants: vec![MemberGrant { member: alice.clone(), role_ids: vec![role.role_id.clone()] }],
roles: vec![role],
};
assert!(!r.is_admin(&alice), "a social-only role holder is not an admin");
assert!(r.is_privileged(&alice), "though they do hold a role");
}
#[test]
fn hierarchy_only_strictly_higher_can_manage() {
let owner = "00".repeat(32);
let admin = "aa".repeat(32);
let moderator = "bb".repeat(32);
let member = "cc".repeat(32);
let admin_role = Role::admin("a".repeat(64));
let mod_role = Role {
role_id: "b".repeat(64),
name: "Mod".into(),
position: 2,
permissions: Permissions(Permissions::MANAGE_ROLES | Permissions::KICK),
scope: RoleScope::Server,
color: 0,
};
let admin_pos = admin_role.position; let mod_pos = mod_role.position; let r = CommunityRoles {
grants: vec![
MemberGrant { member: admin.clone(), role_ids: vec![admin_role.role_id.clone()] },
MemberGrant { member: moderator.clone(), role_ids: vec![mod_role.role_id.clone()] },
],
roles: vec![admin_role, mod_role],
};
let owner_ref = Some(owner.as_str());
assert!(r.can_manage_position(&owner, owner_ref, admin_pos));
assert!(r.can_manage_member(&owner, owner_ref, &admin));
assert!(!r.can_manage_member(&admin, owner_ref, &owner), "owner is never a valid target");
assert!(!r.can_manage_position(&admin, owner_ref, admin_pos), "admin can't grant a peer-rank role");
assert!(r.can_manage_position(&admin, owner_ref, mod_pos), "admin outranks Mod");
assert!(r.can_manage_member(&admin, owner_ref, &moderator), "admin outranks the mod member");
assert!(r.can_manage_member(&admin, owner_ref, &member), "admin outranks a roleless member");
assert!(!r.can_manage_member(&admin, owner_ref, &admin), "admin can't manage a peer admin");
assert!(!r.can_manage_position(&moderator, owner_ref, admin_pos));
assert!(!r.can_manage_position(&member, owner_ref, mod_pos), "no role, no MANAGE_ROLES → nothing");
}
#[test]
fn can_act_on_member_gates_on_permission_and_rank() {
let owner = "00".repeat(32);
let admin = "aa".repeat(32);
let kicker = "bb".repeat(32);
let member = "cc".repeat(32);
let admin_role = Role::admin("a".repeat(64)); let kick_role = Role {
role_id: "b".repeat(64),
name: "Mod".into(),
position: 2,
permissions: Permissions(Permissions::KICK), scope: RoleScope::Server,
color: 0,
};
let r = CommunityRoles {
grants: vec![
MemberGrant { member: admin.clone(), role_ids: vec![admin_role.role_id.clone()] },
MemberGrant { member: kicker.clone(), role_ids: vec![kick_role.role_id.clone()] },
],
roles: vec![admin_role, kick_role],
};
let o = Some(owner.as_str());
use Permissions as P;
assert!(r.can_act_on_member(&admin, o, &kicker, P::BAN));
assert!(r.can_act_on_member(&admin, o, &member, P::BAN));
assert!(!r.can_act_on_member(&admin, o, &admin, P::BAN), "no banning a peer admin (equal rank)");
assert!(!r.can_act_on_member(&admin, o, &owner, P::BAN), "the owner is never a valid target");
assert!(r.can_act_on_member(&admin, o, &member, P::MANAGE_MESSAGES), "admin can hide a member's msg");
assert!(!r.can_act_on_member(&kicker, o, &member, P::BAN), "no BAN permission → can't ban");
assert!(!r.can_act_on_member(&kicker, o, &member, P::MANAGE_MESSAGES), "no MANAGE_MESSAGES → can't hide");
assert!(r.can_act_on_member(&kicker, o, &member, P::KICK), "but it CAN kick a plain member");
assert!(r.can_act_on_member(&owner, o, &admin, P::BAN));
assert!(!r.can_act_on_member(&member, o, &kicker, P::BAN));
}
#[test]
fn effective_permissions_union_and_position() {
let alice = "aa".repeat(32);
let r = admin_roster(&alice);
assert!(r.has_permission(&alice, Permissions::BAN));
assert!(r.has_permission(&alice, Permissions::MANAGE_ROLES));
assert_eq!(r.highest_position(&alice), Some(1));
assert!(r.is_privileged(&alice));
let bob = "bb".repeat(32);
assert!(!r.has_permission(&bob, Permissions::BAN));
assert_eq!(r.highest_position(&bob), None);
assert!(!r.is_privileged(&bob));
}
#[test]
fn multiple_roles_union_perms_and_take_highest_position() {
let alice = "aa".repeat(32);
let mod_role = Role {
role_id: "b".repeat(64),
name: "Mod".into(),
position: 2,
permissions: Permissions(Permissions::MANAGE_MESSAGES | Permissions::KICK),
scope: RoleScope::Server,
color: 0,
};
let admin = Role::admin("a".repeat(64));
let r = CommunityRoles {
grants: vec![MemberGrant {
member: alice.clone(),
role_ids: vec![admin.role_id.clone(), mod_role.role_id.clone()],
}],
roles: vec![admin, mod_role],
};
assert!(r.has_permission(&alice, Permissions::BAN)); assert!(r.has_permission(&alice, Permissions::MANAGE_MESSAGES)); assert_eq!(r.highest_position(&alice), Some(1));
}
#[test]
fn round_trips_json() {
let alice = "aa".repeat(32);
let r = admin_roster(&alice);
let json = serde_json::to_string(&r).unwrap();
let back: CommunityRoles = serde_json::from_str(&json).unwrap();
assert_eq!(r, back);
assert!(json.contains("\"kind\":\"server\""));
}
#[test]
fn is_authorized_owner_supreme_admin_by_permission_demoted_rejected() {
let owner = "00".repeat(32);
let alice = "aa".repeat(32);
let r = admin_roster(&alice); assert!(r.is_authorized(&owner, Some(&owner), Permissions::BAN));
assert!(r.is_authorized(&alice, Some(&owner), Permissions::BAN));
let bob = "bb".repeat(32);
assert!(!r.is_authorized(&bob, Some(&owner), Permissions::BAN));
let demoted = CommunityRoles { roles: r.roles.clone(), grants: vec![] };
assert!(!demoted.is_authorized(&alice, Some(&owner), Permissions::BAN));
assert!(demoted.is_authorized(&owner, Some(&owner), Permissions::BAN));
}
#[test]
fn channel_scope_round_trips() {
let scope = RoleScope::Channel("cc".repeat(32));
let json = serde_json::to_string(&scope).unwrap();
let back: RoleScope = serde_json::from_str(&json).unwrap();
assert_eq!(scope, back);
}
}