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 PIN_MESSAGES: u64 = 1 << 11;
39 pub const ADMIN_FOUNDING_MASK: u64 = Self::MANAGE_ROLES
49 | Self::MANAGE_CHANNELS
50 | Self::MANAGE_METADATA
51 | Self::KICK
52 | Self::BAN
53 | Self::MANAGE_MESSAGES
54 | Self::CREATE_INVITE
55 | Self::VIEW_AUDIT_LOG
56 | Self::MENTION_EVERYONE;
57
58 pub const ADMIN_ALL: u64 = Self::ADMIN_FOUNDING_MASK | Self::PIN_MESSAGES;
62
63 pub const MANAGEMENT_MASK: u64 = Self::MANAGE_ROLES
68 | Self::MANAGE_CHANNELS
69 | Self::MANAGE_METADATA
70 | Self::KICK
71 | Self::BAN
72 | Self::MANAGE_MESSAGES
73 | Self::CREATE_INVITE
74 | Self::VIEW_AUDIT_LOG
75 | Self::PIN_MESSAGES;
76
77 pub fn empty() -> Self {
78 Permissions(0)
79 }
80 pub fn admin() -> Self {
81 Permissions(Self::ADMIN_ALL)
82 }
83 pub fn is_management(self) -> bool {
86 self.0 & Self::MANAGEMENT_MASK != 0
87 }
88 pub fn contains(self, bits: u64) -> bool {
90 self.0 & bits == bits
91 }
92 pub fn union(self, other: Permissions) -> Permissions {
93 Permissions(self.0 | other.0)
94 }
95}
96
97#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
100#[serde(rename_all = "snake_case", tag = "kind", content = "channel_id")]
101pub enum RoleScope {
102 Server,
103 Channel(String),
105}
106
107#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
110pub struct Role {
111 pub role_id: String,
113 pub name: String,
114 pub position: u32,
119 pub permissions: Permissions,
120 pub scope: RoleScope,
121 #[serde(default)]
123 pub color: u32,
124}
125
126impl Role {
127 pub fn admin(role_id: String) -> Self {
130 Role {
131 role_id,
132 name: "Admin".to_string(),
133 position: 1,
134 permissions: Permissions::admin(),
135 scope: RoleScope::Server,
136 color: 0,
137 }
138 }
139}
140
141#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
145pub struct MemberGrant {
146 pub member: String,
147 #[serde(default)]
148 pub role_ids: Vec<String>,
149}
150
151#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
154pub struct CommunityRoles {
155 #[serde(default)]
156 pub roles: Vec<Role>,
157 #[serde(default)]
158 pub grants: Vec<MemberGrant>,
159}
160
161impl CommunityRoles {
162 pub fn role(&self, role_id: &str) -> Option<&Role> {
164 self.roles.iter().find(|r| r.role_id == role_id)
165 }
166
167 pub fn roles_of<'a>(&'a self, member_hex: &'a str) -> impl Iterator<Item = &'a Role> + 'a {
169 self.grants
170 .iter()
171 .filter(move |g| g.member == member_hex)
172 .flat_map(move |g| g.role_ids.iter())
173 .filter_map(move |rid| self.role(rid))
174 }
175
176 pub fn effective_permissions(&self, member_hex: &str) -> Permissions {
178 self.roles_of(member_hex)
179 .fold(Permissions::empty(), |acc, r| acc.union(r.permissions))
180 }
181
182 pub fn has_permission(&self, member_hex: &str, bits: u64) -> bool {
184 self.effective_permissions(member_hex).contains(bits)
185 }
186
187 pub fn highest_position(&self, member_hex: &str) -> Option<u32> {
190 self.roles_of(member_hex).map(|r| r.position).min()
191 }
192
193 pub fn is_privileged(&self, member_hex: &str) -> bool {
195 self.roles_of(member_hex).next().is_some()
196 }
197
198 pub fn is_admin(&self, member_hex: &str) -> bool {
201 self.roles_of(member_hex).any(|r| r.permissions.is_management())
202 }
203
204 pub fn is_authorized(&self, actor_hex: &str, owner_hex: Option<&str>, permission: u64) -> bool {
209 if owner_hex == Some(actor_hex) {
210 return true;
211 }
212 self.has_permission(actor_hex, permission)
213 }
214
215 pub fn can_manage_position(&self, actor_hex: &str, owner_hex: Option<&str>, target_position: u32) -> bool {
223 self.can_act_on_position(actor_hex, owner_hex, target_position, Permissions::MANAGE_ROLES)
224 }
225
226 pub fn can_manage_member(&self, actor_hex: &str, owner_hex: Option<&str>, target_hex: &str) -> bool {
230 self.can_act_on_member(actor_hex, owner_hex, target_hex, Permissions::MANAGE_ROLES)
231 }
232
233 pub fn outranks(&self, actor_hex: &str, owner_hex: Option<&str>, target_position: u32) -> bool {
238 if owner_hex == Some(actor_hex) {
239 return true;
240 }
241 match self.highest_position(actor_hex) {
242 Some(p) => p < target_position,
243 None => false,
244 }
245 }
246
247 pub fn can_act_on_position(&self, actor_hex: &str, owner_hex: Option<&str>, target_position: u32, permission: u64) -> bool {
252 if owner_hex == Some(actor_hex) {
253 return true;
254 }
255 self.has_permission(actor_hex, permission) && self.outranks(actor_hex, owner_hex, target_position)
256 }
257
258 pub fn can_act_on_member(&self, actor_hex: &str, owner_hex: Option<&str>, target_hex: &str, permission: u64) -> bool {
262 if owner_hex == Some(target_hex) {
263 return false;
264 }
265 let target_position = self.highest_position(target_hex).unwrap_or(u32::MAX);
266 self.can_act_on_position(actor_hex, owner_hex, target_position, permission)
267 }
268
269 pub fn channel_roles(&self, channel_hex: &str) -> Vec<&Role> {
289 let wanted = channel_hex.to_ascii_lowercase();
290 let mut out: Vec<&Role> = self
291 .roles
292 .iter()
293 .filter(|r| matches!(&r.scope, RoleScope::Channel(c) if c.eq_ignore_ascii_case(&wanted)))
294 .collect();
295 out.sort_by_key(|r| r.position);
296 out
297 }
298
299 pub fn channel_role_ids(&self, channel_hex: &str) -> Vec<String> {
301 self.channel_roles(channel_hex).into_iter().map(|r| r.role_id.clone()).collect()
302 }
303
304 pub fn is_entitled(
311 &self,
312 owner_hex: Option<&str>,
313 member_hex: &str,
314 channel_hex: &str,
315 with: &[String],
316 without: &[String],
317 ) -> bool {
318 if owner_hex == Some(member_hex) {
319 return true;
320 }
321 let mut held: std::collections::HashSet<&str> =
322 self.roles_of(member_hex).map(|r| r.role_id.as_str()).collect();
323 for id in with {
324 held.insert(id.as_str());
325 }
326 for id in without {
327 held.remove(id.as_str());
328 }
329 self.channel_role_ids(channel_hex).iter().any(|id| held.contains(id.as_str()))
330 }
331
332 pub fn effective_permissions_in(&self, member_hex: &str, channel_hex: &str) -> Permissions {
341 let wanted = channel_hex.to_ascii_lowercase();
342 self.roles_of(member_hex)
343 .filter(|r| match &r.scope {
344 RoleScope::Server => true,
345 RoleScope::Channel(c) => c.eq_ignore_ascii_case(&wanted),
346 })
347 .fold(Permissions::empty(), |acc, r| acc.union(r.permissions))
348 }
349
350 pub fn is_authorized_in(&self, actor_hex: &str, owner_hex: Option<&str>, channel_hex: &str, permission: u64) -> bool {
353 if owner_hex == Some(actor_hex) {
354 return true;
355 }
356 self.effective_permissions_in(actor_hex, channel_hex).contains(permission)
357 }
358}
359
360#[cfg(test)]
361mod tests {
362 use super::*;
363
364 fn admin_roster(member: &str) -> CommunityRoles {
365 let role = Role::admin("a".repeat(64));
366 CommunityRoles {
367 grants: vec![MemberGrant {
368 member: member.to_string(),
369 role_ids: vec![role.role_id.clone()],
370 }],
371 roles: vec![role],
372 }
373 }
374
375 #[test]
376 fn admin_role_holds_every_management_bit() {
377 let p = Permissions::admin();
378 for bit in [
379 Permissions::MANAGE_ROLES,
380 Permissions::MANAGE_CHANNELS,
381 Permissions::MANAGE_METADATA,
382 Permissions::KICK,
383 Permissions::BAN,
384 Permissions::MANAGE_MESSAGES,
385 Permissions::CREATE_INVITE,
386 Permissions::VIEW_AUDIT_LOG,
387 Permissions::MENTION_EVERYONE,
388 ] {
389 assert!(p.contains(bit), "admin must hold bit {bit}");
390 }
391 }
392
393 #[test]
394 fn social_only_role_is_not_management_nor_admin() {
395 let social = Permissions(Permissions::MENTION_EVERYONE);
399 assert!(!social.is_management(), "a purely-social permission is not management");
400 let role = Role {
401 role_id: "c".repeat(64),
402 name: "Hype".into(),
403 position: 5,
404 permissions: social,
405 scope: RoleScope::Server,
406 color: 0,
407 };
408 let alice = "aa".repeat(32);
409 let r = CommunityRoles {
410 grants: vec![MemberGrant { member: alice.clone(), role_ids: vec![role.role_id.clone()] }],
411 roles: vec![role],
412 };
413 assert!(!r.is_admin(&alice), "a social-only role holder is not an admin");
414 assert!(r.is_privileged(&alice), "though they do hold a role");
415 }
416
417 #[test]
418 fn hierarchy_only_strictly_higher_can_manage() {
419 let owner = "00".repeat(32);
421 let admin = "aa".repeat(32);
422 let moderator = "bb".repeat(32);
423 let member = "cc".repeat(32);
424 let admin_role = Role::admin("a".repeat(64));
425 let mod_role = Role {
426 role_id: "b".repeat(64),
427 name: "Mod".into(),
428 position: 2,
429 permissions: Permissions(Permissions::MANAGE_ROLES | Permissions::KICK),
430 scope: RoleScope::Server,
431 color: 0,
432 };
433 let admin_pos = admin_role.position; let mod_pos = mod_role.position; let r = CommunityRoles {
436 grants: vec![
437 MemberGrant { member: admin.clone(), role_ids: vec![admin_role.role_id.clone()] },
438 MemberGrant { member: moderator.clone(), role_ids: vec![mod_role.role_id.clone()] },
439 ],
440 roles: vec![admin_role, mod_role],
441 };
442 let owner_ref = Some(owner.as_str());
443
444 assert!(r.can_manage_position(&owner, owner_ref, admin_pos));
446 assert!(r.can_manage_member(&owner, owner_ref, &admin));
447 assert!(!r.can_manage_member(&admin, owner_ref, &owner), "owner is never a valid target");
448
449 assert!(!r.can_manage_position(&admin, owner_ref, admin_pos), "admin can't grant a peer-rank role");
452 assert!(r.can_manage_position(&admin, owner_ref, mod_pos), "admin outranks Mod");
453 assert!(r.can_manage_member(&admin, owner_ref, &moderator), "admin outranks the mod member");
454 assert!(r.can_manage_member(&admin, owner_ref, &member), "admin outranks a roleless member");
455 assert!(!r.can_manage_member(&admin, owner_ref, &admin), "admin can't manage a peer admin");
456
457 assert!(!r.can_manage_position(&moderator, owner_ref, admin_pos));
459 assert!(!r.can_manage_position(&member, owner_ref, mod_pos), "no role, no MANAGE_ROLES → nothing");
460 }
461
462 #[test]
463 fn can_act_on_member_gates_on_permission_and_rank() {
464 let owner = "00".repeat(32);
468 let admin = "aa".repeat(32);
469 let kicker = "bb".repeat(32);
470 let member = "cc".repeat(32);
471 let admin_role = Role::admin("a".repeat(64)); let kick_role = Role {
473 role_id: "b".repeat(64),
474 name: "Mod".into(),
475 position: 2,
476 permissions: Permissions(Permissions::KICK), scope: RoleScope::Server,
478 color: 0,
479 };
480 let r = CommunityRoles {
481 grants: vec![
482 MemberGrant { member: admin.clone(), role_ids: vec![admin_role.role_id.clone()] },
483 MemberGrant { member: kicker.clone(), role_ids: vec![kick_role.role_id.clone()] },
484 ],
485 roles: vec![admin_role, kick_role],
486 };
487 let o = Some(owner.as_str());
488 use Permissions as P;
489
490 assert!(r.can_act_on_member(&admin, o, &kicker, P::BAN));
492 assert!(r.can_act_on_member(&admin, o, &member, P::BAN));
493 assert!(!r.can_act_on_member(&admin, o, &admin, P::BAN), "no banning a peer admin (equal rank)");
494 assert!(!r.can_act_on_member(&admin, o, &owner, P::BAN), "the owner is never a valid target");
495 assert!(r.can_act_on_member(&admin, o, &member, P::MANAGE_MESSAGES), "admin can hide a member's msg");
496
497 assert!(!r.can_act_on_member(&kicker, o, &member, P::BAN), "no BAN permission → can't ban");
499 assert!(!r.can_act_on_member(&kicker, o, &member, P::MANAGE_MESSAGES), "no MANAGE_MESSAGES → can't hide");
500 assert!(r.can_act_on_member(&kicker, o, &member, P::KICK), "but it CAN kick a plain member");
501
502 assert!(r.can_act_on_member(&owner, o, &admin, P::BAN));
504 assert!(!r.can_act_on_member(&member, o, &kicker, P::BAN));
506 }
507
508 #[test]
509 fn effective_permissions_union_and_position() {
510 let alice = "aa".repeat(32);
511 let r = admin_roster(&alice);
512 assert!(r.has_permission(&alice, Permissions::BAN));
513 assert!(r.has_permission(&alice, Permissions::MANAGE_ROLES));
514 assert_eq!(r.highest_position(&alice), Some(1));
515 assert!(r.is_privileged(&alice));
516
517 let bob = "bb".repeat(32);
519 assert!(!r.has_permission(&bob, Permissions::BAN));
520 assert_eq!(r.highest_position(&bob), None);
521 assert!(!r.is_privileged(&bob));
522 }
523
524 #[test]
525 fn multiple_roles_union_perms_and_take_highest_position() {
526 let alice = "aa".repeat(32);
527 let mod_role = Role {
528 role_id: "b".repeat(64),
529 name: "Mod".into(),
530 position: 2,
531 permissions: Permissions(Permissions::MANAGE_MESSAGES | Permissions::KICK),
532 scope: RoleScope::Server,
533 color: 0,
534 };
535 let admin = Role::admin("a".repeat(64));
536 let r = CommunityRoles {
537 grants: vec![MemberGrant {
538 member: alice.clone(),
539 role_ids: vec![admin.role_id.clone(), mod_role.role_id.clone()],
540 }],
541 roles: vec![admin, mod_role],
542 };
543 assert!(r.has_permission(&alice, Permissions::BAN)); assert!(r.has_permission(&alice, Permissions::MANAGE_MESSAGES)); assert_eq!(r.highest_position(&alice), Some(1));
547 }
548
549 #[test]
550 fn round_trips_json() {
551 let alice = "aa".repeat(32);
552 let r = admin_roster(&alice);
553 let json = serde_json::to_string(&r).unwrap();
554 let back: CommunityRoles = serde_json::from_str(&json).unwrap();
555 assert_eq!(r, back);
556 assert!(json.contains("\"kind\":\"server\""));
557 }
558
559 #[test]
560 fn is_authorized_owner_supreme_admin_by_permission_demoted_rejected() {
561 let owner = "00".repeat(32);
562 let alice = "aa".repeat(32);
563 let r = admin_roster(&alice); assert!(r.is_authorized(&owner, Some(&owner), Permissions::BAN));
566 assert!(r.is_authorized(&alice, Some(&owner), Permissions::BAN));
568 let bob = "bb".repeat(32);
570 assert!(!r.is_authorized(&bob, Some(&owner), Permissions::BAN));
571 let demoted = CommunityRoles { roles: r.roles.clone(), grants: vec![] };
572 assert!(!demoted.is_authorized(&alice, Some(&owner), Permissions::BAN));
573 assert!(demoted.is_authorized(&owner, Some(&owner), Permissions::BAN));
575 }
576
577 #[test]
578 fn channel_scope_round_trips() {
579 let scope = RoleScope::Channel("cc".repeat(32));
580 let json = serde_json::to_string(&scope).unwrap();
581 let back: RoleScope = serde_json::from_str(&json).unwrap();
582 assert_eq!(scope, back);
583 }
584
585 fn scoped_roster(chan: &str, member: &str, admin: &str) -> CommunityRoles {
590 let scoped = Role {
591 role_id: "11".repeat(32),
592 name: "Testers".into(),
593 position: 5,
594 permissions: Permissions(Permissions::MANAGE_MESSAGES),
595 scope: RoleScope::Channel(chan.to_string()),
596 color: 0,
597 };
598 let server = Role::admin("22".repeat(32));
599 CommunityRoles {
600 grants: vec![
601 MemberGrant { member: member.to_string(), role_ids: vec![scoped.role_id.clone()] },
602 MemberGrant { member: admin.to_string(), role_ids: vec![server.role_id.clone()] },
603 ],
604 roles: vec![scoped, server],
605 }
606 }
607
608 #[test]
609 fn a_channels_scoped_roles_are_its_access_list() {
610 let chan = "cc".repeat(32);
611 let other = "dd".repeat(32);
612 let owner = "00".repeat(32);
613 let member = "aa".repeat(32);
614 let admin = "bb".repeat(32);
615 let r = scoped_roster(&chan, &member, &admin);
616
617 assert_eq!(r.channel_roles(&chan).len(), 1, "the scoped role is the access list");
618 assert!(r.channel_roles(&other).is_empty(), "another channel gets none of it");
619
620 assert!(r.is_entitled(Some(&owner), &member, &chan, &[], &[]), "the granted holder is entitled");
621 assert!(r.is_entitled(Some(&owner), &owner, &chan, &[], &[]), "the owner is always entitled");
622 assert!(!r.is_entitled(Some(&owner), &admin, &chan, &[], &[]), "rank alone is not entitlement");
625 assert!(!r.is_entitled(Some(&owner), &member, &other, &[], &[]), "entitlement does not spill to another channel");
626 }
627
628 #[test]
629 fn the_grant_overlay_settles_against_a_publish_the_fold_has_not_caught() {
630 let chan = "cc".repeat(32);
631 let owner = "00".repeat(32);
632 let member = "aa".repeat(32);
633 let stranger = "ee".repeat(32);
634 let r = scoped_roster(&chan, &member, &"bb".repeat(32));
635 let scoped_id = r.channel_role_ids(&chan)[0].clone();
636
637 assert!(
638 r.is_entitled(Some(&owner), &stranger, &chan, std::slice::from_ref(&scoped_id), &[]),
639 "a just-published grant settles as entitled before the fold catches up"
640 );
641 assert!(
642 !r.is_entitled(Some(&owner), &member, &chan, &[], std::slice::from_ref(&scoped_id)),
643 "a just-published revoke settles as unentitled before the fold catches up"
644 );
645 }
646
647 #[test]
648 fn channel_narrowing_is_offer_side_only() {
649 let chan = "cc".repeat(32);
650 let other = "dd".repeat(32);
651 let owner = "00".repeat(32);
652 let member = "aa".repeat(32);
653 let r = scoped_roster(&chan, &member, &"bb".repeat(32));
654
655 assert!(r.is_authorized_in(&member, Some(&owner), &chan, Permissions::MANAGE_MESSAGES));
657 assert!(!r.is_authorized_in(&member, Some(&owner), &other, Permissions::MANAGE_MESSAGES));
658
659 assert!(
663 r.is_authorized(&member, Some(&owner), Permissions::MANAGE_MESSAGES),
664 "the scope-blind honor path is unchanged"
665 );
666 }
667
668 #[test]
672 fn founding_mask_is_frozen_at_the_original_nine_bits() {
673 const ORIGINAL_NINE: u64 = (1 << 0) | (1 << 1) | (1 << 2) | (1 << 3) | (1 << 4)
674 | (1 << 5) | (1 << 6) | (1 << 8) | (1 << 9);
675 assert_eq!(Permissions::ADMIN_FOUNDING_MASK, ORIGINAL_NINE);
676 assert!(Permissions(ORIGINAL_NINE).contains(Permissions::ADMIN_FOUNDING_MASK));
678 assert!(Permissions::admin().contains(Permissions::ADMIN_FOUNDING_MASK));
680 assert!(Permissions::admin().contains(Permissions::PIN_MESSAGES));
681 assert!(!Permissions(ORIGINAL_NINE).contains(Permissions::ADMIN_ALL));
684 }
685}