Our Personal Data Server from scratch! tranquil.farm
oauth atproto pds rust postgresql objectstorage fun

fix: oauth consolidation, include-scope improvements #4

merged opened by lewis.moe targeting main from fix/oauth-on-niche-apps
  • auth extraction should be happening in the auth crate, yes, who coulda thought
  • include: scope should actually be doing the right thing and going out and requesting stuff to expand out the perms
  • more tests!!!1!
  • more correct parsing of the #bsky-appview or whatever suffixes on did webs that come through auth
Labels

None yet.

assignee
Participants 2
AT URI
at://did:plc:3fwecdnvtcscjnrx2p4n7alz/sh.tangled.repo.pull/3md3bluniqt22
+829 -716
Interdiff #2 โ†’ #3
.sqlx/query-06eb7c6e1983b6121526ba63612236391290c2e63d37d2bb1cd89ea822950a82.json

This file has not been changed.

.sqlx/query-5031b96c65078d6c54954ce6e57ff9cbba4c48dd8a7546882ab5647114ffab4a.json

This file has not been changed.

.sqlx/query-6258398accee69e0c5f455a3c0ecc273b3da6ef5bb4d8660adafe63d8e3cd2d4.json

This file has not been changed.

.sqlx/query-a4dc8fb22bd094d414c55b9da20b610f7b122b485ab0fd0d0646d68ae8e64fe6.json

This file has not been changed.

.sqlx/query-dec3a21a8e60cc8d2c5dad727750bc88f5535dedae244f7b6e4afa95769b8f1a.json

This file has not been changed.

Cargo.lock

This file has not been changed.

