Skip to main content

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}