This repository has no description
9.7 kB
301 lines
1use std::collections::{BTreeMap, BTreeSet};
2
3use crate::source::{AclRow, SourceDid, SourceRepoDid, SourceRepoName, SourceRepoObject};
4
5const DOMAIN: &str = "thisserver";
6const SKIPPED_ROLES: [&str; 6] = [
7 "repo:push",
8 "repo:settings",
9 "repo:invite",
10 "repo:delete",
11 "repo:create",
12 "server:invite",
13];
14
15#[derive(Debug, thiserror::Error)]
16pub enum CasbinError {
17 #[error("unrecognized acl row: {p_type},{v0},{v1},{v2},{v3}")]
18 UnrecognizedRow {
19 p_type: String,
20 v0: String,
21 v1: String,
22 v2: String,
23 v3: String,
24 },
25 #[error("acl names two server owners: {first} and {second}")]
26 TwoServerOwners { first: SourceDid, second: SourceDid },
27}
28
29#[derive(Debug, Default, PartialEq, Eq)]
30pub struct AclRoster {
31 pub server_owner: Option<SourceDid>,
32 pub members: BTreeSet<SourceDid>,
33 pub owner_markers: BTreeMap<SourceRepoDid, BTreeSet<SourceDid>>,
34 pub collaborators: BTreeMap<SourceRepoDid, BTreeSet<SourceDid>>,
35 pub slash_collaborators: BTreeMap<SourceRepoDid, BTreeSet<SourceDid>>,
36 pub slash_owner_markers: u64,
37 pub slash_collab_rows: u64,
38 pub unresolved_slash_forms: Vec<String>,
39}
40
41#[derive(Debug, Clone, PartialEq, Eq)]
42pub enum SlashTarget {
43 Unique(SourceRepoDid),
44 Ambiguous,
45}
46
47pub type SlashResolver = BTreeMap<(SourceDid, SourceRepoName), SlashTarget>;
48
49pub fn resolver(
50 pairs: impl Iterator<Item = (SourceDid, SourceRepoName, SourceRepoDid)>,
51) -> SlashResolver {
52 pairs.fold(BTreeMap::new(), |mut map, (owner, name, repo)| {
53 map.entry((owner, name))
54 .and_modify(|target| *target = SlashTarget::Ambiguous)
55 .or_insert(SlashTarget::Unique(repo));
56 map
57 })
58}
59
60pub fn decode(rows: &[AclRow], resolve: &SlashResolver) -> Result<AclRoster, CasbinError> {
61 rows.iter().try_fold(AclRoster::default(), |roster, row| {
62 step(roster, row, resolve)
63 })
64}
65
66fn step(
67 mut roster: AclRoster,
68 row: &AclRow,
69 resolve: &SlashResolver,
70) -> Result<AclRoster, CasbinError> {
71 match (
72 row.p_type.as_str(),
73 row.v0.as_str(),
74 row.v1.as_str(),
75 row.v2.as_str(),
76 row.v3.as_str(),
77 ) {
78 // loadbearing ordering because of how old knots used to work
79 ("g", "server:owner", "server:member", DOMAIN, _) => Ok(roster),
80 ("g", did, "server:member", DOMAIN, _) => {
81 roster.members.insert(SourceDid::from_column(did));
82 Ok(roster)
83 }
84 ("g", did, "server:owner", DOMAIN, _) => match roster.server_owner.take() {
85 Some(first) if first.as_str() != did => Err(CasbinError::TwoServerOwners {
86 first,
87 second: SourceDid::from_column(did),
88 }),
89 _ => {
90 roster.server_owner = Some(SourceDid::from_column(did));
91 Ok(roster)
92 }
93 },
94 ("p", _, DOMAIN, _, role) if SKIPPED_ROLES.contains(&role) => Ok(roster),
95 ("p", did, DOMAIN, object, "repo:owner") => Ok(mark(
96 roster,
97 Marker::Owner,
98 &SourceDid::from_column(did),
99 &SourceRepoObject::from_column(object),
100 resolve,
101 )),
102 ("p", did, DOMAIN, object, "repo:collaborator") => Ok(mark(
103 roster,
104 Marker::Collaborator,
105 &SourceDid::from_column(did),
106 &SourceRepoObject::from_column(object),
107 resolve,
108 )),
109 _ => Err(CasbinError::UnrecognizedRow {
110 p_type: row.p_type.clone(),
111 v0: row.v0.clone(),
112 v1: row.v1.clone(),
113 v2: row.v2.clone(),
114 v3: row.v3.clone(),
115 }),
116 }
117}
118
119#[derive(Clone, Copy)]
120enum Marker {
121 Owner,
122 Collaborator,
123}
124
125fn mark(
126 mut roster: AclRoster,
127 marker: Marker,
128 did: &SourceDid,
129 object: &SourceRepoObject,
130 resolve: &SlashResolver,
131) -> AclRoster {
132 match (object.as_str().split_once('/'), marker) {
133 (None, Marker::Owner) => {
134 roster
135 .owner_markers
136 .entry(SourceRepoDid::from_column(object.as_str()))
137 .or_default()
138 .insert(did.clone());
139 }
140 (None, Marker::Collaborator) => {
141 roster
142 .collaborators
143 .entry(SourceRepoDid::from_column(object.as_str()))
144 .or_default()
145 .insert(did.clone());
146 }
147 (Some((owner, name)), marker) => {
148 match marker {
149 Marker::Owner => roster.slash_owner_markers += 1,
150 Marker::Collaborator => roster.slash_collab_rows += 1,
151 }
152 let resolved = resolve.get(&(
153 SourceDid::from_column(owner),
154 SourceRepoName::from_column(name),
155 ));
156 match (resolved, marker) {
157 (Some(SlashTarget::Unique(repo)), Marker::Collaborator) => {
158 roster
159 .slash_collaborators
160 .entry(repo.clone())
161 .or_default()
162 .insert(did.clone());
163 }
164 (Some(SlashTarget::Unique(_)), Marker::Owner) => {}
165 (Some(SlashTarget::Ambiguous), _) | (None, _) => {
166 roster.unresolved_slash_forms.push(object.to_string())
167 }
168 }
169 }
170 }
171 roster
172}
173
174#[cfg(test)]
175mod tests {
176 use super::*;
177
178 fn g(did: &str, role: &str) -> AclRow {
179 AclRow {
180 p_type: "g".into(),
181 v0: did.into(),
182 v1: role.into(),
183 v2: DOMAIN.into(),
184 v3: String::new(),
185 }
186 }
187
188 fn p(did: &str, object: &str, role: &str) -> AclRow {
189 AclRow {
190 p_type: "p".into(),
191 v0: did.into(),
192 v1: DOMAIN.into(),
193 v2: object.into(),
194 v3: role.into(),
195 }
196 }
197
198 #[test]
199 fn decodes_members_owner_and_repo_markers() {
200 let rows = [
201 g("did:plc:nel", "server:owner"),
202 g("server:owner", "server:member"),
203 g("did:plc:olaren", "server:member"),
204 g("did:plc:teq", "server:member"),
205 p("did:plc:nel", "did:plc:squid", "repo:owner"),
206 p("did:plc:nel", "did:plc:squid", "repo:push"),
207 p("did:plc:nel", "did:plc:squid", "repo:settings"),
208 p("did:plc:nel", "did:plc:squid", "repo:invite"),
209 p("did:plc:nel", "did:plc:squid", "repo:delete"),
210 p("did:plc:teq", "did:plc:squid", "repo:collaborator"),
211 p("did:plc:nel", "", "repo:create"),
212 p("did:plc:nel", "", "server:invite"),
213 ];
214 let roster = decode(&rows, &SlashResolver::new()).unwrap();
215 assert_eq!(
216 roster.server_owner.as_ref().map(SourceDid::as_str),
217 Some("did:plc:nel")
218 );
219 assert_eq!(roster.members.len(), 2);
220 assert_eq!(
221 roster.owner_markers["did:plc:squid"],
222 BTreeSet::from([SourceDid::from_column("did:plc:nel")])
223 );
224 assert_eq!(
225 roster.collaborators["did:plc:squid"],
226 BTreeSet::from([SourceDid::from_column("did:plc:teq")])
227 );
228 }
229
230 #[test]
231 fn slash_forms_resolve_through_repo_keys_unless_ambiguous() {
232 let resolve = resolver(
233 [
234 ("did:plc:nel", "anemone", "did:plc:limpet"),
235 ("did:plc:isabel", "mussel", "did:plc:whelk"),
236 ("did:plc:isabel", "mussel", "did:plc:conch"),
237 ]
238 .into_iter()
239 .map(|(owner, name, repo)| {
240 (
241 SourceDid::from_column(owner),
242 SourceRepoName::from_column(name),
243 SourceRepoDid::from_column(repo),
244 )
245 }),
246 );
247 assert_eq!(
248 resolve[&(
249 SourceDid::from_column("did:plc:isabel"),
250 SourceRepoName::from_column("mussel")
251 )],
252 SlashTarget::Ambiguous
253 );
254
255 let roster = decode(
256 &[
257 p("did:plc:nel", "did:plc:nel/anemone", "repo:owner"),
258 p("did:plc:isabel", "did:plc:nel/anemone", "repo:collaborator"),
259 p("did:plc:nel", "did:plc:nel/vanished", "repo:owner"),
260 p("did:plc:teq", "did:plc:isabel/mussel", "repo:collaborator"),
261 ],
262 &resolve,
263 )
264 .unwrap();
265 assert_eq!(roster.slash_owner_markers, 2);
266 assert_eq!(roster.slash_collab_rows, 2);
267 assert!(roster.owner_markers.is_empty());
268 assert!(roster.collaborators.is_empty());
269 assert!(roster.slash_collaborators["did:plc:limpet"].contains("did:plc:isabel"));
270 assert_eq!(
271 roster.slash_collaborators.len(),
272 1,
273 "an ambiguous slash form grants nobody"
274 );
275 assert_eq!(
276 roster.unresolved_slash_forms,
277 ["did:plc:nel/vanished", "did:plc:isabel/mussel"]
278 );
279 }
280
281 #[test]
282 fn decode_rejects_unknown_roles_and_two_server_owners() {
283 assert!(matches!(
284 decode(
285 &[p("did:plc:nel", "did:plc:squid", "repo:mystery")],
286 &SlashResolver::new()
287 ),
288 Err(CasbinError::UnrecognizedRow { .. })
289 ));
290 assert!(matches!(
291 decode(
292 &[
293 g("did:plc:nel", "server:owner"),
294 g("did:plc:bailey", "server:owner"),
295 ],
296 &SlashResolver::new()
297 ),
298 Err(CasbinError::TwoServerOwners { .. })
299 ));
300 }
301}