Skip to main content

openclaw_channels/
allowlist.rs

1//! Allowlist for access control.
2
3use serde::{Deserialize, Serialize};
4
5use openclaw_core::types::{ChannelId, PeerId};
6
7/// Allowlist entry.
8#[derive(Debug, Clone, Serialize, Deserialize)]
9pub struct AllowlistEntry {
10    /// Channel pattern ("*" for any).
11    pub channel: String,
12    /// Peer ID pattern ("*" for any).
13    pub peer_id: String,
14    /// Optional label.
15    pub label: Option<String>,
16}
17
18impl AllowlistEntry {
19    /// Create a new allowlist entry.
20    #[must_use]
21    pub fn new(channel: impl Into<String>, peer_id: impl Into<String>) -> Self {
22        Self {
23            channel: channel.into(),
24            peer_id: peer_id.into(),
25            label: None,
26        }
27    }
28
29    /// Create an entry that allows any peer on a channel.
30    #[must_use]
31    pub fn channel_wide(channel: impl Into<String>) -> Self {
32        Self::new(channel, "*")
33    }
34
35    /// Create an entry that allows a specific peer on any channel.
36    #[must_use]
37    pub fn peer_anywhere(peer_id: impl Into<String>) -> Self {
38        Self::new("*", peer_id)
39    }
40
41    /// Check if this entry matches.
42    #[must_use]
43    pub fn matches(&self, channel: &ChannelId, peer_id: &PeerId) -> bool {
44        let channel_matches = self.channel == "*" || self.channel == channel.as_ref();
45        let peer_matches = self.peer_id == "*" || self.peer_id == peer_id.as_ref();
46        channel_matches && peer_matches
47    }
48}
49
50/// Allowlist for controlling access.
51#[derive(Debug, Clone, Default)]
52pub struct Allowlist {
53    entries: Vec<AllowlistEntry>,
54    default_allow: bool,
55}
56
57impl Allowlist {
58    /// Create a new allowlist (default deny).
59    #[must_use]
60    pub const fn new() -> Self {
61        Self {
62            entries: Vec::new(),
63            default_allow: false,
64        }
65    }
66
67    /// Create an open allowlist (default allow).
68    #[must_use]
69    pub const fn open() -> Self {
70        Self {
71            entries: Vec::new(),
72            default_allow: true,
73        }
74    }
75
76    /// Add an entry.
77    pub fn add(&mut self, entry: AllowlistEntry) {
78        self.entries.push(entry);
79    }
80
81    /// Check if access is allowed.
82    #[must_use]
83    pub fn is_allowed(&self, channel: &ChannelId, peer_id: &PeerId) -> bool {
84        if self.entries.is_empty() {
85            return self.default_allow;
86        }
87
88        self.entries.iter().any(|e| e.matches(channel, peer_id))
89    }
90
91    /// Get all entries.
92    #[must_use]
93    pub fn entries(&self) -> &[AllowlistEntry] {
94        &self.entries
95    }
96}
97
98#[cfg(test)]
99mod tests {
100    use super::*;
101
102    #[test]
103    fn test_allowlist_empty_deny() {
104        let allowlist = Allowlist::new();
105        assert!(!allowlist.is_allowed(&ChannelId::telegram(), &PeerId::new("123")));
106    }
107
108    #[test]
109    fn test_allowlist_empty_allow() {
110        let allowlist = Allowlist::open();
111        assert!(allowlist.is_allowed(&ChannelId::telegram(), &PeerId::new("123")));
112    }
113
114    #[test]
115    fn test_allowlist_specific() {
116        let mut allowlist = Allowlist::new();
117        allowlist.add(AllowlistEntry::new("telegram", "123"));
118
119        assert!(allowlist.is_allowed(&ChannelId::telegram(), &PeerId::new("123")));
120        assert!(!allowlist.is_allowed(&ChannelId::telegram(), &PeerId::new("456")));
121        assert!(!allowlist.is_allowed(&ChannelId::discord(), &PeerId::new("123")));
122    }
123
124    #[test]
125    fn test_allowlist_wildcard() {
126        let mut allowlist = Allowlist::new();
127        allowlist.add(AllowlistEntry::channel_wide("telegram"));
128
129        assert!(allowlist.is_allowed(&ChannelId::telegram(), &PeerId::new("123")));
130        assert!(allowlist.is_allowed(&ChannelId::telegram(), &PeerId::new("456")));
131        assert!(!allowlist.is_allowed(&ChannelId::discord(), &PeerId::new("123")));
132    }
133}