1use serde::{Deserialize, Serialize};
18
19#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
23#[serde(transparent)]
24pub struct Permissions(pub u64);
25
26impl Permissions {
27 pub const MANAGE_ROLES: u64 = 1 << 0;
28 pub const MANAGE_CHANNELS: u64 = 1 << 1;
29 pub const MANAGE_METADATA: u64 = 1 << 2;
30 pub const KICK: u64 = 1 << 3;
31 pub const BAN: u64 = 1 << 4;
32 pub const MANAGE_MESSAGES: u64 = 1 << 5;
33 pub const CREATE_INVITE: u64 = 1 << 6;
34 pub const VIEW_AUDIT_LOG: u64 = 1 << 8;
35 pub const MENTION_EVERYONE: u64 = 1 << 9;
36 pub const ADMIN_ALL: u64 = Self::MANAGE_ROLES
43 | Self::MANAGE_CHANNELS
44 | Self::MANAGE_METADATA
45 | Self::KICK
46 | Self::BAN
47 | Self::MANAGE_MESSAGES
48 | Self::CREATE_INVITE
49 | Self::VIEW_AUDIT_LOG
50 | Self::MENTION_EVERYONE;
51
52 pub const MANAGEMENT_MASK: u64 = Self::MANAGE_ROLES
57 | Self::MANAGE_CHANNELS
58 | Self::MANAGE_METADATA
59 | Self::KICK
60 | Self::BAN
61 | Self::MANAGE_MESSAGES
62 | Self::CREATE_INVITE
63 | Self::VIEW_AUDIT_LOG;
64
65 pub fn empty() -> Self {
66 Permissions(0)
67 }
68 pub fn admin() -> Self {
69 Permissions(Self::ADMIN_ALL)
70 }
71 pub fn is_management(self) -> bool {
74 self.0 & Self::MANAGEMENT_MASK != 0
75 }
76 pub fn contains(self, bits: u64) -> bool {
78 self.0 & bits == bits
79 }
80 pub fn union(self, other: Permissions) -> Permissions {
81 Permissions(self.0 | other.0)
82 }
83}
84
85#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
88#[serde(rename_all = "snake_case", tag = "kind", content = "channel_id")]
89pub enum RoleScope {
90 Server,
91 Channel(String),
93}
94
95#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
98pub struct Role {
99 pub role_id: String,
101 pub name: String,
102 pub position: u32,
107 pub permissions: Permissions,
108 pub scope: RoleScope,
109 #[serde(default)]
111 pub color: u32,
112}
113
114impl Role {
115 pub fn admin(role_id: String) -> Self {
118 Role {
119 role_id,
120 name: "Admin".to_string(),
121 position: 1,
122 permissions: Permissions::admin(),
123 scope: RoleScope::Server,
124 color: 0,
125 }
126 }
127}
128
129#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
133pub struct MemberGrant {
134 pub member: String,
135 #[serde(default)]
136 pub role_ids: Vec<String>,
137}
138
139#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
142pub struct CommunityRoles {
143 #[serde(default)]
144 pub roles: Vec<Role>,
145 #[serde(default)]
146 pub grants: Vec<MemberGrant>,
147}
148
149impl CommunityRoles {
150 pub fn role(&self, role_id: &str) -> Option<&Role> {
152 self.roles.iter().find(|r| r.role_id == role_id)
153 }
154
155 pub fn roles_of<'a>(&'a self, member_hex: &'a str) -> impl Iterator<Item = &'a Role> + 'a {
157 self.grants
158 .iter()
159 .filter(move |g| g.member == member_hex)
160 .flat_map(move |g| g.role_ids.iter())
161 .filter_map(move |rid| self.role(rid))
162 }
163
164 pub fn effective_permissions(&self, member_hex: &str) -> Permissions {
166 self.roles_of(member_hex)
167 .fold(Permissions::empty(), |acc, r| acc.union(r.permissions))
168 }
169
170 pub fn has_permission(&self, member_hex: &str, bits: u64) -> bool {
172 self.effective_permissions(member_hex).contains(bits)
173 }
174
175 pub fn highest_position(&self, member_hex: &str) -> Option<u32> {
178 self.roles_of(member_hex).map(|r| r.position).min()
179 }
180
181 pub fn is_privileged(&self, member_hex: &str) -> bool {
183 self.roles_of(member_hex).next().is_some()
184 }
185
186 pub fn is_admin(&self, member_hex: &str) -> bool {
189 self.roles_of(member_hex).any(|r| r.permissions.is_management())
190 }
191
192 pub fn is_authorized(&self, actor_hex: &str, owner_hex: Option<&str>, permission: u64) -> bool {
197 if owner_hex == Some(actor_hex) {
198 return true;
199 }
200 self.has_permission(actor_hex, permission)
201 }
202
203 pub fn can_manage_position(&self, actor_hex: &str, owner_hex: Option<&str>, target_position: u32) -> bool {
211 self.can_act_on_position(actor_hex, owner_hex, target_position, Permissions::MANAGE_ROLES)
212 }
213
214 pub fn can_manage_member(&self, actor_hex: &str, owner_hex: Option<&str>, target_hex: &str) -> bool {
218 self.can_act_on_member(actor_hex, owner_hex, target_hex, Permissions::MANAGE_ROLES)
219 }
220
221 pub fn outranks(&self, actor_hex: &str, owner_hex: Option<&str>, target_position: u32) -> bool {
226 if owner_hex == Some(actor_hex) {
227 return true;
228 }
229 match self.highest_position(actor_hex) {
230 Some(p) => p < target_position,
231 None => false,
232 }
233 }
234
235 pub fn can_act_on_position(&self, actor_hex: &str, owner_hex: Option<&str>, target_position: u32, permission: u64) -> bool {
240 if owner_hex == Some(actor_hex) {
241 return true;
242 }
243 self.has_permission(actor_hex, permission) && self.outranks(actor_hex, owner_hex, target_position)
244 }
245
246 pub fn can_act_on_member(&self, actor_hex: &str, owner_hex: Option<&str>, target_hex: &str, permission: u64) -> bool {
250 if owner_hex == Some(target_hex) {
251 return false;
252 }
253 let target_position = self.highest_position(target_hex).unwrap_or(u32::MAX);
254 self.can_act_on_position(actor_hex, owner_hex, target_position, permission)
255 }
256}
257
258#[cfg(test)]
259mod tests {
260 use super::*;
261
262 fn admin_roster(member: &str) -> CommunityRoles {
263 let role = Role::admin("a".repeat(64));
264 CommunityRoles {
265 grants: vec![MemberGrant {
266 member: member.to_string(),
267 role_ids: vec![role.role_id.clone()],
268 }],
269 roles: vec![role],
270 }
271 }
272
273 #[test]
274 fn admin_role_holds_every_management_bit() {
275 let p = Permissions::admin();
276 for bit in [
277 Permissions::MANAGE_ROLES,
278 Permissions::MANAGE_CHANNELS,
279 Permissions::MANAGE_METADATA,
280 Permissions::KICK,
281 Permissions::BAN,
282 Permissions::MANAGE_MESSAGES,
283 Permissions::CREATE_INVITE,
284 Permissions::VIEW_AUDIT_LOG,
285 Permissions::MENTION_EVERYONE,
286 ] {
287 assert!(p.contains(bit), "admin must hold bit {bit}");
288 }
289 }
290
291 #[test]
292 fn social_only_role_is_not_management_nor_admin() {
293 let social = Permissions(Permissions::MENTION_EVERYONE);
297 assert!(!social.is_management(), "a purely-social permission is not management");
298 let role = Role {
299 role_id: "c".repeat(64),
300 name: "Hype".into(),
301 position: 5,
302 permissions: social,
303 scope: RoleScope::Server,
304 color: 0,
305 };
306 let alice = "aa".repeat(32);
307 let r = CommunityRoles {
308 grants: vec![MemberGrant { member: alice.clone(), role_ids: vec![role.role_id.clone()] }],
309 roles: vec![role],
310 };
311 assert!(!r.is_admin(&alice), "a social-only role holder is not an admin");
312 assert!(r.is_privileged(&alice), "though they do hold a role");
313 }
314
315 #[test]
316 fn hierarchy_only_strictly_higher_can_manage() {
317 let owner = "00".repeat(32);
319 let admin = "aa".repeat(32);
320 let moderator = "bb".repeat(32);
321 let member = "cc".repeat(32);
322 let admin_role = Role::admin("a".repeat(64));
323 let mod_role = Role {
324 role_id: "b".repeat(64),
325 name: "Mod".into(),
326 position: 2,
327 permissions: Permissions(Permissions::MANAGE_ROLES | Permissions::KICK),
328 scope: RoleScope::Server,
329 color: 0,
330 };
331 let admin_pos = admin_role.position; let mod_pos = mod_role.position; let r = CommunityRoles {
334 grants: vec![
335 MemberGrant { member: admin.clone(), role_ids: vec![admin_role.role_id.clone()] },
336 MemberGrant { member: moderator.clone(), role_ids: vec![mod_role.role_id.clone()] },
337 ],
338 roles: vec![admin_role, mod_role],
339 };
340 let owner_ref = Some(owner.as_str());
341
342 assert!(r.can_manage_position(&owner, owner_ref, admin_pos));
344 assert!(r.can_manage_member(&owner, owner_ref, &admin));
345 assert!(!r.can_manage_member(&admin, owner_ref, &owner), "owner is never a valid target");
346
347 assert!(!r.can_manage_position(&admin, owner_ref, admin_pos), "admin can't grant a peer-rank role");
350 assert!(r.can_manage_position(&admin, owner_ref, mod_pos), "admin outranks Mod");
351 assert!(r.can_manage_member(&admin, owner_ref, &moderator), "admin outranks the mod member");
352 assert!(r.can_manage_member(&admin, owner_ref, &member), "admin outranks a roleless member");
353 assert!(!r.can_manage_member(&admin, owner_ref, &admin), "admin can't manage a peer admin");
354
355 assert!(!r.can_manage_position(&moderator, owner_ref, admin_pos));
357 assert!(!r.can_manage_position(&member, owner_ref, mod_pos), "no role, no MANAGE_ROLES → nothing");
358 }
359
360 #[test]
361 fn can_act_on_member_gates_on_permission_and_rank() {
362 let owner = "00".repeat(32);
366 let admin = "aa".repeat(32);
367 let kicker = "bb".repeat(32);
368 let member = "cc".repeat(32);
369 let admin_role = Role::admin("a".repeat(64)); let kick_role = Role {
371 role_id: "b".repeat(64),
372 name: "Mod".into(),
373 position: 2,
374 permissions: Permissions(Permissions::KICK), scope: RoleScope::Server,
376 color: 0,
377 };
378 let r = CommunityRoles {
379 grants: vec![
380 MemberGrant { member: admin.clone(), role_ids: vec![admin_role.role_id.clone()] },
381 MemberGrant { member: kicker.clone(), role_ids: vec![kick_role.role_id.clone()] },
382 ],
383 roles: vec![admin_role, kick_role],
384 };
385 let o = Some(owner.as_str());
386 use Permissions as P;
387
388 assert!(r.can_act_on_member(&admin, o, &kicker, P::BAN));
390 assert!(r.can_act_on_member(&admin, o, &member, P::BAN));
391 assert!(!r.can_act_on_member(&admin, o, &admin, P::BAN), "no banning a peer admin (equal rank)");
392 assert!(!r.can_act_on_member(&admin, o, &owner, P::BAN), "the owner is never a valid target");
393 assert!(r.can_act_on_member(&admin, o, &member, P::MANAGE_MESSAGES), "admin can hide a member's msg");
394
395 assert!(!r.can_act_on_member(&kicker, o, &member, P::BAN), "no BAN permission → can't ban");
397 assert!(!r.can_act_on_member(&kicker, o, &member, P::MANAGE_MESSAGES), "no MANAGE_MESSAGES → can't hide");
398 assert!(r.can_act_on_member(&kicker, o, &member, P::KICK), "but it CAN kick a plain member");
399
400 assert!(r.can_act_on_member(&owner, o, &admin, P::BAN));
402 assert!(!r.can_act_on_member(&member, o, &kicker, P::BAN));
404 }
405
406 #[test]
407 fn effective_permissions_union_and_position() {
408 let alice = "aa".repeat(32);
409 let r = admin_roster(&alice);
410 assert!(r.has_permission(&alice, Permissions::BAN));
411 assert!(r.has_permission(&alice, Permissions::MANAGE_ROLES));
412 assert_eq!(r.highest_position(&alice), Some(1));
413 assert!(r.is_privileged(&alice));
414
415 let bob = "bb".repeat(32);
417 assert!(!r.has_permission(&bob, Permissions::BAN));
418 assert_eq!(r.highest_position(&bob), None);
419 assert!(!r.is_privileged(&bob));
420 }
421
422 #[test]
423 fn multiple_roles_union_perms_and_take_highest_position() {
424 let alice = "aa".repeat(32);
425 let mod_role = Role {
426 role_id: "b".repeat(64),
427 name: "Mod".into(),
428 position: 2,
429 permissions: Permissions(Permissions::MANAGE_MESSAGES | Permissions::KICK),
430 scope: RoleScope::Server,
431 color: 0,
432 };
433 let admin = Role::admin("a".repeat(64));
434 let r = CommunityRoles {
435 grants: vec![MemberGrant {
436 member: alice.clone(),
437 role_ids: vec![admin.role_id.clone(), mod_role.role_id.clone()],
438 }],
439 roles: vec![admin, mod_role],
440 };
441 assert!(r.has_permission(&alice, Permissions::BAN)); assert!(r.has_permission(&alice, Permissions::MANAGE_MESSAGES)); assert_eq!(r.highest_position(&alice), Some(1));
445 }
446
447 #[test]
448 fn round_trips_json() {
449 let alice = "aa".repeat(32);
450 let r = admin_roster(&alice);
451 let json = serde_json::to_string(&r).unwrap();
452 let back: CommunityRoles = serde_json::from_str(&json).unwrap();
453 assert_eq!(r, back);
454 assert!(json.contains("\"kind\":\"server\""));
455 }
456
457 #[test]
458 fn is_authorized_owner_supreme_admin_by_permission_demoted_rejected() {
459 let owner = "00".repeat(32);
460 let alice = "aa".repeat(32);
461 let r = admin_roster(&alice); assert!(r.is_authorized(&owner, Some(&owner), Permissions::BAN));
464 assert!(r.is_authorized(&alice, Some(&owner), Permissions::BAN));
466 let bob = "bb".repeat(32);
468 assert!(!r.is_authorized(&bob, Some(&owner), Permissions::BAN));
469 let demoted = CommunityRoles { roles: r.roles.clone(), grants: vec![] };
470 assert!(!demoted.is_authorized(&alice, Some(&owner), Permissions::BAN));
471 assert!(demoted.is_authorized(&owner, Some(&owner), Permissions::BAN));
473 }
474
475 #[test]
476 fn channel_scope_round_trips() {
477 let scope = RoleScope::Channel("cc".repeat(32));
478 let json = serde_json::to_string(&scope).unwrap();
479 let back: RoleScope = serde_json::from_str(&json).unwrap();
480 assert_eq!(scope, back);
481 }
482}