Our Personal Data Server from scratch!
0
fork

Configure Feed

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

fix(auth): use authextractor for serviceauth too now

+83 -187
+16 -16
Cargo.lock
··· 6094 6094 6095 6095 [[package]] 6096 6096 name = "tranquil-auth" 6097 - version = "0.4.0" 6097 + version = "0.4.1" 6098 6098 dependencies = [ 6099 6099 "anyhow", 6100 6100 "base32", ··· 6117 6117 6118 6118 [[package]] 6119 6119 name = "tranquil-cache" 6120 - version = "0.4.0" 6120 + version = "0.4.1" 6121 6121 dependencies = [ 6122 6122 "async-trait", 6123 6123 "base64 0.22.1", ··· 6131 6131 6132 6132 [[package]] 6133 6133 name = "tranquil-comms" 6134 - version = "0.4.0" 6134 + version = "0.4.1" 6135 6135 dependencies = [ 6136 6136 "async-trait", 6137 6137 "base64 0.22.1", ··· 6146 6146 6147 6147 [[package]] 6148 6148 name = "tranquil-config" 6149 - version = "0.4.0" 6149 + version = "0.4.1" 6150 6150 dependencies = [ 6151 6151 "confique", 6152 6152 "serde", ··· 6154 6154 6155 6155 [[package]] 6156 6156 name = "tranquil-crypto" 6157 - version = "0.4.0" 6157 + version = "0.4.1" 6158 6158 dependencies = [ 6159 6159 "aes-gcm", 6160 6160 "base64 0.22.1", ··· 6170 6170 6171 6171 [[package]] 6172 6172 name = "tranquil-db" 6173 - version = "0.4.0" 6173 + version = "0.4.1" 6174 6174 dependencies = [ 6175 6175 "async-trait", 6176 6176 "chrono", ··· 6187 6187 6188 6188 [[package]] 6189 6189 name = "tranquil-db-traits" 6190 - version = "0.4.0" 6190 + version = "0.4.1" 6191 6191 dependencies = [ 6192 6192 "async-trait", 6193 6193 "base64 0.22.1", ··· 6203 6203 6204 6204 [[package]] 6205 6205 name = "tranquil-infra" 6206 - version = "0.4.0" 6206 + version = "0.4.1" 6207 6207 dependencies = [ 6208 6208 "async-trait", 6209 6209 "bytes", ··· 6214 6214 6215 6215 [[package]] 6216 6216 name = "tranquil-lexicon" 6217 - version = "0.4.0" 6217 + version = "0.4.1" 6218 6218 dependencies = [ 6219 6219 "chrono", 6220 6220 "hickory-resolver", ··· 6232 6232 6233 6233 [[package]] 6234 6234 name = "tranquil-oauth" 6235 - version = "0.4.0" 6235 + version = "0.4.1" 6236 6236 dependencies = [ 6237 6237 "anyhow", 6238 6238 "axum", ··· 6255 6255 6256 6256 [[package]] 6257 6257 name = "tranquil-pds" 6258 - version = "0.4.0" 6258 + version = "0.4.1" 6259 6259 dependencies = [ 6260 6260 "aes-gcm", 6261 6261 "anyhow", ··· 6343 6343 6344 6344 [[package]] 6345 6345 name = "tranquil-repo" 6346 - version = "0.4.0" 6346 + version = "0.4.1" 6347 6347 dependencies = [ 6348 6348 "bytes", 6349 6349 "cid", ··· 6355 6355 6356 6356 [[package]] 6357 6357 name = "tranquil-ripple" 6358 - version = "0.4.0" 6358 + version = "0.4.1" 6359 6359 dependencies = [ 6360 6360 "async-trait", 6361 6361 "backon", ··· 6380 6380 6381 6381 [[package]] 6382 6382 name = "tranquil-scopes" 6383 - version = "0.4.0" 6383 + version = "0.4.1" 6384 6384 dependencies = [ 6385 6385 "axum", 6386 6386 "futures", ··· 6396 6396 6397 6397 [[package]] 6398 6398 name = "tranquil-storage" 6399 - version = "0.4.0" 6399 + version = "0.4.1" 6400 6400 dependencies = [ 6401 6401 "async-trait", 6402 6402 "aws-config", ··· 6413 6413 6414 6414 [[package]] 6415 6415 name = "tranquil-types" 6416 - version = "0.4.0" 6416 + version = "0.4.1" 6417 6417 dependencies = [ 6418 6418 "chrono", 6419 6419 "cid",
+1 -1
Cargo.toml
··· 20 20 ] 21 21 22 22 [workspace.package] 23 - version = "0.4.0" 23 + version = "0.4.1" 24 24 edition = "2024" 25 25 license = "AGPL-3.0-or-later" 26 26
+4 -6
crates/tranquil-config/src/lib.rs
··· 327 327 errors: &mut Vec<String>, 328 328 ) { 329 329 self.validate_sso_provider(prefix, p, errors); 330 - if p.get_enabled() { 331 - if p.get_issuer().is_none() { 332 - errors.push(format!( 333 - "{prefix}.issuer is required when {prefix}.enabled = true" 334 - )); 335 - } 330 + if p.get_enabled() && p.get_issuer().is_none() { 331 + errors.push(format!( 332 + "{prefix}.issuer is required when {prefix}.enabled = true" 333 + )); 336 334 } 337 335 } 338 336
+1 -3
crates/tranquil-lexicon/src/formats.rs
··· 46 46 } 47 47 48 48 pub fn is_valid_cid(s: &str) -> bool { 49 - s.len() >= 8 50 - && s.chars().all(|c| c.is_ascii_alphanumeric()) 51 - && s.starts_with(|c: char| c == 'b' || c == 'z' || c == 'Q') 49 + s.len() >= 8 && s.chars().all(|c| c.is_ascii_alphanumeric()) && s.starts_with(['b', 'z', 'Q']) 52 50 } 53 51 54 52 pub fn is_valid_language(s: &str) -> bool {
+8 -9
crates/tranquil-lexicon/src/validate.rs
··· 339 339 } 340 340 } 341 341 342 - if let Some(max_size) = lex_blob.max_size { 343 - if let Some(size) = obj.get("size").and_then(|v| v.as_u64()) { 344 - if size > max_size { 345 - return Err(LexValidationError::field( 346 - path, 347 - format!("blob size {} exceeds max_size {}", size, max_size), 348 - )); 349 - } 350 - } 342 + if let (Some(max_size), Some(size)) = 343 + (lex_blob.max_size, obj.get("size").and_then(|v| v.as_u64())) 344 + && size > max_size 345 + { 346 + return Err(LexValidationError::field( 347 + path, 348 + format!("blob size {} exceeds max_size {}", size, max_size), 349 + )); 351 350 } 352 351 353 352 Ok(())
+13 -110
crates/tranquil-pds/src/api/server/service_auth.rs
··· 1 - use crate::AccountStatus; 2 1 use crate::api::error::ApiError; 2 + use crate::auth::extractor::{Auth, Permissive}; 3 3 use crate::state::AppState; 4 4 use crate::types::Did; 5 - use axum::http::Method; 6 5 use axum::{ 7 6 Json, 8 7 extract::{Query, State}, ··· 10 9 response::{IntoResponse, Response}, 11 10 }; 12 11 use serde::{Deserialize, Serialize}; 13 - use serde_json::json; 14 12 use std::collections::HashSet; 15 13 use std::sync::LazyLock; 16 14 use tracing::{error, info, warn}; ··· 59 57 60 58 pub async fn get_service_auth( 61 59 State(state): State<AppState>, 62 - headers: axum::http::HeaderMap, 60 + auth: Auth<Permissive>, 63 61 Query(params): Query<GetServiceAuthParams>, 64 62 ) -> Response { 65 - let auth_header = crate::util::get_header_str(&headers, axum::http::header::AUTHORIZATION); 66 - let dpop_proof = crate::util::get_header_str(&headers, crate::util::HEADER_DPOP); 67 63 info!( 68 - has_auth_header = auth_header.is_some(), 69 - has_dpop_proof = dpop_proof.is_some(), 64 + did = %&auth.did, 65 + is_oauth = auth.is_oauth(), 70 66 aud = %params.aud, 71 67 lxm = ?params.lxm, 72 68 "getServiceAuth called" 73 69 ); 74 - let auth_header = match auth_header { 75 - Some(h) => h.trim(), 76 - None => { 77 - warn!("getServiceAuth: no Authorization header"); 78 - return ApiError::AuthenticationRequired.into_response(); 79 - } 80 - }; 81 70 82 - let extracted = match crate::auth::extract_auth_token_from_header(Some(auth_header)) { 83 - Some(e) => e, 84 - None => { 85 - warn!(auth_scheme = ?auth_header.split_whitespace().next(), "getServiceAuth: invalid auth scheme"); 86 - return ApiError::AuthenticationRequired.into_response(); 87 - } 88 - }; 89 - let token = extracted.token; 90 - 91 - let auth_user = if extracted.scheme.is_dpop() { 92 - match crate::oauth::verify::verify_oauth_access_token( 93 - state.oauth_repo.as_ref(), 94 - &token, 95 - dpop_proof, 96 - Method::GET.as_str(), 97 - &crate::util::build_full_url(&format!( 98 - "/xrpc/com.atproto.server.getServiceAuth?aud={}&lxm={}", 99 - params.aud, 100 - params.lxm.as_ref().map_or("", |n| n.as_str()) 101 - )), 102 - ) 103 - .await 104 - { 105 - Ok(result) => { 106 - let did: Did = match result.did.parse() { 107 - Ok(d) => d, 108 - Err(_) => { 109 - return ApiError::InternalError(Some("Invalid DID in token".into())) 110 - .into_response(); 111 - } 112 - }; 113 - crate::auth::AuthenticatedUser { 114 - did, 115 - is_admin: false, 116 - status: AccountStatus::Active, 117 - scope: result.scope, 118 - key_bytes: None, 119 - controller_did: None, 120 - auth_source: crate::auth::AuthSource::OAuth, 121 - } 122 - } 123 - Err(crate::oauth::OAuthError::UseDpopNonce(nonce)) => { 124 - return ( 125 - StatusCode::UNAUTHORIZED, 126 - [("DPoP-Nonce", nonce)], 127 - Json(json!({ 128 - "error": "use_dpop_nonce", 129 - "message": "DPoP nonce required" 130 - })), 131 - ) 132 - .into_response(); 133 - } 134 - Err(crate::oauth::OAuthError::ExpiredToken(msg)) => { 135 - warn!(error = %msg, "getServiceAuth DPoP token expired"); 136 - return ApiError::OAuthExpiredToken(Some(msg)).into_response(); 137 - } 138 - Err(e) => { 139 - warn!(error = ?e, "getServiceAuth DPoP auth validation failed"); 140 - return ApiError::AuthenticationFailed(Some(format!("{:?}", e))).into_response(); 141 - } 142 - } 143 - } else { 144 - match crate::auth::validate_bearer_token_for_service_auth(state.user_repo.as_ref(), &token) 145 - .await 146 - { 147 - Ok(user) => user, 148 - Err(e) => { 149 - warn!(error = ?e, "getServiceAuth auth validation failed"); 150 - return ApiError::from(e).into_response(); 151 - } 152 - } 153 - }; 154 - info!( 155 - did = %&auth_user.did, 156 - is_oauth = auth_user.is_oauth(), 157 - has_key = auth_user.key_bytes.is_some(), 158 - "getServiceAuth auth validated" 159 - ); 160 - let key_bytes = match &auth_user.key_bytes { 71 + let key_bytes = match &auth.key_bytes { 161 72 Some(kb) => kb.clone(), 162 73 None => { 163 - warn!(did = %&auth_user.did, "getServiceAuth: OAuth token has no key_bytes, fetching from DB"); 164 - match state.user_repo.get_user_info_by_did(&auth_user.did).await { 74 + warn!(did = %&auth.did, "getServiceAuth: no key_bytes in auth, fetching from DB"); 75 + match state.user_repo.get_user_info_by_did(&auth.did).await { 165 76 Ok(Some(info)) => match info.key_bytes { 166 77 Some(key_bytes_enc) => { 167 78 match crate::config::decrypt_key(&key_bytes_enc, info.encryption_version) { ··· 202 113 203 114 if let Some(method) = lxm { 204 115 if let Err(e) = crate::auth::scope_check::check_rpc_scope( 205 - &auth_user.auth_source, 206 - auth_user.scope.as_deref(), 116 + &auth.auth_source, 117 + auth.scope.as_deref(), 207 118 params.aud.as_str(), 208 119 method.as_str(), 209 120 ) { 210 121 return e; 211 122 } 212 - } else if auth_user.is_oauth() { 213 - let permissions = auth_user.permissions(); 123 + } else if auth.is_oauth() { 124 + let permissions = auth.permissions(); 214 125 if !permissions.has_full_access() { 215 126 return ApiError::InvalidRequest( 216 127 "OAuth tokens with granular scopes must specify an lxm parameter".into(), ··· 219 130 } 220 131 } 221 132 222 - let is_takendown = state 223 - .user_repo 224 - .get_status_by_did(&auth_user.did) 225 - .await 226 - .ok() 227 - .flatten() 228 - .is_some_and(|s| s.takedown_ref.is_some()); 229 - 230 - if is_takendown && lxm != Some(&*CREATE_ACCOUNT_NSID) { 133 + if auth.status.is_takendown() && lxm != Some(&*CREATE_ACCOUNT_NSID) { 231 134 return ApiError::InvalidToken(Some("Bad token scope".into())).into_response(); 232 135 } 233 136 ··· 265 168 } 266 169 267 170 let service_token = match crate::auth::create_service_token( 268 - &auth_user.did, 171 + &auth.did, 269 172 params.aud.as_str(), 270 173 lxm_for_token, 271 174 &key_bytes,
+1 -1
crates/tranquil-pds/src/api/server/session.rs
··· 68 68 let pds_host = &tranquil_config::get().server.hostname; 69 69 let hostname_for_handles = tranquil_config::get().server.hostname_without_port(); 70 70 let normalized_identifier = 71 - NormalizedLoginIdentifier::normalize(&input.identifier, &hostname_for_handles); 71 + NormalizedLoginIdentifier::normalize(&input.identifier, hostname_for_handles); 72 72 info!( 73 73 "Normalized identifier: {} -> {}", 74 74 input.identifier, normalized_identifier
+3 -2
crates/tranquil-pds/src/auth/mod.rs
··· 355 355 ) 356 356 .await; 357 357 358 - let status_cache_key = crate::cache_keys::user_status_key(&did.to_string()); 358 + let status_cache_key = crate::cache_keys::user_status_key(did.as_ref()); 359 359 let cached = CachedUserStatus { 360 360 deactivated: user.deactivated_at.is_some(), 361 361 takendown: user.takedown_ref.is_some(), ··· 394 394 match verify_access_token_typed(token, &decrypted_key) { 395 395 Ok(token_data) => { 396 396 let jti = &token_data.claims.jti; 397 - let session_cache_key = crate::cache_keys::session_key(&did, &jti); 397 + let session_cache_key = crate::cache_keys::session_key(&did, jti); 398 398 let mut session_valid = false; 399 399 400 400 if let Some(c) = cache { ··· 530 530 AnyStatus, 531 531 } 532 532 533 + #[allow(clippy::too_many_arguments)] 533 534 pub async fn validate_token_with_dpop( 534 535 user_repo: &dyn UserRepository, 535 536 oauth_repo: &dyn OAuthRepository,
+1 -3
crates/tranquil-pds/src/lib.rs
··· 653 653 get(oauth::endpoints::oauth_authorization_server), 654 654 ); 655 655 656 - if cfg!(feature = "frontend") {} 657 - 658 656 let router = Router::new() 659 657 .nest_service("/xrpc", xrpc_service) 660 658 .nest("/oauth", oauth_router) ··· 716 714 717 715 let spa_router = Router::new().fallback_service(ServeFile::new(&index_path)); 718 716 719 - let serve_dir = ServeDir::new(&frontend_dir).not_found_service(ServeFile::new(&index_path)); 717 + let serve_dir = ServeDir::new(frontend_dir).not_found_service(ServeFile::new(&index_path)); 720 718 721 719 return router 722 720 .route(
+5 -5
crates/tranquil-pds/src/oauth/endpoints/authorize.rs
··· 256 256 if let Some(ref login_hint) = request_data.parameters.login_hint { 257 257 tracing::info!(login_hint = %login_hint, "Checking login_hint for delegation"); 258 258 let hostname_for_handles = tranquil_config::get().server.hostname_without_port(); 259 - let normalized = NormalizedLoginIdentifier::normalize(login_hint, &hostname_for_handles); 259 + let normalized = NormalizedLoginIdentifier::normalize(login_hint, hostname_for_handles); 260 260 tracing::info!(normalized = %normalized, "Normalized login_hint"); 261 261 262 262 match state ··· 530 530 }; 531 531 let hostname_for_handles = tranquil_config::get().server.hostname_without_port(); 532 532 let normalized_username = 533 - NormalizedLoginIdentifier::normalize(&form.username, &hostname_for_handles); 533 + NormalizedLoginIdentifier::normalize(&form.username, hostname_for_handles); 534 534 tracing::debug!( 535 535 original_username = %form.username, 536 536 normalized_username = %normalized_username, ··· 2102 2102 ) -> Response { 2103 2103 let hostname_for_handles = tranquil_config::get().server.hostname_without_port(); 2104 2104 let bare_identifier = 2105 - BareLoginIdentifier::from_identifier(&query.identifier, &hostname_for_handles); 2105 + BareLoginIdentifier::from_identifier(&query.identifier, hostname_for_handles); 2106 2106 2107 2107 let user = state 2108 2108 .user_repo ··· 2134 2134 ) -> Response { 2135 2135 let hostname_for_handles = tranquil_config::get().server.hostname_without_port(); 2136 2136 let normalized_identifier = 2137 - NormalizedLoginIdentifier::normalize(&query.identifier, &hostname_for_handles); 2137 + NormalizedLoginIdentifier::normalize(&query.identifier, hostname_for_handles); 2138 2138 2139 2139 let user = state 2140 2140 .user_repo ··· 2242 2242 2243 2243 let hostname_for_handles = tranquil_config::get().server.hostname_without_port(); 2244 2244 let normalized_username = 2245 - NormalizedLoginIdentifier::normalize(&form.identifier, &hostname_for_handles); 2245 + NormalizedLoginIdentifier::normalize(&form.identifier, hostname_for_handles); 2246 2246 2247 2247 let user = match state 2248 2248 .user_repo
+4 -4
crates/tranquil-pds/src/sso/endpoints.rs
··· 774 774 }; 775 775 776 776 let available_domains = tranquil_config::get().server.available_user_domain_list(); 777 - if let Some(ref d) = query.domain { 778 - if !available_domains.iter().any(|ad| ad == d) { 779 - return Err(ApiError::InvalidRequest("Unknown user domain".into())); 780 - } 777 + if let Some(ref d) = query.domain 778 + && !available_domains.iter().any(|ad| ad == d) 779 + { 780 + return Err(ApiError::InvalidRequest("Unknown user domain".into())); 781 781 } 782 782 let domain = query.domain.as_deref().unwrap_or(&available_domains[0]); 783 783 let full_handle = format!("{}.{}", validated, domain);
+1 -1
crates/tranquil-pds/src/state.rs
··· 224 224 .acquire_timeout(std::time::Duration::from_secs(acquire_timeout_secs)) 225 225 .idle_timeout(std::time::Duration::from_secs(300)) 226 226 .max_lifetime(std::time::Duration::from_secs(1800)) 227 - .connect(&database_url) 227 + .connect(database_url) 228 228 .await 229 229 .map_err(|e| format!("Failed to connect to Postgres: {}", e))?; 230 230
+25 -26
crates/tranquil-pds/src/validation/mod.rs
··· 152 152 check_string_field(obj, "description")?; 153 153 } 154 154 "app.bsky.feed.generator" => { 155 - if let Some(rkey) = rkey { 156 - if crate::moderation::has_explicit_slur(rkey) { 157 - return Err(ValidationError::BannedContent { 158 - path: "rkey".to_string(), 159 - }); 160 - } 155 + if let Some(rkey) = rkey 156 + && crate::moderation::has_explicit_slur(rkey) 157 + { 158 + return Err(ValidationError::BannedContent { 159 + path: "rkey".to_string(), 160 + }); 161 161 } 162 162 check_string_field(obj, "displayName")?; 163 163 } ··· 169 169 fn check_post_banned_content(obj: &serde_json::Map<String, Value>) -> Result<(), ValidationError> { 170 170 if let Some(tags) = obj.get("tags").and_then(|v| v.as_array()) { 171 171 tags.iter().enumerate().try_for_each(|(i, tag)| { 172 - if let Some(tag_str) = tag.as_str() { 173 - if crate::moderation::has_explicit_slur(tag_str) { 174 - return Err(ValidationError::BannedContent { 175 - path: format!("tags/{}", i), 176 - }); 177 - } 172 + if let Some(tag_str) = tag.as_str() 173 + && crate::moderation::has_explicit_slur(tag_str) 174 + { 175 + return Err(ValidationError::BannedContent { 176 + path: format!("tags/{}", i), 177 + }); 178 178 } 179 179 Ok(()) 180 180 })?; ··· 187 187 .get("$type") 188 188 .and_then(|v| v.as_str()) 189 189 .is_some_and(|t| t == "app.bsky.richtext.facet#tag"); 190 - if is_tag { 191 - if let Some(tag) = feature.get("tag").and_then(|v| v.as_str()) { 192 - if crate::moderation::has_explicit_slur(tag) { 193 - return Err(ValidationError::BannedContent { 194 - path: format!("facets/{}/features/{}/tag", i, j), 195 - }); 196 - } 197 - } 190 + if is_tag 191 + && let Some(tag) = feature.get("tag").and_then(|v| v.as_str()) 192 + && crate::moderation::has_explicit_slur(tag) 193 + { 194 + return Err(ValidationError::BannedContent { 195 + path: format!("facets/{}/features/{}/tag", i, j), 196 + }); 198 197 } 199 198 Ok(()) 200 199 })?; ··· 209 208 obj: &serde_json::Map<String, Value>, 210 209 field: &str, 211 210 ) -> Result<(), ValidationError> { 212 - if let Some(value) = obj.get(field).and_then(|v| v.as_str()) { 213 - if crate::moderation::has_explicit_slur(value) { 214 - return Err(ValidationError::BannedContent { 215 - path: field.to_string(), 216 - }); 217 - } 211 + if let Some(value) = obj.get(field).and_then(|v| v.as_str()) 212 + && crate::moderation::has_explicit_slur(value) 213 + { 214 + return Err(ValidationError::BannedContent { 215 + path: field.to_string(), 216 + }); 218 217 } 219 218 Ok(()) 220 219 }