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 pub fn channel_roles(&self, channel_hex: &str) -> Vec<&Role> {
277 let wanted = channel_hex.to_ascii_lowercase();
278 let mut out: Vec<&Role> = self
279 .roles
280 .iter()
281 .filter(|r| matches!(&r.scope, RoleScope::Channel(c) if c.eq_ignore_ascii_case(&wanted)))
282 .collect();
283 out.sort_by_key(|r| r.position);
284 out
285 }
286
287 pub fn channel_role_ids(&self, channel_hex: &str) -> Vec<String> {
289 self.channel_roles(channel_hex).into_iter().map(|r| r.role_id.clone()).collect()
290 }
291
292 pub fn is_entitled(
299 &self,
300 owner_hex: Option<&str>,
301 member_hex: &str,
302 channel_hex: &str,
303 with: &[String],
304 without: &[String],
305 ) -> bool {
306 if owner_hex == Some(member_hex) {
307 return true;
308 }
309 let mut held: std::collections::HashSet<&str> =
310 self.roles_of(member_hex).map(|r| r.role_id.as_str()).collect();
311 for id in with {
312 held.insert(id.as_str());
313 }
314 for id in without {
315 held.remove(id.as_str());
316 }
317 self.channel_role_ids(channel_hex).iter().any(|id| held.contains(id.as_str()))
318 }
319
320 pub fn effective_permissions_in(&self, member_hex: &str, channel_hex: &str) -> Permissions {
329 let wanted = channel_hex.to_ascii_lowercase();
330 self.roles_of(member_hex)
331 .filter(|r| match &r.scope {
332 RoleScope::Server => true,
333 RoleScope::Channel(c) => c.eq_ignore_ascii_case(&wanted),
334 })
335 .fold(Permissions::empty(), |acc, r| acc.union(r.permissions))
336 }
337
338 pub fn is_authorized_in(&self, actor_hex: &str, owner_hex: Option<&str>, channel_hex: &str, permission: u64) -> bool {
341 if owner_hex == Some(actor_hex) {
342 return true;
343 }
344 self.effective_permissions_in(actor_hex, channel_hex).contains(permission)
345 }
346}
347
348#[cfg(test)]
349mod tests {
350 use super::*;
351
352 fn admin_roster(member: &str) -> CommunityRoles {
353 let role = Role::admin("a".repeat(64));
354 CommunityRoles {
355 grants: vec![MemberGrant {
356 member: member.to_string(),
357 role_ids: vec![role.role_id.clone()],
358 }],
359 roles: vec![role],
360 }
361 }
362
363 #[test]
364 fn admin_role_holds_every_management_bit() {
365 let p = Permissions::admin();
366 for bit in [
367 Permissions::MANAGE_ROLES,
368 Permissions::MANAGE_CHANNELS,
369 Permissions::MANAGE_METADATA,
370 Permissions::KICK,
371 Permissions::BAN,
372 Permissions::MANAGE_MESSAGES,
373 Permissions::CREATE_INVITE,
374 Permissions::VIEW_AUDIT_LOG,
375 Permissions::MENTION_EVERYONE,
376 ] {
377 assert!(p.contains(bit), "admin must hold bit {bit}");
378 }
379 }
380
381 #[test]
382 fn social_only_role_is_not_management_nor_admin() {
383 let social = Permissions(Permissions::MENTION_EVERYONE);
387 assert!(!social.is_management(), "a purely-social permission is not management");
388 let role = Role {
389 role_id: "c".repeat(64),
390 name: "Hype".into(),
391 position: 5,
392 permissions: social,
393 scope: RoleScope::Server,
394 color: 0,
395 };
396 let alice = "aa".repeat(32);
397 let r = CommunityRoles {
398 grants: vec![MemberGrant { member: alice.clone(), role_ids: vec![role.role_id.clone()] }],
399 roles: vec![role],
400 };
401 assert!(!r.is_admin(&alice), "a social-only role holder is not an admin");
402 assert!(r.is_privileged(&alice), "though they do hold a role");
403 }
404
405 #[test]
406 fn hierarchy_only_strictly_higher_can_manage() {
407 let owner = "00".repeat(32);
409 let admin = "aa".repeat(32);
410 let moderator = "bb".repeat(32);
411 let member = "cc".repeat(32);
412 let admin_role = Role::admin("a".repeat(64));
413 let mod_role = Role {
414 role_id: "b".repeat(64),
415 name: "Mod".into(),
416 position: 2,
417 permissions: Permissions(Permissions::MANAGE_ROLES | Permissions::KICK),
418 scope: RoleScope::Server,
419 color: 0,
420 };
421 let admin_pos = admin_role.position; let mod_pos = mod_role.position; let r = CommunityRoles {
424 grants: vec![
425 MemberGrant { member: admin.clone(), role_ids: vec![admin_role.role_id.clone()] },
426 MemberGrant { member: moderator.clone(), role_ids: vec![mod_role.role_id.clone()] },
427 ],
428 roles: vec![admin_role, mod_role],
429 };
430 let owner_ref = Some(owner.as_str());
431
432 assert!(r.can_manage_position(&owner, owner_ref, admin_pos));
434 assert!(r.can_manage_member(&owner, owner_ref, &admin));
435 assert!(!r.can_manage_member(&admin, owner_ref, &owner), "owner is never a valid target");
436
437 assert!(!r.can_manage_position(&admin, owner_ref, admin_pos), "admin can't grant a peer-rank role");
440 assert!(r.can_manage_position(&admin, owner_ref, mod_pos), "admin outranks Mod");
441 assert!(r.can_manage_member(&admin, owner_ref, &moderator), "admin outranks the mod member");
442 assert!(r.can_manage_member(&admin, owner_ref, &member), "admin outranks a roleless member");
443 assert!(!r.can_manage_member(&admin, owner_ref, &admin), "admin can't manage a peer admin");
444
445 assert!(!r.can_manage_position(&moderator, owner_ref, admin_pos));
447 assert!(!r.can_manage_position(&member, owner_ref, mod_pos), "no role, no MANAGE_ROLES → nothing");
448 }
449
450 #[test]
451 fn can_act_on_member_gates_on_permission_and_rank() {
452 let owner = "00".repeat(32);
456 let admin = "aa".repeat(32);
457 let kicker = "bb".repeat(32);
458 let member = "cc".repeat(32);
459 let admin_role = Role::admin("a".repeat(64)); let kick_role = Role {
461 role_id: "b".repeat(64),
462 name: "Mod".into(),
463 position: 2,
464 permissions: Permissions(Permissions::KICK), scope: RoleScope::Server,
466 color: 0,
467 };
468 let r = CommunityRoles {
469 grants: vec![
470 MemberGrant { member: admin.clone(), role_ids: vec![admin_role.role_id.clone()] },
471 MemberGrant { member: kicker.clone(), role_ids: vec![kick_role.role_id.clone()] },
472 ],
473 roles: vec![admin_role, kick_role],
474 };
475 let o = Some(owner.as_str());
476 use Permissions as P;
477
478 assert!(r.can_act_on_member(&admin, o, &kicker, P::BAN));
480 assert!(r.can_act_on_member(&admin, o, &member, P::BAN));
481 assert!(!r.can_act_on_member(&admin, o, &admin, P::BAN), "no banning a peer admin (equal rank)");
482 assert!(!r.can_act_on_member(&admin, o, &owner, P::BAN), "the owner is never a valid target");
483 assert!(r.can_act_on_member(&admin, o, &member, P::MANAGE_MESSAGES), "admin can hide a member's msg");
484
485 assert!(!r.can_act_on_member(&kicker, o, &member, P::BAN), "no BAN permission → can't ban");
487 assert!(!r.can_act_on_member(&kicker, o, &member, P::MANAGE_MESSAGES), "no MANAGE_MESSAGES → can't hide");
488 assert!(r.can_act_on_member(&kicker, o, &member, P::KICK), "but it CAN kick a plain member");
489
490 assert!(r.can_act_on_member(&owner, o, &admin, P::BAN));
492 assert!(!r.can_act_on_member(&member, o, &kicker, P::BAN));
494 }
495
496 #[test]
497 fn effective_permissions_union_and_position() {
498 let alice = "aa".repeat(32);
499 let r = admin_roster(&alice);
500 assert!(r.has_permission(&alice, Permissions::BAN));
501 assert!(r.has_permission(&alice, Permissions::MANAGE_ROLES));
502 assert_eq!(r.highest_position(&alice), Some(1));
503 assert!(r.is_privileged(&alice));
504
505 let bob = "bb".repeat(32);
507 assert!(!r.has_permission(&bob, Permissions::BAN));
508 assert_eq!(r.highest_position(&bob), None);
509 assert!(!r.is_privileged(&bob));
510 }
511
512 #[test]
513 fn multiple_roles_union_perms_and_take_highest_position() {
514 let alice = "aa".repeat(32);
515 let mod_role = Role {
516 role_id: "b".repeat(64),
517 name: "Mod".into(),
518 position: 2,
519 permissions: Permissions(Permissions::MANAGE_MESSAGES | Permissions::KICK),
520 scope: RoleScope::Server,
521 color: 0,
522 };
523 let admin = Role::admin("a".repeat(64));
524 let r = CommunityRoles {
525 grants: vec![MemberGrant {
526 member: alice.clone(),
527 role_ids: vec![admin.role_id.clone(), mod_role.role_id.clone()],
528 }],
529 roles: vec![admin, mod_role],
530 };
531 assert!(r.has_permission(&alice, Permissions::BAN)); assert!(r.has_permission(&alice, Permissions::MANAGE_MESSAGES)); assert_eq!(r.highest_position(&alice), Some(1));
535 }
536
537 #[test]
538 fn round_trips_json() {
539 let alice = "aa".repeat(32);
540 let r = admin_roster(&alice);
541 let json = serde_json::to_string(&r).unwrap();
542 let back: CommunityRoles = serde_json::from_str(&json).unwrap();
543 assert_eq!(r, back);
544 assert!(json.contains("\"kind\":\"server\""));
545 }
546
547 #[test]
548 fn is_authorized_owner_supreme_admin_by_permission_demoted_rejected() {
549 let owner = "00".repeat(32);
550 let alice = "aa".repeat(32);
551 let r = admin_roster(&alice); assert!(r.is_authorized(&owner, Some(&owner), Permissions::BAN));
554 assert!(r.is_authorized(&alice, Some(&owner), Permissions::BAN));
556 let bob = "bb".repeat(32);
558 assert!(!r.is_authorized(&bob, Some(&owner), Permissions::BAN));
559 let demoted = CommunityRoles { roles: r.roles.clone(), grants: vec![] };
560 assert!(!demoted.is_authorized(&alice, Some(&owner), Permissions::BAN));
561 assert!(demoted.is_authorized(&owner, Some(&owner), Permissions::BAN));
563 }
564
565 #[test]
566 fn channel_scope_round_trips() {
567 let scope = RoleScope::Channel("cc".repeat(32));
568 let json = serde_json::to_string(&scope).unwrap();
569 let back: RoleScope = serde_json::from_str(&json).unwrap();
570 assert_eq!(scope, back);
571 }
572
573 fn scoped_roster(chan: &str, member: &str, admin: &str) -> CommunityRoles {
578 let scoped = Role {
579 role_id: "11".repeat(32),
580 name: "Testers".into(),
581 position: 5,
582 permissions: Permissions(Permissions::MANAGE_MESSAGES),
583 scope: RoleScope::Channel(chan.to_string()),
584 color: 0,
585 };
586 let server = Role::admin("22".repeat(32));
587 CommunityRoles {
588 grants: vec![
589 MemberGrant { member: member.to_string(), role_ids: vec![scoped.role_id.clone()] },
590 MemberGrant { member: admin.to_string(), role_ids: vec![server.role_id.clone()] },
591 ],
592 roles: vec![scoped, server],
593 }
594 }
595
596 #[test]
597 fn a_channels_scoped_roles_are_its_access_list() {
598 let chan = "cc".repeat(32);
599 let other = "dd".repeat(32);
600 let owner = "00".repeat(32);
601 let member = "aa".repeat(32);
602 let admin = "bb".repeat(32);
603 let r = scoped_roster(&chan, &member, &admin);
604
605 assert_eq!(r.channel_roles(&chan).len(), 1, "the scoped role is the access list");
606 assert!(r.channel_roles(&other).is_empty(), "another channel gets none of it");
607
608 assert!(r.is_entitled(Some(&owner), &member, &chan, &[], &[]), "the granted holder is entitled");
609 assert!(r.is_entitled(Some(&owner), &owner, &chan, &[], &[]), "the owner is always entitled");
610 assert!(!r.is_entitled(Some(&owner), &admin, &chan, &[], &[]), "rank alone is not entitlement");
613 assert!(!r.is_entitled(Some(&owner), &member, &other, &[], &[]), "entitlement does not spill to another channel");
614 }
615
616 #[test]
617 fn the_grant_overlay_settles_against_a_publish_the_fold_has_not_caught() {
618 let chan = "cc".repeat(32);
619 let owner = "00".repeat(32);
620 let member = "aa".repeat(32);
621 let stranger = "ee".repeat(32);
622 let r = scoped_roster(&chan, &member, &"bb".repeat(32));
623 let scoped_id = r.channel_role_ids(&chan)[0].clone();
624
625 assert!(
626 r.is_entitled(Some(&owner), &stranger, &chan, std::slice::from_ref(&scoped_id), &[]),
627 "a just-published grant settles as entitled before the fold catches up"
628 );
629 assert!(
630 !r.is_entitled(Some(&owner), &member, &chan, &[], std::slice::from_ref(&scoped_id)),
631 "a just-published revoke settles as unentitled before the fold catches up"
632 );
633 }
634
635 #[test]
636 fn channel_narrowing_is_offer_side_only() {
637 let chan = "cc".repeat(32);
638 let other = "dd".repeat(32);
639 let owner = "00".repeat(32);
640 let member = "aa".repeat(32);
641 let r = scoped_roster(&chan, &member, &"bb".repeat(32));
642
643 assert!(r.is_authorized_in(&member, Some(&owner), &chan, Permissions::MANAGE_MESSAGES));
645 assert!(!r.is_authorized_in(&member, Some(&owner), &other, Permissions::MANAGE_MESSAGES));
646
647 assert!(
651 r.is_authorized(&member, Some(&owner), Permissions::MANAGE_MESSAGES),
652 "the scope-blind honor path is unchanged"
653 );
654 }
655}