bob_adapters/
access_control.rs1use bob_core::{
4 ports::AccessControlPort,
5 types::{AccessDecision, ChannelAccessPolicy},
6};
7
8#[derive(Debug, Clone, Default)]
13pub struct StaticAccessControl {
14 policies: Vec<ChannelAccessPolicy>,
15}
16
17impl StaticAccessControl {
18 #[must_use]
20 pub fn new(policies: Vec<ChannelAccessPolicy>) -> Self {
21 Self { policies }
22 }
23}
24
25impl AccessControlPort for StaticAccessControl {
26 fn check_access(&self, channel: &str, sender_id: &str) -> AccessDecision {
27 match self.policies.iter().find(|p| p.channel == channel) {
28 Some(policy) if !policy.allow_from.is_empty() => {
29 if policy.allow_from.iter().any(|id| id == sender_id) {
30 AccessDecision::Allow
31 } else {
32 AccessDecision::Deny
33 }
34 }
35 _ => AccessDecision::Allow,
37 }
38 }
39
40 fn policies(&self) -> &[ChannelAccessPolicy] {
41 &self.policies
42 }
43}
44
45#[cfg(test)]
46mod tests {
47 use super::*;
48
49 #[test]
50 fn allowed_sender_passes() {
51 let ac = StaticAccessControl::new(vec![ChannelAccessPolicy {
52 channel: "telegram".into(),
53 allow_from: vec!["alice".into(), "bob".into()],
54 }]);
55 assert_eq!(ac.check_access("telegram", "alice"), AccessDecision::Allow);
56 assert_eq!(ac.check_access("telegram", "bob"), AccessDecision::Allow);
57 }
58
59 #[test]
60 fn denied_sender_blocked() {
61 let ac = StaticAccessControl::new(vec![ChannelAccessPolicy {
62 channel: "telegram".into(),
63 allow_from: vec!["alice".into()],
64 }]);
65 assert_eq!(ac.check_access("telegram", "eve"), AccessDecision::Deny);
66 }
67
68 #[test]
69 fn empty_allow_from_allows_all() {
70 let ac = StaticAccessControl::new(vec![ChannelAccessPolicy {
71 channel: "cli".into(),
72 allow_from: vec![],
73 }]);
74 assert_eq!(ac.check_access("cli", "anyone"), AccessDecision::Allow);
75 }
76
77 #[test]
78 fn unknown_channel_allows_all() {
79 let ac = StaticAccessControl::new(vec![ChannelAccessPolicy {
80 channel: "telegram".into(),
81 allow_from: vec!["alice".into()],
82 }]);
83 assert_eq!(ac.check_access("discord", "random"), AccessDecision::Allow);
84 }
85
86 #[test]
87 fn multiple_channels_different_policies() {
88 let ac = StaticAccessControl::new(vec![
89 ChannelAccessPolicy { channel: "telegram".into(), allow_from: vec!["alice".into()] },
90 ChannelAccessPolicy {
91 channel: "discord".into(),
92 allow_from: vec!["bob".into(), "carol".into()],
93 },
94 ]);
95 assert_eq!(ac.check_access("telegram", "alice"), AccessDecision::Allow);
96 assert_eq!(ac.check_access("telegram", "bob"), AccessDecision::Deny);
97 assert_eq!(ac.check_access("discord", "bob"), AccessDecision::Allow);
98 assert_eq!(ac.check_access("discord", "carol"), AccessDecision::Allow);
99 assert_eq!(ac.check_access("discord", "alice"), AccessDecision::Deny);
100 }
101
102 #[test]
103 fn no_policies_allows_everything() {
104 let ac = StaticAccessControl::default();
105 assert_eq!(ac.check_access("anything", "anyone"), AccessDecision::Allow);
106 assert_eq!(ac.policies().len(), 0);
107 }
108
109 #[test]
110 fn policies_accessor_returns_configured_list() {
111 let policies = vec![ChannelAccessPolicy {
112 channel: "telegram".into(),
113 allow_from: vec!["alice".into()],
114 }];
115 let ac = StaticAccessControl::new(policies.clone());
116 assert_eq!(ac.policies().len(), 1);
117 assert_eq!(ac.policies()[0].channel, "telegram");
118 }
119}