+7 -21
crates/tranquil-pds/src/api/actor/preferences.rs
··· 1 1 use crate::api::error::ApiError; 2 - use crate::auth::RequiredAuth; 2 + use crate::auth::{Active, Auth}; 3 3 use crate::state::AppState; 4 4 use axum::{ 5 5 Json, ··· 32 32 pub struct GetPreferencesOutput { 33 33 pub preferences: Vec<Value>, 34 34 } 35 - pub async fn get_preferences(State(state): State<AppState>, auth: RequiredAuth) -> Response { 36 - let user = match auth.0.require_user() { 37 - Ok(u) => u, 38 - Err(e) => return e.into_response(), 39 - }; 40 - if let Err(e) = user.require_not_takendown() { 41 - return e.into_response(); 42 - } 43 - let has_full_access = user.permissions().has_full_access(); 44 - let user_id: uuid::Uuid = match state.user_repo.get_id_by_did(&user.did).await { 35 + pub async fn get_preferences(State(state): State<AppState>, auth: Auth<Active>) -> Response { 36 + let has_full_access = auth.permissions().has_full_access(); 37 + let user_id: uuid::Uuid = match state.user_repo.get_id_by_did(&auth.did).await { 45 38 Ok(Some(id)) => id, 46 39 _ => { 47 40 return ApiError::InternalError(Some("User not found".into())).into_response(); ··· 96 89 } 97 90 pub async fn put_preferences( 98 91 State(state): State<AppState>, 99 - auth: RequiredAuth, 92 + auth: Auth<Active>, 100 93 Json(input): Json<PutPreferencesInput>, 101 94 ) -> Response { 102 - let user = match auth.0.require_user() { 103 - Ok(u) => u, 104 - Err(e) => return e.into_response(), 105 - }; 106 - if let Err(e) = user.require_not_takendown() { 107 - return e.into_response(); 108 - } 109 - let has_full_access = user.permissions().has_full_access(); 110 - let user_id: uuid::Uuid = match state.user_repo.get_id_by_did(&user.did).await { 95 + let has_full_access = auth.permissions().has_full_access(); 96 + let user_id: uuid::Uuid = match state.user_repo.get_id_by_did(&auth.did).await { 111 97 Ok(Some(id)) => id, 112 98 _ => { 113 99 return ApiError::InternalError(Some("User not found".into())).into_response();
+2 -4
crates/tranquil-pds/src/api/admin/account/delete.rs
··· 1 1 use crate::api::EmptyResponse; 2 2 use crate::api::error::ApiError; 3 - use crate::auth::RequiredAuth; 3 + use crate::auth::{Admin, Auth}; 4 4 use crate::state::AppState; 5 5 use crate::types::Did; 6 6 use axum::{ ··· 18 18 19 19 pub async fn delete_account( 20 20 State(state): State<AppState>, 21 - auth: RequiredAuth, 21 + _auth: Auth<Admin>, 22 22 Json(input): Json<DeleteAccountInput>, 23 23 ) -> Result<Response, ApiError> { 24 - auth.0.require_user()?.require_active()?.require_admin()?; 25 - 26 24 let did = &input.did; 27 25 let (user_id, handle) = state 28 26 .user_repo
+2 -4
crates/tranquil-pds/src/api/admin/account/email.rs
··· 1 1 use crate::api::error::{ApiError, AtpJson}; 2 - use crate::auth::RequiredAuth; 2 + use crate::auth::{Admin, Auth}; 3 3 use crate::state::AppState; 4 4 use crate::types::Did; 5 5 use axum::{ ··· 28 28 29 29 pub async fn send_email( 30 30 State(state): State<AppState>, 31 - auth: RequiredAuth, 31 + _auth: Auth<Admin>, 32 32 AtpJson(input): AtpJson<SendEmailInput>, 33 33 ) -> Result<Response, ApiError> { 34 - auth.0.require_user()?.require_active()?.require_admin()?; 35 - 36 34 let content = input.content.trim(); 37 35 if content.is_empty() { 38 36 return Err(ApiError::InvalidRequest("content is required".into()));
+3 -7
crates/tranquil-pds/src/api/admin/account/info.rs
··· 1 1 use crate::api::error::ApiError; 2 - use crate::auth::RequiredAuth; 2 + use crate::auth::{Admin, Auth}; 3 3 use crate::state::AppState; 4 4 use crate::types::{Did, Handle}; 5 5 use axum::{ ··· 67 67 68 68 pub async fn get_account_info( 69 69 State(state): State<AppState>, 70 - auth: RequiredAuth, 70 + _auth: Auth<Admin>, 71 71 Query(params): Query<GetAccountInfoParams>, 72 72 ) -> Result<Response, ApiError> { 73 - auth.0.require_user()?.require_active()?.require_admin()?; 74 - 75 73 let account = state 76 74 .infra_repo 77 75 .get_admin_account_info_by_did(&params.did) ··· 199 197 200 198 pub async fn get_account_infos( 201 199 State(state): State<AppState>, 202 - auth: RequiredAuth, 200 + _auth: Auth<Admin>, 203 201 RawQuery(raw_query): RawQuery, 204 202 ) -> Result<Response, ApiError> { 205 - auth.0.require_user()?.require_active()?.require_admin()?; 206 - 207 203 let dids: Vec<String> = crate::util::parse_repeated_query_param(raw_query.as_deref(), "dids") 208 204 .into_iter() 209 205 .filter(|d| !d.is_empty())
+2 -4
crates/tranquil-pds/src/api/admin/account/search.rs
··· 1 1 use crate::api::error::ApiError; 2 - use crate::auth::RequiredAuth; 2 + use crate::auth::{Admin, Auth}; 3 3 use crate::state::AppState; 4 4 use crate::types::{Did, Handle}; 5 5 use axum::{ ··· 50 50 51 51 pub async fn search_accounts( 52 52 State(state): State<AppState>, 53 - auth: RequiredAuth, 53 + _auth: Auth<Admin>, 54 54 Query(params): Query<SearchAccountsParams>, 55 55 ) -> Result<Response, ApiError> { 56 - auth.0.require_user()?.require_active()?.require_admin()?; 57 - 58 56 let limit = params.limit.clamp(1, 100); 59 57 let email_filter = params.email.as_deref().map(|e| format!("%{}%", e)); 60 58 let handle_filter = params.handle.as_deref().map(|h| format!("%{}%", h));
+4 -10
crates/tranquil-pds/src/api/admin/account/update.rs
··· 1 1 use crate::api::EmptyResponse; 2 2 use crate::api::error::ApiError; 3 - use crate::auth::RequiredAuth; 3 + use crate::auth::{Admin, Auth}; 4 4 use crate::state::AppState; 5 5 use crate::types::{Did, Handle, PlainPassword}; 6 6 use axum::{ ··· 19 19 20 20 pub async fn update_account_email( 21 21 State(state): State<AppState>, 22 - auth: RequiredAuth, 22 + _auth: Auth<Admin>, 23 23 Json(input): Json<UpdateAccountEmailInput>, 24 24 ) -> Result<Response, ApiError> { 25 - auth.0.require_user()?.require_active()?.require_admin()?; 26 - 27 25 let account = input.account.trim(); 28 26 let email = input.email.trim(); 29 27 if account.is_empty() || email.is_empty() { ··· 57 55 58 56 pub async fn update_account_handle( 59 57 State(state): State<AppState>, 60 - auth: RequiredAuth, 58 + _auth: Auth<Admin>, 61 59 Json(input): Json<UpdateAccountHandleInput>, 62 60 ) -> Result<Response, ApiError> { 63 - auth.0.require_user()?.require_active()?.require_admin()?; 64 - 65 61 let did = &input.did; 66 62 let input_handle = input.handle.trim(); 67 63 if input_handle.is_empty() { ··· 141 137 142 138 pub async fn update_account_password( 143 139 State(state): State<AppState>, 144 - auth: RequiredAuth, 140 + _auth: Auth<Admin>, 145 141 Json(input): Json<UpdateAccountPasswordInput>, 146 142 ) -> Result<Response, ApiError> { 147 - auth.0.require_user()?.require_active()?.require_admin()?; 148 - 149 143 let did = &input.did; 150 144 let password = input.password.trim(); 151 145 if password.is_empty() {
+2 -4
crates/tranquil-pds/src/api/admin/config.rs
··· 1 1 use crate::api::error::ApiError; 2 - use crate::auth::RequiredAuth; 2 + use crate::auth::{Admin, Auth}; 3 3 use crate::state::AppState; 4 4 use axum::{Json, extract::State}; 5 5 use serde::{Deserialize, Serialize}; ··· 78 78 79 79 pub async fn update_server_config( 80 80 State(state): State<AppState>, 81 - auth: RequiredAuth, 81 + _auth: Auth<Admin>, 82 82 Json(req): Json<UpdateServerConfigRequest>, 83 83 ) -> Result<Json<UpdateServerConfigResponse>, ApiError> { 84 - auth.0.require_user()?.require_active()?.require_admin()?; 85 - 86 84 if let Some(server_name) = req.server_name { 87 85 let trimmed = server_name.trim(); 88 86 if trimmed.is_empty() || trimmed.len() > 100 {
+5 -13
crates/tranquil-pds/src/api/admin/invite.rs
··· 1 1 use crate::api::EmptyResponse; 2 2 use crate::api::error::ApiError; 3 - use crate::auth::RequiredAuth; 3 + use crate::auth::{Admin, Auth}; 4 4 use crate::state::AppState; 5 5 use axum::{ 6 6 Json, ··· 21 21 22 22 pub async fn disable_invite_codes( 23 23 State(state): State<AppState>, 24 - auth: RequiredAuth, 24 + _auth: Auth<Admin>, 25 25 Json(input): Json<DisableInviteCodesInput>, 26 26 ) -> Result<Response, ApiError> { 27 - auth.0.require_user()?.require_active()?.require_admin()?; 28 - 29 27 if let Some(codes) = &input.codes 30 28 && let Err(e) = state.infra_repo.disable_invite_codes_by_code(codes).await 31 29 { ··· 80 78 81 79 pub async fn get_invite_codes( 82 80 State(state): State<AppState>, 83 - auth: RequiredAuth, 81 + _auth: Auth<Admin>, 84 82 Query(params): Query<GetInviteCodesParams>, 85 83 ) -> Result<Response, ApiError> { 86 - auth.0.require_user()?.require_active()?.require_admin()?; 87 - 88 84 let limit = params.limit.unwrap_or(100).clamp(1, 500); 89 85 let sort_order = match params.sort.as_deref() { 90 86 Some("usage") => InviteCodeSortOrder::Usage, ··· 173 169 174 170 pub async fn disable_account_invites( 175 171 State(state): State<AppState>, 176 - auth: RequiredAuth, 172 + _auth: Auth<Admin>, 177 173 Json(input): Json<DisableAccountInvitesInput>, 178 174 ) -> Result<Response, ApiError> { 179 - auth.0.require_user()?.require_active()?.require_admin()?; 180 - 181 175 let account = input.account.trim(); 182 176 if account.is_empty() { 183 177 return Err(ApiError::InvalidRequest("account is required".into())); ··· 207 201 208 202 pub async fn enable_account_invites( 209 203 State(state): State<AppState>, 210 - auth: RequiredAuth, 204 + _auth: Auth<Admin>, 211 205 Json(input): Json<EnableAccountInvitesInput>, 212 206 ) -> Result<Response, ApiError> { 213 - auth.0.require_user()?.require_active()?.require_admin()?; 214 - 215 207 let account = input.account.trim(); 216 208 if account.is_empty() { 217 209 return Err(ApiError::InvalidRequest("account is required".into()));
+2 -4
crates/tranquil-pds/src/api/admin/server_stats.rs
··· 1 1 use crate::api::error::ApiError; 2 - use crate::auth::RequiredAuth; 2 + use crate::auth::{Admin, Auth}; 3 3 use crate::state::AppState; 4 4 use axum::{ 5 5 Json, ··· 19 19 20 20 pub async fn get_server_stats( 21 21 State(state): State<AppState>, 22 - auth: RequiredAuth, 22 + _auth: Auth<Admin>, 23 23 ) -> Result<Response, ApiError> { 24 - auth.0.require_user()?.require_active()?.require_admin()?; 25 - 26 24 let user_count = state.user_repo.count_users().await.unwrap_or(0); 27 25 let repo_count = state.repo_repo.count_repos().await.unwrap_or(0); 28 26 let record_count = state.repo_repo.count_all_records().await.unwrap_or(0);
+3 -7
crates/tranquil-pds/src/api/admin/status.rs
··· 1 1 use crate::api::error::ApiError; 2 - use crate::auth::RequiredAuth; 2 + use crate::auth::{Admin, Auth}; 3 3 use crate::state::AppState; 4 4 use crate::types::{CidLink, Did}; 5 5 use axum::{ ··· 35 35 36 36 pub async fn get_subject_status( 37 37 State(state): State<AppState>, 38 - auth: RequiredAuth, 38 + _auth: Auth<Admin>, 39 39 Query(params): Query<GetSubjectStatusParams>, 40 40 ) -> Result<Response, ApiError> { 41 - auth.0.require_user()?.require_active()?.require_admin()?; 42 - 43 41 if params.did.is_none() && params.uri.is_none() && params.blob.is_none() { 44 42 return Err(ApiError::InvalidRequest( 45 43 "Must provide did, uri, or blob".into(), ··· 169 167 170 168 pub async fn update_subject_status( 171 169 State(state): State<AppState>, 172 - auth: RequiredAuth, 170 + _auth: Auth<Admin>, 173 171 Json(input): Json<UpdateSubjectStatusInput>, 174 172 ) -> Result<Response, ApiError> { 175 - auth.0.require_user()?.require_active()?.require_admin()?; 176 - 177 173 let subject_type = input.subject.get("$type").and_then(|t| t.as_str()); 178 174 match subject_type { 179 175 Some("com.atproto.admin.defs#repoRef") => {
+17 -23
crates/tranquil-pds/src/api/backup.rs
··· 1 1 use crate::api::error::ApiError; 2 2 use crate::api::{EmptyResponse, EnabledResponse}; 3 - use crate::auth::RequiredAuth; 3 + use crate::auth::{Active, Auth}; 4 4 use crate::scheduled::generate_full_backup; 5 5 use crate::state::AppState; 6 6 use crate::storage::{BackupStorage, backup_retention_count}; ··· 37 37 38 38 pub async fn list_backups( 39 39 State(state): State<AppState>, 40 - auth: RequiredAuth, 40 + auth: Auth<Active>, 41 41 ) -> Result<Response, crate::api::error::ApiError> { 42 - let user = auth.0.require_user()?.require_active()?; 43 - let (user_id, backup_enabled) = match state.backup_repo.get_user_backup_status(&user.did).await 42 + let (user_id, backup_enabled) = match state.backup_repo.get_user_backup_status(&auth.did).await 44 43 { 45 44 Ok(Some(status)) => status, 46 45 Ok(None) => { ··· 89 88 90 89 pub async fn get_backup( 91 90 State(state): State<AppState>, 92 - auth: RequiredAuth, 91 + auth: Auth<Active>, 93 92 Query(query): Query<GetBackupQuery>, 94 93 ) -> Result<Response, crate::api::error::ApiError> { 95 - let user = auth.0.require_user()?.require_active()?; 96 94 let backup_id = match uuid::Uuid::parse_str(&query.id) { 97 95 Ok(id) => id, 98 96 Err(_) => { ··· 102 100 103 101 let backup_info = match state 104 102 .backup_repo 105 - .get_backup_storage_info(backup_id, &user.did) 103 + .get_backup_storage_info(backup_id, &auth.did) 106 104 .await 107 105 { 108 106 Ok(Some(b)) => b, ··· 157 155 158 156 pub async fn create_backup( 159 157 State(state): State<AppState>, 160 - auth: RequiredAuth, 158 + auth: Auth<Active>, 161 159 ) -> Result<Response, crate::api::error::ApiError> { 162 - let auth_user = auth.0.require_user()?.require_active()?; 163 160 let backup_storage = match state.backup_storage.as_ref() { 164 161 Some(storage) => storage, 165 162 None => { ··· 167 164 } 168 165 }; 169 166 170 - let user = match state.backup_repo.get_user_for_backup(&auth_user.did).await { 167 + let user = match state.backup_repo.get_user_for_backup(&auth.did).await { 171 168 Ok(Some(u)) => u, 172 169 Ok(None) => { 173 170 return Ok(ApiError::AccountNotFound.into_response()); ··· 327 324 328 325 pub async fn delete_backup( 329 326 State(state): State<AppState>, 330 - auth: RequiredAuth, 327 + auth: Auth<Active>, 331 328 Query(query): Query<DeleteBackupQuery>, 332 329 ) -> Result<Response, crate::api::error::ApiError> { 333 - let user = auth.0.require_user()?.require_active()?; 334 330 let backup_id = match uuid::Uuid::parse_str(&query.id) { 335 331 Ok(id) => id, 336 332 Err(_) => { ··· 340 336 341 337 let backup = match state 342 338 .backup_repo 343 - .get_backup_for_deletion(backup_id, &user.did) 339 + .get_backup_for_deletion(backup_id, &auth.did) 344 340 .await 345 341 { 346 342 Ok(Some(b)) => b, ··· 372 368 return Ok(ApiError::InternalError(Some("Failed to delete backup".into())).into_response()); 373 369 } 374 370 375 - info!(did = %user.did, backup_id = %backup_id, "Deleted backup"); 371 + info!(did = %auth.did, backup_id = %backup_id, "Deleted backup"); 376 372 377 373 Ok(EmptyResponse::ok().into_response()) 378 374 } ··· 385 381 386 382 pub async fn set_backup_enabled( 387 383 State(state): State<AppState>, 388 - auth: RequiredAuth, 384 + auth: Auth<Active>, 389 385 Json(input): Json<SetBackupEnabledInput>, 390 386 ) -> Result<Response, crate::api::error::ApiError> { 391 - let user = auth.0.require_user()?.require_active()?; 392 387 let deactivated_at = match state 393 388 .backup_repo 394 - .get_user_deactivated_status(&user.did) 389 + .get_user_deactivated_status(&auth.did) 395 390 .await 396 391 { 397 392 Ok(Some(status)) => status, ··· 410 405 411 406 if let Err(e) = state 412 407 .backup_repo 413 - .update_backup_enabled(&user.did, input.enabled) 408 + .update_backup_enabled(&auth.did, input.enabled) 414 409 .await 415 410 { 416 411 error!("DB error updating backup_enabled: {:?}", e); ··· 419 414 ); 420 415 } 421 416 422 - info!(did = %user.did, enabled = input.enabled, "Updated backup_enabled setting"); 417 + info!(did = %auth.did, enabled = input.enabled, "Updated backup_enabled setting"); 423 418 424 419 Ok(EnabledResponse::response(input.enabled).into_response()) 425 420 } 426 421 427 422 pub async fn export_blobs( 428 423 State(state): State<AppState>, 429 - auth: RequiredAuth, 424 + auth: Auth<Active>, 430 425 ) -> Result<Response, crate::api::error::ApiError> { 431 - let user = auth.0.require_user()?.require_active()?; 432 - let user_id = match state.backup_repo.get_user_id_by_did(&user.did).await { 426 + let user_id = match state.backup_repo.get_user_id_by_did(&auth.did).await { 433 427 Ok(Some(id)) => id, 434 428 Ok(None) => { 435 429 return Ok(ApiError::AccountNotFound.into_response()); ··· 546 540 547 541 let zip_bytes = zip_buffer.into_inner(); 548 542 549 - info!(did = %user.did, blob_count = blobs.len(), size_bytes = zip_bytes.len(), "Exported blobs"); 543 + info!(did = %auth.did, blob_count = blobs.len(), size_bytes = zip_bytes.len(), "Exported blobs"); 550 544 551 545 Ok(( 552 546 StatusCode::OK,
+31 -38
crates/tranquil-pds/src/api/delegation.rs
··· 1 1 use crate::api::error::ApiError; 2 2 use crate::api::repo::record::utils::create_signed_commit; 3 - use crate::auth::RequiredAuth; 3 + use crate::auth::{Active, Auth}; 4 4 use crate::delegation::{DelegationActionType, SCOPE_PRESETS, scopes}; 5 5 use crate::state::{AppState, RateLimitKind}; 6 6 use crate::types::{Did, Handle, Nsid, Rkey}; ··· 35 35 36 36 pub async fn list_controllers( 37 37 State(state): State<AppState>, 38 - auth: RequiredAuth, 38 + auth: Auth<Active>, 39 39 ) -> Result<Response, ApiError> { 40 - let user = auth.0.require_user()?.require_active()?; 41 40 let controllers = match state 42 41 .delegation_repo 43 - .get_delegations_for_account(&user.did) 42 + .get_delegations_for_account(&auth.did) 44 43 .await 45 44 { 46 45 Ok(c) => c, ··· 75 74 76 75 pub async fn add_controller( 77 76 State(state): State<AppState>, 78 - auth: RequiredAuth, 77 + auth: Auth<Active>, 79 78 Json(input): Json<AddControllerInput>, 80 79 ) -> Result<Response, ApiError> { 81 - let user = auth.0.require_user()?.require_active()?; 82 80 if let Err(e) = scopes::validate_delegation_scopes(&input.granted_scopes) { 83 81 return Ok(ApiError::InvalidScopes(e).into_response()); 84 82 } ··· 95 93 return Ok(ApiError::ControllerNotFound.into_response()); 96 94 } 97 95 98 - match state.delegation_repo.controls_any_accounts(&user.did).await { 96 + match state.delegation_repo.controls_any_accounts(&auth.did).await { 99 97 Ok(true) => { 100 98 return Ok(ApiError::InvalidDelegation( 101 99 "Cannot add controllers to an account that controls other accounts".into(), ··· 136 134 match state 137 135 .delegation_repo 138 136 .create_delegation( 139 - &user.did, 137 + &auth.did, 140 138 &input.controller_did, 141 139 &input.granted_scopes, 142 - &user.did, 140 + &auth.did, 143 141 ) 144 142 .await 145 143 { ··· 147 145 let _ = state 148 146 .delegation_repo 149 147 .log_delegation_action( 150 - &user.did, 151 - &user.did, 148 + &auth.did, 149 + &auth.did, 152 150 Some(&input.controller_did), 153 151 DelegationActionType::GrantCreated, 154 152 Some(serde_json::json!({ ··· 181 179 182 180 pub async fn remove_controller( 183 181 State(state): State<AppState>, 184 - auth: RequiredAuth, 182 + auth: Auth<Active>, 185 183 Json(input): Json<RemoveControllerInput>, 186 184 ) -> Result<Response, ApiError> { 187 - let user = auth.0.require_user()?.require_active()?; 188 185 match state 189 186 .delegation_repo 190 - .revoke_delegation(&user.did, &input.controller_did, &user.did) 187 + .revoke_delegation(&auth.did, &input.controller_did, &auth.did) 191 188 .await 192 189 { 193 190 Ok(true) => { 194 191 let revoked_app_passwords = state 195 192 .session_repo 196 - .delete_app_passwords_by_controller(&user.did, &input.controller_did) 193 + .delete_app_passwords_by_controller(&auth.did, &input.controller_did) 197 194 .await 198 195 .unwrap_or(0) as usize; 199 196 200 197 let revoked_oauth_tokens = state 201 198 .oauth_repo 202 - .revoke_tokens_for_controller(&user.did, &input.controller_did) 199 + .revoke_tokens_for_controller(&auth.did, &input.controller_did) 203 200 .await 204 201 .unwrap_or(0); 205 202 206 203 let _ = state 207 204 .delegation_repo 208 205 .log_delegation_action( 209 - &user.did, 210 - &user.did, 206 + &auth.did, 207 + &auth.did, 211 208 Some(&input.controller_did), 212 209 DelegationActionType::GrantRevoked, 213 210 Some(serde_json::json!({ ··· 243 240 244 241 pub async fn update_controller_scopes( 245 242 State(state): State<AppState>, 246 - auth: RequiredAuth, 243 + auth: Auth<Active>, 247 244 Json(input): Json<UpdateControllerScopesInput>, 248 245 ) -> Result<Response, ApiError> { 249 - let user = auth.0.require_user()?.require_active()?; 250 246 if let Err(e) = scopes::validate_delegation_scopes(&input.granted_scopes) { 251 247 return Ok(ApiError::InvalidScopes(e).into_response()); 252 248 } 253 249 254 250 match state 255 251 .delegation_repo 256 - .update_delegation_scopes(&user.did, &input.controller_did, &input.granted_scopes) 252 + .update_delegation_scopes(&auth.did, &input.controller_did, &input.granted_scopes) 257 253 .await 258 254 { 259 255 Ok(true) => { 260 256 let _ = state 261 257 .delegation_repo 262 258 .log_delegation_action( 263 - &user.did, 264 - &user.did, 259 + &auth.did, 260 + &auth.did, 265 261 Some(&input.controller_did), 266 262 DelegationActionType::ScopesModified, 267 263 Some(serde_json::json!({ ··· 307 303 308 304 pub async fn list_controlled_accounts( 309 305 State(state): State<AppState>, 310 - auth: RequiredAuth, 306 + auth: Auth<Active>, 311 307 ) -> Result<Response, ApiError> { 312 - let user = auth.0.require_user()?.require_active()?; 313 308 let accounts = match state 314 309 .delegation_repo 315 - .get_accounts_controlled_by(&user.did) 310 + .get_accounts_controlled_by(&auth.did) 316 311 .await 317 312 { 318 313 Ok(a) => a, ··· 371 366 372 367 pub async fn get_audit_log( 373 368 State(state): State<AppState>, 374 - auth: RequiredAuth, 369 + auth: Auth<Active>, 375 370 Query(params): Query<AuditLogParams>, 376 371 ) -> Result<Response, ApiError> { 377 - let user = auth.0.require_user()?.require_active()?; 378 372 let limit = params.limit.clamp(1, 100); 379 373 let offset = params.offset.max(0); 380 374 381 375 let entries = match state 382 376 .delegation_repo 383 - .get_audit_log_for_account(&user.did, limit, offset) 377 + .get_audit_log_for_account(&auth.did, limit, offset) 384 378 .await 385 379 { 386 380 Ok(e) => e, ··· 394 388 395 389 let total = state 396 390 .delegation_repo 397 - .count_audit_log_entries(&user.did) 391 + .count_audit_log_entries(&auth.did) 398 392 .await 399 393 .unwrap_or_default(); 400 394 ··· 463 457 pub async fn create_delegated_account( 464 458 State(state): State<AppState>, 465 459 headers: HeaderMap, 466 - auth: RequiredAuth, 460 + auth: Auth<Active>, 467 461 Json(input): Json<CreateDelegatedAccountInput>, 468 462 ) -> Result<Response, ApiError> { 469 - let user = auth.0.require_user()?.require_active()?; 470 463 let client_ip = extract_client_ip(&headers); 471 464 if !state 472 465 .check_rate_limit(RateLimitKind::AccountCreation, &client_ip) ··· 483 476 return Ok(ApiError::InvalidScopes(e).into_response()); 484 477 } 485 478 486 - match state.delegation_repo.has_any_controllers(&user.did).await { 479 + match state.delegation_repo.has_any_controllers(&auth.did).await { 487 480 Ok(true) => { 488 481 return Ok(ApiError::InvalidDelegation( 489 482 "Cannot create delegated accounts from a controlled account".into(), ··· 602 595 603 596 let did = Did::new_unchecked(&genesis_result.did); 604 597 let handle = Handle::new_unchecked(&handle); 605 - info!(did = %did, handle = %handle, controller = %&user.did, "Created DID for delegated account"); 598 + info!(did = %did, handle = %handle, controller = %&auth.did, "Created DID for delegated account"); 606 599 607 600 let encrypted_key_bytes = match crate::config::encrypt_key(&secret_key_bytes) { 608 601 Ok(bytes) => bytes, ··· 642 635 handle: handle.clone(), 643 636 email: email.clone(), 644 637 did: did.clone(), 645 - controller_did: user.did.clone(), 638 + controller_did: auth.did.clone(), 646 639 controller_scopes: input.controller_scopes.clone(), 647 640 encrypted_key_bytes, 648 641 encryption_version: crate::config::ENCRYPTION_VERSION, ··· 702 695 .delegation_repo 703 696 .log_delegation_action( 704 697 &did, 705 - &user.did, 706 - Some(&user.did), 698 + &auth.did, 699 + Some(&auth.did), 707 700 DelegationActionType::GrantCreated, 708 701 Some(json!({ 709 702 "account_created": true, ··· 714 707 ) 715 708 .await; 716 709 717 - info!(did = %did, handle = %handle, controller = %&user.did, "Delegated account created"); 710 + info!(did = %did, handle = %handle, controller = %&auth.did, "Delegated account created"); 718 711 719 712 Ok(Json(CreateDelegatedAccountResponse { did, handle }).into_response()) 720 713 }
+7
crates/tranquil-pds/src/api/error.rs
··· 543 543 crate::auth::extractor::AuthError::AccountDeactivated => Self::AccountDeactivated, 544 544 crate::auth::extractor::AuthError::AccountTakedown => Self::AccountTakedown, 545 545 crate::auth::extractor::AuthError::AdminRequired => Self::AdminRequired, 546 + crate::auth::extractor::AuthError::ServiceAuthNotAllowed => Self::AuthenticationFailed( 547 + Some("Service authentication not allowed for this endpoint".to_string()), 548 + ), 549 + crate::auth::extractor::AuthError::SigningKeyRequired => Self::InvalidSigningKey, 550 + crate::auth::extractor::AuthError::InsufficientScope(msg) => { 551 + Self::InsufficientScope(Some(msg)) 552 + } 546 553 crate::auth::extractor::AuthError::OAuthExpiredToken(msg) => { 547 554 Self::OAuthExpiredToken(Some(msg)) 548 555 }
crates/tranquil-pds/src/api/identity/account.rs

This file has not been changed.

+9 -11
crates/tranquil-pds/src/api/identity/did.rs
··· 1 1 use crate::api::{ApiError, DidResponse, EmptyResponse}; 2 - use crate::auth::RequiredAuth; 2 + use crate::auth::{Auth, NotTakendown}; 3 3 use crate::plc::signing_key_to_did_key; 4 4 use crate::state::AppState; 5 5 use crate::types::Handle; ··· 518 518 519 519 pub async fn get_recommended_did_credentials( 520 520 State(state): State<AppState>, 521 - auth: RequiredAuth, 521 + auth: Auth<NotTakendown>, 522 522 ) -> Result<Response, ApiError> { 523 - let auth_user = auth.0.require_user()?.require_not_takendown()?; 524 523 let handle = state 525 524 .user_repo 526 - .get_handle_by_did(&auth_user.did) 525 + .get_handle_by_did(&auth.did) 527 526 .await 528 527 .map_err(|_| ApiError::InternalError(None))? 529 528 .ok_or(ApiError::InternalError(None))?; 530 529 531 - let key_bytes = auth_user.key_bytes.clone().ok_or_else(|| { 530 + let key_bytes = auth.key_bytes.clone().ok_or_else(|| { 532 531 ApiError::AuthenticationFailed(Some("OAuth tokens cannot get DID credentials".into())) 533 532 })?; 534 533 ··· 537 536 let signing_key = k256::ecdsa::SigningKey::from_slice(&key_bytes) 538 537 .map_err(|_| ApiError::InternalError(None))?; 539 538 let did_key = signing_key_to_did_key(&signing_key); 540 - let rotation_keys = if auth_user.did.starts_with("did:web:") { 539 + let rotation_keys = if auth.did.starts_with("did:web:") { 541 540 vec![] 542 541 } else { 543 542 let server_rotation_key = match std::env::var("PLC_ROTATION_KEY") { ··· 575 574 576 575 pub async fn update_handle( 577 576 State(state): State<AppState>, 578 - auth: RequiredAuth, 577 + auth: Auth<NotTakendown>, 579 578 Json(input): Json<UpdateHandleInput>, 580 579 ) -> Result<Response, ApiError> { 581 - let auth_user = auth.0.require_user()?.require_not_takendown()?; 582 580 if let Err(e) = crate::auth::scope_check::check_identity_scope( 583 - auth_user.is_oauth, 584 - auth_user.scope.as_deref(), 581 + auth.is_oauth(), 582 + auth.scope.as_deref(), 585 583 crate::oauth::scopes::IdentityAttr::Handle, 586 584 ) { 587 585 return Ok(e); 588 586 } 589 - let did = auth_user.did.clone(); 587 + let did = auth.did.clone(); 590 588 if !state 591 589 .check_rate_limit(crate::state::RateLimitKind::HandleUpdate, &did) 592 590 .await
+6 -10
crates/tranquil-pds/src/api/identity/plc/request.rs
··· 1 1 use crate::api::EmptyResponse; 2 2 use crate::api::error::ApiError; 3 - use crate::auth::RequiredAuth; 3 + use crate::auth::{Auth, NotTakendown}; 4 4 use crate::state::AppState; 5 5 use axum::{ 6 6 extract::State, ··· 15 15 16 16 pub async fn request_plc_operation_signature( 17 17 State(state): State<AppState>, 18 - auth: RequiredAuth, 18 + auth: Auth<NotTakendown>, 19 19 ) -> Result<Response, ApiError> { 20 - let auth_user = auth.0.require_user()?.require_not_takendown()?; 21 20 if let Err(e) = crate::auth::scope_check::check_identity_scope( 22 - auth_user.is_oauth, 23 - auth_user.scope.as_deref(), 21 + auth.is_oauth(), 22 + auth.scope.as_deref(), 24 23 crate::oauth::scopes::IdentityAttr::Wildcard, 25 24 ) { 26 25 return Ok(e); 27 26 } 28 27 let user_id = state 29 28 .user_repo 30 - .get_id_by_did(&auth_user.did) 29 + .get_id_by_did(&auth.did) 31 30 .await 32 31 .map_err(|e| { 33 32 error!("DB error: {:?}", e); ··· 59 58 { 60 59 warn!("Failed to enqueue PLC operation notification: {:?}", e); 61 60 } 62 - info!( 63 - "PLC operation signature requested for user {}", 64 - auth_user.did 65 - ); 61 + info!("PLC operation signature requested for user {}", auth.did); 66 62 Ok(EmptyResponse::ok().into_response()) 67 63 }
+5 -6
crates/tranquil-pds/src/api/identity/plc/sign.rs
··· 1 1 use crate::api::ApiError; 2 - use crate::auth::RequiredAuth; 2 + use crate::auth::{Auth, NotTakendown}; 3 3 use crate::circuit_breaker::with_circuit_breaker; 4 4 use crate::plc::{PlcClient, PlcError, PlcService, create_update_op, sign_operation}; 5 5 use crate::state::AppState; ··· 40 40 41 41 pub async fn sign_plc_operation( 42 42 State(state): State<AppState>, 43 - auth: RequiredAuth, 43 + auth: Auth<NotTakendown>, 44 44 Json(input): Json<SignPlcOperationInput>, 45 45 ) -> Result<Response, ApiError> { 46 - let auth_user = auth.0.require_user()?.require_not_takendown()?; 47 46 if let Err(e) = crate::auth::scope_check::check_identity_scope( 48 - auth_user.is_oauth, 49 - auth_user.scope.as_deref(), 47 + auth.is_oauth(), 48 + auth.scope.as_deref(), 50 49 crate::oauth::scopes::IdentityAttr::Wildcard, 51 50 ) { 52 51 return Ok(e); 53 52 } 54 - let did = &auth_user.did; 53 + let did = &auth.did; 55 54 if did.starts_with("did:web:") { 56 55 return Err(ApiError::InvalidRequest( 57 56 "PLC operations are only valid for did:plc identities".into(),
+5 -6
crates/tranquil-pds/src/api/identity/plc/submit.rs
··· 1 1 use crate::api::{ApiError, EmptyResponse}; 2 - use crate::auth::RequiredAuth; 2 + use crate::auth::{Auth, NotTakendown}; 3 3 use crate::circuit_breaker::with_circuit_breaker; 4 4 use crate::plc::{PlcClient, signing_key_to_did_key, validate_plc_operation}; 5 5 use crate::state::AppState; ··· 20 20 21 21 pub async fn submit_plc_operation( 22 22 State(state): State<AppState>, 23 - auth: RequiredAuth, 23 + auth: Auth<NotTakendown>, 24 24 Json(input): Json<SubmitPlcOperationInput>, 25 25 ) -> Result<Response, ApiError> { 26 - let auth_user = auth.0.require_user()?.require_not_takendown()?; 27 26 if let Err(e) = crate::auth::scope_check::check_identity_scope( 28 - auth_user.is_oauth, 29 - auth_user.scope.as_deref(), 27 + auth.is_oauth(), 28 + auth.scope.as_deref(), 30 29 crate::oauth::scopes::IdentityAttr::Wildcard, 31 30 ) { 32 31 return Ok(e); 33 32 } 34 - let did = &auth_user.did; 33 + let did = &auth.did; 35 34 if did.starts_with("did:web:") { 36 35 return Err(ApiError::InvalidRequest( 37 36 "PLC operations are only valid for did:plc identities".into(),
+5 -10
crates/tranquil-pds/src/api/moderation/mod.rs
··· 1 1 use crate::api::ApiError; 2 2 use crate::api::proxy_client::{is_ssrf_safe, proxy_client}; 3 - use crate::auth::RequiredAuth; 3 + use crate::auth::{AnyUser, Auth}; 4 4 use crate::state::AppState; 5 5 use axum::{ 6 6 Json, ··· 42 42 43 43 pub async fn create_report( 44 44 State(state): State<AppState>, 45 - auth: RequiredAuth, 45 + auth: Auth<AnyUser>, 46 46 Json(input): Json<CreateReportInput>, 47 47 ) -> Response { 48 - let auth_user = match auth.0.require_user() { 49 - Ok(u) => u, 50 - Err(e) => return e.into_response(), 51 - }; 52 - let did = &auth_user.did; 48 + let did = &auth.did; 53 49 54 50 if let Some((service_url, service_did)) = get_report_service_config() { 55 - return proxy_to_report_service(&state, auth_user, &service_url, &service_did, &input) 56 - .await; 51 + return proxy_to_report_service(&state, &auth, &service_url, &service_did, &input).await; 57 52 } 58 53 59 - create_report_locally(&state, did, auth_user.status.is_takendown(), input).await 54 + create_report_locally(&state, did, auth.status.is_takendown(), input).await 60 55 } 61 56 62 57 async fn proxy_to_report_service(
+20 -25
crates/tranquil-pds/src/api/notification_prefs.rs
··· 1 1 use crate::api::error::ApiError; 2 - use crate::auth::RequiredAuth; 2 + use crate::auth::{Active, Auth}; 3 3 use crate::state::AppState; 4 4 use axum::{ 5 5 Json, ··· 25 25 26 26 pub async fn get_notification_prefs( 27 27 State(state): State<AppState>, 28 - auth: RequiredAuth, 28 + auth: Auth<Active>, 29 29 ) -> Result<Response, ApiError> { 30 - let user = auth.0.require_user()?.require_active()?; 31 30 let prefs = state 32 31 .user_repo 33 - .get_notification_prefs(&user.did) 32 + .get_notification_prefs(&auth.did) 34 33 .await 35 34 .map_err(|e| ApiError::InternalError(Some(format!("Database error: {}", e))))? 36 35 .ok_or(ApiError::AccountNotFound)?; ··· 66 65 67 66 pub async fn get_notification_history( 68 67 State(state): State<AppState>, 69 - auth: RequiredAuth, 68 + auth: Auth<Active>, 70 69 ) -> Result<Response, ApiError> { 71 - let user = auth.0.require_user()?.require_active()?; 72 - 73 70 let user_id = state 74 71 .user_repo 75 - .get_id_by_did(&user.did) 72 + .get_id_by_did(&auth.did) 76 73 .await 77 74 .map_err(|e| ApiError::InternalError(Some(format!("Database error: {}", e))))? 78 75 .ok_or(ApiError::AccountNotFound)?; ··· 187 184 188 185 pub async fn update_notification_prefs( 189 186 State(state): State<AppState>, 190 - auth: RequiredAuth, 187 + auth: Auth<Active>, 191 188 Json(input): Json<UpdateNotificationPrefsInput>, 192 189 ) -> Result<Response, ApiError> { 193 - let user = auth.0.require_user()?.require_active()?; 194 - 195 190 let user_row = state 196 191 .user_repo 197 - .get_id_handle_email_by_did(&user.did) 192 + .get_id_handle_email_by_did(&auth.did) 198 193 .await 199 194 .map_err(|e| ApiError::InternalError(Some(format!("Database error: {}", e))))? 200 195 .ok_or(ApiError::AccountNotFound)?; ··· 214 209 } 215 210 state 216 211 .user_repo 217 - .update_preferred_comms_channel(&user.did, channel) 212 + .update_preferred_comms_channel(&auth.did, channel) 218 213 .await 219 214 .map_err(|e| ApiError::InternalError(Some(format!("Database error: {}", e))))?; 220 - info!(did = %user.did, channel = %channel, "Updated preferred notification channel"); 215 + info!(did = %auth.did, channel = %channel, "Updated preferred notification channel"); 221 216 } 222 217 223 218 if let Some(ref new_email) = input.email { ··· 234 229 request_channel_verification( 235 230 &state, 236 231 user_id, 237 - &user.did, 232 + &auth.did, 238 233 "email", 239 234 &email_clean, 240 235 Some(&handle), ··· 242 237 .await 243 238 .map_err(|e| ApiError::InternalError(Some(e)))?; 244 239 verification_required.push("email".to_string()); 245 - info!(did = %user.did, "Requested email verification"); 240 + info!(did = %auth.did, "Requested email verification"); 246 241 } 247 242 } 248 243 ··· 253 248 .clear_discord(user_id) 254 249 .await 255 250 .map_err(|e| ApiError::InternalError(Some(format!("Database error: {}", e))))?; 256 - info!(did = %user.did, "Cleared Discord ID"); 251 + info!(did = %auth.did, "Cleared Discord ID"); 257 252 } else { 258 - request_channel_verification(&state, user_id, &user.did, "discord", discord_id, None) 253 + request_channel_verification(&state, user_id, &auth.did, "discord", discord_id, None) 259 254 .await 260 255 .map_err(|e| ApiError::InternalError(Some(e)))?; 261 256 verification_required.push("discord".to_string()); 262 - info!(did = %user.did, "Requested Discord verification"); 257 + info!(did = %auth.did, "Requested Discord verification"); 263 258 } 264 259 } 265 260 ··· 271 266 .clear_telegram(user_id) 272 267 .await 273 268 .map_err(|e| ApiError::InternalError(Some(format!("Database error: {}", e))))?; 274 - info!(did = %user.did, "Cleared Telegram username"); 269 + info!(did = %auth.did, "Cleared Telegram username"); 275 270 } else { 276 271 request_channel_verification( 277 272 &state, 278 273 user_id, 279 - &user.did, 274 + &auth.did, 280 275 "telegram", 281 276 telegram_clean, 282 277 None, ··· 284 279 .await 285 280 .map_err(|e| ApiError::InternalError(Some(e)))?; 286 281 verification_required.push("telegram".to_string()); 287 - info!(did = %user.did, "Requested Telegram verification"); 282 + info!(did = %auth.did, "Requested Telegram verification"); 288 283 } 289 284 } 290 285 ··· 295 290 .clear_signal(user_id) 296 291 .await 297 292 .map_err(|e| ApiError::InternalError(Some(format!("Database error: {}", e))))?; 298 - info!(did = %user.did, "Cleared Signal number"); 293 + info!(did = %auth.did, "Cleared Signal number"); 299 294 } else { 300 - request_channel_verification(&state, user_id, &user.did, "signal", signal, None) 295 + request_channel_verification(&state, user_id, &auth.did, "signal", signal, None) 301 296 .await 302 297 .map_err(|e| ApiError::InternalError(Some(e)))?; 303 298 verification_required.push("signal".to_string()); 304 - info!(did = %user.did, "Requested Signal verification"); 299 + info!(did = %auth.did, "Requested Signal verification"); 305 300 } 306 301 } 307 302
+1 -1
crates/tranquil-pds/src/api/proxy.rs
··· 238 238 { 239 239 Ok(auth_user) => { 240 240 if let Err(e) = crate::auth::scope_check::check_rpc_scope( 241 - auth_user.is_oauth, 241 + auth_user.is_oauth(), 242 242 auth_user.scope.as_deref(), 243 243 &resolved.did, 244 244 method,
+13 -29
crates/tranquil-pds/src/api/repo/blob.rs
··· 1 1 use crate::api::error::ApiError; 2 - use crate::auth::{AuthenticatedEntity, RequiredAuth}; 2 + use crate::auth::{Auth, AuthAny, NotTakendown, Permissive}; 3 3 use crate::delegation::DelegationActionType; 4 4 use crate::state::AppState; 5 5 use crate::types::{CidLink, Did}; ··· 44 44 pub async fn upload_blob( 45 45 State(state): State<AppState>, 46 46 headers: axum::http::HeaderMap, 47 - auth: RequiredAuth, 47 + auth: AuthAny<Permissive>, 48 48 body: Body, 49 49 ) -> Result<Response, ApiError> { 50 - let (did, controller_did): (Did, Option<Did>) = match &auth.0 { 51 - AuthenticatedEntity::Service { did, claims } => { 52 - match &claims.lxm { 53 - Some(lxm) if lxm == "*" || lxm == "com.atproto.repo.uploadBlob" => {} 54 - Some(lxm) => { 55 - return Err(ApiError::AuthorizationError(format!( 56 - "Token lxm '{}' does not permit 'com.atproto.repo.uploadBlob'", 57 - lxm 58 - ))); 59 - } 60 - None => { 61 - return Err(ApiError::AuthorizationError( 62 - "Token missing lxm claim".to_string(), 63 - )); 64 - } 65 - } 66 - (did.clone(), None) 50 + let (did, controller_did): (Did, Option<Did>) = match &auth { 51 + AuthAny::Service(service) => { 52 + service.require_lxm("com.atproto.repo.uploadBlob")?; 53 + (service.did.clone(), None) 67 54 } 68 - AuthenticatedEntity::User(auth_user) => { 69 - if auth_user.status.is_takendown() { 55 + AuthAny::User(user) => { 56 + if user.status.is_takendown() { 70 57 return Err(ApiError::AccountTakedown); 71 58 } 72 59 let mime_type_for_check = headers ··· 74 61 .and_then(|h| h.to_str().ok()) 75 62 .unwrap_or("application/octet-stream"); 76 63 if let Err(e) = crate::auth::scope_check::check_blob_scope( 77 - auth_user.is_oauth, 78 - auth_user.scope.as_deref(), 64 + user.is_oauth(), 65 + user.scope.as_deref(), 79 66 mime_type_for_check, 80 67 ) { 81 68 return Ok(e); 82 69 } 83 - let ctrl_did = auth_user.controller_did.clone(); 84 - (auth_user.did.clone(), ctrl_did) 70 + (user.did.clone(), user.controller_did.clone()) 85 71 } 86 72 }; 87 73 ··· 238 224 239 225 pub async fn list_missing_blobs( 240 226 State(state): State<AppState>, 241 - auth: RequiredAuth, 227 + auth: Auth<NotTakendown>, 242 228 Query(params): Query<ListMissingBlobsParams>, 243 229 ) -> Result<Response, ApiError> { 244 - let auth_user = auth.0.require_user()?.require_not_takendown()?; 245 - 246 - let did = &auth_user.did; 230 + let did = &auth.did; 247 231 let user = state 248 232 .user_repo 249 233 .get_by_did(did)
+3 -4
crates/tranquil-pds/src/api/repo/import.rs
··· 1 1 use crate::api::EmptyResponse; 2 2 use crate::api::error::ApiError; 3 3 use crate::api::repo::record::create_signed_commit; 4 - use crate::auth::RequiredAuth; 4 + use crate::auth::{Auth, NotTakendown}; 5 5 use crate::state::AppState; 6 6 use crate::sync::import::{ImportError, apply_import, parse_car}; 7 7 use crate::sync::verify::CarVerifier; ··· 23 23 24 24 pub async fn import_repo( 25 25 State(state): State<AppState>, 26 - auth: RequiredAuth, 26 + auth: Auth<NotTakendown>, 27 27 body: Bytes, 28 28 ) -> Result<Response, ApiError> { 29 29 let accepting_imports = std::env::var("ACCEPTING_REPO_IMPORTS") ··· 44 44 max_size 45 45 ))); 46 46 } 47 - let auth_user = auth.0.require_user()?.require_not_takendown()?; 48 - let did = &auth_user.did; 47 + let did = &auth.did; 49 48 let user = state 50 49 .user_repo 51 50 .get_by_did(did)
+6 -7
crates/tranquil-pds/src/api/repo/record/batch.rs
··· 1 1 use super::validation::validate_record_with_status; 2 2 use crate::api::error::ApiError; 3 3 use crate::api::repo::record::utils::{CommitParams, RecordOp, commit_and_log, extract_blob_cids}; 4 - use crate::auth::RequiredAuth; 4 + use crate::auth::{Active, Auth}; 5 5 use crate::delegation::DelegationActionType; 6 6 use crate::repo::tracking::TrackingBlockStore; 7 7 use crate::state::AppState; ··· 262 262 263 263 pub async fn apply_writes( 264 264 State(state): State<AppState>, 265 - auth: RequiredAuth, 265 + auth: Auth<Active>, 266 266 Json(input): Json<ApplyWritesInput>, 267 267 ) -> Result<Response, ApiError> { 268 268 info!( ··· 270 270 input.repo, 271 271 input.writes.len() 272 272 ); 273 - let auth_user = auth.0.require_user()?.require_active()?; 274 - let did = auth_user.did.clone(); 275 - let is_oauth = auth_user.is_oauth; 276 - let scope = auth_user.scope.clone(); 277 - let controller_did = auth_user.controller_did.clone(); 273 + let did = auth.did.clone(); 274 + let is_oauth = auth.is_oauth(); 275 + let scope = auth.scope.clone(); 276 + let controller_did = auth.controller_did.clone(); 278 277 if input.repo.as_str() != did { 279 278 return Err(ApiError::InvalidRepo( 280 279 "Repo does not match authenticated user".into(),
+9 -10
crates/tranquil-pds/src/api/repo/record/delete.rs
··· 1 1 use crate::api::error::ApiError; 2 2 use crate::api::repo::record::utils::{CommitParams, RecordOp, commit_and_log}; 3 3 use crate::api::repo::record::write::{CommitInfo, prepare_repo_write}; 4 - use crate::auth::RequiredAuth; 4 + use crate::auth::{Active, Auth}; 5 5 use crate::delegation::DelegationActionType; 6 6 use crate::repo::tracking::TrackingBlockStore; 7 7 use crate::state::AppState; ··· 40 40 41 41 pub async fn delete_record( 42 42 State(state): State<AppState>, 43 - auth: RequiredAuth, 43 + auth: Auth<Active>, 44 44 Json(input): Json<DeleteRecordInput>, 45 45 ) -> Result<Response, crate::api::error::ApiError> { 46 - let user = auth.0.require_user()?.require_active()?; 47 - let auth = match prepare_repo_write(&state, user, &input.repo).await { 46 + let repo_auth = match prepare_repo_write(&state, &auth, &input.repo).await { 48 47 Ok(res) => res, 49 48 Err(err_res) => return Ok(err_res), 50 49 }; 51 50 52 51 if let Err(e) = crate::auth::scope_check::check_repo_scope( 53 - auth.is_oauth, 54 - auth.scope.as_deref(), 52 + repo_auth.is_oauth, 53 + repo_auth.scope.as_deref(), 55 54 crate::oauth::RepoAction::Delete, 56 55 &input.collection, 57 56 ) { 58 57 return Ok(e); 59 58 } 60 59 61 - let did = auth.did; 62 - let user_id = auth.user_id; 63 - let current_root_cid = auth.current_root_cid; 64 - let controller_did = auth.controller_did; 60 + let did = repo_auth.did; 61 + let user_id = repo_auth.user_id; 62 + let current_root_cid = repo_auth.current_root_cid; 63 + let controller_did = repo_auth.controller_did; 65 64 66 65 if let Some(swap_commit) = &input.swap_commit 67 66 && Cid::from_str(swap_commit).ok() != Some(current_root_cid)
+20 -22
crates/tranquil-pds/src/api/repo/record/write.rs
··· 3 3 use crate::api::repo::record::utils::{ 4 4 CommitParams, RecordOp, commit_and_log, extract_backlinks, extract_blob_cids, 5 5 }; 6 - use crate::auth::RequiredAuth; 6 + use crate::auth::{Active, Auth}; 7 7 use crate::delegation::DelegationActionType; 8 8 use crate::repo::tracking::TrackingBlockStore; 9 9 use crate::state::AppState; ··· 90 90 did: auth_user.did.clone(), 91 91 user_id, 92 92 current_root_cid, 93 - is_oauth: auth_user.is_oauth, 93 + is_oauth: auth_user.is_oauth(), 94 94 scope: auth_user.scope.clone(), 95 95 controller_did: auth_user.controller_did.clone(), 96 96 }) ··· 124 124 } 125 125 pub async fn create_record( 126 126 State(state): State<AppState>, 127 - auth: RequiredAuth, 127 + auth: Auth<Active>, 128 128 Json(input): Json<CreateRecordInput>, 129 129 ) -> Result<Response, crate::api::error::ApiError> { 130 - let user = auth.0.require_user()?.require_active()?; 131 - let auth = match prepare_repo_write(&state, user, &input.repo).await { 130 + let repo_auth = match prepare_repo_write(&state, &auth, &input.repo).await { 132 131 Ok(res) => res, 133 132 Err(err_res) => return Ok(err_res), 134 133 }; 135 134 136 135 if let Err(e) = crate::auth::scope_check::check_repo_scope( 137 - auth.is_oauth, 138 - auth.scope.as_deref(), 136 + repo_auth.is_oauth, 137 + repo_auth.scope.as_deref(), 139 138 crate::oauth::RepoAction::Create, 140 139 &input.collection, 141 140 ) { 142 141 return Ok(e); 143 142 } 144 143 145 - let did = auth.did; 146 - let user_id = auth.user_id; 147 - let current_root_cid = auth.current_root_cid; 148 - let controller_did = auth.controller_did; 144 + let did = repo_auth.did; 145 + let user_id = repo_auth.user_id; 146 + let current_root_cid = repo_auth.current_root_cid; 147 + let controller_did = repo_auth.controller_did; 149 148 150 149 if let Some(swap_commit) = &input.swap_commit 151 150 && Cid::from_str(swap_commit).ok() != Some(current_root_cid) ··· 432 431 } 433 432 pub async fn put_record( 434 433 State(state): State<AppState>, 435 - auth: RequiredAuth, 434 + auth: Auth<Active>, 436 435 Json(input): Json<PutRecordInput>, 437 436 ) -> Result<Response, crate::api::error::ApiError> { 438 - let user = auth.0.require_user()?.require_active()?; 439 - let auth = match prepare_repo_write(&state, user, &input.repo).await { 437 + let repo_auth = match prepare_repo_write(&state, &auth, &input.repo).await { 440 438 Ok(res) => res, 441 439 Err(err_res) => return Ok(err_res), 442 440 }; 443 441 444 442 if let Err(e) = crate::auth::scope_check::check_repo_scope( 445 - auth.is_oauth, 446 - auth.scope.as_deref(), 443 + repo_auth.is_oauth, 444 + repo_auth.scope.as_deref(), 447 445 crate::oauth::RepoAction::Create, 448 446 &input.collection, 449 447 ) { 450 448 return Ok(e); 451 449 } 452 450 if let Err(e) = crate::auth::scope_check::check_repo_scope( 453 - auth.is_oauth, 454 - auth.scope.as_deref(), 451 + repo_auth.is_oauth, 452 + repo_auth.scope.as_deref(), 455 453 crate::oauth::RepoAction::Update, 456 454 &input.collection, 457 455 ) { 458 456 return Ok(e); 459 457 } 460 458 461 - let did = auth.did; 462 - let user_id = auth.user_id; 463 - let current_root_cid = auth.current_root_cid; 464 - let controller_did = auth.controller_did; 459 + let did = repo_auth.did; 460 + let user_id = repo_auth.user_id; 461 + let current_root_cid = repo_auth.current_root_cid; 462 + let controller_did = repo_auth.controller_did; 465 463 466 464 if let Some(swap_commit) = &input.swap_commit 467 465 && Cid::from_str(swap_commit).ok() != Some(current_root_cid)
+14 -18
crates/tranquil-pds/src/api/server/account_status.rs
··· 1 1 use crate::api::EmptyResponse; 2 2 use crate::api::error::ApiError; 3 + use crate::auth::{Active, Auth, NotTakendown}; 3 4 use crate::cache::Cache; 4 5 use crate::plc::PlcClient; 5 6 use crate::state::AppState; ··· 40 41 41 42 pub async fn check_account_status( 42 43 State(state): State<AppState>, 43 - auth: crate::auth::RequiredAuth, 44 + auth: Auth<NotTakendown>, 44 45 ) -> Result<Response, ApiError> { 45 - let user = auth.0.require_user()?.require_not_takendown()?; 46 - let did = &user.did; 46 + let did = &auth.did; 47 47 let user_id = state 48 48 .user_repo 49 49 .get_id_by_did(did) ··· 306 306 307 307 pub async fn activate_account( 308 308 State(state): State<AppState>, 309 - auth: crate::auth::RequiredAuth, 309 + auth: Auth<NotTakendown>, 310 310 ) -> Result<Response, ApiError> { 311 311 info!("[MIGRATION] activateAccount called"); 312 - let auth_user = auth.0.require_user()?.require_not_takendown()?; 313 312 info!( 314 313 "[MIGRATION] activateAccount: Authenticated user did={}", 315 - auth_user.did 314 + auth.did 316 315 ); 317 316 318 317 if let Err(e) = crate::auth::scope_check::check_account_scope( 319 - auth_user.is_oauth, 320 - auth_user.scope.as_deref(), 318 + auth.is_oauth(), 319 + auth.scope.as_deref(), 321 320 crate::oauth::scopes::AccountAttr::Repo, 322 321 crate::oauth::scopes::AccountAction::Manage, 323 322 ) { ··· 325 324 return Ok(e); 326 325 } 327 326 328 - let did = auth_user.did.clone(); 327 + let did = auth.did.clone(); 329 328 330 329 info!( 331 330 "[MIGRATION] activateAccount: Validating DID document for did={}", ··· 471 470 472 471 pub async fn deactivate_account( 473 472 State(state): State<AppState>, 474 - auth: crate::auth::RequiredAuth, 473 + auth: Auth<Active>, 475 474 Json(input): Json<DeactivateAccountInput>, 476 475 ) -> Result<Response, ApiError> { 477 - let auth_user = auth.0.require_user()?.require_active()?; 478 - 479 476 if let Err(e) = crate::auth::scope_check::check_account_scope( 480 - auth_user.is_oauth, 481 - auth_user.scope.as_deref(), 477 + auth.is_oauth(), 478 + auth.scope.as_deref(), 482 479 crate::oauth::scopes::AccountAttr::Repo, 483 480 crate::oauth::scopes::AccountAction::Manage, 484 481 ) { ··· 491 488 .and_then(|s| chrono::DateTime::parse_from_rfc3339(s).ok()) 492 489 .map(|dt| dt.with_timezone(&chrono::Utc)); 493 490 494 - let did = auth_user.did.clone(); 491 + let did = auth.did.clone(); 495 492 496 493 let handle = state.user_repo.get_handle_by_did(&did).await.ok().flatten(); 497 494 ··· 524 521 525 522 pub async fn request_account_delete( 526 523 State(state): State<AppState>, 527 - auth: crate::auth::RequiredAuth, 524 + auth: Auth<NotTakendown>, 528 525 ) -> Result<Response, ApiError> { 529 - let user = auth.0.require_user()?.require_not_takendown()?; 530 - let did = &user.did; 526 + let did = &auth.did; 531 527 532 528 if !crate::api::server::reauth::check_legacy_session_mfa(&*state.session_repo, did).await { 533 529 return Ok(crate::api::server::reauth::legacy_mfa_required_response(
+13 -16
crates/tranquil-pds/src/api/server/app_password.rs
··· 1 1 use crate::api::EmptyResponse; 2 2 use crate::api::error::ApiError; 3 - use crate::auth::{RequiredAuth, generate_app_password}; 3 + use crate::auth::{Active, Auth, generate_app_password}; 4 4 use crate::delegation::{DelegationActionType, intersect_scopes}; 5 5 use crate::state::{AppState, RateLimitKind}; 6 6 use axum::{ ··· 33 33 34 34 pub async fn list_app_passwords( 35 35 State(state): State<AppState>, 36 - auth: RequiredAuth, 36 + auth: Auth<Active>, 37 37 ) -> Result<Response, ApiError> { 38 - let auth_user = auth.0.require_user()?.require_active()?; 39 38 let user = state 40 39 .user_repo 41 - .get_by_did(&auth_user.did) 40 + .get_by_did(&auth.did) 42 41 .await 43 42 .map_err(|e| { 44 43 error!("DB error getting user: {:?}", e); ··· 91 90 pub async fn create_app_password( 92 91 State(state): State<AppState>, 93 92 headers: HeaderMap, 94 - auth: RequiredAuth, 93 + auth: Auth<Active>, 95 94 Json(input): Json<CreateAppPasswordInput>, 96 95 ) -> Result<Response, ApiError> { 97 - let auth_user = auth.0.require_user()?.require_active()?; 98 96 let client_ip = crate::rate_limit::extract_client_ip(&headers, None); 99 97 if !state 100 98 .check_rate_limit(RateLimitKind::AppPassword, &client_ip) ··· 106 104 107 105 let user = state 108 106 .user_repo 109 - .get_by_did(&auth_user.did) 107 + .get_by_did(&auth.did) 110 108 .await 111 109 .map_err(|e| { 112 110 error!("DB error getting user: {:?}", e); ··· 132 130 return Err(ApiError::DuplicateAppPassword); 133 131 } 134 132 135 - let (final_scopes, controller_did) = if let Some(ref controller) = auth_user.controller_did { 133 + let (final_scopes, controller_did) = if let Some(ref controller) = auth.controller_did { 136 134 let grant = state 137 135 .delegation_repo 138 - .get_delegation(&auth_user.did, controller) 136 + .get_delegation(&auth.did, controller) 139 137 .await 140 138 .ok() 141 139 .flatten(); ··· 198 196 let _ = state 199 197 .delegation_repo 200 198 .log_delegation_action( 201 - &auth_user.did, 199 + &auth.did, 202 200 controller, 203 201 Some(controller), 204 202 DelegationActionType::AccountAction, ··· 229 227 230 228 pub async fn revoke_app_password( 231 229 State(state): State<AppState>, 232 - auth: RequiredAuth, 230 + auth: Auth<Active>, 233 231 Json(input): Json<RevokeAppPasswordInput>, 234 232 ) -> Result<Response, ApiError> { 235 - let auth_user = auth.0.require_user()?.require_active()?; 236 233 let user = state 237 234 .user_repo 238 - .get_by_did(&auth_user.did) 235 + .get_by_did(&auth.did) 239 236 .await 240 237 .map_err(|e| { 241 238 error!("DB error getting user: {:?}", e); ··· 250 247 251 248 let sessions_to_invalidate = state 252 249 .session_repo 253 - .get_session_jtis_by_app_password(&auth_user.did, name) 250 + .get_session_jtis_by_app_password(&auth.did, name) 254 251 .await 255 252 .unwrap_or_default(); 256 253 257 254 state 258 255 .session_repo 259 - .delete_sessions_by_app_password(&auth_user.did, name) 256 + .delete_sessions_by_app_password(&auth.did, name) 260 257 .await 261 258 .map_err(|e| { 262 259 error!("DB error revoking sessions for app password: {:?}", e); ··· 264 261 })?; 265 262 266 263 futures::future::join_all(sessions_to_invalidate.iter().map(|jti| { 267 - let cache_key = format!("auth:session:{}:{}", &auth_user.did, jti); 264 + let cache_key = format!("auth:session:{}:{}", &auth.did, jti); 268 265 let cache = state.cache.clone(); 269 266 async move { 270 267 let _ = cache.delete(&cache_key).await;
+19 -24
crates/tranquil-pds/src/api/server/email.rs
··· 1 1 use crate::api::error::ApiError; 2 2 use crate::api::{EmptyResponse, TokenRequiredResponse, VerifiedResponse}; 3 - use crate::auth::RequiredAuth; 3 + use crate::auth::{Active, Auth}; 4 4 use crate::state::{AppState, RateLimitKind}; 5 5 use axum::{ 6 6 Json, ··· 45 45 pub async fn request_email_update( 46 46 State(state): State<AppState>, 47 47 headers: axum::http::HeaderMap, 48 - auth: RequiredAuth, 48 + auth: Auth<Active>, 49 49 input: Option<Json<RequestEmailUpdateInput>>, 50 50 ) -> Result<Response, ApiError> { 51 - let auth_user = auth.0.require_user()?.require_active()?; 52 51 let client_ip = crate::rate_limit::extract_client_ip(&headers, None); 53 52 if !state 54 53 .check_rate_limit(RateLimitKind::EmailUpdate, &client_ip) ··· 59 58 } 60 59 61 60 if let Err(e) = crate::auth::scope_check::check_account_scope( 62 - auth_user.is_oauth, 63 - auth_user.scope.as_deref(), 61 + auth.is_oauth(), 62 + auth.scope.as_deref(), 64 63 crate::oauth::scopes::AccountAttr::Email, 65 64 crate::oauth::scopes::AccountAction::Manage, 66 65 ) { ··· 69 68 70 69 let user = state 71 70 .user_repo 72 - .get_email_info_by_did(&auth_user.did) 71 + .get_email_info_by_did(&auth.did) 73 72 .await 74 73 .map_err(|e| { 75 74 error!("DB error: {:?}", e); ··· 87 86 88 87 if token_required { 89 88 let code = crate::auth::verification_token::generate_channel_update_token( 90 - &auth_user.did, 89 + &auth.did, 91 90 "email_update", 92 91 &current_email.to_lowercase(), 93 92 ); ··· 104 103 authorized: false, 105 104 }; 106 105 if let Ok(json) = serde_json::to_string(&pending) { 107 - let cache_key = email_update_cache_key(&auth_user.did); 106 + let cache_key = email_update_cache_key(&auth.did); 108 107 if let Err(e) = state.cache.set(&cache_key, &json, EMAIL_UPDATE_TTL).await { 109 108 warn!("Failed to cache pending email update: {:?}", e); 110 109 } ··· 141 140 pub async fn confirm_email( 142 141 State(state): State<AppState>, 143 142 headers: axum::http::HeaderMap, 144 - auth: RequiredAuth, 143 + auth: Auth<Active>, 145 144 Json(input): Json<ConfirmEmailInput>, 146 145 ) -> Result<Response, ApiError> { 147 - let auth_user = auth.0.require_user()?.require_active()?; 148 146 let client_ip = crate::rate_limit::extract_client_ip(&headers, None); 149 147 if !state 150 148 .check_rate_limit(RateLimitKind::EmailUpdate, &client_ip) ··· 155 153 } 156 154 157 155 if let Err(e) = crate::auth::scope_check::check_account_scope( 158 - auth_user.is_oauth, 159 - auth_user.scope.as_deref(), 156 + auth.is_oauth(), 157 + auth.scope.as_deref(), 160 158 crate::oauth::scopes::AccountAttr::Email, 161 159 crate::oauth::scopes::AccountAction::Manage, 162 160 ) { 163 161 return Ok(e); 164 162 } 165 163 166 - let did = &auth_user.did; 164 + let did = &auth.did; 167 165 let user = state 168 166 .user_repo 169 167 .get_email_info_by_did(did) ··· 235 233 236 234 pub async fn update_email( 237 235 State(state): State<AppState>, 238 - auth: RequiredAuth, 236 + auth: Auth<Active>, 239 237 Json(input): Json<UpdateEmailInput>, 240 238 ) -> Result<Response, ApiError> { 241 - let auth_user = auth.0.require_user()?.require_active()?; 242 - 243 239 if let Err(e) = crate::auth::scope_check::check_account_scope( 244 - auth_user.is_oauth, 245 - auth_user.scope.as_deref(), 240 + auth.is_oauth(), 241 + auth.scope.as_deref(), 246 242 crate::oauth::scopes::AccountAttr::Email, 247 243 crate::oauth::scopes::AccountAction::Manage, 248 244 ) { 249 245 return Ok(e); 250 246 } 251 247 252 - let did = &auth_user.did; 248 + let did = &auth.did; 253 249 let user = state 254 250 .user_repo 255 251 .get_email_info_by_did(did) ··· 504 500 pub async fn check_email_update_status( 505 501 State(state): State<AppState>, 506 502 headers: axum::http::HeaderMap, 507 - auth: RequiredAuth, 503 + auth: Auth<Active>, 508 504 ) -> Result<Response, ApiError> { 509 - let auth_user = auth.0.require_user()?.require_active()?; 510 505 let client_ip = crate::rate_limit::extract_client_ip(&headers, None); 511 506 if !state 512 507 .check_rate_limit(RateLimitKind::VerificationCheck, &client_ip) ··· 516 511 } 517 512 518 513 if let Err(e) = crate::auth::scope_check::check_account_scope( 519 - auth_user.is_oauth, 520 - auth_user.scope.as_deref(), 514 + auth.is_oauth(), 515 + auth.scope.as_deref(), 521 516 crate::oauth::scopes::AccountAttr::Email, 522 517 crate::oauth::scopes::AccountAction::Read, 523 518 ) { 524 519 return Ok(e); 525 520 } 526 521 527 - let cache_key = email_update_cache_key(&auth_user.did); 522 + let cache_key = email_update_cache_key(&auth.did); 528 523 let pending_json = match state.cache.get(&cache_key).await { 529 524 Some(json) => json, 530 525 None => {
+7 -10
crates/tranquil-pds/src/api/server/invite.rs
··· 1 1 use crate::api::ApiError; 2 - use crate::auth::RequiredAuth; 2 + use crate::auth::{Active, Admin, Auth}; 3 3 use crate::state::AppState; 4 4 use crate::types::Did; 5 5 use axum::{ ··· 43 43 44 44 pub async fn create_invite_code( 45 45 State(state): State<AppState>, 46 - auth: RequiredAuth, 46 + auth: Auth<Admin>, 47 47 Json(input): Json<CreateInviteCodeInput>, 48 48 ) -> Result<Response, ApiError> { 49 - let auth_user = auth.0.require_user()?.require_active()?.require_admin()?; 50 49 if input.use_count < 1 { 51 50 return Err(ApiError::InvalidRequest( 52 51 "useCount must be at least 1".into(), ··· 57 56 Some(acct) => acct 58 57 .parse() 59 58 .map_err(|_| ApiError::InvalidDid("Invalid DID format".into()))?, 60 - None => auth_user.did.clone(), 59 + None => auth.did.clone(), 61 60 }; 62 61 let code = gen_invite_code(); 63 62 ··· 99 98 100 99 pub async fn create_invite_codes( 101 100 State(state): State<AppState>, 102 - auth: RequiredAuth, 101 + auth: Auth<Admin>, 103 102 Json(input): Json<CreateInviteCodesInput>, 104 103 ) -> Result<Response, ApiError> { 105 - let auth_user = auth.0.require_user()?.require_active()?.require_admin()?; 106 104 if input.use_count < 1 { 107 105 return Err(ApiError::InvalidRequest( 108 106 "useCount must be at least 1".into(), ··· 116 114 .map(|a| a.parse()) 117 115 .collect::<Result<Vec<Did>, _>>() 118 116 .map_err(|_| ApiError::InvalidDid("Invalid DID format".into()))?, 119 - _ => vec![auth_user.did.clone()], 117 + _ => vec![auth.did.clone()], 120 118 }; 121 119 122 120 let admin_user_id = state ··· 195 193 196 194 pub async fn get_account_invite_codes( 197 195 State(state): State<AppState>, 198 - auth: RequiredAuth, 196 + auth: Auth<Active>, 199 197 axum::extract::Query(params): axum::extract::Query<GetAccountInviteCodesParams>, 200 198 ) -> Result<Response, ApiError> { 201 - let auth_user = auth.0.require_user()?.require_active()?; 202 199 let include_used = params.include_used.unwrap_or(true); 203 200 204 201 let codes_info = state 205 202 .infra_repo 206 - .get_invite_codes_for_account(&auth_user.did) 203 + .get_invite_codes_for_account(&auth.did) 207 204 .await 208 205 .map_err(|e| { 209 206 error!("DB error fetching invite codes: {:?}", e);
+10 -14
crates/tranquil-pds/src/api/server/migration.rs
··· 1 1 use crate::api::ApiError; 2 - use crate::auth::RequiredAuth; 2 + use crate::auth::{Active, Auth}; 3 3 use crate::state::AppState; 4 4 use axum::{ 5 5 Json, ··· 36 36 37 37 pub async fn update_did_document( 38 38 State(state): State<AppState>, 39 - auth: RequiredAuth, 39 + auth: Auth<Active>, 40 40 Json(input): Json<UpdateDidDocumentInput>, 41 41 ) -> Result<Response, ApiError> { 42 - let auth_user = auth.0.require_user()?.require_active()?; 43 - 44 - if !auth_user.did.starts_with("did:web:") { 42 + if !auth.did.starts_with("did:web:") { 45 43 return Err(ApiError::InvalidRequest( 46 44 "DID document updates are only available for did:web accounts".into(), 47 45 )); ··· 49 47 50 48 let user = state 51 49 .user_repo 52 - .get_user_for_did_doc(&auth_user.did) 50 + .get_user_for_did_doc(&auth.did) 53 51 .await 54 52 .map_err(|e| { 55 53 tracing::error!("DB error getting user: {:?}", e); ··· 118 116 let endpoint_clean = endpoint.trim().trim_end_matches('/'); 119 117 state 120 118 .user_repo 121 - .update_migrated_to_pds(&auth_user.did, endpoint_clean) 119 + .update_migrated_to_pds(&auth.did, endpoint_clean) 122 120 .await 123 121 .map_err(|e| { 124 122 tracing::error!("DB error updating service endpoint: {:?}", e); ··· 126 124 })?; 127 125 } 128 126 129 - let did_doc = build_did_document(&state, &auth_user.did).await; 127 + let did_doc = build_did_document(&state, &auth.did).await; 130 128 131 - tracing::info!("Updated DID document for {}", &auth_user.did); 129 + tracing::info!("Updated DID document for {}", &auth.did); 132 130 133 131 Ok(( 134 132 StatusCode::OK, ··· 142 140 143 141 pub async fn get_did_document( 144 142 State(state): State<AppState>, 145 - auth: RequiredAuth, 143 + auth: Auth<Active>, 146 144 ) -> Result<Response, ApiError> { 147 - let auth_user = auth.0.require_user()?.require_active()?; 148 - 149 - if !auth_user.did.starts_with("did:web:") { 145 + if !auth.did.starts_with("did:web:") { 150 146 return Err(ApiError::InvalidRequest( 151 147 "This endpoint is only available for did:web accounts".into(), 152 148 )); 153 149 } 154 150 155 - let did_doc = build_did_document(&state, &auth_user.did).await; 151 + let did_doc = build_did_document(&state, &auth.did).await; 156 152 157 153 Ok((StatusCode::OK, Json(json!({ "didDocument": did_doc }))).into_response()) 158 154 }
+24 -31
crates/tranquil-pds/src/api/server/passkeys.rs
··· 1 1 use crate::api::EmptyResponse; 2 2 use crate::api::error::ApiError; 3 - use crate::auth::RequiredAuth; 4 3 use crate::auth::webauthn::WebAuthnConfig; 4 + use crate::auth::{Active, Auth}; 5 5 use crate::state::AppState; 6 6 use axum::{ 7 7 Json, ··· 34 34 35 35 pub async fn start_passkey_registration( 36 36 State(state): State<AppState>, 37 - auth: RequiredAuth, 37 + auth: Auth<Active>, 38 38 Json(input): Json<StartRegistrationInput>, 39 39 ) -> Result<Response, ApiError> { 40 - let auth_user = auth.0.require_user()?.require_active()?; 41 40 let webauthn = get_webauthn()?; 42 41 43 42 let handle = state 44 43 .user_repo 45 - .get_handle_by_did(&auth_user.did) 44 + .get_handle_by_did(&auth.did) 46 45 .await 47 46 .map_err(|e| { 48 47 error!("DB error fetching user: {:?}", e); ··· 52 51 53 52 let existing_passkeys = state 54 53 .user_repo 55 - .get_passkeys_for_user(&auth_user.did) 54 + .get_passkeys_for_user(&auth.did) 56 55 .await 57 56 .map_err(|e| { 58 57 error!("DB error fetching existing passkeys: {:?}", e); ··· 67 66 let display_name = input.friendly_name.as_deref().unwrap_or(&handle); 68 67 69 68 let (ccr, reg_state) = webauthn 70 - .start_registration(&auth_user.did, &handle, display_name, exclude_credentials) 69 + .start_registration(&auth.did, &handle, display_name, exclude_credentials) 71 70 .map_err(|e| { 72 71 error!("Failed to start passkey registration: {}", e); 73 72 ApiError::InternalError(Some("Failed to start registration".into())) ··· 80 79 81 80 state 82 81 .user_repo 83 - .save_webauthn_challenge(&auth_user.did, "registration", &state_json) 82 + .save_webauthn_challenge(&auth.did, "registration", &state_json) 84 83 .await 85 84 .map_err(|e| { 86 85 error!("Failed to save registration state: {:?}", e); ··· 89 88 90 89 let options = serde_json::to_value(&ccr).unwrap_or(serde_json::json!({})); 91 90 92 - info!(did = %auth_user.did, "Passkey registration started"); 91 + info!(did = %auth.did, "Passkey registration started"); 93 92 94 93 Ok(Json(StartRegistrationResponse { options }).into_response()) 95 94 } ··· 110 109 111 110 pub async fn finish_passkey_registration( 112 111 State(state): State<AppState>, 113 - auth: RequiredAuth, 112 + auth: Auth<Active>, 114 113 Json(input): Json<FinishRegistrationInput>, 115 114 ) -> Result<Response, ApiError> { 116 - let auth_user = auth.0.require_user()?.require_active()?; 117 115 let webauthn = get_webauthn()?; 118 116 119 117 let reg_state_json = state 120 118 .user_repo 121 - .load_webauthn_challenge(&auth_user.did, "registration") 119 + .load_webauthn_challenge(&auth.did, "registration") 122 120 .await 123 121 .map_err(|e| { 124 122 error!("DB error loading registration state: {:?}", e); ··· 153 151 let passkey_id = state 154 152 .user_repo 155 153 .save_passkey( 156 - &auth_user.did, 154 + &auth.did, 157 155 passkey.cred_id(), 158 156 &public_key, 159 157 input.friendly_name.as_deref(), ··· 166 164 167 165 if let Err(e) = state 168 166 .user_repo 169 - .delete_webauthn_challenge(&auth_user.did, "registration") 167 + .delete_webauthn_challenge(&auth.did, "registration") 170 168 .await 171 169 { 172 170 warn!("Failed to delete registration state: {:?}", e); ··· 177 175 passkey.cred_id(), 178 176 ); 179 177 180 - info!(did = %auth_user.did, passkey_id = %passkey_id, "Passkey registered"); 178 + info!(did = %auth.did, passkey_id = %passkey_id, "Passkey registered"); 181 179 182 180 Ok(Json(FinishRegistrationResponse { 183 181 id: passkey_id.to_string(), ··· 204 202 205 203 pub async fn list_passkeys( 206 204 State(state): State<AppState>, 207 - auth: RequiredAuth, 205 + auth: Auth<Active>, 208 206 ) -> Result<Response, ApiError> { 209 - let auth_user = auth.0.require_user()?.require_active()?; 210 207 let passkeys = state 211 208 .user_repo 212 - .get_passkeys_for_user(&auth_user.did) 209 + .get_passkeys_for_user(&auth.did) 213 210 .await 214 211 .map_err(|e| { 215 212 error!("DB error fetching passkeys: {:?}", e); ··· 241 238 242 239 pub async fn delete_passkey( 243 240 State(state): State<AppState>, 244 - auth: RequiredAuth, 241 + auth: Auth<Active>, 245 242 Json(input): Json<DeletePasskeyInput>, 246 243 ) -> Result<Response, ApiError> { 247 - let auth_user = auth.0.require_user()?.require_active()?; 248 - if !crate::api::server::reauth::check_legacy_session_mfa(&*state.session_repo, &auth_user.did) 249 - .await 244 + if !crate::api::server::reauth::check_legacy_session_mfa(&*state.session_repo, &auth.did).await 250 245 { 251 246 return Ok(crate::api::server::reauth::legacy_mfa_required_response( 252 247 &*state.user_repo, 253 248 &*state.session_repo, 254 - &auth_user.did, 249 + &auth.did, 255 250 ) 256 251 .await); 257 252 } 258 253 259 - if crate::api::server::reauth::check_reauth_required(&*state.session_repo, &auth_user.did).await 260 - { 254 + if crate::api::server::reauth::check_reauth_required(&*state.session_repo, &auth.did).await { 261 255 return Ok(crate::api::server::reauth::reauth_required_response( 262 256 &*state.user_repo, 263 257 &*state.session_repo, 264 - &auth_user.did, 258 + &auth.did, 265 259 ) 266 260 .await); 267 261 } 268 262 269 263 let id: uuid::Uuid = input.id.parse().map_err(|_| ApiError::InvalidId)?; 270 264 271 - match state.user_repo.delete_passkey(id, &auth_user.did).await { 265 + match state.user_repo.delete_passkey(id, &auth.did).await { 272 266 Ok(true) => { 273 - info!(did = %auth_user.did, passkey_id = %id, "Passkey deleted"); 267 + info!(did = %auth.did, passkey_id = %id, "Passkey deleted"); 274 268 Ok(EmptyResponse::ok().into_response()) 275 269 } 276 270 Ok(false) => Err(ApiError::PasskeyNotFound), ··· 290 284 291 285 pub async fn update_passkey( 292 286 State(state): State<AppState>, 293 - auth: RequiredAuth, 287 + auth: Auth<Active>, 294 288 Json(input): Json<UpdatePasskeyInput>, 295 289 ) -> Result<Response, ApiError> { 296 - let auth_user = auth.0.require_user()?.require_active()?; 297 290 let id: uuid::Uuid = input.id.parse().map_err(|_| ApiError::InvalidId)?; 298 291 299 292 match state 300 293 .user_repo 301 - .update_passkey_name(id, &auth_user.did, &input.friendly_name) 294 + .update_passkey_name(id, &auth.did, &input.friendly_name) 302 295 .await 303 296 { 304 297 Ok(true) => { 305 - info!(did = %auth_user.did, passkey_id = %id, "Passkey renamed"); 298 + info!(did = %auth.did, passkey_id = %id, "Passkey renamed"); 306 299 Ok(EmptyResponse::ok().into_response()) 307 300 } 308 301 Ok(false) => Err(ApiError::PasskeyNotFound),
+24 -30
crates/tranquil-pds/src/api/server/password.rs
··· 1 1 use crate::api::error::ApiError; 2 2 use crate::api::{EmptyResponse, HasPasswordResponse, SuccessResponse}; 3 - use crate::auth::RequiredAuth; 3 + use crate::auth::{Active, Auth}; 4 4 use crate::state::{AppState, RateLimitKind}; 5 5 use crate::types::PlainPassword; 6 6 use crate::validation::validate_password; ··· 227 227 228 228 pub async fn change_password( 229 229 State(state): State<AppState>, 230 - auth: RequiredAuth, 230 + auth: Auth<Active>, 231 231 Json(input): Json<ChangePasswordInput>, 232 232 ) -> Result<Response, ApiError> { 233 - let auth_user = auth.0.require_user()?.require_active()?; 234 - if !crate::api::server::reauth::check_legacy_session_mfa(&*state.session_repo, &auth_user.did) 235 - .await 233 + if !crate::api::server::reauth::check_legacy_session_mfa(&*state.session_repo, &auth.did).await 236 234 { 237 235 return Ok(crate::api::server::reauth::legacy_mfa_required_response( 238 236 &*state.user_repo, 239 237 &*state.session_repo, 240 - &auth_user.did, 238 + &auth.did, 241 239 ) 242 240 .await); 243 241 } ··· 257 255 } 258 256 let user = state 259 257 .user_repo 260 - .get_id_and_password_hash_by_did(&auth_user.did) 258 + .get_id_and_password_hash_by_did(&auth.did) 261 259 .await 262 260 .map_err(|e| { 263 261 error!("DB error in change_password: {:?}", e); ··· 296 294 ApiError::InternalError(None) 297 295 })?; 298 296 299 - info!(did = %&auth_user.did, "Password changed successfully"); 297 + info!(did = %&auth.did, "Password changed successfully"); 300 298 Ok(EmptyResponse::ok().into_response()) 301 299 } 302 300 303 301 pub async fn get_password_status( 304 302 State(state): State<AppState>, 305 - auth: RequiredAuth, 303 + auth: Auth<Active>, 306 304 ) -> Result<Response, ApiError> { 307 - let auth_user = auth.0.require_user()?.require_active()?; 308 - match state.user_repo.has_password_by_did(&auth_user.did).await { 305 + match state.user_repo.has_password_by_did(&auth.did).await { 309 306 Ok(Some(has)) => Ok(HasPasswordResponse::response(has).into_response()), 310 307 Ok(None) => Err(ApiError::AccountNotFound), 311 308 Err(e) => { ··· 317 314 318 315 pub async fn remove_password( 319 316 State(state): State<AppState>, 320 - auth: RequiredAuth, 317 + auth: Auth<Active>, 321 318 ) -> Result<Response, ApiError> { 322 - let auth_user = auth.0.require_user()?.require_active()?; 323 - if !crate::api::server::reauth::check_legacy_session_mfa(&*state.session_repo, &auth_user.did) 324 - .await 319 + if !crate::api::server::reauth::check_legacy_session_mfa(&*state.session_repo, &auth.did).await 325 320 { 326 321 return Ok(crate::api::server::reauth::legacy_mfa_required_response( 327 322 &*state.user_repo, 328 323 &*state.session_repo, 329 - &auth_user.did, 324 + &auth.did, 330 325 ) 331 326 .await); 332 327 } ··· 334 329 if crate::api::server::reauth::check_reauth_required_cached( 335 330 &*state.session_repo, 336 331 &state.cache, 337 - &auth_user.did, 332 + &auth.did, 338 333 ) 339 334 .await 340 335 { 341 336 return Ok(crate::api::server::reauth::reauth_required_response( 342 337 &*state.user_repo, 343 338 &*state.session_repo, 344 - &auth_user.did, 339 + &auth.did, 345 340 ) 346 341 .await); 347 342 } 348 343 349 344 let has_passkeys = state 350 345 .user_repo 351 - .has_passkeys(&auth_user.did) 346 + .has_passkeys(&auth.did) 352 347 .await 353 348 .unwrap_or(false); 354 349 if !has_passkeys { ··· 359 354 360 355 let user = state 361 356 .user_repo 362 - .get_password_info_by_did(&auth_user.did) 357 + .get_password_info_by_did(&auth.did) 363 358 .await 364 359 .map_err(|e| { 365 360 error!("DB error: {:?}", e); ··· 382 377 ApiError::InternalError(None) 383 378 })?; 384 379 385 - info!(did = %&auth_user.did, "Password removed - account is now passkey-only"); 380 + info!(did = %&auth.did, "Password removed - account is now passkey-only"); 386 381 Ok(SuccessResponse::ok().into_response()) 387 382 } 388 383 ··· 394 389 395 390 pub async fn set_password( 396 391 State(state): State<AppState>, 397 - auth: RequiredAuth, 392 + auth: Auth<Active>, 398 393 Json(input): Json<SetPasswordInput>, 399 394 ) -> Result<Response, ApiError> { 400 - let auth_user = auth.0.require_user()?.require_active()?; 401 395 let has_password = state 402 396 .user_repo 403 - .has_password_by_did(&auth_user.did) 397 + .has_password_by_did(&auth.did) 404 398 .await 405 399 .ok() 406 400 .flatten() 407 401 .unwrap_or(false); 408 402 let has_passkeys = state 409 403 .user_repo 410 - .has_passkeys(&auth_user.did) 404 + .has_passkeys(&auth.did) 411 405 .await 412 406 .unwrap_or(false); 413 407 let has_totp = state 414 408 .user_repo 415 - .has_totp_enabled(&auth_user.did) 409 + .has_totp_enabled(&auth.did) 416 410 .await 417 411 .unwrap_or(false); 418 412 ··· 422 416 && crate::api::server::reauth::check_reauth_required_cached( 423 417 &*state.session_repo, 424 418 &state.cache, 425 - &auth_user.did, 419 + &auth.did, 426 420 ) 427 421 .await 428 422 { 429 423 return Ok(crate::api::server::reauth::reauth_required_response( 430 424 &*state.user_repo, 431 425 &*state.session_repo, 432 - &auth_user.did, 426 + &auth.did, 433 427 ) 434 428 .await); 435 429 } ··· 444 438 445 439 let user = state 446 440 .user_repo 447 - .get_password_info_by_did(&auth_user.did) 441 + .get_password_info_by_did(&auth.did) 448 442 .await 449 443 .map_err(|e| { 450 444 error!("DB error: {:?}", e); ··· 479 473 ApiError::InternalError(None) 480 474 })?; 481 475 482 - info!(did = %&auth_user.did, "Password set for passkey-only account"); 476 + info!(did = %&auth.did, "Password set for passkey-only account"); 483 477 Ok(SuccessResponse::ok().into_response()) 484 478 }
+30 -38
crates/tranquil-pds/src/api/server/reauth.rs
··· 10 10 use tracing::{error, info, warn}; 11 11 use tranquil_db_traits::{SessionRepository, UserRepository}; 12 12 13 - use crate::auth::RequiredAuth; 13 + use crate::auth::{Active, Auth}; 14 14 use crate::state::{AppState, RateLimitKind}; 15 15 use crate::types::PlainPassword; 16 16 ··· 26 26 27 27 pub async fn get_reauth_status( 28 28 State(state): State<AppState>, 29 - auth: RequiredAuth, 29 + auth: Auth<Active>, 30 30 ) -> Result<Response, ApiError> { 31 - let auth_user = auth.0.require_user()?.require_active()?; 32 31 let last_reauth_at = state 33 32 .session_repo 34 - .get_last_reauth_at(&auth_user.did) 33 + .get_last_reauth_at(&auth.did) 35 34 .await 36 35 .map_err(|e| { 37 36 error!("DB error: {:?}", e); ··· 40 39 41 40 let reauth_required = is_reauth_required(last_reauth_at); 42 41 let available_methods = 43 - get_available_reauth_methods(&*state.user_repo, &*state.session_repo, &auth_user.did).await; 42 + get_available_reauth_methods(&*state.user_repo, &*state.session_repo, &auth.did).await; 44 43 45 44 Ok(Json(ReauthStatusResponse { 46 45 last_reauth_at, ··· 64 63 65 64 pub async fn reauth_password( 66 65 State(state): State<AppState>, 67 - auth: RequiredAuth, 66 + auth: Auth<Active>, 68 67 Json(input): Json<PasswordReauthInput>, 69 68 ) -> Result<Response, ApiError> { 70 - let auth_user = auth.0.require_user()?.require_active()?; 71 69 let password_hash = state 72 70 .user_repo 73 - .get_password_hash_by_did(&auth_user.did) 71 + .get_password_hash_by_did(&auth.did) 74 72 .await 75 73 .map_err(|e| { 76 74 error!("DB error: {:?}", e); ··· 83 81 if !password_valid { 84 82 let app_password_hashes = state 85 83 .session_repo 86 - .get_app_password_hashes_by_did(&auth_user.did) 84 + .get_app_password_hashes_by_did(&auth.did) 87 85 .await 88 86 .unwrap_or_default(); 89 87 ··· 92 90 }); 93 91 94 92 if !app_password_valid { 95 - warn!(did = %&auth_user.did, "Re-auth failed: invalid password"); 93 + warn!(did = %&auth.did, "Re-auth failed: invalid password"); 96 94 return Err(ApiError::InvalidPassword("Password is incorrect".into())); 97 95 } 98 96 } 99 97 100 - let reauthed_at = update_last_reauth_cached(&*state.session_repo, &state.cache, &auth_user.did) 98 + let reauthed_at = update_last_reauth_cached(&*state.session_repo, &state.cache, &auth.did) 101 99 .await 102 100 .map_err(|e| { 103 101 error!("DB error updating reauth: {:?}", e); 104 102 ApiError::InternalError(None) 105 103 })?; 106 104 107 - info!(did = %&auth_user.did, "Re-auth successful via password"); 105 + info!(did = %&auth.did, "Re-auth successful via password"); 108 106 Ok(Json(ReauthResponse { reauthed_at }).into_response()) 109 107 } 110 108 ··· 116 114 117 115 pub async fn reauth_totp( 118 116 State(state): State<AppState>, 119 - auth: RequiredAuth, 117 + auth: Auth<Active>, 120 118 Json(input): Json<TotpReauthInput>, 121 119 ) -> Result<Response, ApiError> { 122 - let auth_user = auth.0.require_user()?.require_active()?; 123 120 if !state 124 - .check_rate_limit(RateLimitKind::TotpVerify, &auth_user.did) 121 + .check_rate_limit(RateLimitKind::TotpVerify, &auth.did) 125 122 .await 126 123 { 127 - warn!(did = %&auth_user.did, "TOTP verification rate limit exceeded"); 124 + warn!(did = %&auth.did, "TOTP verification rate limit exceeded"); 128 125 return Err(ApiError::RateLimitExceeded(Some( 129 126 "Too many verification attempts. Please try again in a few minutes.".into(), 130 127 ))); 131 128 } 132 129 133 - let valid = crate::api::server::totp::verify_totp_or_backup_for_user( 134 - &state, 135 - &auth_user.did, 136 - &input.code, 137 - ) 138 - .await; 130 + let valid = 131 + crate::api::server::totp::verify_totp_or_backup_for_user(&state, &auth.did, &input.code) 132 + .await; 139 133 140 134 if !valid { 141 - warn!(did = %&auth_user.did, "Re-auth failed: invalid TOTP code"); 135 + warn!(did = %&auth.did, "Re-auth failed: invalid TOTP code"); 142 136 return Err(ApiError::InvalidCode(Some( 143 137 "Invalid TOTP or backup code".into(), 144 138 ))); 145 139 } 146 140 147 - let reauthed_at = update_last_reauth_cached(&*state.session_repo, &state.cache, &auth_user.did) 141 + let reauthed_at = update_last_reauth_cached(&*state.session_repo, &state.cache, &auth.did) 148 142 .await 149 143 .map_err(|e| { 150 144 error!("DB error updating reauth: {:?}", e); 151 145 ApiError::InternalError(None) 152 146 })?; 153 147 154 - info!(did = %&auth_user.did, "Re-auth successful via TOTP"); 148 + info!(did = %&auth.did, "Re-auth successful via TOTP"); 155 149 Ok(Json(ReauthResponse { reauthed_at }).into_response()) 156 150 } 157 151 ··· 163 157 164 158 pub async fn reauth_passkey_start( 165 159 State(state): State<AppState>, 166 - auth: RequiredAuth, 160 + auth: Auth<Active>, 167 161 ) -> Result<Response, ApiError> { 168 - let auth_user = auth.0.require_user()?.require_active()?; 169 162 let pds_hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 170 163 171 164 let stored_passkeys = state 172 165 .user_repo 173 - .get_passkeys_for_user(&auth_user.did) 166 + .get_passkeys_for_user(&auth.did) 174 167 .await 175 168 .map_err(|e| { 176 169 error!("Failed to get passkeys: {:?}", e); ··· 209 202 210 203 state 211 204 .user_repo 212 - .save_webauthn_challenge(&auth_user.did, "authentication", &state_json) 205 + .save_webauthn_challenge(&auth.did, "authentication", &state_json) 213 206 .await 214 207 .map_err(|e| { 215 208 error!("Failed to save authentication state: {:?}", e); ··· 228 221 229 222 pub async fn reauth_passkey_finish( 230 223 State(state): State<AppState>, 231 - auth: RequiredAuth, 224 + auth: Auth<Active>, 232 225 Json(input): Json<PasskeyReauthFinishInput>, 233 226 ) -> Result<Response, ApiError> { 234 - let auth_user = auth.0.require_user()?.require_active()?; 235 227 let pds_hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 236 228 237 229 let auth_state_json = state 238 230 .user_repo 239 - .load_webauthn_challenge(&auth_user.did, "authentication") 231 + .load_webauthn_challenge(&auth.did, "authentication") 240 232 .await 241 233 .map_err(|e| { 242 234 error!("Failed to load authentication state: {:?}", e); ··· 264 256 let auth_result = webauthn 265 257 .finish_authentication(&credential, &auth_state) 266 258 .map_err(|e| { 267 - warn!(did = %&auth_user.did, "Passkey re-auth failed: {:?}", e); 259 + warn!(did = %&auth.did, "Passkey re-auth failed: {:?}", e); 268 260 ApiError::AuthenticationFailed(Some("Passkey authentication failed".into())) 269 261 })?; 270 262 ··· 275 267 .await 276 268 { 277 269 Ok(false) => { 278 - warn!(did = %&auth_user.did, "Passkey counter anomaly detected - possible cloned key"); 270 + warn!(did = %&auth.did, "Passkey counter anomaly detected - possible cloned key"); 279 271 let _ = state 280 272 .user_repo 281 - .delete_webauthn_challenge(&auth_user.did, "authentication") 273 + .delete_webauthn_challenge(&auth.did, "authentication") 282 274 .await; 283 275 return Err(ApiError::PasskeyCounterAnomaly); 284 276 } ··· 290 282 291 283 let _ = state 292 284 .user_repo 293 - .delete_webauthn_challenge(&auth_user.did, "authentication") 285 + .delete_webauthn_challenge(&auth.did, "authentication") 294 286 .await; 295 287 296 - let reauthed_at = update_last_reauth_cached(&*state.session_repo, &state.cache, &auth_user.did) 288 + let reauthed_at = update_last_reauth_cached(&*state.session_repo, &state.cache, &auth.did) 297 289 .await 298 290 .map_err(|e| { 299 291 error!("DB error updating reauth: {:?}", e); 300 292 ApiError::InternalError(None) 301 293 })?; 302 294 303 - info!(did = %&auth_user.did, "Re-auth successful via passkey"); 295 + info!(did = %&auth.did, "Re-auth successful via passkey"); 304 296 Ok(Json(ReauthResponse { reauthed_at }).into_response()) 305 297 } 306 298
+36 -46
crates/tranquil-pds/src/api/server/session.rs
··· 1 1 use crate::api::error::ApiError; 2 2 use crate::api::{EmptyResponse, SuccessResponse}; 3 - use crate::auth::RequiredAuth; 3 + use crate::auth::{Active, Auth, NotTakendown}; 4 4 use crate::state::{AppState, RateLimitKind}; 5 5 use crate::types::{AccountState, Did, Handle, PlainPassword}; 6 6 use axum::{ ··· 279 279 280 280 pub async fn get_session( 281 281 State(state): State<AppState>, 282 - auth: RequiredAuth, 282 + auth: Auth<NotTakendown>, 283 283 ) -> Result<Response, ApiError> { 284 - let auth_user = auth.0.require_user()?.require_not_takendown()?; 285 - let permissions = auth_user.permissions(); 284 + let permissions = auth.permissions(); 286 285 let can_read_email = permissions.allows_email_read(); 287 286 288 - let did_for_doc = auth_user.did.clone(); 287 + let did_for_doc = auth.did.clone(); 289 288 let did_resolver = state.did_resolver.clone(); 290 289 let (db_result, did_doc) = tokio::join!( 291 - state.user_repo.get_session_info_by_did(&auth_user.did), 290 + state.user_repo.get_session_info_by_did(&auth.did), 292 291 did_resolver.resolve_did_document(&did_for_doc) 293 292 ); 294 293 match db_result { ··· 317 316 let email_confirmed_value = can_read_email && row.email_verified; 318 317 let mut response = json!({ 319 318 "handle": handle, 320 - "did": &auth_user.did, 319 + "did": &auth.did, 321 320 "active": account_state.is_active(), 322 321 "preferredChannel": preferred_channel, 323 322 "preferredChannelVerified": preferred_channel_verified, ··· 351 350 pub async fn delete_session( 352 351 State(state): State<AppState>, 353 352 headers: axum::http::HeaderMap, 354 - auth: RequiredAuth, 353 + _auth: Auth<Active>, 355 354 ) -> Result<Response, ApiError> { 356 - auth.0.require_user()?.require_active()?; 357 355 let extracted = crate::auth::extract_auth_token_from_header( 358 356 headers.get("Authorization").and_then(|h| h.to_str().ok()), 359 357 ) ··· 794 792 pub async fn list_sessions( 795 793 State(state): State<AppState>, 796 794 headers: HeaderMap, 797 - auth: RequiredAuth, 795 + auth: Auth<Active>, 798 796 ) -> Result<Response, ApiError> { 799 - let auth_user = auth.0.require_user()?.require_active()?; 800 797 let current_jti = headers 801 798 .get("authorization") 802 799 .and_then(|v| v.to_str().ok()) ··· 805 802 806 803 let jwt_rows = state 807 804 .session_repo 808 - .list_sessions_by_did(&auth_user.did) 805 + .list_sessions_by_did(&auth.did) 809 806 .await 810 807 .map_err(|e| { 811 808 error!("DB error fetching JWT sessions: {:?}", e); ··· 814 811 815 812 let oauth_rows = state 816 813 .oauth_repo 817 - .list_sessions_by_did(&auth_user.did) 814 + .list_sessions_by_did(&auth.did) 818 815 .await 819 816 .map_err(|e| { 820 817 error!("DB error fetching OAuth sessions: {:?}", e); ··· 830 827 is_current: current_jti.as_ref() == Some(&row.access_jti), 831 828 }); 832 829 833 - let is_oauth = auth_user.is_oauth; 830 + let is_oauth = auth.is_oauth(); 834 831 let oauth_sessions = oauth_rows.into_iter().map(|row| { 835 832 let client_name = extract_client_name(&row.client_id); 836 833 let is_current_oauth = is_oauth && current_jti.as_deref() == Some(row.token_id.as_str()); ··· 868 865 869 866 pub async fn revoke_session( 870 867 State(state): State<AppState>, 871 - auth: RequiredAuth, 868 + auth: Auth<Active>, 872 869 Json(input): Json<RevokeSessionInput>, 873 870 ) -> Result<Response, ApiError> { 874 - let auth_user = auth.0.require_user()?.require_active()?; 875 871 if let Some(jwt_id) = input.session_id.strip_prefix("jwt:") { 876 872 let session_id: i32 = jwt_id 877 873 .parse() 878 874 .map_err(|_| ApiError::InvalidRequest("Invalid session ID".into()))?; 879 875 let access_jti = state 880 876 .session_repo 881 - .get_session_access_jti_by_id(session_id, &auth_user.did) 877 + .get_session_access_jti_by_id(session_id, &auth.did) 882 878 .await 883 879 .map_err(|e| { 884 880 error!("DB error in revoke_session: {:?}", e); ··· 893 889 error!("DB error deleting session: {:?}", e); 894 890 ApiError::InternalError(None) 895 891 })?; 896 - let cache_key = format!("auth:session:{}:{}", &auth_user.did, access_jti); 892 + let cache_key = format!("auth:session:{}:{}", &auth.did, access_jti); 897 893 if let Err(e) = state.cache.delete(&cache_key).await { 898 894 warn!("Failed to invalidate session cache: {:?}", e); 899 895 } 900 - info!(did = %&auth_user.did, session_id = %session_id, "JWT session revoked"); 896 + info!(did = %&auth.did, session_id = %session_id, "JWT session revoked"); 901 897 } else if let Some(oauth_id) = input.session_id.strip_prefix("oauth:") { 902 898 let session_id: i32 = oauth_id 903 899 .parse() 904 900 .map_err(|_| ApiError::InvalidRequest("Invalid session ID".into()))?; 905 901 let deleted = state 906 902 .oauth_repo 907 - .delete_session_by_id(session_id, &auth_user.did) 903 + .delete_session_by_id(session_id, &auth.did) 908 904 .await 909 905 .map_err(|e| { 910 906 error!("DB error deleting OAuth session: {:?}", e); ··· 913 909 if deleted == 0 { 914 910 return Err(ApiError::SessionNotFound); 915 911 } 916 - info!(did = %&auth_user.did, session_id = %session_id, "OAuth session revoked"); 912 + info!(did = %&auth.did, session_id = %session_id, "OAuth session revoked"); 917 913 } else { 918 914 return Err(ApiError::InvalidRequest("Invalid session ID format".into())); 919 915 } ··· 923 919 pub async fn revoke_all_sessions( 924 920 State(state): State<AppState>, 925 921 headers: HeaderMap, 926 - auth: RequiredAuth, 922 + auth: Auth<Active>, 927 923 ) -> Result<Response, ApiError> { 928 - let auth_user = auth.0.require_user()?.require_active()?; 929 924 let jti = crate::auth::extract_auth_token_from_header( 930 925 headers.get("authorization").and_then(|v| v.to_str().ok()), 931 926 ) 932 927 .and_then(|extracted| crate::auth::get_jti_from_token(&extracted.token).ok()) 933 928 .ok_or(ApiError::InvalidToken(None))?; 934 929 935 - if auth_user.is_oauth { 930 + if auth.is_oauth() { 936 931 state 937 932 .session_repo 938 - .delete_sessions_by_did(&auth_user.did) 933 + .delete_sessions_by_did(&auth.did) 939 934 .await 940 935 .map_err(|e| { 941 936 error!("DB error revoking JWT sessions: {:?}", e); ··· 944 939 let jti_typed = TokenId::from(jti.clone()); 945 940 state 946 941 .oauth_repo 947 - .delete_sessions_by_did_except(&auth_user.did, &jti_typed) 942 + .delete_sessions_by_did_except(&auth.did, &jti_typed) 948 943 .await 949 944 .map_err(|e| { 950 945 error!("DB error revoking OAuth sessions: {:?}", e); ··· 953 948 } else { 954 949 state 955 950 .session_repo 956 - .delete_sessions_by_did_except_jti(&auth_user.did, &jti) 951 + .delete_sessions_by_did_except_jti(&auth.did, &jti) 957 952 .await 958 953 .map_err(|e| { 959 954 error!("DB error revoking JWT sessions: {:?}", e); ··· 961 956 })?; 962 957 state 963 958 .oauth_repo 964 - .delete_sessions_by_did(&auth_user.did) 959 + .delete_sessions_by_did(&auth.did) 965 960 .await 966 961 .map_err(|e| { 967 962 error!("DB error revoking OAuth sessions: {:?}", e); ··· 969 964 })?; 970 965 } 971 966 972 - info!(did = %&auth_user.did, "All other sessions revoked"); 967 + info!(did = %&auth.did, "All other sessions revoked"); 973 968 Ok(SuccessResponse::ok().into_response()) 974 969 } 975 970 ··· 982 977 983 978 pub async fn get_legacy_login_preference( 984 979 State(state): State<AppState>, 985 - auth: RequiredAuth, 980 + auth: Auth<Active>, 986 981 ) -> Result<Response, ApiError> { 987 - let auth_user = auth.0.require_user()?.require_active()?; 988 982 let pref = state 989 983 .user_repo 990 - .get_legacy_login_pref(&auth_user.did) 984 + .get_legacy_login_pref(&auth.did) 991 985 .await 992 986 .map_err(|e| { 993 987 error!("DB error: {:?}", e); ··· 1009 1003 1010 1004 pub async fn update_legacy_login_preference( 1011 1005 State(state): State<AppState>, 1012 - auth: RequiredAuth, 1006 + auth: Auth<Active>, 1013 1007 Json(input): Json<UpdateLegacyLoginInput>, 1014 1008 ) -> Result<Response, ApiError> { 1015 - let auth_user = auth.0.require_user()?.require_active()?; 1016 - if !crate::api::server::reauth::check_legacy_session_mfa(&*state.session_repo, &auth_user.did) 1017 - .await 1009 + if !crate::api::server::reauth::check_legacy_session_mfa(&*state.session_repo, &auth.did).await 1018 1010 { 1019 1011 return Ok(crate::api::server::reauth::legacy_mfa_required_response( 1020 1012 &*state.user_repo, 1021 1013 &*state.session_repo, 1022 - &auth_user.did, 1014 + &auth.did, 1023 1015 ) 1024 1016 .await); 1025 1017 } 1026 1018 1027 - if crate::api::server::reauth::check_reauth_required(&*state.session_repo, &auth_user.did).await 1028 - { 1019 + if crate::api::server::reauth::check_reauth_required(&*state.session_repo, &auth.did).await { 1029 1020 return Ok(crate::api::server::reauth::reauth_required_response( 1030 1021 &*state.user_repo, 1031 1022 &*state.session_repo, 1032 - &auth_user.did, 1023 + &auth.did, 1033 1024 ) 1034 1025 .await); 1035 1026 } 1036 1027 1037 1028 let updated = state 1038 1029 .user_repo 1039 - .update_legacy_login(&auth_user.did, input.allow_legacy_login) 1030 + .update_legacy_login(&auth.did, input.allow_legacy_login) 1040 1031 .await 1041 1032 .map_err(|e| { 1042 1033 error!("DB error: {:?}", e); ··· 1046 1037 return Err(ApiError::AccountNotFound); 1047 1038 } 1048 1039 info!( 1049 - did = %&auth_user.did, 1040 + did = %&auth.did, 1050 1041 allow_legacy_login = input.allow_legacy_login, 1051 1042 "Legacy login preference updated" 1052 1043 ); ··· 1066 1057 1067 1058 pub async fn update_locale( 1068 1059 State(state): State<AppState>, 1069 - auth: RequiredAuth, 1060 + auth: Auth<Active>, 1070 1061 Json(input): Json<UpdateLocaleInput>, 1071 1062 ) -> Result<Response, ApiError> { 1072 - let auth_user = auth.0.require_user()?.require_active()?; 1073 1063 if !VALID_LOCALES.contains(&input.preferred_locale.as_str()) { 1074 1064 return Err(ApiError::InvalidRequest(format!( 1075 1065 "Invalid locale. Valid options: {}", ··· 1079 1069 1080 1070 let updated = state 1081 1071 .user_repo 1082 - .update_locale(&auth_user.did, &input.preferred_locale) 1072 + .update_locale(&auth.did, &input.preferred_locale) 1083 1073 .await 1084 1074 .map_err(|e| { 1085 1075 error!("DB error updating locale: {:?}", e); ··· 1089 1079 return Err(ApiError::AccountNotFound); 1090 1080 } 1091 1081 info!( 1092 - did = %&auth_user.did, 1082 + did = %&auth.did, 1093 1083 locale = %input.preferred_locale, 1094 1084 "User locale preference updated" 1095 1085 );
+32 -38
crates/tranquil-pds/src/api/server/totp.rs
··· 1 1 use crate::api::EmptyResponse; 2 2 use crate::api::error::ApiError; 3 - use crate::auth::RequiredAuth; 3 + use crate::auth::{Active, Auth}; 4 4 use crate::auth::{ 5 5 decrypt_totp_secret, encrypt_totp_secret, generate_backup_codes, generate_qr_png_base64, 6 6 generate_totp_secret, generate_totp_uri, hash_backup_code, is_backup_code_format, ··· 28 28 29 29 pub async fn create_totp_secret( 30 30 State(state): State<AppState>, 31 - auth: RequiredAuth, 31 + auth: Auth<Active>, 32 32 ) -> Result<Response, ApiError> { 33 - let auth_user = auth.0.require_user()?.require_active()?; 34 - match state.user_repo.get_totp_record(&auth_user.did).await { 33 + match state.user_repo.get_totp_record(&auth.did).await { 35 34 Ok(Some(record)) if record.verified => return Err(ApiError::TotpAlreadyEnabled), 36 35 Ok(_) => {} 37 36 Err(e) => { ··· 44 43 45 44 let handle = state 46 45 .user_repo 47 - .get_handle_by_did(&auth_user.did) 46 + .get_handle_by_did(&auth.did) 48 47 .await 49 48 .map_err(|e| { 50 49 error!("DB error fetching handle: {:?}", e); ··· 67 66 68 67 state 69 68 .user_repo 70 - .upsert_totp_secret(&auth_user.did, &encrypted_secret, ENCRYPTION_VERSION) 69 + .upsert_totp_secret(&auth.did, &encrypted_secret, ENCRYPTION_VERSION) 71 70 .await 72 71 .map_err(|e| { 73 72 error!("Failed to store TOTP secret: {:?}", e); ··· 76 75 77 76 let secret_base32 = base32::encode(base32::Alphabet::Rfc4648 { padding: false }, &secret); 78 77 79 - info!(did = %&auth_user.did, "TOTP secret created (pending verification)"); 78 + info!(did = %&auth.did, "TOTP secret created (pending verification)"); 80 79 81 80 Ok(Json(CreateTotpSecretResponse { 82 81 secret: secret_base32, ··· 99 98 100 99 pub async fn enable_totp( 101 100 State(state): State<AppState>, 102 - auth: RequiredAuth, 101 + auth: Auth<Active>, 103 102 Json(input): Json<EnableTotpInput>, 104 103 ) -> Result<Response, ApiError> { 105 - let auth_user = auth.0.require_user()?.require_active()?; 106 104 if !state 107 - .check_rate_limit(RateLimitKind::TotpVerify, &auth_user.did) 105 + .check_rate_limit(RateLimitKind::TotpVerify, &auth.did) 108 106 .await 109 107 { 110 - warn!(did = %&auth_user.did, "TOTP verification rate limit exceeded"); 108 + warn!(did = %&auth.did, "TOTP verification rate limit exceeded"); 111 109 return Err(ApiError::RateLimitExceeded(None)); 112 110 } 113 111 114 - let totp_record = match state.user_repo.get_totp_record(&auth_user.did).await { 112 + let totp_record = match state.user_repo.get_totp_record(&auth.did).await { 115 113 Ok(Some(row)) => row, 116 114 Ok(None) => return Err(ApiError::TotpNotEnabled), 117 115 Err(e) => { ··· 152 150 153 151 state 154 152 .user_repo 155 - .enable_totp_with_backup_codes(&auth_user.did, &backup_hashes) 153 + .enable_totp_with_backup_codes(&auth.did, &backup_hashes) 156 154 .await 157 155 .map_err(|e| { 158 156 error!("Failed to enable TOTP: {:?}", e); 159 157 ApiError::InternalError(None) 160 158 })?; 161 159 162 - info!(did = %&auth_user.did, "TOTP enabled with {} backup codes", backup_codes.len()); 160 + info!(did = %&auth.did, "TOTP enabled with {} backup codes", backup_codes.len()); 163 161 164 162 Ok(Json(EnableTotpResponse { backup_codes }).into_response()) 165 163 } ··· 172 170 173 171 pub async fn disable_totp( 174 172 State(state): State<AppState>, 175 - auth: RequiredAuth, 173 + auth: Auth<Active>, 176 174 Json(input): Json<DisableTotpInput>, 177 175 ) -> Result<Response, ApiError> { 178 - let auth_user = auth.0.require_user()?.require_active()?; 179 - if !crate::api::server::reauth::check_legacy_session_mfa(&*state.session_repo, &auth_user.did) 180 - .await 176 + if !crate::api::server::reauth::check_legacy_session_mfa(&*state.session_repo, &auth.did).await 181 177 { 182 178 return Ok(crate::api::server::reauth::legacy_mfa_required_response( 183 179 &*state.user_repo, 184 180 &*state.session_repo, 185 - &auth_user.did, 181 + &auth.did, 186 182 ) 187 183 .await); 188 184 } 189 185 190 186 if !state 191 - .check_rate_limit(RateLimitKind::TotpVerify, &auth_user.did) 187 + .check_rate_limit(RateLimitKind::TotpVerify, &auth.did) 192 188 .await 193 189 { 194 - warn!(did = %&auth_user.did, "TOTP verification rate limit exceeded"); 190 + warn!(did = %&auth.did, "TOTP verification rate limit exceeded"); 195 191 return Err(ApiError::RateLimitExceeded(None)); 196 192 } 197 193 198 194 let password_hash = state 199 195 .user_repo 200 - .get_password_hash_by_did(&auth_user.did) 196 + .get_password_hash_by_did(&auth.did) 201 197 .await 202 198 .map_err(|e| { 203 199 error!("DB error fetching user: {:?}", e); ··· 210 206 return Err(ApiError::InvalidPassword("Password is incorrect".into())); 211 207 } 212 208 213 - let totp_record = match state.user_repo.get_totp_record(&auth_user.did).await { 209 + let totp_record = match state.user_repo.get_totp_record(&auth.did).await { 214 210 Ok(Some(row)) if row.verified => row, 215 211 Ok(Some(_)) | Ok(None) => return Err(ApiError::TotpNotEnabled), 216 212 Err(e) => { ··· 221 217 222 218 let code = input.code.trim(); 223 219 let code_valid = if is_backup_code_format(code) { 224 - verify_backup_code_for_user(&state, &auth_user.did, code).await 220 + verify_backup_code_for_user(&state, &auth.did, code).await 225 221 } else { 226 222 let secret = decrypt_totp_secret( 227 223 &totp_record.secret_encrypted, ··· 242 238 243 239 state 244 240 .user_repo 245 - .delete_totp_and_backup_codes(&auth_user.did) 241 + .delete_totp_and_backup_codes(&auth.did) 246 242 .await 247 243 .map_err(|e| { 248 244 error!("Failed to delete TOTP: {:?}", e); 249 245 ApiError::InternalError(None) 250 246 })?; 251 247 252 - info!(did = %&auth_user.did, "TOTP disabled"); 248 + info!(did = %&auth.did, "TOTP disabled"); 253 249 254 250 Ok(EmptyResponse::ok().into_response()) 255 251 } ··· 264 260 265 261 pub async fn get_totp_status( 266 262 State(state): State<AppState>, 267 - auth: RequiredAuth, 263 + auth: Auth<Active>, 268 264 ) -> Result<Response, ApiError> { 269 - let auth_user = auth.0.require_user()?.require_active()?; 270 - let enabled = match state.user_repo.get_totp_record(&auth_user.did).await { 265 + let enabled = match state.user_repo.get_totp_record(&auth.did).await { 271 266 Ok(Some(row)) => row.verified, 272 267 Ok(None) => false, 273 268 Err(e) => { ··· 278 273 279 274 let backup_count = state 280 275 .user_repo 281 - .count_unused_backup_codes(&auth_user.did) 276 + .count_unused_backup_codes(&auth.did) 282 277 .await 283 278 .map_err(|e| { 284 279 error!("DB error counting backup codes: {:?}", e); ··· 307 302 308 303 pub async fn regenerate_backup_codes( 309 304 State(state): State<AppState>, 310 - auth: RequiredAuth, 305 + auth: Auth<Active>, 311 306 Json(input): Json<RegenerateBackupCodesInput>, 312 307 ) -> Result<Response, ApiError> { 313 - let auth_user = auth.0.require_user()?.require_active()?; 314 308 if !state 315 - .check_rate_limit(RateLimitKind::TotpVerify, &auth_user.did) 309 + .check_rate_limit(RateLimitKind::TotpVerify, &auth.did) 316 310 .await 317 311 { 318 - warn!(did = %&auth_user.did, "TOTP verification rate limit exceeded"); 312 + warn!(did = %&auth.did, "TOTP verification rate limit exceeded"); 319 313 return Err(ApiError::RateLimitExceeded(None)); 320 314 } 321 315 322 316 let password_hash = state 323 317 .user_repo 324 - .get_password_hash_by_did(&auth_user.did) 318 + .get_password_hash_by_did(&auth.did) 325 319 .await 326 320 .map_err(|e| { 327 321 error!("DB error fetching user: {:?}", e); ··· 334 328 return Err(ApiError::InvalidPassword("Password is incorrect".into())); 335 329 } 336 330 337 - let totp_record = match state.user_repo.get_totp_record(&auth_user.did).await { 331 + let totp_record = match state.user_repo.get_totp_record(&auth.did).await { 338 332 Ok(Some(row)) if row.verified => row, 339 333 Ok(Some(_)) | Ok(None) => return Err(ApiError::TotpNotEnabled), 340 334 Err(e) => { ··· 371 365 372 366 state 373 367 .user_repo 374 - .replace_backup_codes(&auth_user.did, &backup_hashes) 368 + .replace_backup_codes(&auth.did, &backup_hashes) 375 369 .await 376 370 .map_err(|e| { 377 371 error!("Failed to regenerate backup codes: {:?}", e); 378 372 ApiError::InternalError(None) 379 373 })?; 380 374 381 - info!(did = %&auth_user.did, "Backup codes regenerated"); 375 + info!(did = %&auth.did, "Backup codes regenerated"); 382 376 383 377 Ok(Json(RegenerateBackupCodesResponse { backup_codes }).into_response()) 384 378 }
+9 -12
crates/tranquil-pds/src/api/server/trusted_devices.rs
··· 11 11 use tranquil_db_traits::OAuthRepository; 12 12 use tranquil_types::DeviceId; 13 13 14 - use crate::auth::RequiredAuth; 14 + use crate::auth::{Active, Auth}; 15 15 use crate::state::AppState; 16 16 17 17 const TRUST_DURATION_DAYS: i64 = 30; ··· 73 73 74 74 pub async fn list_trusted_devices( 75 75 State(state): State<AppState>, 76 - auth: RequiredAuth, 76 + auth: Auth<Active>, 77 77 ) -> Result<Response, ApiError> { 78 - let auth_user = auth.0.require_user()?.require_active()?; 79 78 let rows = state 80 79 .oauth_repo 81 - .list_trusted_devices(&auth_user.did) 80 + .list_trusted_devices(&auth.did) 82 81 .await 83 82 .map_err(|e| { 84 83 error!("DB error: {:?}", e); ··· 112 111 113 112 pub async fn revoke_trusted_device( 114 113 State(state): State<AppState>, 115 - auth: RequiredAuth, 114 + auth: Auth<Active>, 116 115 Json(input): Json<RevokeTrustedDeviceInput>, 117 116 ) -> Result<Response, ApiError> { 118 - let auth_user = auth.0.require_user()?.require_active()?; 119 117 let device_id = DeviceId::from(input.device_id.clone()); 120 118 match state 121 119 .oauth_repo 122 - .device_belongs_to_user(&device_id, &auth_user.did) 120 + .device_belongs_to_user(&device_id, &auth.did) 123 121 .await 124 122 { 125 123 Ok(true) => {} ··· 141 139 ApiError::InternalError(None) 142 140 })?; 143 141 144 - info!(did = %&auth_user.did, device_id = %input.device_id, "Trusted device revoked"); 142 + info!(did = %&auth.did, device_id = %input.device_id, "Trusted device revoked"); 145 143 Ok(SuccessResponse::ok().into_response()) 146 144 } 147 145 ··· 154 152 155 153 pub async fn update_trusted_device( 156 154 State(state): State<AppState>, 157 - auth: RequiredAuth, 155 + auth: Auth<Active>, 158 156 Json(input): Json<UpdateTrustedDeviceInput>, 159 157 ) -> Result<Response, ApiError> { 160 - let auth_user = auth.0.require_user()?.require_active()?; 161 158 let device_id = DeviceId::from(input.device_id.clone()); 162 159 match state 163 160 .oauth_repo 164 - .device_belongs_to_user(&device_id, &auth_user.did) 161 + .device_belongs_to_user(&device_id, &auth.did) 165 162 .await 166 163 { 167 164 Ok(true) => {} ··· 183 180 ApiError::InternalError(None) 184 181 })?; 185 182 186 - info!(did = %auth_user.did, device_id = %input.device_id, "Trusted device updated"); 183 + info!(did = %auth.did, device_id = %input.device_id, "Trusted device updated"); 187 184 Ok(SuccessResponse::ok().into_response()) 188 185 } 189 186
+5 -8
crates/tranquil-pds/src/api/temp.rs
··· 1 1 use crate::api::error::ApiError; 2 - use crate::auth::{OptionalAuth, RequiredAuth}; 2 + use crate::auth::{Active, Auth, Permissive}; 3 3 use crate::state::AppState; 4 4 use axum::{ 5 5 Json, ··· 21 21 pub estimated_time_ms: Option<i64>, 22 22 } 23 23 24 - pub async fn check_signup_queue(auth: OptionalAuth) -> Response { 25 - if let Some(entity) = auth.0 26 - && let Some(user) = entity.as_user() 27 - && user.is_oauth 24 + pub async fn check_signup_queue(auth: Option<Auth<Permissive>>) -> Response { 25 + if let Some(ref user) = auth 26 + && user.is_oauth() 28 27 { 29 28 return ApiError::Forbidden.into_response(); 30 29 } ··· 50 49 51 50 pub async fn dereference_scope( 52 51 State(state): State<AppState>, 53 - auth: RequiredAuth, 52 + _auth: Auth<Active>, 54 53 Json(input): Json<DereferenceScopeInput>, 55 54 ) -> Result<Response, ApiError> { 56 - let _user = auth.0.require_user()?.require_active()?; 57 - 58 55 let scope_parts: Vec<&str> = input.scope.split_whitespace().collect(); 59 56 let mut resolved_scopes: Vec<String> = Vec::new(); 60 57
crates/tranquil-pds/src/auth/auth_extractor.rs

This file has not been changed.

+330 -82
crates/tranquil-pds/src/auth/extractor.rs
··· 1 + use std::marker::PhantomData; 2 + 1 3 use axum::{ 2 - extract::FromRequestParts, 4 + extract::{FromRequestParts, OptionalFromRequestParts}, 3 5 http::{StatusCode, header::AUTHORIZATION, request::Parts}, 4 6 response::{IntoResponse, Response}, 5 7 }; 6 8 use tracing::{debug, error, info}; 7 9 8 10 use super::{ 9 - AccountStatus, AuthenticatedUser, ServiceTokenClaims, ServiceTokenVerifier, is_service_token, 10 - validate_bearer_token_for_service_auth, 11 + AccountStatus, AuthSource, AuthenticatedUser, ServiceTokenClaims, ServiceTokenVerifier, 12 + is_service_token, validate_bearer_token_for_service_auth, 11 13 }; 12 14 use crate::api::error::ApiError; 15 + use crate::oauth::scopes::{RepoAction, ScopePermissions}; 13 16 use crate::state::AppState; 14 17 use crate::types::Did; 15 18 use crate::util::build_full_url; ··· 23 26 AccountDeactivated, 24 27 AccountTakedown, 25 28 AdminRequired, 29 + ServiceAuthNotAllowed, 30 + SigningKeyRequired, 31 + InsufficientScope(String), 26 32 OAuthExpiredToken(String), 27 33 UseDpopNonce(String), 28 34 InvalidDpopProof(String), ··· 53 59 })), 54 60 ) 55 61 .into_response(), 62 + Self::InsufficientScope(msg) => ApiError::InsufficientScope(Some(msg)).into_response(), 56 63 other => ApiError::from(other).into_response(), 57 64 } 58 65 } ··· 112 119 None 113 120 } 114 121 115 - pub enum AuthenticatedEntity { 116 - User(AuthenticatedUser), 117 - Service { 118 - did: Did, 119 - claims: ServiceTokenClaims, 120 - }, 122 + pub trait AuthPolicy: Send + Sync + 'static { 123 + fn validate(user: &AuthenticatedUser) -> Result<(), AuthError>; 121 124 } 122 125 123 - impl AuthenticatedEntity { 124 - pub fn did(&self) -> &Did { 125 - match self { 126 - Self::User(user) => &user.did, 127 - Self::Service { did, .. } => did, 128 - } 129 - } 126 + pub struct Permissive; 130 127 131 - pub fn as_user(&self) -> Option<&AuthenticatedUser> { 132 - match self { 133 - Self::User(user) => Some(user), 134 - Self::Service { .. } => None, 135 - } 128 + impl AuthPolicy for Permissive { 129 + fn validate(_user: &AuthenticatedUser) -> Result<(), AuthError> { 130 + Ok(()) 136 131 } 132 + } 137 133 138 - pub fn as_service(&self) -> Option<(&Did, &ServiceTokenClaims)> { 139 - match self { 140 - Self::User(_) => None, 141 - Self::Service { did, claims } => Some((did, claims)), 142 - } 143 - } 134 + pub struct Active; 144 135 145 - pub fn require_user(&self) -> Result<&AuthenticatedUser, ApiError> { 146 - match self { 147 - Self::User(user) => Ok(user), 148 - Self::Service { .. } => Err(ApiError::AuthenticationFailed(Some( 149 - "User authentication required".to_string(), 150 - ))), 136 + impl AuthPolicy for Active { 137 + fn validate(user: &AuthenticatedUser) -> Result<(), AuthError> { 138 + if user.status.is_deactivated() { 139 + return Err(AuthError::AccountDeactivated); 151 140 } 141 + if user.status.is_takendown() { 142 + return Err(AuthError::AccountTakedown); 143 + } 144 + Ok(()) 152 145 } 146 + } 153 147 154 - pub fn require_service(&self) -> Result<(&Did, &ServiceTokenClaims), ApiError> { 155 - match self { 156 - Self::User(_) => Err(ApiError::AuthenticationFailed(Some( 157 - "Service authentication required".to_string(), 158 - ))), 159 - Self::Service { did, claims } => Ok((did, claims)), 148 + pub struct NotTakendown; 149 + 150 + impl AuthPolicy for NotTakendown { 151 + fn validate(user: &AuthenticatedUser) -> Result<(), AuthError> { 152 + if user.status.is_takendown() { 153 + return Err(AuthError::AccountTakedown); 160 154 } 155 + Ok(()) 161 156 } 157 + } 162 158 163 - pub fn require_service_lxm( 164 - &self, 165 - expected_lxm: &str, 166 - ) -> Result<(&Did, &ServiceTokenClaims), ApiError> { 167 - let (did, claims) = self.require_service()?; 168 - match &claims.lxm { 169 - Some(lxm) if lxm == "*" || lxm == expected_lxm => Ok((did, claims)), 170 - Some(lxm) => Err(ApiError::AuthorizationError(format!( 171 - "Token lxm '{}' does not permit '{}'", 172 - lxm, expected_lxm 173 - ))), 174 - None => Err(ApiError::AuthorizationError( 175 - "Token missing lxm claim".to_string(), 176 - )), 177 - } 159 + pub struct AnyUser; 160 + 161 + impl AuthPolicy for AnyUser { 162 + fn validate(_user: &AuthenticatedUser) -> Result<(), AuthError> { 163 + Ok(()) 178 164 } 165 + } 179 166 180 - pub fn into_user(self) -> Result<AuthenticatedUser, ApiError> { 181 - match self { 182 - Self::User(user) => Ok(user), 183 - Self::Service { .. } => Err(ApiError::AuthenticationFailed(Some( 184 - "User authentication required".to_string(), 185 - ))), 167 + pub struct Admin; 168 + 169 + impl AuthPolicy for Admin { 170 + fn validate(user: &AuthenticatedUser) -> Result<(), AuthError> { 171 + if user.status.is_deactivated() { 172 + return Err(AuthError::AccountDeactivated); 186 173 } 174 + if user.status.is_takendown() { 175 + return Err(AuthError::AccountTakedown); 176 + } 177 + if !user.is_admin { 178 + return Err(AuthError::AdminRequired); 179 + } 180 + Ok(()) 187 181 } 188 182 } 189 183 ··· 246 240 key_bytes: user_info.key_bytes.and_then(|kb| { 247 241 crate::config::decrypt_key(&kb, user_info.encryption_version).ok() 248 242 }), 249 - is_oauth: true, 250 243 is_admin: user_info.is_admin, 251 244 status, 252 245 scope: result.scope, 253 246 controller_did: None, 247 + auth_source: AuthSource::OAuth, 254 248 }) 255 249 } 256 250 Err(crate::oauth::OAuthError::ExpiredToken(msg)) => Err(AuthError::OAuthExpiredToken(msg)), ··· 262 256 } 263 257 } 264 258 265 - async fn verify_service_token(token: &str) -> Result<(Did, ServiceTokenClaims), AuthError> { 259 + async fn verify_service_token(token: &str) -> Result<ServiceTokenClaims, AuthError> { 266 260 let verifier = ServiceTokenVerifier::new(); 267 261 let claims = verifier 268 262 .verify_service_token(token, None) ··· 272 266 AuthError::AuthenticationFailed 273 267 })?; 274 268 275 - let did: Did = claims 276 - .iss 277 - .parse() 278 - .map_err(|_| AuthError::AuthenticationFailed)?; 269 + debug!("Service token verified for DID: {}", claims.iss); 270 + Ok(claims) 271 + } 279 272 280 - debug!("Service token verified for DID: {}", did); 281 - 282 - Ok((did, claims)) 273 + enum ExtractedAuth { 274 + User(AuthenticatedUser), 275 + Service(ServiceTokenClaims), 283 276 } 284 277 285 278 async fn extract_auth_internal( 286 279 parts: &mut Parts, 287 280 state: &AppState, 288 - ) -> Result<AuthenticatedEntity, AuthError> { 281 + ) -> Result<ExtractedAuth, AuthError> { 289 282 let auth_header = parts 290 283 .headers 291 284 .get(AUTHORIZATION) ··· 297 290 extract_auth_token_from_header(Some(auth_header)).ok_or(AuthError::InvalidFormat)?; 298 291 299 292 if is_service_token(&extracted.token) { 300 - let (did, claims) = verify_service_token(&extracted.token).await?; 301 - return Ok(AuthenticatedEntity::Service { did, claims }); 293 + let claims = verify_service_token(&extracted.token).await?; 294 + return Ok(ExtractedAuth::Service(claims)); 302 295 } 303 296 304 297 let dpop_proof = parts.headers.get("DPoP").and_then(|h| h.to_str().ok()); ··· 306 299 let uri = build_full_url(&parts.uri.to_string()); 307 300 308 301 match validate_bearer_token_for_service_auth(state.user_repo.as_ref(), &extracted.token).await { 309 - Ok(user) if !user.is_oauth => { 310 - return Ok(AuthenticatedEntity::User(user)); 302 + Ok(user) if !user.auth_source.is_oauth() => { 303 + return Ok(ExtractedAuth::User(user)); 311 304 } 312 305 Ok(_) => {} 313 306 Err(super::TokenValidationError::TokenExpired) => { ··· 319 312 320 313 let user = verify_oauth_token_and_build_user(state, &extracted.token, dpop_proof, method, &uri) 321 314 .await?; 315 + Ok(ExtractedAuth::User(user)) 316 + } 322 317 323 - Ok(AuthenticatedEntity::User(user)) 318 + async fn extract_user_auth_internal( 319 + parts: &mut Parts, 320 + state: &AppState, 321 + ) -> Result<AuthenticatedUser, AuthError> { 322 + match extract_auth_internal(parts, state).await? { 323 + ExtractedAuth::User(user) => Ok(user), 324 + ExtractedAuth::Service(_) => Err(AuthError::ServiceAuthNotAllowed), 325 + } 324 326 } 325 327 326 - pub struct RequiredAuth(pub AuthenticatedEntity); 328 + pub struct Auth<P: AuthPolicy = Active>(pub AuthenticatedUser, PhantomData<P>); 327 329 328 - impl FromRequestParts<AppState> for RequiredAuth { 330 + impl<P: AuthPolicy> Auth<P> { 331 + pub fn into_inner(self) -> AuthenticatedUser { 332 + self.0 333 + } 334 + 335 + pub fn needs_scope_check(&self) -> bool { 336 + self.0.is_oauth() 337 + } 338 + 339 + pub fn permissions(&self) -> ScopePermissions { 340 + self.0.permissions() 341 + } 342 + 343 + #[allow(clippy::result_large_err)] 344 + pub fn check_repo_scope(&self, action: RepoAction, collection: &str) -> Result<(), Response> { 345 + if !self.needs_scope_check() { 346 + return Ok(()); 347 + } 348 + self.permissions() 349 + .assert_repo(action, collection) 350 + .map_err(|e| ApiError::InsufficientScope(Some(e.to_string())).into_response()) 351 + } 352 + } 353 + 354 + impl<P: AuthPolicy> std::ops::Deref for Auth<P> { 355 + type Target = AuthenticatedUser; 356 + 357 + fn deref(&self) -> &Self::Target { 358 + &self.0 359 + } 360 + } 361 + 362 + impl<P: AuthPolicy> FromRequestParts<AppState> for Auth<P> { 329 363 type Rejection = AuthError; 330 364 331 365 async fn from_request_parts( 332 366 parts: &mut Parts, 333 367 state: &AppState, 334 368 ) -> Result<Self, Self::Rejection> { 335 - extract_auth_internal(parts, state).await.map(RequiredAuth) 369 + let user = extract_user_auth_internal(parts, state).await?; 370 + P::validate(&user)?; 371 + Ok(Auth(user, PhantomData)) 336 372 } 337 373 } 338 374 339 - pub struct OptionalAuth(pub Option<AuthenticatedEntity>); 375 + impl<P: AuthPolicy> OptionalFromRequestParts<AppState> for Auth<P> { 376 + type Rejection = AuthError; 340 377 341 - impl FromRequestParts<AppState> for OptionalAuth { 342 - type Rejection = std::convert::Infallible; 378 + async fn from_request_parts( 379 + parts: &mut Parts, 380 + state: &AppState, 381 + ) -> Result<Option<Self>, Self::Rejection> { 382 + match extract_user_auth_internal(parts, state).await { 383 + Ok(user) => { 384 + P::validate(&user)?; 385 + Ok(Some(Auth(user, PhantomData))) 386 + } 387 + Err(AuthError::MissingToken) => Ok(None), 388 + Err(e) => Err(e), 389 + } 390 + } 391 + } 343 392 393 + pub struct ServiceAuth { 394 + pub did: Did, 395 + pub claims: ServiceTokenClaims, 396 + } 397 + 398 + impl ServiceAuth { 399 + pub fn require_lxm(&self, expected_lxm: &str) -> Result<(), ApiError> { 400 + match &self.claims.lxm { 401 + Some(lxm) if lxm == "*" || lxm == expected_lxm => Ok(()), 402 + Some(lxm) => Err(ApiError::AuthorizationError(format!( 403 + "Token lxm '{}' does not permit '{}'", 404 + lxm, expected_lxm 405 + ))), 406 + None => Err(ApiError::AuthorizationError( 407 + "Token missing lxm claim".to_string(), 408 + )), 409 + } 410 + } 411 + } 412 + 413 + impl FromRequestParts<AppState> for ServiceAuth { 414 + type Rejection = AuthError; 415 + 344 416 async fn from_request_parts( 345 417 parts: &mut Parts, 346 418 state: &AppState, 347 419 ) -> Result<Self, Self::Rejection> { 348 - Ok(OptionalAuth(extract_auth_internal(parts, state).await.ok())) 420 + match extract_auth_internal(parts, state).await? { 421 + ExtractedAuth::Service(claims) => { 422 + let did: Did = claims 423 + .iss 424 + .parse() 425 + .map_err(|_| AuthError::AuthenticationFailed)?; 426 + Ok(ServiceAuth { did, claims }) 427 + } 428 + ExtractedAuth::User(_) => Err(AuthError::AuthenticationFailed), 429 + } 430 + } 431 + } 432 + 433 + pub enum AuthAny<P: AuthPolicy = Active> { 434 + User(Auth<P>), 435 + Service(ServiceAuth), 436 + } 437 + 438 + impl<P: AuthPolicy> AuthAny<P> { 439 + pub fn did(&self) -> &Did { 440 + match self { 441 + Self::User(auth) => &auth.did, 442 + Self::Service(auth) => &auth.did, 443 + } 444 + } 445 + 446 + pub fn as_user(&self) -> Option<&Auth<P>> { 447 + match self { 448 + Self::User(auth) => Some(auth), 449 + Self::Service(_) => None, 450 + } 451 + } 452 + 453 + pub fn as_service(&self) -> Option<&ServiceAuth> { 454 + match self { 455 + Self::User(_) => None, 456 + Self::Service(auth) => Some(auth), 457 + } 458 + } 459 + 460 + pub fn is_service(&self) -> bool { 461 + matches!(self, Self::Service(_)) 462 + } 463 + 464 + pub fn require_lxm(&self, expected_lxm: &str) -> Result<(), ApiError> { 465 + match self { 466 + Self::User(_) => Ok(()), 467 + Self::Service(auth) => auth.require_lxm(expected_lxm), 468 + } 469 + } 470 + } 471 + 472 + impl<P: AuthPolicy> FromRequestParts<AppState> for AuthAny<P> { 473 + type Rejection = AuthError; 474 + 475 + async fn from_request_parts( 476 + parts: &mut Parts, 477 + state: &AppState, 478 + ) -> Result<Self, Self::Rejection> { 479 + match extract_auth_internal(parts, state).await? { 480 + ExtractedAuth::User(user) => { 481 + P::validate(&user)?; 482 + Ok(AuthAny::User(Auth(user, PhantomData))) 483 + } 484 + ExtractedAuth::Service(claims) => { 485 + let did: Did = claims 486 + .iss 487 + .parse() 488 + .map_err(|_| AuthError::AuthenticationFailed)?; 489 + Ok(AuthAny::Service(ServiceAuth { did, claims })) 490 + } 491 + } 492 + } 493 + } 494 + 495 + impl<P: AuthPolicy> OptionalFromRequestParts<AppState> for AuthAny<P> { 496 + type Rejection = AuthError; 497 + 498 + async fn from_request_parts( 499 + parts: &mut Parts, 500 + state: &AppState, 501 + ) -> Result<Option<Self>, Self::Rejection> { 502 + match extract_auth_internal(parts, state).await { 503 + Ok(ExtractedAuth::User(user)) => { 504 + P::validate(&user)?; 505 + Ok(Some(AuthAny::User(Auth(user, PhantomData)))) 506 + } 507 + Ok(ExtractedAuth::Service(claims)) => { 508 + let did: Did = claims 509 + .iss 510 + .parse() 511 + .map_err(|_| AuthError::AuthenticationFailed)?; 512 + Ok(Some(AuthAny::Service(ServiceAuth { did, claims }))) 513 + } 514 + Err(AuthError::MissingToken) => Ok(None), 515 + Err(e) => Err(e), 516 + } 517 + } 518 + } 519 + 520 + pub struct SigningAuth<P: AuthPolicy = Active> { 521 + pub did: Did, 522 + pub key_bytes: Vec<u8>, 523 + pub is_admin: bool, 524 + pub status: AccountStatus, 525 + pub scope: Option<String>, 526 + pub controller_did: Option<Did>, 527 + is_oauth: bool, 528 + _policy: PhantomData<P>, 529 + } 530 + 531 + impl<P: AuthPolicy> SigningAuth<P> { 532 + pub fn needs_scope_check(&self) -> bool { 533 + self.is_oauth 534 + } 535 + 536 + pub fn permissions(&self) -> ScopePermissions { 537 + if let Some(ref scope) = self.scope 538 + && scope != super::SCOPE_ACCESS 539 + { 540 + return ScopePermissions::from_scope_string(Some(scope)); 541 + } 542 + if !self.is_oauth { 543 + return ScopePermissions::from_scope_string(Some("atproto")); 544 + } 545 + ScopePermissions::from_scope_string(self.scope.as_deref()) 546 + } 547 + 548 + #[allow(clippy::result_large_err)] 549 + pub fn check_repo_scope(&self, action: RepoAction, collection: &str) -> Result<(), Response> { 550 + if !self.needs_scope_check() { 551 + return Ok(()); 552 + } 553 + self.permissions() 554 + .assert_repo(action, collection) 555 + .map_err(|e| ApiError::InsufficientScope(Some(e.to_string())).into_response()) 556 + } 557 + } 558 + 559 + impl<P: AuthPolicy> FromRequestParts<AppState> for SigningAuth<P> { 560 + type Rejection = AuthError; 561 + 562 + async fn from_request_parts( 563 + parts: &mut Parts, 564 + state: &AppState, 565 + ) -> Result<Self, Self::Rejection> { 566 + let user = extract_user_auth_internal(parts, state).await?; 567 + P::validate(&user)?; 568 + 569 + let key_bytes = match user.key_bytes { 570 + Some(kb) => kb, 571 + None => { 572 + let user_with_key = state 573 + .user_repo 574 + .get_with_key_by_did(&user.did) 575 + .await 576 + .ok() 577 + .flatten() 578 + .ok_or(AuthError::SigningKeyRequired)?; 579 + crate::config::decrypt_key( 580 + &user_with_key.key_bytes, 581 + user_with_key.encryption_version, 582 + ) 583 + .map_err(|_| AuthError::SigningKeyRequired)? 584 + } 585 + }; 586 + 587 + Ok(SigningAuth { 588 + did: user.did, 589 + key_bytes, 590 + is_admin: user.is_admin, 591 + status: user.status, 592 + scope: user.scope, 593 + controller_did: user.controller_did, 594 + is_oauth: user.auth_source.is_oauth(), 595 + _policy: PhantomData, 596 + }) 349 597 } 350 598 } 351 599
+75 -7
crates/tranquil-pds/src/auth/mod.rs
··· 3 3 use std::time::Duration; 4 4 5 5 use crate::AccountStatus; 6 + use crate::api::ApiError; 6 7 use crate::cache::Cache; 7 8 use crate::oauth::scopes::ScopePermissions; 8 9 use crate::types::Did; ··· 16 17 pub mod webauthn; 17 18 18 19 pub use extractor::{ 19 - AuthError, AuthenticatedEntity, ExtractedToken, OptionalAuth, RequiredAuth, 20 - extract_auth_token_from_header, extract_bearer_token_from_header, 20 + Active, Admin, AnyUser, Auth, AuthAny, AuthError, AuthPolicy, ExtractedToken, NotTakendown, 21 + Permissive, ServiceAuth, SigningAuth, extract_auth_token_from_header, 22 + extract_bearer_token_from_header, 21 23 }; 22 24 pub use service::{ServiceTokenClaims, ServiceTokenVerifier, is_service_token}; 23 25 ··· 93 95 } 94 96 } 95 97 98 + pub enum AuthSource { 99 + Session, 100 + OAuth, 101 + Service { claims: ServiceTokenClaims }, 102 + } 103 + 104 + impl AuthSource { 105 + pub fn is_oauth(&self) -> bool { 106 + matches!(self, Self::OAuth) 107 + } 108 + 109 + pub fn is_service(&self) -> bool { 110 + matches!(self, Self::Service { .. }) 111 + } 112 + 113 + pub fn service_claims(&self) -> Option<&ServiceTokenClaims> { 114 + match self { 115 + Self::Service { claims } => Some(claims), 116 + _ => None, 117 + } 118 + } 119 + } 120 + 96 121 pub struct AuthenticatedUser { 97 122 pub did: Did, 98 123 pub key_bytes: Option<Vec<u8>>, 99 - pub is_oauth: bool, 100 124 pub is_admin: bool, 101 125 pub status: AccountStatus, 102 126 pub scope: Option<String>, 103 127 pub controller_did: Option<Did>, 128 + pub auth_source: AuthSource, 104 129 } 105 130 106 131 impl AuthenticatedUser { 132 + pub fn is_oauth(&self) -> bool { 133 + self.auth_source.is_oauth() 134 + } 107 135 136 + pub fn is_service(&self) -> bool { 137 + self.auth_source.is_service() 138 + } 108 139 140 + pub fn service_claims(&self) -> Option<&ServiceTokenClaims> { 141 + self.auth_source.service_claims() 142 + } 109 143 144 + pub fn require_lxm(&self, expected_lxm: &str) -> Result<(), ApiError> { 145 + match self.auth_source.service_claims() { 146 + Some(claims) => match &claims.lxm { 147 + Some(lxm) if lxm == "*" || lxm == expected_lxm => Ok(()), 148 + Some(lxm) => Err(ApiError::AuthorizationError(format!( 149 + "Token lxm '{}' does not permit '{}'", 150 + lxm, expected_lxm 151 + ))), 152 + None => Err(ApiError::AuthorizationError( 153 + "Token missing lxm claim".to_string(), 154 + )), 155 + }, 156 + None => Ok(()), 157 + } 158 + } 159 + 160 + pub fn require_user(&self) -> Result<&Self, ApiError> { 161 + if self.is_service() { 162 + return Err(ApiError::AuthenticationFailed(Some( 163 + "User authentication required".to_string(), 164 + ))); 165 + } 166 + Ok(self) 167 + } 168 + 169 + pub fn as_user(&self) -> Option<&Self> { 170 + if self.is_service() { None } else { Some(self) } 171 + } 172 + } 173 + 174 + impl AuthenticatedUser { 175 + 176 + 177 + 110 178 { 111 179 return ScopePermissions::from_scope_string(Some(scope)); 112 180 } 113 - if !self.is_oauth { 181 + if !self.is_oauth() { 114 182 return ScopePermissions::from_scope_string(Some("atproto")); 115 183 } 116 184 ScopePermissions::from_scope_string(self.scope.as_deref()) ··· 348 416 return Ok(AuthenticatedUser { 349 417 did: did.clone(), 350 418 key_bytes: Some(decrypted_key), 351 - is_oauth: false, 352 419 is_admin, 353 420 status, 354 421 scope: token_data.claims.scope.clone(), 355 422 controller_did, 423 + auth_source: AuthSource::Session, 356 424 }); 357 425 } 358 426 } ··· 396 464 return Ok(AuthenticatedUser { 397 465 did: Did::new_unchecked(oauth_token.did), 398 466 key_bytes, 399 - is_oauth: true, 400 467 is_admin: oauth_token.is_admin, 401 468 status, 402 469 scope: oauth_info.scope, 403 470 controller_did: oauth_info.controller_did.map(Did::new_unchecked), 471 + auth_source: AuthSource::OAuth, 404 472 }); 405 473 } else { 406 474 return Err(TokenValidationError::TokenExpired); ··· 480 548 Ok(AuthenticatedUser { 481 549 did: Did::new_unchecked(result.did), 482 550 key_bytes, 483 - is_oauth: true, 484 551 is_admin: user_info.is_admin, 485 552 status, 486 553 scope: result.scope, 487 554 controller_did: None, 555 + auth_source: AuthSource::OAuth, 488 556 }) 489 557 } 490 558 Err(crate::oauth::OAuthError::ExpiredToken(_)) => {
crates/tranquil-pds/src/lib.rs

This file has not been changed.

+2 -27
crates/tranquil-pds/src/oauth/endpoints/authorize.rs
··· 3644 3644 pub async fn establish_session( 3645 3645 State(state): State<AppState>, 3646 3646 headers: HeaderMap, 3647 - auth: crate::auth::RequiredAuth, 3647 + auth: crate::auth::Auth<crate::auth::Active>, 3648 3648 ) -> Response { 3649 - let user = match auth.0.require_user() { 3650 - Ok(u) => match u.require_active() { 3651 - Ok(u) => u, 3652 - Err(_) => { 3653 - return ( 3654 - StatusCode::FORBIDDEN, 3655 - Json(serde_json::json!({ 3656 - "error": "access_denied", 3657 - "error_description": "Account is deactivated" 3658 - })), 3659 - ) 3660 - .into_response(); 3661 - } 3662 - }, 3663 - Err(_) => { 3664 - return ( 3665 - StatusCode::UNAUTHORIZED, 3666 - Json(serde_json::json!({ 3667 - "error": "invalid_token", 3668 - "error_description": "Authentication required" 3669 - })), 3670 - ) 3671 - .into_response(); 3672 - } 3673 - }; 3674 - let did = &user.did; 3649 + let did = &auth.did; 3675 3650 3676 3651 let existing_device = extract_device_cookie(&headers); 3677 3652
+3 -26
crates/tranquil-pds/src/oauth/endpoints/delegation.rs
··· 1 - use crate::auth::RequiredAuth; 1 + use crate::auth::{Active, Auth}; 2 2 use crate::delegation::DelegationActionType; 3 3 use crate::state::{AppState, RateLimitKind}; 4 4 use crate::types::PlainPassword; ··· 463 463 pub async fn delegation_auth_token( 464 464 State(state): State<AppState>, 465 465 headers: HeaderMap, 466 - auth: RequiredAuth, 466 + auth: Auth<Active>, 467 467 Json(form): Json<DelegationTokenAuthSubmit>, 468 468 ) -> Response { 469 - let user = match auth.0.require_user() { 470 - Ok(u) => match u.require_active() { 471 - Ok(u) => u, 472 - Err(_) => { 473 - return Json(DelegationAuthResponse { 474 - success: false, 475 - needs_totp: None, 476 - redirect_uri: None, 477 - error: Some("Account is deactivated".to_string()), 478 - }) 479 - .into_response(); 480 - } 481 - }, 482 - Err(_) => { 483 - return Json(DelegationAuthResponse { 484 - success: false, 485 - needs_totp: None, 486 - redirect_uri: None, 487 - error: Some("Authentication required".to_string()), 488 - }) 489 - .into_response(); 490 - } 491 - }; 492 - let controller_did = &user.did; 469 + let controller_did = &auth.did; 493 470 494 471 let delegated_did: Did = match form.delegated_did.parse() { 495 472 Ok(d) => d,
+1 -1
crates/tranquil-pds/src/oauth/verify.rs
··· 396 396 token: &str, 397 397 ) -> Result<LegacyAuthResult, ()> { 398 398 match crate::auth::validate_bearer_token(user_repo, token).await { 399 - Ok(user) if !user.is_oauth => Ok(LegacyAuthResult { did: user.did }), 399 + Ok(user) if !user.is_oauth() => Ok(LegacyAuthResult { did: user.did }), 400 400 _ => Err(()), 401 401 } 402 402 }
+2 -4
crates/tranquil-pds/src/sso/endpoints.rs
··· 644 644 645 645 pub async fn get_linked_accounts( 646 646 State(state): State<AppState>, 647 - auth: crate::auth::RequiredAuth, 647 + auth: crate::auth::Auth<crate::auth::Active>, 648 648 ) -> Result<Json<LinkedAccountsResponse>, ApiError> { 649 - let auth = auth.0.require_user()?.require_active()?; 650 649 let identities = state 651 650 .sso_repo 652 651 .get_external_identities_by_did(&auth.did) ··· 680 679 681 680 pub async fn unlink_account( 682 681 State(state): State<AppState>, 683 - auth: crate::auth::RequiredAuth, 682 + auth: crate::auth::Auth<crate::auth::Active>, 684 683 Json(input): Json<UnlinkAccountRequest>, 685 684 ) -> Result<Json<UnlinkAccountResponse>, ApiError> { 686 - let auth = auth.0.require_user()?.require_active()?; 687 685 if !state 688 686 .check_rate_limit(RateLimitKind::SsoUnlink, auth.did.as_str()) 689 687 .await
crates/tranquil-pds/tests/auth_extractor.rs

This file has not been changed.

crates/tranquil-pds/tests/common/mod.rs

This file has not been changed.

crates/tranquil-pds/tests/oauth_security.rs

This file has not been changed.

crates/tranquil-scopes/Cargo.toml

This file has not been changed.

crates/tranquil-scopes/src/permission_set.rs

This file has not been changed.

crates/tranquil-scopes/src/permissions.rs

This file has not been changed.

crates/tranquil-storage/src/lib.rs

This file has not been changed.

frontend/src/lib/api.ts

This file has not been changed.

frontend/src/lib/auth.svelte.ts

This file has not been changed.

frontend/src/lib/migration/atproto-client.ts

This file has not been changed.

frontend/src/lib/migration/flow.svelte.ts

This file has not been changed.

frontend/src/lib/migration/offline-flow.svelte.ts

This file has not been changed.

frontend/src/lib/oauth.ts

This file has not been changed.

frontend/src/locales/en.json

This file has not been changed.

frontend/src/locales/fi.json

This file has not been changed.

frontend/src/locales/ja.json

This file has not been changed.

frontend/src/locales/ko.json

This file has not been changed.

frontend/src/locales/sv.json

This file has not been changed.

frontend/src/locales/zh.json

This file has not been changed.

frontend/src/routes/Migration.svelte

This file has not been changed.

frontend/src/routes/OAuthAccounts.svelte

This file has not been changed.

frontend/src/routes/OAuthConsent.svelte

This file has not been changed.

+4 -4
crates/tranquil-pds/src/api/server/service_auth.rs
··· 95 95 { 96 96 Ok(result) => crate::auth::AuthenticatedUser { 97 97 did: Did::new_unchecked(result.did), 98 - is_oauth: true, 99 98 is_admin: false, 100 99 status: AccountStatus::Active, 101 100 scope: result.scope, 102 101 key_bytes: None, 103 102 controller_did: None, 103 + auth_source: crate::auth::AuthSource::OAuth, 104 104 }, 105 105 Err(crate::oauth::OAuthError::UseDpopNonce(nonce)) => { 106 106 return ( ··· 131 131 }; 132 132 info!( 133 133 did = %&auth_user.did, 134 - is_oauth = auth_user.is_oauth, 134 + is_oauth = auth_user.is_oauth(), 135 135 has_key = auth_user.key_bytes.is_some(), 136 136 "getServiceAuth auth validated" 137 137 ); ··· 180 180 181 181 if let Some(method) = lxm { 182 182 if let Err(e) = crate::auth::scope_check::check_rpc_scope( 183 - auth_user.is_oauth, 183 + auth_user.is_oauth(), 184 184 auth_user.scope.as_deref(), 185 185 &params.aud, 186 186 method, 187 187 ) { 188 188 return e; 189 189 } 190 - } else if auth_user.is_oauth { 190 + } else if auth_user.is_oauth() { 191 191 let permissions = auth_user.permissions(); 192 192 if !permissions.has_full_access() { 193 193 return ApiError::InvalidRequest(

History

6 rounds 1 comment
sign up or login to add to the discussion
3 commits
expand
fix: oauth consolidation, include-scope improvements
fix: consolidate auth extractors & standardize usage
fix: match ref pds permission-levels for some endpoints
expand 0 comments
pull request successfully merged
3 commits
expand
fix: oauth consolidation, include-scope improvements
fix: consolidate auth extractors & standardize usage
fix: match ref pds permission-levels for some endpoints
expand 0 comments
lewis.moe submitted #3
2 commits
expand
fix: oauth consolidation, include-scope improvements
fix: consolidate auth extractors & standardize usage
expand 0 comments
2 commits
expand
fix: oauth consolidation, include-scope improvements
fix: consolidate auth extractors & standardize usage
expand 1 comment

so three things:

crates/tranquil-pds/src/auth/auth_extractor.rs does not seem to be used at all anywhere?

crates/tranquil-pds/src/api/temp.rs feels like it just ... shouldnt excist based on the name

and i still dont really like these extractors. the separation of inter service auth is weird to me. inter-service auth is a form of user auth. it shouldnt be separated out from the other types. the AuthExtractor should just be AuthExtractor(pub AuthenticatedUser).

i also discovered https://docs.rs/axum/0.8.8/axum/extract/trait.OptionalFromRequestParts.html which we should be able to reduce optional vs not optional with just an AuthExtractor vs Option.

principly id want whether or not its required that the account is active or not and whether its an admin account or not to both also be type safe configurations on the extractor. probably with generics of some sort. but i cant think of a specific design i like right now so. if you come up with one feel free to do it. otherwise we can do it later

1 commit
expand
fix: oauth consolidation, include-scope improvements
expand 0 comments
lewis.moe submitted #0
1 commit
expand
fix: oauth consolidation, include-scope improvements
expand 0 comments