this repo has no description
at main 8.8 kB view raw
1use crate::api::EmptyResponse; 2use crate::api::error::ApiError; 3use crate::auth::BearerAuth; 4use crate::delegation::{self, DelegationActionType}; 5use crate::state::{AppState, RateLimitKind}; 6use crate::util::get_user_id_by_did; 7use axum::{ 8 Json, 9 extract::State, 10 http::HeaderMap, 11 response::{IntoResponse, Response}, 12}; 13use serde::{Deserialize, Serialize}; 14use serde_json::json; 15use tracing::{error, warn}; 16 17#[derive(Serialize)] 18#[serde(rename_all = "camelCase")] 19pub struct AppPassword { 20 pub name: String, 21 pub created_at: String, 22 pub privileged: bool, 23 #[serde(skip_serializing_if = "Option::is_none")] 24 pub scopes: Option<String>, 25 #[serde(skip_serializing_if = "Option::is_none")] 26 pub created_by_controller: Option<String>, 27} 28 29#[derive(Serialize)] 30pub struct ListAppPasswordsOutput { 31 pub passwords: Vec<AppPassword>, 32} 33 34pub async fn list_app_passwords( 35 State(state): State<AppState>, 36 BearerAuth(auth_user): BearerAuth, 37) -> Response { 38 let user_id = match get_user_id_by_did(&state.db, &auth_user.did).await { 39 Ok(id) => id, 40 Err(e) => return ApiError::from(e).into_response(), 41 }; 42 match sqlx::query!( 43 "SELECT name, created_at, privileged, scopes, created_by_controller_did FROM app_passwords WHERE user_id = $1 ORDER BY created_at DESC", 44 user_id 45 ) 46 .fetch_all(&state.db) 47 .await 48 { 49 Ok(rows) => { 50 let passwords: Vec<AppPassword> = rows 51 .iter() 52 .map(|row| AppPassword { 53 name: row.name.clone(), 54 created_at: row.created_at.to_rfc3339(), 55 privileged: row.privileged, 56 scopes: row.scopes.clone(), 57 created_by_controller: row.created_by_controller_did.clone(), 58 }) 59 .collect(); 60 Json(ListAppPasswordsOutput { passwords }).into_response() 61 } 62 Err(e) => { 63 error!("DB error listing app passwords: {:?}", e); 64 ApiError::InternalError(None).into_response() 65 } 66 } 67} 68 69#[derive(Deserialize)] 70pub struct CreateAppPasswordInput { 71 pub name: String, 72 pub privileged: Option<bool>, 73 pub scopes: Option<String>, 74} 75 76#[derive(Serialize)] 77#[serde(rename_all = "camelCase")] 78pub struct CreateAppPasswordOutput { 79 pub name: String, 80 pub password: String, 81 pub created_at: String, 82 pub privileged: bool, 83 #[serde(skip_serializing_if = "Option::is_none")] 84 pub scopes: Option<String>, 85} 86 87pub async fn create_app_password( 88 State(state): State<AppState>, 89 headers: HeaderMap, 90 BearerAuth(auth_user): BearerAuth, 91 Json(input): Json<CreateAppPasswordInput>, 92) -> Response { 93 let client_ip = crate::rate_limit::extract_client_ip(&headers, None); 94 if !state 95 .check_rate_limit(RateLimitKind::AppPassword, &client_ip) 96 .await 97 { 98 warn!(ip = %client_ip, "App password creation rate limit exceeded"); 99 return ApiError::RateLimitExceeded(None).into_response(); 100 } 101 let user_id = match get_user_id_by_did(&state.db, &auth_user.did).await { 102 Ok(id) => id, 103 Err(e) => return ApiError::from(e).into_response(), 104 }; 105 let name = input.name.trim(); 106 if name.is_empty() { 107 return ApiError::InvalidRequest("name is required".into()).into_response(); 108 } 109 let existing = sqlx::query!( 110 "SELECT id FROM app_passwords WHERE user_id = $1 AND name = $2", 111 user_id, 112 name 113 ) 114 .fetch_optional(&state.db) 115 .await; 116 if let Ok(Some(_)) = existing { 117 return ApiError::DuplicateAppPassword.into_response(); 118 } 119 120 let (final_scopes, controller_did) = if let Some(ref controller) = auth_user.controller_did { 121 let grant = delegation::get_delegation(&state.db, &auth_user.did, controller) 122 .await 123 .ok() 124 .flatten(); 125 let granted_scopes = grant.map(|g| g.granted_scopes).unwrap_or_default(); 126 127 let requested = input.scopes.as_deref().unwrap_or("atproto"); 128 let intersected = delegation::intersect_scopes(requested, &granted_scopes); 129 130 if intersected.is_empty() && !granted_scopes.is_empty() { 131 return ApiError::InsufficientScope(None).into_response(); 132 } 133 134 let scope_result = if intersected.is_empty() { 135 None 136 } else { 137 Some(intersected) 138 }; 139 (scope_result, Some(controller.clone())) 140 } else { 141 (input.scopes.clone(), None) 142 }; 143 144 let password: String = (0..4) 145 .map(|_| { 146 use rand::Rng; 147 let mut rng = rand::thread_rng(); 148 let chars: Vec<char> = "abcdefghijklmnopqrstuvwxyz234567".chars().collect(); 149 (0..4) 150 .map(|_| chars[rng.gen_range(0..chars.len())]) 151 .collect::<String>() 152 }) 153 .collect::<Vec<String>>() 154 .join("-"); 155 let password_clone = password.clone(); 156 let password_hash = match tokio::task::spawn_blocking(move || { 157 bcrypt::hash(&password_clone, bcrypt::DEFAULT_COST) 158 }) 159 .await 160 { 161 Ok(Ok(h)) => h, 162 Ok(Err(e)) => { 163 error!("Failed to hash password: {:?}", e); 164 return ApiError::InternalError(None).into_response(); 165 } 166 Err(e) => { 167 error!("Failed to spawn blocking task: {:?}", e); 168 return ApiError::InternalError(None).into_response(); 169 } 170 }; 171 let privileged = input.privileged.unwrap_or(false); 172 let created_at = chrono::Utc::now(); 173 match sqlx::query!( 174 "INSERT INTO app_passwords (user_id, name, password_hash, created_at, privileged, scopes, created_by_controller_did) VALUES ($1, $2, $3, $4, $5, $6, $7)", 175 user_id, 176 name, 177 password_hash, 178 created_at, 179 privileged, 180 final_scopes, 181 controller_did.as_deref() 182 ) 183 .execute(&state.db) 184 .await 185 { 186 Ok(_) => { 187 if let Some(ref controller) = controller_did { 188 let _ = delegation::log_delegation_action( 189 &state.db, 190 &auth_user.did, 191 controller, 192 Some(controller), 193 DelegationActionType::AccountAction, 194 Some(json!({ 195 "action": "create_app_password", 196 "name": name, 197 "scopes": final_scopes 198 })), 199 None, 200 None, 201 ) 202 .await; 203 } 204 Json(CreateAppPasswordOutput { 205 name: name.to_string(), 206 password, 207 created_at: created_at.to_rfc3339(), 208 privileged, 209 scopes: final_scopes, 210 }) 211 .into_response() 212 } 213 Err(e) => { 214 error!("DB error creating app password: {:?}", e); 215 ApiError::InternalError(None).into_response() 216 } 217 } 218} 219 220#[derive(Deserialize)] 221pub struct RevokeAppPasswordInput { 222 pub name: String, 223} 224 225pub async fn revoke_app_password( 226 State(state): State<AppState>, 227 BearerAuth(auth_user): BearerAuth, 228 Json(input): Json<RevokeAppPasswordInput>, 229) -> Response { 230 let user_id = match get_user_id_by_did(&state.db, &auth_user.did).await { 231 Ok(id) => id, 232 Err(e) => return ApiError::from(e).into_response(), 233 }; 234 let name = input.name.trim(); 235 if name.is_empty() { 236 return ApiError::InvalidRequest("name is required".into()).into_response(); 237 } 238 let sessions_to_invalidate = sqlx::query_scalar!( 239 "SELECT access_jti FROM session_tokens WHERE did = $1 AND app_password_name = $2", 240 &auth_user.did, 241 name 242 ) 243 .fetch_all(&state.db) 244 .await 245 .unwrap_or_default(); 246 if let Err(e) = sqlx::query!( 247 "DELETE FROM session_tokens WHERE did = $1 AND app_password_name = $2", 248 &auth_user.did, 249 name 250 ) 251 .execute(&state.db) 252 .await 253 { 254 error!("DB error revoking sessions for app password: {:?}", e); 255 return ApiError::InternalError(None).into_response(); 256 } 257 futures::future::join_all(sessions_to_invalidate.iter().map(|jti| { 258 let cache_key = format!("auth:session:{}:{}", &auth_user.did, jti); 259 let cache = state.cache.clone(); 260 async move { 261 let _ = cache.delete(&cache_key).await; 262 } 263 })) 264 .await; 265 if let Err(e) = sqlx::query!( 266 "DELETE FROM app_passwords WHERE user_id = $1 AND name = $2", 267 user_id, 268 name 269 ) 270 .execute(&state.db) 271 .await 272 { 273 error!("DB error revoking app password: {:?}", e); 274 return ApiError::InternalError(None).into_response(); 275 } 276 EmptyResponse::ok().into_response() 277}