1use indexmap::IndexMap;
21
22#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
29pub enum Principal {
30 Public,
32 Role(String),
34 Other(String),
36}
37
38impl Principal {
39 pub fn parse(s: &str) -> Self {
40 if s == "*" {
41 Principal::Public
42 } else if let Some(name) = s.strip_prefix("role:") {
43 Principal::Role(name.to_string())
44 } else {
45 Principal::Other(s.to_string())
46 }
47 }
48
49 pub fn as_key(&self) -> String {
50 match self {
51 Principal::Public => "*".to_string(),
52 Principal::Role(name) => format!("role:{name}"),
53 Principal::Other(s) => s.clone(),
54 }
55 }
56}
57
58#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
60pub struct Permissions {
61 pub read: bool,
62 pub write: bool,
63}
64
65impl Permissions {
66 pub fn is_empty(&self) -> bool {
67 !self.read && !self.write
68 }
69}
70
71#[derive(Clone, Debug, Default, PartialEq, Eq)]
74pub struct Acl(IndexMap<Principal, Permissions>);
75
76impl Acl {
77 pub fn new() -> Self {
78 Self(IndexMap::new())
79 }
80
81 pub fn set(&mut self, principal: Principal, perms: Permissions) {
82 self.0.insert(principal, perms);
83 }
84
85 pub fn get(&self, principal: &Principal) -> Option<Permissions> {
86 self.0.get(principal).copied()
87 }
88
89 pub fn is_empty(&self) -> bool {
90 self.0.is_empty()
91 }
92
93 pub fn iter(&self) -> impl Iterator<Item = (&Principal, &Permissions)> {
94 self.0.iter()
95 }
96
97 pub fn to_perms(&self) -> (Vec<String>, Vec<String>) {
100 let mut rperm = Vec::new();
101 let mut wperm = Vec::new();
102 for (principal, perms) in &self.0 {
103 if perms.read {
104 rperm.push(principal.as_key());
105 }
106 if perms.write {
107 wperm.push(principal.as_key());
108 }
109 }
110 (rperm, wperm)
111 }
112
113 pub fn from_perms(rperm: Option<&[String]>, wperm: Option<&[String]>) -> Option<Acl> {
119 if rperm.is_none() && wperm.is_none() {
120 return None;
121 }
122 let mut acl = Acl::new();
123 for entry in rperm.unwrap_or(&[]) {
124 acl.0.entry(Principal::parse(entry)).or_default().read = true;
125 }
126 for entry in wperm.unwrap_or(&[]) {
127 acl.0.entry(Principal::parse(entry)).or_default().write = true;
128 }
129 Some(acl)
130 }
131
132 pub fn to_json(&self) -> String {
134 let mut out = String::from("{");
135 let mut first = true;
136 for (principal, perms) in &self.0 {
137 if perms.is_empty() {
138 continue;
139 }
140 if !first {
141 out.push(',');
142 }
143 first = false;
144 crate::value::write_json_string(&principal.as_key(), &mut out);
145 out.push(':');
146 out.push('{');
147 match (perms.read, perms.write) {
148 (true, true) => out.push_str(r#""read":true,"write":true"#),
149 (true, false) => out.push_str(r#""read":true"#),
150 (false, true) => out.push_str(r#""write":true"#),
151 (false, false) => {}
152 }
153 out.push('}');
154 }
155 out.push('}');
156 out
157 }
158}
159
160#[cfg(test)]
161mod tests {
162 use super::*;
163
164 fn p(s: &str) -> Principal {
165 Principal::parse(s)
166 }
167
168 #[test]
169 fn principal_round_trips_every_shape() {
170 for s in [
171 "*",
172 "role:Admin",
173 "abc123",
174 "*unresolved",
175 "role:with:colons",
176 ] {
177 assert_eq!(Principal::parse(s).as_key(), s, "{s} did not round trip");
178 }
179 assert_eq!(p("*"), Principal::Public);
180 assert_eq!(p("role:Admin"), Principal::Role("Admin".into()));
181 assert_eq!(p("*unresolved"), Principal::Other("*unresolved".into()));
183 }
184
185 #[test]
186 fn storage_round_trip_is_lossless() {
187 let mut acl = Acl::new();
188 acl.set(
189 Principal::Public,
190 Permissions {
191 read: true,
192 write: false,
193 },
194 );
195 acl.set(
196 Principal::Role("Admin".into()),
197 Permissions {
198 read: true,
199 write: true,
200 },
201 );
202
203 let (r, w) = acl.to_perms();
204 assert_eq!(r, vec!["*", "role:Admin"]);
205 assert_eq!(w, vec!["role:Admin"]);
206 assert_eq!(Acl::from_perms(Some(&r), Some(&w)), Some(acl));
207 }
208
209 #[test]
211 fn upstream_quirk_false_flags_are_dropped_from_json() {
212 let mut acl = Acl::new();
213 acl.set(
214 Principal::Public,
215 Permissions {
216 read: true,
217 write: false,
218 },
219 );
220 assert_eq!(acl.to_json(), r#"{"*":{"read":true}}"#);
221
222 let mut both = Acl::new();
223 both.set(
224 Principal::Public,
225 Permissions {
226 read: true,
227 write: true,
228 },
229 );
230 assert_eq!(both.to_json(), r#"{"*":{"read":true,"write":true}}"#);
231
232 let mut write_only = Acl::new();
233 write_only.set(
234 Principal::Public,
235 Permissions {
236 read: false,
237 write: true,
238 },
239 );
240 assert_eq!(write_only.to_json(), r#"{"*":{"write":true}}"#);
241 }
242
243 #[test]
244 fn an_entry_with_no_permissions_disappears_entirely() {
245 let mut acl = Acl::new();
246 acl.set(Principal::Other("abc".into()), Permissions::default());
247 assert_eq!(acl.to_json(), "{}");
248 let (r, w) = acl.to_perms();
249 assert!(r.is_empty() && w.is_empty());
250 }
251
252 #[test]
253 fn absent_columns_and_empty_columns_are_different() {
254 assert_eq!(Acl::from_perms(None, None), None);
256 let empty: [String; 0] = [];
258 assert_eq!(Acl::from_perms(Some(&empty), None), Some(Acl::new()));
259 assert_eq!(
260 Acl::from_perms(Some(&empty), None).map(|a| a.to_json()),
261 Some("{}".to_string())
262 );
263 }
264
265 #[test]
266 fn merges_the_two_columns_per_principal() {
267 let r = vec!["*".to_string(), "role:Admin".to_string()];
268 let w = vec!["role:Admin".to_string()];
269 let acl = Acl::from_perms(Some(&r), Some(&w)).unwrap();
270 assert_eq!(
271 acl.get(&Principal::Public),
272 Some(Permissions {
273 read: true,
274 write: false
275 })
276 );
277 assert_eq!(
278 acl.get(&Principal::Role("Admin".into())),
279 Some(Permissions {
280 read: true,
281 write: true
282 })
283 );
284 }
285
286 #[test]
287 fn insertion_order_is_preserved_in_both_directions() {
288 let r = vec!["z".to_string(), "a".to_string(), "m".to_string()];
289 let acl = Acl::from_perms(Some(&r), None).unwrap();
290 let (out, _) = acl.to_perms();
291 assert_eq!(out, r, "order must not be sorted");
292 }
293}