this repo has no description
at main 4.8 kB view raw
1use reqwest::Client; 2use serde::Deserialize; 3use std::collections::HashMap; 4use std::sync::LazyLock; 5use tokio::sync::RwLock; 6use tracing::{debug, warn}; 7 8static LEXICON_CACHE: LazyLock<RwLock<HashMap<String, CachedLexicon>>> = 9 LazyLock::new(|| RwLock::new(HashMap::new())); 10 11#[derive(Clone)] 12struct CachedLexicon { 13 expanded_scope: String, 14 cached_at: std::time::Instant, 15} 16 17const CACHE_TTL_SECS: u64 = 3600; 18 19#[derive(Debug, Deserialize)] 20struct LexiconDoc { 21 defs: HashMap<String, LexiconDef>, 22} 23 24#[derive(Debug, Deserialize)] 25struct LexiconDef { 26 #[serde(rename = "type")] 27 def_type: String, 28 permissions: Option<Vec<PermissionEntry>>, 29} 30 31#[derive(Debug, Deserialize)] 32struct PermissionEntry { 33 resource: String, 34 collection: Option<Vec<String>>, 35} 36 37pub async fn expand_include_scopes(scope_string: &str) -> String { 38 let futures: Vec<_> = scope_string 39 .split_whitespace() 40 .map(|scope| async move { 41 match scope.strip_prefix("include:") { 42 Some(nsid) => { 43 let nsid_base = nsid.split('?').next().unwrap_or(nsid); 44 expand_permission_set(nsid_base).await.unwrap_or_else(|e| { 45 warn!(nsid = nsid_base, error = %e, "Failed to expand permission set, keeping original"); 46 scope.to_string() 47 }) 48 } 49 None => scope.to_string(), 50 } 51 }) 52 .collect(); 53 54 futures::future::join_all(futures).await.join(" ") 55} 56 57async fn expand_permission_set(nsid: &str) -> Result<String, String> { 58 { 59 let cache = LEXICON_CACHE.read().await; 60 if let Some(cached) = cache.get(nsid) 61 && cached.cached_at.elapsed().as_secs() < CACHE_TTL_SECS 62 { 63 debug!(nsid, "Using cached permission set expansion"); 64 return Ok(cached.expanded_scope.clone()); 65 } 66 } 67 68 let parts: Vec<&str> = nsid.split('.').collect(); 69 if parts.len() < 3 { 70 return Err(format!("Invalid NSID format: {}", nsid)); 71 } 72 73 let domain_parts: Vec<&str> = parts[..2].iter().rev().cloned().collect(); 74 let domain = domain_parts.join("."); 75 let path = parts[2..].join("/"); 76 77 let url = format!("https://{}/lexicons/{}.json", domain, path); 78 debug!(nsid, url = %url, "Fetching permission set lexicon"); 79 80 let client = Client::builder() 81 .timeout(std::time::Duration::from_secs(10)) 82 .build() 83 .map_err(|e| format!("Failed to create HTTP client: {}", e))?; 84 85 let response = client 86 .get(&url) 87 .header("Accept", "application/json") 88 .send() 89 .await 90 .map_err(|e| format!("Failed to fetch lexicon: {}", e))?; 91 92 if !response.status().is_success() { 93 return Err(format!( 94 "Failed to fetch lexicon: HTTP {}", 95 response.status() 96 )); 97 } 98 99 let lexicon: LexiconDoc = response 100 .json() 101 .await 102 .map_err(|e| format!("Failed to parse lexicon: {}", e))?; 103 104 let main_def = lexicon 105 .defs 106 .get("main") 107 .ok_or("Missing 'main' definition in lexicon")?; 108 109 if main_def.def_type != "permission-set" { 110 return Err(format!( 111 "Expected permission-set type, got: {}", 112 main_def.def_type 113 )); 114 } 115 116 let permissions = main_def 117 .permissions 118 .as_ref() 119 .ok_or("Missing permissions in permission-set")?; 120 121 let mut collections: Vec<String> = permissions 122 .iter() 123 .filter(|perm| perm.resource == "repo") 124 .filter_map(|perm| perm.collection.as_ref()) 125 .flatten() 126 .cloned() 127 .collect(); 128 129 if collections.is_empty() { 130 return Err("No repo collections found in permission-set".to_string()); 131 } 132 133 collections.sort(); 134 135 let collection_params: Vec<String> = collections 136 .iter() 137 .map(|c| format!("collection={}", c)) 138 .collect(); 139 140 let expanded = format!("repo?{}", collection_params.join("&")); 141 142 { 143 let mut cache = LEXICON_CACHE.write().await; 144 cache.insert( 145 nsid.to_string(), 146 CachedLexicon { 147 expanded_scope: expanded.clone(), 148 cached_at: std::time::Instant::now(), 149 }, 150 ); 151 } 152 153 debug!(nsid, expanded = %expanded, "Successfully expanded permission set"); 154 Ok(expanded) 155} 156 157#[cfg(test)] 158mod tests { 159 #[test] 160 fn test_nsid_to_url() { 161 let nsid = "io.atcr.authFullApp"; 162 let parts: Vec<&str> = nsid.split('.').collect(); 163 let domain_parts: Vec<&str> = parts[..2].iter().rev().cloned().collect(); 164 let domain = domain_parts.join("."); 165 let path = parts[2..].join("/"); 166 167 assert_eq!(domain, "atcr.io"); 168 assert_eq!(path, "authFullApp"); 169 } 170}