This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

core / knot2 / crates / knot-migrate / src / casbin.rs
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}