Skip to main content

vector_core/community/
roles.rs

1//! Concord role graph (GROUP_PROTOCOL.md).
2//!
3//! The MVP exposes a single auto-generated "Admin" role, but this is the FULL graph:
4//! roles are data (not a hardcoded `is_admin` flag), enforcement is capability-based
5//! (effective-permission bits + position), and the engine reads an arbitrary set of
6//! roles + grants. Mod roles, per-channel mods, and custom roles later are just more
7//! `Role` records flowing through the same code — additive, no enforcement changes.
8//!
9//! WIRE MODEL (per-entity): roles and grants do NOT live in the GroupRoot, and they
10//! are NOT one consolidated blob. Each role is its own addressable RoleMetadata event
11//! (vsk=1, d-tag = role_id) and each member's grants are their own Grant event
12//! (vsk=3, d-tag = an opaque per-member locator). So two managers editing *different*
13//! roles or *different* members never clobber each other — only same-coordinate edits
14//! converge (authority-first). `CommunityRoles` here is the in-memory AGGREGATION a
15//! client builds from those fetched per-entity events, not an on-wire document.
16
17use serde::{Deserialize, Serialize};
18
19/// Management/moderation permission bits. Access (read/post a channel) is NOT
20/// here — that is key possession (the two-mechanism split). Bit positions are part of
21/// the wire format and are FROZEN: append a reserved bit, never renumber or reuse one.
22#[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    /// Curate a Channel's Pin List (CORD-04 §7) — editorial power: a pin makes
37    /// its message permanently attributable to every future member.
38    pub const PIN_MESSAGES: u64 = 1 << 11;
39    // Reserved / retired (claim the bit so it's never reassigned):
40    // `1 << 7` was MANAGE_INVITES — RETIRED (per-creator ownership: no one can manage another's
41    // links, so there is nothing to grant; `CREATE_INVITE` mints your own, `BAN` owns the revoking rekey).
42    // MANAGE_EMOJI = 1 << 10, MANAGE_EVENTS = 1 << 12.
43
44    /// The founding nine bits — the mask every Admin role published before
45    /// PIN_MESSAGES existed carries. FROZEN FOREVER: this is the floor the
46    /// admin-role finder tests with, so it must never gain a bit. Widening it
47    /// would orphan every published Admin role and mint duplicates.
48    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    /// Every management bit currently defined — what a newly minted "Admin"
59    /// role holds. MAY widen as bits land; anything that *identifies* an
60    /// existing admin role must test [`Self::ADMIN_FOUNDING_MASK`] instead.
61    pub const ADMIN_ALL: u64 = Self::ADMIN_FOUNDING_MASK | Self::PIN_MESSAGES;
62
63    /// Control-plane bits: exercising any of these signs a control/metadata edition (keyless model —
64    /// the actor's own npub signature IS the authority, re-verified against the roster). Every
65    /// management bit EXCEPT purely-social ones (`MENTION_EVERYONE` acts at the message layer, not the
66    /// control plane). A role with any of these is a "management role" — its holder shows the admin crown.
67    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    /// The STAFF bits (CORD-04 §3): the permissions whose authorized actions
78    /// land as Control Plane editions. A member holding ANY of them — plus
79    /// always the owner — is staff: the set that holds the `control_root`
80    /// write key (CORD-02 §2). `KICK` writes to the Guestbook and
81    /// `MANAGE_MESSAGES` to Chat planes; neither needs it. The list is
82    /// normative — a future permission whose actions are Control editions
83    /// joins it by explicit spec amendment, never by local judgment.
84    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    /// True iff this role carries any management permission (vs. a purely-social role) — i.e. its
98    /// holder counts as an admin.
99    pub fn is_management(self) -> bool {
100        self.0 & Self::MANAGEMENT_MASK != 0
101    }
102    /// True iff every bit in `bits` is set.
103    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/// Discord's "any channel" vs "this channel". Server-scope acts everywhere;
112/// channel-scope is rejected against any other channel.
113#[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 id, lowercase hex.
118    Channel(String),
119}
120
121/// A role: its own addressable RoleMetadata event (vsk=1). Fully general; the MVP
122/// auto-creates exactly one (`Role::admin`).
123#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
124pub struct Role {
125    /// Random opaque 32-byte hex id, stable across renames; also the event's d-tag.
126    pub role_id: String,
127    pub name: String,
128    /// Lower = higher authority. Owner is the implicit top (position 0, never a Role).
129    /// Authoritative ordering will move to a single RoleOrder entity when
130    /// drag-reorder ships; until then this declared value is the position (no reordering
131    /// to race in the owner-only MVP).
132    pub position: u32,
133    pub permissions: Permissions,
134    pub scope: RoleScope,
135    /// UI badge color (e.g. the Admin crown); 0 = theme default. Cosmetic.
136    #[serde(default)]
137    pub color: u32,
138    /// Fields another client wrote that this one doesn't model. An edition replaces the
139    /// whole entity, so they ride every republish untouched (CORD-02 §6): dropping them
140    /// would erase another client's data for every member.
141    #[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    /// The MVP's auto-created server-scope Admin role: all management bits, position 1
147    /// (just below the owner). `role_id` is a fresh random opaque id minted at creation.
148    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/// One member's role grants (vsk=3) — its own addressable event so granting Alice
161/// never clobbers Bob's grants. `member` is the grantee's pubkey, lowercase hex (same form
162/// as the banlist), and `role_ids` are the roles they hold (a member can have several).
163#[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/// The role graph a client AGGREGATES from the fetched per-entity events (RoleMetadata +
171/// per-member Grant). Not an on-wire document — the enforcement engine queries this.
172#[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    /// Look up a role definition by id.
182    pub fn role(&self, role_id: &str) -> Option<&Role> {
183        self.roles.iter().find(|r| r.role_id == role_id)
184    }
185
186    /// The roles granted to `member_hex` (resolved through the grant list).
187    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    /// Effective permissions for a member = union of their granted roles' bits.
196    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    /// True iff the member's effective permissions include every bit in `bits`.
202    pub fn has_permission(&self, member_hex: &str, bits: u64) -> bool {
203        self.effective_permissions(member_hex).contains(bits)
204    }
205
206    /// Highest authority (lowest position index) among the member's roles; `None` if
207    /// they hold no role. The owner (implicit position 0) is handled by the caller.
208    pub fn highest_position(&self, member_hex: &str) -> Option<u32> {
209        self.roles_of(member_hex).map(|r| r.position).min()
210    }
211
212    /// True iff the member holds at least one role (i.e. is privileged below the owner).
213    pub fn is_privileged(&self, member_hex: &str) -> bool {
214        self.roles_of(member_hex).next().is_some()
215    }
216
217    /// True iff the member holds a role with management permissions — an "admin" (vs. a member who
218    /// holds only a non-management/social role). Drives the member-list crown.
219    pub fn is_admin(&self, member_hex: &str) -> bool {
220        self.roles_of(member_hex).any(|r| r.permissions.is_management())
221    }
222
223    /// Whether a member is STAFF (CORD-04 §3): the owner, or any holder of a
224    /// Control-writing bit ([`Permissions::STAFF_MASK`]) — the set entitled to
225    /// the `control_root` (CORD-02 §2).
226    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    /// Whether a grant's role set leaves its member staff, judged against this
234    /// roster's role definitions — the granter-side trigger for delivering the
235    /// `control_root` inside the Grant itself (CORD-04 §3). An id with no known
236    /// definition contributes nothing (never a reason to hand out the secret).
237    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    /// Is `actor_hex` authorized for an action requiring `permission`? The **owner** (the
245    /// proven owner npub, if known) is supreme and always authorized; otherwise the actor must hold
246    /// a role granting `permission`. This is the grant-set check the inner-author-proof gates on: a
247    /// demoted member is no longer in the grant set, so their actions stop being honored.
248    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    /// escalation defense — may `actor_hex` manage something sitting at `target_position`
256    /// (grant/revoke/edit/reorder a role)? The actor must **strictly outrank** it (their highest
257    /// authority is a *lower* position number) AND hold `MANAGE_ROLES`. The owner is supreme
258    /// (implicit position 0, above every role) and always may. Equal cannot act on equal: an admin
259    /// can never grant/revoke a peer admin at the same position — only someone strictly above can.
260    /// This is what stops an admin granting the Admin role (`pos == pos`, refused) while still
261    /// letting them manage any role beneath them.
262    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    /// May `actor_hex` act on MEMBER `target_hex` for a role change (add/remove a role)? Resolves the
267    /// target's highest authority and applies the `MANAGE_ROLES` position rule. The **owner is never a
268    /// valid target** (supreme, unremovable — the sole hardcoded exception).
269    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    /// The pure position test (no permission bit): does `actor_hex` **strictly outrank**
274    /// `target_position`? The owner (implicit position 0) outranks everything; a roleless actor
275    /// outranks nothing. This is the position half of every authority check — callers AND it with the
276    /// specific permission the action needs (`BAN`, `MANAGE_MESSAGES`, `MANAGE_ROLES`, ...).
277    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    /// Generalized authority test: may `actor_hex` perform an action requiring `permission` against a
288    /// target at `target_position`? Owner is supreme; otherwise the actor must hold `permission` AND
289    /// strictly outrank the target. (`can_manage_position` is this with `MANAGE_ROLES`; bans pass
290    /// `BAN`, moderation-hides pass `MANAGE_MESSAGES`.)
291    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    /// Generalized member-targeting authority test (ban / kick / hide / role-change). The **owner is
299    /// never a valid target**; a roleless member sits below everyone. The actor needs `permission`
300    /// plus a strict outrank of the target's highest role.
301    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    // ── Private-Channel entitlement (CORD-03/04/06) ──────────────────────────
310    //
311    // CORD-03 defines a Private Channel as "readable only by granted
312    // role-holders", its key "delivered on grant and rekeyed on removal"; the
313    // binding that names those role-holders is CORD-04 §2's
314    // `scope: {"kind":"channel","channel_id":...}`. So **the roles scoped to a
315    // channel are its access list**.
316    //
317    // Read access is enforced by key possession alone — nothing here grants it.
318    // This is the routing that decides who a key is delivered TO on grant, and
319    // who a rekey keeps on revoke.
320
321    /// The roles conferring read access to `channel_hex`, in display order
322    /// (highest authority first).
323    ///
324    /// A Private Channel with none is readable by nobody but the owner and
325    /// whoever already holds the key — degenerate rather than "open", so
326    /// clients shouldn't create it, but one arriving that way from elsewhere
327    /// still reads correctly and its key holders keep reading it.
328    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    /// [`channel_roles`], by id.
340    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    /// Is `member_hex` entitled to `channel_hex`'s key? The owner always is
345    /// (position 0, supreme and unremovable).
346    ///
347    /// `with`/`without` overlay a Grant that was JUST published, so a caller can
348    /// settle key custody against the change it just made rather than against a
349    /// fold that lags its own publish.
350    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    /// Effective permissions for an action TARGETING one channel: server-scope
373    /// roles plus roles scoped to that channel.
374    ///
375    /// ⚠️ **Offer-side only.** The fold stays scope-agnostic in every
376    /// implementation (CORD-04 §3, and [`effective_permissions`] is what judges
377    /// inbound authority) — this narrows only what THIS client offers its own
378    /// user, never what it honors from others. Tightening the honor path would
379    /// retroactively invalidate actions shipped clients already folded.
380    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    /// [`is_authorized`](Self::is_authorized), judged against one channel per
391    /// [`effective_permissions_in`](Self::effective_permissions_in). Offer-side only.
392    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        // A role holding ONLY a social bit (MENTION_EVERYONE) is not "management" and its holder is
436        // NOT an admin — so it gets no crown and no management-secret delivery. Guards the
437        // MANAGEMENT_MASK invariant against a future fat-finger that folds a social bit in.
438        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        // owner (implicit pos 0) > admin (pos 1) > mod (pos 2); a plain member holds no role.
460        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; // 1
474        let mod_pos = mod_role.position; // 2
475        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        // Owner is supreme — outranks every position and every member, can't be targeted.
485        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        // Equal cannot act on equal: an admin can NOT manage the Admin position (closes self/peer
490        // escalation — granting the Admin role), but CAN manage everything strictly below.
491        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        // A mod can't reach up to the Admin position, and a roleless member can manage nothing.
498        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        // owner (pos 0), a BAN-capable admin (pos 1), a Mod with KICK-only (pos 2, NO ban), a plain
505        // member (no role). Verifies the generalized gate requires BOTH the right permission AND a
506        // strict outrank — the rule ban/hide use, distinct from MANAGE_ROLES.
507        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)); // pos 1, ADMIN_ALL (incl. BAN + MANAGE_MESSAGES)
512        let kick_role = Role {
513            role_id: "b".repeat(64),
514            name: "Mod".into(),
515            position: 2,
516            permissions: Permissions(Permissions::KICK), // KICK only — NO ban, NO manage-messages
517            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        // The BAN-capable admin: bans the Mod and a plain member, but NOT a peer admin, NOT the owner.
531        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        // The Mod has KICK but NOT BAN/MANAGE_MESSAGES → the permission gate refuses even a target it outranks.
538        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        // The owner is supreme for any permission, against anyone.
543        assert!(r.can_act_on_member(&owner, o, &admin, P::BAN));
544        // A plain member can do nothing.
545        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        // A member with no grant has no permissions and no position.
558        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        // Union of both roles' bits, and the *highest* (lowest index) position wins.
584        assert!(r.has_permission(&alice, Permissions::BAN)); // from Admin
585        assert!(r.has_permission(&alice, Permissions::MANAGE_MESSAGES)); // from both
586        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); // alice granted Admin (has BAN)
604        // Owner is always authorized, even without a role.
605        assert!(r.is_authorized(&owner, Some(&owner), Permissions::BAN));
606        // Admin Alice is authorized for BAN via her role.
607        assert!(r.is_authorized(&alice, Some(&owner), Permissions::BAN));
608        // A member with no role (Bob) is not — and neither is Alice once demoted (no grant).
609        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        // ...but the owner stays authorized even with an empty grant set.
614        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    // ── Private-Channel entitlement ──────────────────────────────────────────
626
627    /// A roster with one channel-scoped role (`pos` 5) over `chan`, granted to
628    /// `member`, plus a server-scope Admin held by `admin`.
629    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        // A SERVER-scope Admin is not automatically entitled: entitlement is the
663        // channel's scoped roles, never rank (CORD-03 — key possession, not authority).
664        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        // Narrowed: the scoped role's bits apply in ITS channel and nowhere else.
696        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        // The HONOR path stays scope-agnostic — every implementation folds the
700        // same union (CORD-04 §3). Tightening this would retroactively invalidate
701        // actions shipped clients already folded.
702        assert!(
703            r.is_authorized(&member, Some(&owner), Permissions::MANAGE_MESSAGES),
704            "the scope-blind honor path is unchanged"
705        );
706    }
707
708    /// The founding mask is the admin-FINDER's floor and is frozen forever: a
709    /// role minted under the ORIGINAL nine bits must always satisfy it, or
710    /// every published Admin role orphans and the finder mints duplicates.
711    #[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        // A legacy admin role (pre-PIN_MESSAGES) still passes the finder's test...
717        assert!(Permissions(ORIGINAL_NINE).contains(Permissions::ADMIN_FOUNDING_MASK));
718        // ...and a newly minted one does too (ADMIN_ALL may widen, and has).
719        assert!(Permissions::admin().contains(Permissions::ADMIN_FOUNDING_MASK));
720        assert!(Permissions::admin().contains(Permissions::PIN_MESSAGES));
721        // But a legacy role does NOT contain the widened ADMIN_ALL — the exact
722        // reason nothing may identify admins with it.
723        assert!(!Permissions(ORIGINAL_NINE).contains(Permissions::ADMIN_ALL));
724    }
725}