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 const STAFF_MASK: u64 = Self::MANAGE_ROLES
85 | Self::MANAGE_CHANNELS
86 | Self::MANAGE_METADATA
87 | Self::BAN
88 | Self::CREATE_INVITE
89 | Self::PIN_MESSAGES;
90
91 pub fn empty() -> Self {
92 Permissions(0)
93 }
94 pub fn admin() -> Self {
95 Permissions(Self::ADMIN_ALL)
96 }
97 pub fn is_management(self) -> bool {
100 self.0 & Self::MANAGEMENT_MASK != 0
101 }
102 pub fn contains(self, bits: u64) -> bool {
104 self.0 & bits == bits
105 }
106 pub fn union(self, other: Permissions) -> Permissions {
107 Permissions(self.0 | other.0)
108 }
109}
110
111#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
114#[serde(rename_all = "snake_case", tag = "kind", content = "channel_id")]
115pub enum RoleScope {
116 Server,
117 Channel(String),
119}
120
121#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
124pub struct Role {
125 pub role_id: String,
127 pub name: String,
128 pub position: u32,
133 pub permissions: Permissions,
134 pub scope: RoleScope,
135 #[serde(default)]
137 pub color: u32,
138 #[serde(default, skip_serializing_if = "serde_json::Map::is_empty")]
142 pub extra: serde_json::Map<String, serde_json::Value>,
143}
144
145impl Role {
146 pub fn admin(role_id: String) -> Self {
149 Role {
150 role_id,
151 name: "Admin".to_string(),
152 position: 1,
153 permissions: Permissions::admin(),
154 scope: RoleScope::Server,
155 color: 0, extra: Default::default(),
156 }
157 }
158}
159
160#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
164pub struct MemberGrant {
165 pub member: String,
166 #[serde(default)]
167 pub role_ids: Vec<String>,
168}
169
170#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
173pub struct CommunityRoles {
174 #[serde(default)]
175 pub roles: Vec<Role>,
176 #[serde(default)]
177 pub grants: Vec<MemberGrant>,
178}
179
180impl CommunityRoles {
181 pub fn role(&self, role_id: &str) -> Option<&Role> {
183 self.roles.iter().find(|r| r.role_id == role_id)
184 }
185
186 pub fn roles_of<'a>(&'a self, member_hex: &'a str) -> impl Iterator<Item = &'a Role> + 'a {
188 self.grants
189 .iter()
190 .filter(move |g| g.member == member_hex)
191 .flat_map(move |g| g.role_ids.iter())
192 .filter_map(move |rid| self.role(rid))
193 }
194
195 pub fn effective_permissions(&self, member_hex: &str) -> Permissions {
197 self.roles_of(member_hex)
198 .fold(Permissions::empty(), |acc, r| acc.union(r.permissions))
199 }
200
201 pub fn has_permission(&self, member_hex: &str, bits: u64) -> bool {
203 self.effective_permissions(member_hex).contains(bits)
204 }
205
206 pub fn highest_position(&self, member_hex: &str) -> Option<u32> {
209 self.roles_of(member_hex).map(|r| r.position).min()
210 }
211
212 pub fn is_privileged(&self, member_hex: &str) -> bool {
214 self.roles_of(member_hex).next().is_some()
215 }
216
217 pub fn is_admin(&self, member_hex: &str) -> bool {
220 self.roles_of(member_hex).any(|r| r.permissions.is_management())
221 }
222
223 pub fn is_staff(&self, member_hex: &str, owner_hex: Option<&str>) -> bool {
227 if owner_hex == Some(member_hex) {
228 return true;
229 }
230 self.effective_permissions(member_hex).0 & Permissions::STAFF_MASK != 0
231 }
232
233 pub fn roles_make_staff(&self, role_ids: &[String]) -> bool {
238 role_ids
239 .iter()
240 .filter_map(|rid| self.role(rid))
241 .any(|r| r.permissions.0 & Permissions::STAFF_MASK != 0)
242 }
243
244 pub fn is_authorized(&self, actor_hex: &str, owner_hex: Option<&str>, permission: u64) -> bool {
249 if owner_hex == Some(actor_hex) {
250 return true;
251 }
252 self.has_permission(actor_hex, permission)
253 }
254
255 pub fn can_manage_position(&self, actor_hex: &str, owner_hex: Option<&str>, target_position: u32) -> bool {
263 self.can_act_on_position(actor_hex, owner_hex, target_position, Permissions::MANAGE_ROLES)
264 }
265
266 pub fn can_manage_member(&self, actor_hex: &str, owner_hex: Option<&str>, target_hex: &str) -> bool {
270 self.can_act_on_member(actor_hex, owner_hex, target_hex, Permissions::MANAGE_ROLES)
271 }
272
273 pub fn outranks(&self, actor_hex: &str, owner_hex: Option<&str>, target_position: u32) -> bool {
278 if owner_hex == Some(actor_hex) {
279 return true;
280 }
281 match self.highest_position(actor_hex) {
282 Some(p) => p < target_position,
283 None => false,
284 }
285 }
286
287 pub fn can_act_on_position(&self, actor_hex: &str, owner_hex: Option<&str>, target_position: u32, permission: u64) -> bool {
292 if owner_hex == Some(actor_hex) {
293 return true;
294 }
295 self.has_permission(actor_hex, permission) && self.outranks(actor_hex, owner_hex, target_position)
296 }
297
298 pub fn can_act_on_member(&self, actor_hex: &str, owner_hex: Option<&str>, target_hex: &str, permission: u64) -> bool {
302 if owner_hex == Some(target_hex) {
303 return false;
304 }
305 let target_position = self.highest_position(target_hex).unwrap_or(u32::MAX);
306 self.can_act_on_position(actor_hex, owner_hex, target_position, permission)
307 }
308
309 pub fn channel_roles(&self, channel_hex: &str) -> Vec<&Role> {
329 let wanted = channel_hex.to_ascii_lowercase();
330 let mut out: Vec<&Role> = self
331 .roles
332 .iter()
333 .filter(|r| matches!(&r.scope, RoleScope::Channel(c) if c.eq_ignore_ascii_case(&wanted)))
334 .collect();
335 out.sort_by_key(|r| r.position);
336 out
337 }
338
339 pub fn channel_role_ids(&self, channel_hex: &str) -> Vec<String> {
341 self.channel_roles(channel_hex).into_iter().map(|r| r.role_id.clone()).collect()
342 }
343
344 pub fn is_entitled(
351 &self,
352 owner_hex: Option<&str>,
353 member_hex: &str,
354 channel_hex: &str,
355 with: &[String],
356 without: &[String],
357 ) -> bool {
358 if owner_hex == Some(member_hex) {
359 return true;
360 }
361 let mut held: std::collections::HashSet<&str> =
362 self.roles_of(member_hex).map(|r| r.role_id.as_str()).collect();
363 for id in with {
364 held.insert(id.as_str());
365 }
366 for id in without {
367 held.remove(id.as_str());
368 }
369 self.channel_role_ids(channel_hex).iter().any(|id| held.contains(id.as_str()))
370 }
371
372 pub fn effective_permissions_in(&self, member_hex: &str, channel_hex: &str) -> Permissions {
381 let wanted = channel_hex.to_ascii_lowercase();
382 self.roles_of(member_hex)
383 .filter(|r| match &r.scope {
384 RoleScope::Server => true,
385 RoleScope::Channel(c) => c.eq_ignore_ascii_case(&wanted),
386 })
387 .fold(Permissions::empty(), |acc, r| acc.union(r.permissions))
388 }
389
390 pub fn is_authorized_in(&self, actor_hex: &str, owner_hex: Option<&str>, channel_hex: &str, permission: u64) -> bool {
393 if owner_hex == Some(actor_hex) {
394 return true;
395 }
396 self.effective_permissions_in(actor_hex, channel_hex).contains(permission)
397 }
398}
399
400#[cfg(test)]
401mod tests {
402 use super::*;
403
404 fn admin_roster(member: &str) -> CommunityRoles {
405 let role = Role::admin("a".repeat(64));
406 CommunityRoles {
407 grants: vec![MemberGrant {
408 member: member.to_string(),
409 role_ids: vec![role.role_id.clone()],
410 }],
411 roles: vec![role],
412 }
413 }
414
415 #[test]
416 fn admin_role_holds_every_management_bit() {
417 let p = Permissions::admin();
418 for bit in [
419 Permissions::MANAGE_ROLES,
420 Permissions::MANAGE_CHANNELS,
421 Permissions::MANAGE_METADATA,
422 Permissions::KICK,
423 Permissions::BAN,
424 Permissions::MANAGE_MESSAGES,
425 Permissions::CREATE_INVITE,
426 Permissions::VIEW_AUDIT_LOG,
427 Permissions::MENTION_EVERYONE,
428 ] {
429 assert!(p.contains(bit), "admin must hold bit {bit}");
430 }
431 }
432
433 #[test]
434 fn social_only_role_is_not_management_nor_admin() {
435 let social = Permissions(Permissions::MENTION_EVERYONE);
439 assert!(!social.is_management(), "a purely-social permission is not management");
440 let role = Role {
441 role_id: "c".repeat(64),
442 name: "Hype".into(),
443 position: 5,
444 permissions: social,
445 scope: RoleScope::Server,
446 color: 0, extra: Default::default(),
447 };
448 let alice = "aa".repeat(32);
449 let r = CommunityRoles {
450 grants: vec![MemberGrant { member: alice.clone(), role_ids: vec![role.role_id.clone()] }],
451 roles: vec![role],
452 };
453 assert!(!r.is_admin(&alice), "a social-only role holder is not an admin");
454 assert!(r.is_privileged(&alice), "though they do hold a role");
455 }
456
457 #[test]
458 fn hierarchy_only_strictly_higher_can_manage() {
459 let owner = "00".repeat(32);
461 let admin = "aa".repeat(32);
462 let moderator = "bb".repeat(32);
463 let member = "cc".repeat(32);
464 let admin_role = Role::admin("a".repeat(64));
465 let mod_role = Role {
466 role_id: "b".repeat(64),
467 name: "Mod".into(),
468 position: 2,
469 permissions: Permissions(Permissions::MANAGE_ROLES | Permissions::KICK),
470 scope: RoleScope::Server,
471 color: 0, extra: Default::default(),
472 };
473 let admin_pos = admin_role.position; let mod_pos = mod_role.position; let r = CommunityRoles {
476 grants: vec![
477 MemberGrant { member: admin.clone(), role_ids: vec![admin_role.role_id.clone()] },
478 MemberGrant { member: moderator.clone(), role_ids: vec![mod_role.role_id.clone()] },
479 ],
480 roles: vec![admin_role, mod_role],
481 };
482 let owner_ref = Some(owner.as_str());
483
484 assert!(r.can_manage_position(&owner, owner_ref, admin_pos));
486 assert!(r.can_manage_member(&owner, owner_ref, &admin));
487 assert!(!r.can_manage_member(&admin, owner_ref, &owner), "owner is never a valid target");
488
489 assert!(!r.can_manage_position(&admin, owner_ref, admin_pos), "admin can't grant a peer-rank role");
492 assert!(r.can_manage_position(&admin, owner_ref, mod_pos), "admin outranks Mod");
493 assert!(r.can_manage_member(&admin, owner_ref, &moderator), "admin outranks the mod member");
494 assert!(r.can_manage_member(&admin, owner_ref, &member), "admin outranks a roleless member");
495 assert!(!r.can_manage_member(&admin, owner_ref, &admin), "admin can't manage a peer admin");
496
497 assert!(!r.can_manage_position(&moderator, owner_ref, admin_pos));
499 assert!(!r.can_manage_position(&member, owner_ref, mod_pos), "no role, no MANAGE_ROLES → nothing");
500 }
501
502 #[test]
503 fn can_act_on_member_gates_on_permission_and_rank() {
504 let owner = "00".repeat(32);
508 let admin = "aa".repeat(32);
509 let kicker = "bb".repeat(32);
510 let member = "cc".repeat(32);
511 let admin_role = Role::admin("a".repeat(64)); let kick_role = Role {
513 role_id: "b".repeat(64),
514 name: "Mod".into(),
515 position: 2,
516 permissions: Permissions(Permissions::KICK), scope: RoleScope::Server,
518 color: 0, extra: Default::default(),
519 };
520 let r = CommunityRoles {
521 grants: vec![
522 MemberGrant { member: admin.clone(), role_ids: vec![admin_role.role_id.clone()] },
523 MemberGrant { member: kicker.clone(), role_ids: vec![kick_role.role_id.clone()] },
524 ],
525 roles: vec![admin_role, kick_role],
526 };
527 let o = Some(owner.as_str());
528 use Permissions as P;
529
530 assert!(r.can_act_on_member(&admin, o, &kicker, P::BAN));
532 assert!(r.can_act_on_member(&admin, o, &member, P::BAN));
533 assert!(!r.can_act_on_member(&admin, o, &admin, P::BAN), "no banning a peer admin (equal rank)");
534 assert!(!r.can_act_on_member(&admin, o, &owner, P::BAN), "the owner is never a valid target");
535 assert!(r.can_act_on_member(&admin, o, &member, P::MANAGE_MESSAGES), "admin can hide a member's msg");
536
537 assert!(!r.can_act_on_member(&kicker, o, &member, P::BAN), "no BAN permission → can't ban");
539 assert!(!r.can_act_on_member(&kicker, o, &member, P::MANAGE_MESSAGES), "no MANAGE_MESSAGES → can't hide");
540 assert!(r.can_act_on_member(&kicker, o, &member, P::KICK), "but it CAN kick a plain member");
541
542 assert!(r.can_act_on_member(&owner, o, &admin, P::BAN));
544 assert!(!r.can_act_on_member(&member, o, &kicker, P::BAN));
546 }
547
548 #[test]
549 fn effective_permissions_union_and_position() {
550 let alice = "aa".repeat(32);
551 let r = admin_roster(&alice);
552 assert!(r.has_permission(&alice, Permissions::BAN));
553 assert!(r.has_permission(&alice, Permissions::MANAGE_ROLES));
554 assert_eq!(r.highest_position(&alice), Some(1));
555 assert!(r.is_privileged(&alice));
556
557 let bob = "bb".repeat(32);
559 assert!(!r.has_permission(&bob, Permissions::BAN));
560 assert_eq!(r.highest_position(&bob), None);
561 assert!(!r.is_privileged(&bob));
562 }
563
564 #[test]
565 fn multiple_roles_union_perms_and_take_highest_position() {
566 let alice = "aa".repeat(32);
567 let mod_role = Role {
568 role_id: "b".repeat(64),
569 name: "Mod".into(),
570 position: 2,
571 permissions: Permissions(Permissions::MANAGE_MESSAGES | Permissions::KICK),
572 scope: RoleScope::Server,
573 color: 0, extra: Default::default(),
574 };
575 let admin = Role::admin("a".repeat(64));
576 let r = CommunityRoles {
577 grants: vec![MemberGrant {
578 member: alice.clone(),
579 role_ids: vec![admin.role_id.clone(), mod_role.role_id.clone()],
580 }],
581 roles: vec![admin, mod_role],
582 };
583 assert!(r.has_permission(&alice, Permissions::BAN)); assert!(r.has_permission(&alice, Permissions::MANAGE_MESSAGES)); assert_eq!(r.highest_position(&alice), Some(1));
587 }
588
589 #[test]
590 fn round_trips_json() {
591 let alice = "aa".repeat(32);
592 let r = admin_roster(&alice);
593 let json = serde_json::to_string(&r).unwrap();
594 let back: CommunityRoles = serde_json::from_str(&json).unwrap();
595 assert_eq!(r, back);
596 assert!(json.contains("\"kind\":\"server\""));
597 }
598
599 #[test]
600 fn is_authorized_owner_supreme_admin_by_permission_demoted_rejected() {
601 let owner = "00".repeat(32);
602 let alice = "aa".repeat(32);
603 let r = admin_roster(&alice); assert!(r.is_authorized(&owner, Some(&owner), Permissions::BAN));
606 assert!(r.is_authorized(&alice, Some(&owner), Permissions::BAN));
608 let bob = "bb".repeat(32);
610 assert!(!r.is_authorized(&bob, Some(&owner), Permissions::BAN));
611 let demoted = CommunityRoles { roles: r.roles.clone(), grants: vec![] };
612 assert!(!demoted.is_authorized(&alice, Some(&owner), Permissions::BAN));
613 assert!(demoted.is_authorized(&owner, Some(&owner), Permissions::BAN));
615 }
616
617 #[test]
618 fn channel_scope_round_trips() {
619 let scope = RoleScope::Channel("cc".repeat(32));
620 let json = serde_json::to_string(&scope).unwrap();
621 let back: RoleScope = serde_json::from_str(&json).unwrap();
622 assert_eq!(scope, back);
623 }
624
625 fn scoped_roster(chan: &str, member: &str, admin: &str) -> CommunityRoles {
630 let scoped = Role {
631 role_id: "11".repeat(32),
632 name: "Testers".into(),
633 position: 5,
634 permissions: Permissions(Permissions::MANAGE_MESSAGES),
635 scope: RoleScope::Channel(chan.to_string()),
636 color: 0, extra: Default::default(),
637 };
638 let server = Role::admin("22".repeat(32));
639 CommunityRoles {
640 grants: vec![
641 MemberGrant { member: member.to_string(), role_ids: vec![scoped.role_id.clone()] },
642 MemberGrant { member: admin.to_string(), role_ids: vec![server.role_id.clone()] },
643 ],
644 roles: vec![scoped, server],
645 }
646 }
647
648 #[test]
649 fn a_channels_scoped_roles_are_its_access_list() {
650 let chan = "cc".repeat(32);
651 let other = "dd".repeat(32);
652 let owner = "00".repeat(32);
653 let member = "aa".repeat(32);
654 let admin = "bb".repeat(32);
655 let r = scoped_roster(&chan, &member, &admin);
656
657 assert_eq!(r.channel_roles(&chan).len(), 1, "the scoped role is the access list");
658 assert!(r.channel_roles(&other).is_empty(), "another channel gets none of it");
659
660 assert!(r.is_entitled(Some(&owner), &member, &chan, &[], &[]), "the granted holder is entitled");
661 assert!(r.is_entitled(Some(&owner), &owner, &chan, &[], &[]), "the owner is always entitled");
662 assert!(!r.is_entitled(Some(&owner), &admin, &chan, &[], &[]), "rank alone is not entitlement");
665 assert!(!r.is_entitled(Some(&owner), &member, &other, &[], &[]), "entitlement does not spill to another channel");
666 }
667
668 #[test]
669 fn the_grant_overlay_settles_against_a_publish_the_fold_has_not_caught() {
670 let chan = "cc".repeat(32);
671 let owner = "00".repeat(32);
672 let member = "aa".repeat(32);
673 let stranger = "ee".repeat(32);
674 let r = scoped_roster(&chan, &member, &"bb".repeat(32));
675 let scoped_id = r.channel_role_ids(&chan)[0].clone();
676
677 assert!(
678 r.is_entitled(Some(&owner), &stranger, &chan, std::slice::from_ref(&scoped_id), &[]),
679 "a just-published grant settles as entitled before the fold catches up"
680 );
681 assert!(
682 !r.is_entitled(Some(&owner), &member, &chan, &[], std::slice::from_ref(&scoped_id)),
683 "a just-published revoke settles as unentitled before the fold catches up"
684 );
685 }
686
687 #[test]
688 fn channel_narrowing_is_offer_side_only() {
689 let chan = "cc".repeat(32);
690 let other = "dd".repeat(32);
691 let owner = "00".repeat(32);
692 let member = "aa".repeat(32);
693 let r = scoped_roster(&chan, &member, &"bb".repeat(32));
694
695 assert!(r.is_authorized_in(&member, Some(&owner), &chan, Permissions::MANAGE_MESSAGES));
697 assert!(!r.is_authorized_in(&member, Some(&owner), &other, Permissions::MANAGE_MESSAGES));
698
699 assert!(
703 r.is_authorized(&member, Some(&owner), Permissions::MANAGE_MESSAGES),
704 "the scope-blind honor path is unchanged"
705 );
706 }
707
708 #[test]
712 fn founding_mask_is_frozen_at_the_original_nine_bits() {
713 const ORIGINAL_NINE: u64 = (1 << 0) | (1 << 1) | (1 << 2) | (1 << 3) | (1 << 4)
714 | (1 << 5) | (1 << 6) | (1 << 8) | (1 << 9);
715 assert_eq!(Permissions::ADMIN_FOUNDING_MASK, ORIGINAL_NINE);
716 assert!(Permissions(ORIGINAL_NINE).contains(Permissions::ADMIN_FOUNDING_MASK));
718 assert!(Permissions::admin().contains(Permissions::ADMIN_FOUNDING_MASK));
720 assert!(Permissions::admin().contains(Permissions::PIN_MESSAGES));
721 assert!(!Permissions(ORIGINAL_NINE).contains(Permissions::ADMIN_ALL));
724 }
725}