collet 0.1.1

Relentless agentic coding orchestrator with zero-drop agent loops
Documentation
//! Authorization — per-platform user allowlists.
//!
//! **Fail-safe policy**: empty allowlist = deny all.

use std::collections::HashSet;

/// Per-platform authorization configuration.
#[derive(Debug, Clone)]
pub struct AuthConfig {
    telegram_users: HashSet<i64>,
    slack_users: HashSet<String>,
    discord_users: HashSet<u64>,
}

impl AuthConfig {
    pub fn new(
        telegram_users: Vec<i64>,
        slack_users: Vec<String>,
        discord_users: Vec<u64>,
    ) -> Self {
        Self {
            telegram_users: telegram_users.into_iter().collect(),
            slack_users: slack_users.into_iter().collect(),
            discord_users: discord_users.into_iter().collect(),
        }
    }

    /// Returns `true` if the user is authorized on the given platform.
    ///
    /// Unknown platforms are always denied.
    pub fn is_authorized(&self, platform: &str, user_id: &str) -> bool {
        match platform {
            "telegram" => {
                if self.telegram_users.is_empty() {
                    return false;
                }
                user_id
                    .parse::<i64>()
                    .map(|id| self.telegram_users.contains(&id))
                    .unwrap_or(false)
            }
            "slack" => {
                if self.slack_users.is_empty() {
                    return false;
                }
                self.slack_users.contains(user_id)
            }
            "discord" => {
                if self.discord_users.is_empty() {
                    return false;
                }
                user_id
                    .parse::<u64>()
                    .map(|id| self.discord_users.contains(&id))
                    .unwrap_or(false)
            }
            _ => false, // unknown platform → deny
        }
    }
}

// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn deny_all_when_empty() {
        let auth = AuthConfig::new(vec![], vec![], vec![]);
        assert!(!auth.is_authorized("telegram", "12345"));
        assert!(!auth.is_authorized("slack", "U123"));
        assert!(!auth.is_authorized("discord", "99999"));
    }

    #[test]
    fn telegram_authorized() {
        let auth = AuthConfig::new(vec![12345], vec![], vec![]);
        assert!(auth.is_authorized("telegram", "12345"));
        assert!(!auth.is_authorized("telegram", "99999"));
    }

    #[test]
    fn slack_authorized() {
        let auth = AuthConfig::new(vec![], vec!["U_ALICE".to_string()], vec![]);
        assert!(auth.is_authorized("slack", "U_ALICE"));
        assert!(!auth.is_authorized("slack", "U_BOB"));
    }

    #[test]
    fn discord_authorized() {
        let auth = AuthConfig::new(vec![], vec![], vec![111222333]);
        assert!(auth.is_authorized("discord", "111222333"));
        assert!(!auth.is_authorized("discord", "000"));
    }

    #[test]
    fn unknown_platform_denied() {
        let auth = AuthConfig::new(vec![1], vec!["x".into()], vec![1]);
        assert!(!auth.is_authorized("matrix", "anything"));
    }

    #[test]
    fn invalid_user_id_denied() {
        let auth = AuthConfig::new(vec![12345], vec![], vec![]);
        assert!(!auth.is_authorized("telegram", "not_a_number"));
    }
}