zeroclaw_channels/allowlist.rs
1//! Shared `allowed_users` matching used by every chat channel.
2//!
3//! Each channel (Slack, Discord, IRC, Telegram, Matrix, …) carries an
4//! `allowed_users: Vec<String>` allowlist with the same semantics:
5//!
6//! - `["*"]` (or any list containing `"*"`) means "anyone".
7//! - Empty list means "deny everyone" (channel is on but no inbound is
8//! accepted yet — matches the "configured but not opened" stance the
9//! channel docs use).
10//! - Otherwise, exact match against the user's identifier wins.
11//!
12//! IRC nicks are case-insensitive per RFC 2812; Matrix MXIDs are also
13//! case-insensitive. Most other channels (Slack user IDs, Discord
14//! snowflakes, Telegram usernames) are case-sensitive. The
15//! [`Match::Sensitive`] / [`Match::CaseInsensitive`] selector encodes
16//! that per-channel choice without growing a parallel impl.
17
18/// Case-sensitivity selector for the allowlist comparison. The chat
19/// platform defines which one applies; the helper does not infer.
20#[derive(Debug, Clone, Copy, PartialEq, Eq)]
21pub enum Match {
22 /// Exact `==` match.
23 Sensitive,
24 /// `eq_ignore_ascii_case` — IRC nicks, Matrix MXIDs.
25 CaseInsensitive,
26}
27
28/// Return `true` when `user` is allowed under `allowed`.
29///
30/// Single source of truth for the per-channel `is_user_allowed` checks.
31/// Callers spell their channel's case-sensitivity by passing the
32/// matching [`Match`] variant; the helper handles the wildcard, empty,
33/// and per-entry comparisons identically across every channel.
34#[must_use]
35pub fn is_user_allowed(allowed: &[String], user: &str, mode: Match) -> bool {
36 if allowed.iter().any(|u| u == "*") {
37 return true;
38 }
39 match mode {
40 Match::Sensitive => allowed.iter().any(|u| u == user),
41 Match::CaseInsensitive => allowed.iter().any(|u| u.eq_ignore_ascii_case(user)),
42 }
43}
44
45#[cfg(test)]
46mod tests {
47 use super::*;
48
49 #[test]
50 fn wildcard_allows_anyone() {
51 let list = vec!["*".to_string()];
52 assert!(is_user_allowed(&list, "alice", Match::Sensitive));
53 assert!(is_user_allowed(&list, "ALICE", Match::Sensitive));
54 }
55
56 #[test]
57 fn empty_list_denies_everyone() {
58 assert!(!is_user_allowed(&[], "alice", Match::Sensitive));
59 assert!(!is_user_allowed(&[], "alice", Match::CaseInsensitive));
60 }
61
62 #[test]
63 fn exact_match_case_sensitive() {
64 let list = vec!["alice".to_string()];
65 assert!(is_user_allowed(&list, "alice", Match::Sensitive));
66 assert!(!is_user_allowed(&list, "Alice", Match::Sensitive));
67 }
68
69 #[test]
70 fn exact_match_case_insensitive() {
71 let list = vec!["Alice".to_string()];
72 assert!(is_user_allowed(&list, "alice", Match::CaseInsensitive));
73 assert!(is_user_allowed(&list, "ALICE", Match::CaseInsensitive));
74 }
75}