zerodds_bridge_security/
acl.rs1use std::collections::HashMap;
16
17use crate::auth::AuthSubject;
18
19#[derive(Debug, Clone, Copy, PartialEq, Eq)]
21pub enum AclOp {
22 Read,
24 Write,
26}
27
28#[derive(Debug, Clone, Default, PartialEq, Eq)]
30pub struct AclEntry {
31 pub read: Vec<String>,
33 pub write: Vec<String>,
35}
36
37impl AclEntry {
38 #[must_use]
40 pub fn allow_all() -> Self {
41 Self {
42 read: vec!["*".to_string()],
43 write: vec!["*".to_string()],
44 }
45 }
46}
47
48#[derive(Debug, Clone, Default)]
50pub struct Acl {
51 entries: HashMap<String, AclEntry>,
52 default: Option<AclEntry>,
55}
56
57impl Acl {
58 #[must_use]
60 pub fn deny_all() -> Self {
61 Self {
62 entries: HashMap::new(),
63 default: None,
64 }
65 }
66
67 #[must_use]
69 pub fn allow_all() -> Self {
70 Self {
71 entries: HashMap::new(),
72 default: Some(AclEntry::allow_all()),
73 }
74 }
75
76 pub fn set(&mut self, topic: impl Into<String>, entry: AclEntry) {
78 self.entries.insert(topic.into(), entry);
79 }
80
81 pub fn set_default(&mut self, entry: AclEntry) {
83 self.default = Some(entry);
84 }
85
86 #[must_use]
88 pub fn check(&self, subject: &AuthSubject, op: AclOp, topic: &str) -> bool {
89 let entry = self.entries.get(topic).or(self.default.as_ref());
90 let Some(entry) = entry else {
91 return false;
92 };
93 let list = match op {
94 AclOp::Read => &entry.read,
95 AclOp::Write => &entry.write,
96 };
97 list.iter().any(|pat| match_subject(pat, subject))
98 }
99}
100
101fn match_subject(pat: &str, subject: &AuthSubject) -> bool {
102 if pat == "*" {
103 return true;
104 }
105 if let Some(group) = pat
106 .strip_prefix("*group:")
107 .and_then(|s| s.strip_suffix('*'))
108 {
109 return subject.groups.iter().any(|g| g == group);
110 }
111 pat == subject.name
112}
113
114#[cfg(test)]
115#[allow(clippy::expect_used, clippy::unwrap_used)]
116mod tests {
117 use super::*;
118
119 fn alice() -> AuthSubject {
120 AuthSubject::new("alice").with_group("engineers")
121 }
122 fn bob() -> AuthSubject {
123 AuthSubject::new("bob")
124 }
125
126 #[test]
127 fn deny_by_default() {
128 let acl = Acl::deny_all();
129 assert!(!acl.check(&alice(), AclOp::Read, "Trade"));
130 }
131
132 #[test]
133 fn explicit_allow_for_user() {
134 let mut acl = Acl::deny_all();
135 acl.set(
136 "Trade",
137 AclEntry {
138 read: vec!["alice".into()],
139 write: vec!["alice".into()],
140 },
141 );
142 assert!(acl.check(&alice(), AclOp::Read, "Trade"));
143 assert!(acl.check(&alice(), AclOp::Write, "Trade"));
144 assert!(!acl.check(&bob(), AclOp::Read, "Trade"));
145 }
146
147 #[test]
148 fn star_wildcard_allows_anyone() {
149 let mut acl = Acl::deny_all();
150 acl.set(
151 "Public",
152 AclEntry {
153 read: vec!["*".into()],
154 write: vec!["alice".into()],
155 },
156 );
157 assert!(acl.check(&bob(), AclOp::Read, "Public"));
158 assert!(!acl.check(&bob(), AclOp::Write, "Public"));
159 }
160
161 #[test]
162 fn group_wildcard_allows_members() {
163 let mut acl = Acl::deny_all();
164 acl.set(
165 "EngOnly",
166 AclEntry {
167 read: vec!["*group:engineers*".into()],
168 write: vec!["*group:engineers*".into()],
169 },
170 );
171 assert!(acl.check(&alice(), AclOp::Read, "EngOnly"));
172 assert!(!acl.check(&bob(), AclOp::Read, "EngOnly"));
173 }
174
175 #[test]
176 fn default_entry_used_for_unknown_topic() {
177 let mut acl = Acl::deny_all();
178 acl.set_default(AclEntry {
179 read: vec!["*".into()],
180 write: vec![],
181 });
182 assert!(acl.check(&bob(), AclOp::Read, "AnythingNew"));
183 assert!(!acl.check(&bob(), AclOp::Write, "AnythingNew"));
184 }
185
186 #[test]
187 fn write_uses_write_list_only() {
188 let mut acl = Acl::deny_all();
189 acl.set(
190 "T",
191 AclEntry {
192 read: vec!["*".into()],
193 write: vec!["alice".into()],
194 },
195 );
196 assert!(acl.check(&alice(), AclOp::Write, "T"));
197 assert!(!acl.check(&bob(), AclOp::Write, "T"));
198 }
199
200 #[test]
201 fn allow_all_constructor_lets_everyone_through() {
202 let acl = Acl::allow_all();
203 assert!(acl.check(&bob(), AclOp::Read, "X"));
204 assert!(acl.check(&bob(), AclOp::Write, "Y"));
205 }
206}