From 6c5b5996b7ca348386ac3768c357a9ef7b350ae4 Mon Sep 17 00:00:00 2001 From: lewis Date: Fri, 23 Jan 2026 15:37:10 +0200 Subject: [PATCH] fix: consolidate auth extractors & standardize usage --- .../tranquil-pds/src/api/actor/preferences.rs | 19 +- .../src/api/admin/account/delete.rs | 38 +- .../src/api/admin/account/email.rs | 37 +- .../src/api/admin/account/info.rs | 42 +- .../src/api/admin/account/search.rs | 81 +- .../src/api/admin/account/update.rs | 75 +- crates/tranquil-pds/src/api/admin/config.rs | 4 +- crates/tranquil-pds/src/api/admin/invite.rs | 67 +- .../src/api/admin/server_stats.rs | 12 +- crates/tranquil-pds/src/api/admin/status.rs | 167 ++-- crates/tranquil-pds/src/api/backup.rs | 166 ++-- crates/tranquil-pds/src/api/delegation.rs | 213 +++-- crates/tranquil-pds/src/api/error.rs | 7 + crates/tranquil-pds/src/api/identity/did.rs | 190 ++-- .../src/api/identity/plc/request.rs | 47 +- .../tranquil-pds/src/api/identity/plc/sign.rs | 155 ++-- .../src/api/identity/plc/submit.rs | 119 ++- crates/tranquil-pds/src/api/moderation/mod.rs | 12 +- .../src/api/notification_prefs.rs | 180 ++-- crates/tranquil-pds/src/api/proxy.rs | 2 +- crates/tranquil-pds/src/api/repo/blob.rs | 109 +-- crates/tranquil-pds/src/api/repo/import.rs | 260 +++--- .../tranquil-pds/src/api/repo/record/batch.rs | 128 ++- .../src/api/repo/record/delete.rs | 75 +- .../tranquil-pds/src/api/repo/record/write.rs | 193 ++-- .../src/api/server/account_status.rs | 103 ++- .../src/api/server/app_password.rs | 248 +++--- crates/tranquil-pds/src/api/server/email.rs | 181 ++-- crates/tranquil-pds/src/api/server/invite.rs | 94 +- .../tranquil-pds/src/api/server/migration.rs | 99 +- .../tranquil-pds/src/api/server/passkeys.rs | 255 +++--- .../tranquil-pds/src/api/server/password.rs | 253 +++--- crates/tranquil-pds/src/api/server/reauth.rs | 272 +++--- .../src/api/server/service_auth.rs | 8 +- crates/tranquil-pds/src/api/server/session.rs | 351 ++++---- crates/tranquil-pds/src/api/server/totp.rs | 322 +++---- .../src/api/server/trusted_devices.rs | 112 +-- crates/tranquil-pds/src/api/temp.rs | 18 +- .../tranquil-pds/src/auth/auth_extractor.rs | 2 +- crates/tranquil-pds/src/auth/extractor.rs | 842 +++++++++--------- crates/tranquil-pds/src/auth/mod.rs | 83 +- .../src/oauth/endpoints/authorize.rs | 19 +- .../src/oauth/endpoints/delegation.rs | 14 +- crates/tranquil-pds/src/oauth/verify.rs | 2 +- crates/tranquil-pds/src/sso/endpoints.rs | 4 +- crates/tranquil-scopes/src/permission_set.rs | 90 +- crates/tranquil-scopes/src/permissions.rs | 1 - 47 files changed, 2890 insertions(+), 2881 deletions(-) diff --git a/crates/tranquil-pds/src/api/actor/preferences.rs b/crates/tranquil-pds/src/api/actor/preferences.rs index bd3b65b..3b8f345 100644 --- a/crates/tranquil-pds/src/api/actor/preferences.rs +++ b/crates/tranquil-pds/src/api/actor/preferences.rs @@ -1,5 +1,5 @@ use crate::api::error::ApiError; -use crate::auth::BearerAuthAllowDeactivated; +use crate::auth::{Active, Auth}; use crate::state::AppState; use axum::{ Json, @@ -32,13 +32,9 @@ fn get_age_from_datestring(birth_date: &str) -> Option { pub struct GetPreferencesOutput { pub preferences: Vec, } -pub async fn get_preferences( - State(state): State, - auth: BearerAuthAllowDeactivated, -) -> Response { - let auth_user = auth.0; - let has_full_access = auth_user.permissions().has_full_access(); - let user_id: uuid::Uuid = match state.user_repo.get_id_by_did(&auth_user.did).await { +pub async fn get_preferences(State(state): State, auth: Auth) -> Response { + let has_full_access = auth.permissions().has_full_access(); + let user_id: uuid::Uuid = match state.user_repo.get_id_by_did(&auth.did).await { Ok(Some(id)) => id, _ => { return ApiError::InternalError(Some("User not found".into())).into_response(); @@ -93,12 +89,11 @@ pub struct PutPreferencesInput { } pub async fn put_preferences( State(state): State, - auth: BearerAuthAllowDeactivated, + auth: Auth, Json(input): Json, ) -> Response { - let auth_user = auth.0; - let has_full_access = auth_user.permissions().has_full_access(); - let user_id: uuid::Uuid = match state.user_repo.get_id_by_did(&auth_user.did).await { + let has_full_access = auth.permissions().has_full_access(); + let user_id: uuid::Uuid = match state.user_repo.get_id_by_did(&auth.did).await { Ok(Some(id)) => id, _ => { return ApiError::InternalError(Some("User not found".into())).into_response(); diff --git a/crates/tranquil-pds/src/api/admin/account/delete.rs b/crates/tranquil-pds/src/api/admin/account/delete.rs index 78ad075..5342523 100644 --- a/crates/tranquil-pds/src/api/admin/account/delete.rs +++ b/crates/tranquil-pds/src/api/admin/account/delete.rs @@ -1,6 +1,6 @@ use crate::api::EmptyResponse; use crate::api::error::ApiError; -use crate::auth::BearerAuthAdmin; +use crate::auth::{Admin, Auth}; use crate::state::AppState; use crate::types::Did; use axum::{ @@ -18,28 +18,30 @@ pub struct DeleteAccountInput { pub async fn delete_account( State(state): State, - _auth: BearerAuthAdmin, + _auth: Auth, Json(input): Json, -) -> Response { +) -> Result { let did = &input.did; - let (user_id, handle) = match state.user_repo.get_id_and_handle_by_did(did).await { - Ok(Some(row)) => (row.id, row.handle), - Ok(None) => { - return ApiError::AccountNotFound.into_response(); - } - Err(e) => { + let (user_id, handle) = state + .user_repo + .get_id_and_handle_by_did(did) + .await + .map_err(|e| { error!("DB error in delete_account: {:?}", e); - return ApiError::InternalError(None).into_response(); - } - }; - if let Err(e) = state + ApiError::InternalError(None) + })? + .ok_or(ApiError::AccountNotFound) + .map(|row| (row.id, row.handle))?; + + state .user_repo .admin_delete_account_complete(user_id, did) .await - { - error!("Failed to delete account {}: {:?}", did, e); - return ApiError::InternalError(Some("Failed to delete account".into())).into_response(); - } + .map_err(|e| { + error!("Failed to delete account {}: {:?}", did, e); + ApiError::InternalError(Some("Failed to delete account".into())) + })?; + if let Err(e) = crate::api::repo::record::sequence_account_event(&state, did, false, Some("deleted")).await { @@ -49,5 +51,5 @@ pub async fn delete_account( ); } let _ = state.cache.delete(&format!("handle:{}", handle)).await; - EmptyResponse::ok().into_response() + Ok(EmptyResponse::ok().into_response()) } diff --git a/crates/tranquil-pds/src/api/admin/account/email.rs b/crates/tranquil-pds/src/api/admin/account/email.rs index 837f6b9..0819fd9 100644 --- a/crates/tranquil-pds/src/api/admin/account/email.rs +++ b/crates/tranquil-pds/src/api/admin/account/email.rs @@ -1,5 +1,5 @@ use crate::api::error::{ApiError, AtpJson}; -use crate::auth::BearerAuthAdmin; +use crate::auth::{Admin, Auth}; use crate::state::AppState; use crate::types::Did; use axum::{ @@ -28,29 +28,24 @@ pub struct SendEmailOutput { pub async fn send_email( State(state): State, - _auth: BearerAuthAdmin, + _auth: Auth, AtpJson(input): AtpJson, -) -> Response { +) -> Result { let content = input.content.trim(); if content.is_empty() { - return ApiError::InvalidRequest("content is required".into()).into_response(); + return Err(ApiError::InvalidRequest("content is required".into())); } - let user = match state.user_repo.get_by_did(&input.recipient_did).await { - Ok(Some(row)) => row, - Ok(None) => { - return ApiError::AccountNotFound.into_response(); - } - Err(e) => { + let user = state + .user_repo + .get_by_did(&input.recipient_did) + .await + .map_err(|e| { error!("DB error in send_email: {:?}", e); - return ApiError::InternalError(None).into_response(); - } - }; - let email = match user.email { - Some(e) => e, - None => { - return ApiError::NoEmail.into_response(); - } - }; + ApiError::InternalError(None) + })? + .ok_or(ApiError::AccountNotFound)?; + + let email = user.email.ok_or(ApiError::NoEmail)?; let (user_id, handle) = (user.id, user.handle); let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); let subject = input @@ -76,11 +71,11 @@ pub async fn send_email( handle, input.recipient_did ); - (StatusCode::OK, Json(SendEmailOutput { sent: true })).into_response() + Ok((StatusCode::OK, Json(SendEmailOutput { sent: true })).into_response()) } Err(e) => { warn!("Failed to enqueue admin email: {:?}", e); - (StatusCode::OK, Json(SendEmailOutput { sent: false })).into_response() + Ok((StatusCode::OK, Json(SendEmailOutput { sent: false })).into_response()) } } } diff --git a/crates/tranquil-pds/src/api/admin/account/info.rs b/crates/tranquil-pds/src/api/admin/account/info.rs index 7fd77e9..b1efcfc 100644 --- a/crates/tranquil-pds/src/api/admin/account/info.rs +++ b/crates/tranquil-pds/src/api/admin/account/info.rs @@ -1,5 +1,5 @@ use crate::api::error::ApiError; -use crate::auth::BearerAuthAdmin; +use crate::auth::{Admin, Auth}; use crate::state::AppState; use crate::types::{Did, Handle}; use axum::{ @@ -67,26 +67,23 @@ pub struct GetAccountInfosOutput { pub async fn get_account_info( State(state): State, - _auth: BearerAuthAdmin, + _auth: Auth, Query(params): Query, -) -> Response { - let account = match state +) -> Result { + let account = state .infra_repo .get_admin_account_info_by_did(¶ms.did) .await - { - Ok(Some(a)) => a, - Ok(None) => return ApiError::AccountNotFound.into_response(), - Err(e) => { + .map_err(|e| { error!("DB error in get_account_info: {:?}", e); - return ApiError::InternalError(None).into_response(); - } - }; + ApiError::InternalError(None) + })? + .ok_or(ApiError::AccountNotFound)?; let invited_by = get_invited_by(&state, account.id).await; let invites = get_invites_for_user(&state, account.id).await; - ( + Ok(( StatusCode::OK, Json(AccountInfo { did: account.did, @@ -105,7 +102,7 @@ pub async fn get_account_info( invites, }), ) - .into_response() + .into_response()) } async fn get_invited_by(state: &AppState, user_id: uuid::Uuid) -> Option { @@ -200,30 +197,27 @@ async fn get_invite_code_info(state: &AppState, code: &str) -> Option, - _auth: BearerAuthAdmin, + _auth: Auth, RawQuery(raw_query): RawQuery, -) -> Response { +) -> Result { let dids: Vec = crate::util::parse_repeated_query_param(raw_query.as_deref(), "dids") .into_iter() .filter(|d| !d.is_empty()) .collect(); if dids.is_empty() { - return ApiError::InvalidRequest("dids is required".into()).into_response(); + return Err(ApiError::InvalidRequest("dids is required".into())); } let dids_typed: Vec = dids.iter().filter_map(|d| d.parse().ok()).collect(); - let accounts = match state + let accounts = state .infra_repo .get_admin_account_infos_by_dids(&dids_typed) .await - { - Ok(accounts) => accounts, - Err(e) => { + .map_err(|e| { error!("Failed to fetch account infos: {:?}", e); - return ApiError::InternalError(None).into_response(); - } - }; + ApiError::InternalError(None) + })?; let user_ids: Vec = accounts.iter().map(|u| u.id).collect(); @@ -316,5 +310,5 @@ pub async fn get_account_infos( }) .collect(); - (StatusCode::OK, Json(GetAccountInfosOutput { infos })).into_response() + Ok((StatusCode::OK, Json(GetAccountInfosOutput { infos })).into_response()) } diff --git a/crates/tranquil-pds/src/api/admin/account/search.rs b/crates/tranquil-pds/src/api/admin/account/search.rs index 5280f67..8f4fee1 100644 --- a/crates/tranquil-pds/src/api/admin/account/search.rs +++ b/crates/tranquil-pds/src/api/admin/account/search.rs @@ -1,5 +1,5 @@ use crate::api::error::ApiError; -use crate::auth::BearerAuthAdmin; +use crate::auth::{Admin, Auth}; use crate::state::AppState; use crate::types::{Did, Handle}; use axum::{ @@ -50,14 +50,14 @@ pub struct SearchAccountsOutput { pub async fn search_accounts( State(state): State, - _auth: BearerAuthAdmin, + _auth: Auth, Query(params): Query, -) -> Response { +) -> Result { let limit = params.limit.clamp(1, 100); let email_filter = params.email.as_deref().map(|e| format!("%{}%", e)); let handle_filter = params.handle.as_deref().map(|h| format!("%{}%", h)); let cursor_did: Option = params.cursor.as_ref().and_then(|c| c.parse().ok()); - let result = state + let rows = state .user_repo .search_accounts( cursor_did.as_ref(), @@ -65,44 +65,41 @@ pub async fn search_accounts( handle_filter.as_deref(), limit + 1, ) - .await; - match result { - Ok(rows) => { - let has_more = rows.len() > limit as usize; - let accounts: Vec = rows - .into_iter() - .take(limit as usize) - .map(|row| AccountView { - did: row.did.clone(), - handle: row.handle, - email: row.email, - indexed_at: row.created_at.to_rfc3339(), - email_confirmed_at: if row.email_verified { - Some(row.created_at.to_rfc3339()) - } else { - None - }, - deactivated_at: row.deactivated_at.map(|dt| dt.to_rfc3339()), - invites_disabled: row.invites_disabled, - }) - .collect(); - let next_cursor = if has_more { - accounts.last().map(|a| a.did.to_string()) + .await + .map_err(|e| { + error!("DB error in search_accounts: {:?}", e); + ApiError::InternalError(None) + })?; + + let has_more = rows.len() > limit as usize; + let accounts: Vec = rows + .into_iter() + .take(limit as usize) + .map(|row| AccountView { + did: row.did.clone(), + handle: row.handle, + email: row.email, + indexed_at: row.created_at.to_rfc3339(), + email_confirmed_at: if row.email_verified { + Some(row.created_at.to_rfc3339()) } else { None - }; - ( - StatusCode::OK, - Json(SearchAccountsOutput { - cursor: next_cursor, - accounts, - }), - ) - .into_response() - } - Err(e) => { - error!("DB error in search_accounts: {:?}", e); - ApiError::InternalError(None).into_response() - } - } + }, + deactivated_at: row.deactivated_at.map(|dt| dt.to_rfc3339()), + invites_disabled: row.invites_disabled, + }) + .collect(); + let next_cursor = if has_more { + accounts.last().map(|a| a.did.to_string()) + } else { + None + }; + Ok(( + StatusCode::OK, + Json(SearchAccountsOutput { + cursor: next_cursor, + accounts, + }), + ) + .into_response()) } diff --git a/crates/tranquil-pds/src/api/admin/account/update.rs b/crates/tranquil-pds/src/api/admin/account/update.rs index d29d621..9a2d828 100644 --- a/crates/tranquil-pds/src/api/admin/account/update.rs +++ b/crates/tranquil-pds/src/api/admin/account/update.rs @@ -1,6 +1,6 @@ use crate::api::EmptyResponse; use crate::api::error::ApiError; -use crate::auth::BearerAuthAdmin; +use crate::auth::{Admin, Auth}; use crate::state::AppState; use crate::types::{Did, Handle, PlainPassword}; use axum::{ @@ -19,28 +19,30 @@ pub struct UpdateAccountEmailInput { pub async fn update_account_email( State(state): State, - _auth: BearerAuthAdmin, + _auth: Auth, Json(input): Json, -) -> Response { +) -> Result { let account = input.account.trim(); let email = input.email.trim(); if account.is_empty() || email.is_empty() { - return ApiError::InvalidRequest("account and email are required".into()).into_response(); + return Err(ApiError::InvalidRequest( + "account and email are required".into(), + )); } - let account_did: Did = match account.parse() { - Ok(d) => d, - Err(_) => return ApiError::InvalidDid("Invalid DID format".into()).into_response(), - }; + let account_did: Did = account + .parse() + .map_err(|_| ApiError::InvalidDid("Invalid DID format".into()))?; + match state .user_repo .admin_update_email(&account_did, email) .await { - Ok(0) => ApiError::AccountNotFound.into_response(), - Ok(_) => EmptyResponse::ok().into_response(), + Ok(0) => Err(ApiError::AccountNotFound), + Ok(_) => Ok(EmptyResponse::ok().into_response()), Err(e) => { error!("DB error updating email: {:?}", e); - ApiError::InternalError(None).into_response() + Err(ApiError::InternalError(None)) } } } @@ -53,19 +55,19 @@ pub struct UpdateAccountHandleInput { pub async fn update_account_handle( State(state): State, - _auth: BearerAuthAdmin, + _auth: Auth, Json(input): Json, -) -> Response { +) -> Result { let did = &input.did; let input_handle = input.handle.trim(); if input_handle.is_empty() { - return ApiError::InvalidRequest("handle is required".into()).into_response(); + return Err(ApiError::InvalidRequest("handle is required".into())); } if !input_handle .chars() .all(|c| c.is_ascii_alphanumeric() || c == '.' || c == '-' || c == '_') { - return ApiError::InvalidHandle(None).into_response(); + return Err(ApiError::InvalidHandle(None)); } let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); let hostname_for_handles = hostname.split(':').next().unwrap_or(&hostname); @@ -75,24 +77,27 @@ pub async fn update_account_handle( input_handle.to_string() }; let old_handle = state.user_repo.get_handle_by_did(did).await.ok().flatten(); - let user_id = match state.user_repo.get_id_by_did(did).await { - Ok(Some(id)) => id, - _ => return ApiError::AccountNotFound.into_response(), - }; + let user_id = state + .user_repo + .get_id_by_did(did) + .await + .ok() + .flatten() + .ok_or(ApiError::AccountNotFound)?; let handle_for_check = Handle::new_unchecked(&handle); if let Ok(true) = state .user_repo .check_handle_exists(&handle_for_check, user_id) .await { - return ApiError::HandleTaken.into_response(); + return Err(ApiError::HandleTaken); } match state .user_repo .admin_update_handle(did, &handle_for_check) .await { - Ok(0) => ApiError::AccountNotFound.into_response(), + Ok(0) => Err(ApiError::AccountNotFound), Ok(_) => { if let Some(old) = old_handle { let _ = state.cache.delete(&format!("handle:{}", old)).await; @@ -115,11 +120,11 @@ pub async fn update_account_handle( { warn!("Failed to update PLC handle for admin handle update: {}", e); } - EmptyResponse::ok().into_response() + Ok(EmptyResponse::ok().into_response()) } Err(e) => { error!("DB error updating handle: {:?}", e); - ApiError::InternalError(None).into_response() + Err(ApiError::InternalError(None)) } } } @@ -132,31 +137,29 @@ pub struct UpdateAccountPasswordInput { pub async fn update_account_password( State(state): State, - _auth: BearerAuthAdmin, + _auth: Auth, Json(input): Json, -) -> Response { +) -> Result { let did = &input.did; let password = input.password.trim(); if password.is_empty() { - return ApiError::InvalidRequest("password is required".into()).into_response(); + return Err(ApiError::InvalidRequest("password is required".into())); } - let password_hash = match bcrypt::hash(password, bcrypt::DEFAULT_COST) { - Ok(h) => h, - Err(e) => { - error!("Failed to hash password: {:?}", e); - return ApiError::InternalError(None).into_response(); - } - }; + let password_hash = bcrypt::hash(password, bcrypt::DEFAULT_COST).map_err(|e| { + error!("Failed to hash password: {:?}", e); + ApiError::InternalError(None) + })?; + match state .user_repo .admin_update_password(did, &password_hash) .await { - Ok(0) => ApiError::AccountNotFound.into_response(), - Ok(_) => EmptyResponse::ok().into_response(), + Ok(0) => Err(ApiError::AccountNotFound), + Ok(_) => Ok(EmptyResponse::ok().into_response()), Err(e) => { error!("DB error updating password: {:?}", e); - ApiError::InternalError(None).into_response() + Err(ApiError::InternalError(None)) } } } diff --git a/crates/tranquil-pds/src/api/admin/config.rs b/crates/tranquil-pds/src/api/admin/config.rs index a1e31d3..6d71c73 100644 --- a/crates/tranquil-pds/src/api/admin/config.rs +++ b/crates/tranquil-pds/src/api/admin/config.rs @@ -1,5 +1,5 @@ use crate::api::error::ApiError; -use crate::auth::BearerAuthAdmin; +use crate::auth::{Admin, Auth}; use crate::state::AppState; use axum::{Json, extract::State}; use serde::{Deserialize, Serialize}; @@ -78,7 +78,7 @@ pub async fn get_server_config( pub async fn update_server_config( State(state): State, - _admin: BearerAuthAdmin, + _auth: Auth, Json(req): Json, ) -> Result, ApiError> { if let Some(server_name) = req.server_name { diff --git a/crates/tranquil-pds/src/api/admin/invite.rs b/crates/tranquil-pds/src/api/admin/invite.rs index 27d5075..d6af578 100644 --- a/crates/tranquil-pds/src/api/admin/invite.rs +++ b/crates/tranquil-pds/src/api/admin/invite.rs @@ -1,6 +1,6 @@ use crate::api::EmptyResponse; use crate::api::error::ApiError; -use crate::auth::BearerAuthAdmin; +use crate::auth::{Admin, Auth}; use crate::state::AppState; use axum::{ Json, @@ -21,9 +21,9 @@ pub struct DisableInviteCodesInput { pub async fn disable_invite_codes( State(state): State, - _auth: BearerAuthAdmin, + _auth: Auth, Json(input): Json, -) -> Response { +) -> Result { if let Some(codes) = &input.codes && let Err(e) = state.infra_repo.disable_invite_codes_by_code(codes).await { @@ -40,7 +40,7 @@ pub async fn disable_invite_codes( error!("DB error disabling invite codes by account: {:?}", e); } } - EmptyResponse::ok().into_response() + Ok(EmptyResponse::ok().into_response()) } #[derive(Deserialize)] @@ -78,26 +78,23 @@ pub struct GetInviteCodesOutput { pub async fn get_invite_codes( State(state): State, - _auth: BearerAuthAdmin, + _auth: Auth, Query(params): Query, -) -> Response { +) -> Result { let limit = params.limit.unwrap_or(100).clamp(1, 500); let sort_order = match params.sort.as_deref() { Some("usage") => InviteCodeSortOrder::Usage, _ => InviteCodeSortOrder::Recent, }; - let codes_rows = match state + let codes_rows = state .infra_repo .list_invite_codes(params.cursor.as_deref(), limit, sort_order) .await - { - Ok(rows) => rows, - Err(e) => { + .map_err(|e| { error!("DB error fetching invite codes: {:?}", e); - return ApiError::InternalError(None).into_response(); - } - }; + ApiError::InternalError(None) + })?; let user_ids: Vec = codes_rows.iter().map(|r| r.created_by_user).collect(); let code_strings: Vec = codes_rows.iter().map(|r| r.code.clone()).collect(); @@ -155,14 +152,14 @@ pub async fn get_invite_codes( } else { None }; - ( + Ok(( StatusCode::OK, Json(GetInviteCodesOutput { cursor: next_cursor, codes, }), ) - .into_response() + .into_response()) } #[derive(Deserialize)] @@ -172,27 +169,27 @@ pub struct DisableAccountInvitesInput { pub async fn disable_account_invites( State(state): State, - _auth: BearerAuthAdmin, + _auth: Auth, Json(input): Json, -) -> Response { +) -> Result { let account = input.account.trim(); if account.is_empty() { - return ApiError::InvalidRequest("account is required".into()).into_response(); + return Err(ApiError::InvalidRequest("account is required".into())); } - let account_did: tranquil_types::Did = match account.parse() { - Ok(d) => d, - Err(_) => return ApiError::InvalidDid("Invalid DID format".into()).into_response(), - }; + let account_did: tranquil_types::Did = account + .parse() + .map_err(|_| ApiError::InvalidDid("Invalid DID format".into()))?; + match state .user_repo .set_invites_disabled(&account_did, true) .await { - Ok(true) => EmptyResponse::ok().into_response(), - Ok(false) => ApiError::AccountNotFound.into_response(), + Ok(true) => Ok(EmptyResponse::ok().into_response()), + Ok(false) => Err(ApiError::AccountNotFound), Err(e) => { error!("DB error disabling account invites: {:?}", e); - ApiError::InternalError(None).into_response() + Err(ApiError::InternalError(None)) } } } @@ -204,27 +201,27 @@ pub struct EnableAccountInvitesInput { pub async fn enable_account_invites( State(state): State, - _auth: BearerAuthAdmin, + _auth: Auth, Json(input): Json, -) -> Response { +) -> Result { let account = input.account.trim(); if account.is_empty() { - return ApiError::InvalidRequest("account is required".into()).into_response(); + return Err(ApiError::InvalidRequest("account is required".into())); } - let account_did: tranquil_types::Did = match account.parse() { - Ok(d) => d, - Err(_) => return ApiError::InvalidDid("Invalid DID format".into()).into_response(), - }; + let account_did: tranquil_types::Did = account + .parse() + .map_err(|_| ApiError::InvalidDid("Invalid DID format".into()))?; + match state .user_repo .set_invites_disabled(&account_did, false) .await { - Ok(true) => EmptyResponse::ok().into_response(), - Ok(false) => ApiError::AccountNotFound.into_response(), + Ok(true) => Ok(EmptyResponse::ok().into_response()), + Ok(false) => Err(ApiError::AccountNotFound), Err(e) => { error!("DB error enabling account invites: {:?}", e); - ApiError::InternalError(None).into_response() + Err(ApiError::InternalError(None)) } } } diff --git a/crates/tranquil-pds/src/api/admin/server_stats.rs b/crates/tranquil-pds/src/api/admin/server_stats.rs index 2e2624c..aab42a7 100644 --- a/crates/tranquil-pds/src/api/admin/server_stats.rs +++ b/crates/tranquil-pds/src/api/admin/server_stats.rs @@ -1,4 +1,5 @@ -use crate::auth::BearerAuthAdmin; +use crate::api::error::ApiError; +use crate::auth::{Admin, Auth}; use crate::state::AppState; use axum::{ Json, @@ -16,17 +17,20 @@ pub struct ServerStatsResponse { pub blob_storage_bytes: i64, } -pub async fn get_server_stats(State(state): State, _auth: BearerAuthAdmin) -> Response { +pub async fn get_server_stats( + State(state): State, + _auth: Auth, +) -> Result { let user_count = state.user_repo.count_users().await.unwrap_or(0); let repo_count = state.repo_repo.count_repos().await.unwrap_or(0); let record_count = state.repo_repo.count_all_records().await.unwrap_or(0); let blob_storage_bytes = state.blob_repo.sum_blob_storage().await.unwrap_or(0); - Json(ServerStatsResponse { + Ok(Json(ServerStatsResponse { user_count, repo_count, record_count, blob_storage_bytes, }) - .into_response() + .into_response()) } diff --git a/crates/tranquil-pds/src/api/admin/status.rs b/crates/tranquil-pds/src/api/admin/status.rs index e7e8d8f..0ac4b44 100644 --- a/crates/tranquil-pds/src/api/admin/status.rs +++ b/crates/tranquil-pds/src/api/admin/status.rs @@ -1,5 +1,5 @@ use crate::api::error::ApiError; -use crate::auth::BearerAuthAdmin; +use crate::auth::{Admin, Auth}; use crate::state::AppState; use crate::types::{CidLink, Did}; use axum::{ @@ -35,17 +35,18 @@ pub struct StatusAttr { pub async fn get_subject_status( State(state): State, - _auth: BearerAuthAdmin, + _auth: Auth, Query(params): Query, -) -> Response { +) -> Result { if params.did.is_none() && params.uri.is_none() && params.blob.is_none() { - return ApiError::InvalidRequest("Must provide did, uri, or blob".into()).into_response(); + return Err(ApiError::InvalidRequest( + "Must provide did, uri, or blob".into(), + )); } if let Some(did_str) = ¶ms.did { - let did: Did = match did_str.parse() { - Ok(d) => d, - Err(_) => return ApiError::InvalidDid("Invalid DID format".into()).into_response(), - }; + let did: Did = did_str + .parse() + .map_err(|_| ApiError::InvalidDid("Invalid DID format".into()))?; match state.user_repo.get_status_by_did(&did).await { Ok(Some(status)) => { let deactivated = status.deactivated_at.map(|_| StatusAttr { @@ -56,7 +57,7 @@ pub async fn get_subject_status( applied: true, r#ref: Some(r.clone()), }); - return ( + return Ok(( StatusCode::OK, Json(SubjectStatus { subject: json!({ @@ -67,29 +68,28 @@ pub async fn get_subject_status( deactivated, }), ) - .into_response(); + .into_response()); } Ok(None) => { - return ApiError::SubjectNotFound.into_response(); + return Err(ApiError::SubjectNotFound); } Err(e) => { error!("DB error in get_subject_status: {:?}", e); - return ApiError::InternalError(None).into_response(); + return Err(ApiError::InternalError(None)); } } } if let Some(uri_str) = ¶ms.uri { - let cid: CidLink = match uri_str.parse() { - Ok(c) => c, - Err(_) => return ApiError::InvalidRequest("Invalid CID format".into()).into_response(), - }; + let cid: CidLink = uri_str + .parse() + .map_err(|_| ApiError::InvalidRequest("Invalid CID format".into()))?; match state.repo_repo.get_record_by_cid(&cid).await { Ok(Some(record)) => { let takedown = record.takedown_ref.as_ref().map(|r| StatusAttr { applied: true, r#ref: Some(r.clone()), }); - return ( + return Ok(( StatusCode::OK, Json(SubjectStatus { subject: json!({ @@ -101,36 +101,31 @@ pub async fn get_subject_status( deactivated: None, }), ) - .into_response(); + .into_response()); } Ok(None) => { - return ApiError::RecordNotFound.into_response(); + return Err(ApiError::RecordNotFound); } Err(e) => { error!("DB error in get_subject_status: {:?}", e); - return ApiError::InternalError(None).into_response(); + return Err(ApiError::InternalError(None)); } } } if let Some(blob_cid_str) = ¶ms.blob { - let blob_cid: CidLink = match blob_cid_str.parse() { - Ok(c) => c, - Err(_) => return ApiError::InvalidRequest("Invalid CID format".into()).into_response(), - }; - let did = match ¶ms.did { - Some(d) => d, - None => { - return ApiError::InvalidRequest("Must provide a did to request blob state".into()) - .into_response(); - } - }; + let blob_cid: CidLink = blob_cid_str + .parse() + .map_err(|_| ApiError::InvalidRequest("Invalid CID format".into()))?; + let did = params.did.as_ref().ok_or_else(|| { + ApiError::InvalidRequest("Must provide a did to request blob state".into()) + })?; match state.blob_repo.get_blob_with_takedown(&blob_cid).await { Ok(Some(blob)) => { let takedown = blob.takedown_ref.as_ref().map(|r| StatusAttr { applied: true, r#ref: Some(r.clone()), }); - return ( + return Ok(( StatusCode::OK, Json(SubjectStatus { subject: json!({ @@ -142,18 +137,18 @@ pub async fn get_subject_status( deactivated: None, }), ) - .into_response(); + .into_response()); } Ok(None) => { - return ApiError::BlobNotFound(None).into_response(); + return Err(ApiError::BlobNotFound(None)); } Err(e) => { error!("DB error in get_subject_status: {:?}", e); - return ApiError::InternalError(None).into_response(); + return Err(ApiError::InternalError(None)); } } } - ApiError::InvalidRequest("Invalid subject type".into()).into_response() + Err(ApiError::InvalidRequest("Invalid subject type".into())) } #[derive(Deserialize)] @@ -172,9 +167,9 @@ pub struct StatusAttrInput { pub async fn update_subject_status( State(state): State, - _auth: BearerAuthAdmin, + _auth: Auth, Json(input): Json, -) -> Response { +) -> Result { let subject_type = input.subject.get("$type").and_then(|t| t.as_str()); match subject_type { Some("com.atproto.admin.defs#repoRef") => { @@ -187,13 +182,14 @@ pub async fn update_subject_status( } else { None }; - if let Err(e) = state.user_repo.set_user_takedown(&did, takedown_ref).await { - error!("Failed to update user takedown status for {}: {:?}", did, e); - return ApiError::InternalError(Some( - "Failed to update takedown status".into(), - )) - .into_response(); - } + state + .user_repo + .set_user_takedown(&did, takedown_ref) + .await + .map_err(|e| { + error!("Failed to update user takedown status for {}: {:?}", did, e); + ApiError::InternalError(Some("Failed to update takedown status".into())) + })?; } if let Some(deactivated) = &input.deactivated { let result = if deactivated.applied { @@ -201,16 +197,13 @@ pub async fn update_subject_status( } else { state.user_repo.activate_account(&did).await }; - if let Err(e) = result { + result.map_err(|e| { error!( "Failed to update user deactivation status for {}: {:?}", did, e ); - return ApiError::InternalError(Some( - "Failed to update deactivation status".into(), - )) - .into_response(); - } + ApiError::InternalError(Some("Failed to update deactivation status".into())) + })?; } if let Some(takedown) = &input.takedown { let status = if takedown.applied { @@ -249,7 +242,7 @@ pub async fn update_subject_status( if let Ok(Some(handle)) = state.user_repo.get_handle_by_did(&did).await { let _ = state.cache.delete(&format!("handle:{}", handle)).await; } - return ( + return Ok(( StatusCode::OK, Json(json!({ "subject": input.subject, @@ -262,41 +255,34 @@ pub async fn update_subject_status( })) })), ) - .into_response(); + .into_response()); } } Some("com.atproto.repo.strongRef") => { let uri_str = input.subject.get("uri").and_then(|u| u.as_str()); if let Some(uri_str) = uri_str { - let cid: CidLink = match uri_str.parse() { - Ok(c) => c, - Err(_) => { - return ApiError::InvalidRequest("Invalid CID format".into()) - .into_response(); - } - }; + let cid: CidLink = uri_str + .parse() + .map_err(|_| ApiError::InvalidRequest("Invalid CID format".into()))?; if let Some(takedown) = &input.takedown { let takedown_ref = if takedown.applied { takedown.r#ref.as_deref() } else { None }; - if let Err(e) = state + state .repo_repo .set_record_takedown(&cid, takedown_ref) .await - { - error!( - "Failed to update record takedown status for {}: {:?}", - uri_str, e - ); - return ApiError::InternalError(Some( - "Failed to update takedown status".into(), - )) - .into_response(); - } + .map_err(|e| { + error!( + "Failed to update record takedown status for {}: {:?}", + uri_str, e + ); + ApiError::InternalError(Some("Failed to update takedown status".into())) + })?; } - return ( + return Ok(( StatusCode::OK, Json(json!({ "subject": input.subject, @@ -306,41 +292,34 @@ pub async fn update_subject_status( })) })), ) - .into_response(); + .into_response()); } } Some("com.atproto.admin.defs#repoBlobRef") => { let cid_str = input.subject.get("cid").and_then(|c| c.as_str()); if let Some(cid_str) = cid_str { - let cid: CidLink = match cid_str.parse() { - Ok(c) => c, - Err(_) => { - return ApiError::InvalidRequest("Invalid CID format".into()) - .into_response(); - } - }; + let cid: CidLink = cid_str + .parse() + .map_err(|_| ApiError::InvalidRequest("Invalid CID format".into()))?; if let Some(takedown) = &input.takedown { let takedown_ref = if takedown.applied { takedown.r#ref.as_deref() } else { None }; - if let Err(e) = state + state .blob_repo .update_blob_takedown(&cid, takedown_ref) .await - { - error!( - "Failed to update blob takedown status for {}: {:?}", - cid_str, e - ); - return ApiError::InternalError(Some( - "Failed to update takedown status".into(), - )) - .into_response(); - } + .map_err(|e| { + error!( + "Failed to update blob takedown status for {}: {:?}", + cid_str, e + ); + ApiError::InternalError(Some("Failed to update takedown status".into())) + })?; } - return ( + return Ok(( StatusCode::OK, Json(json!({ "subject": input.subject, @@ -350,10 +329,10 @@ pub async fn update_subject_status( })) })), ) - .into_response(); + .into_response()); } } _ => {} } - ApiError::InvalidRequest("Invalid subject type".into()).into_response() + Err(ApiError::InvalidRequest("Invalid subject type".into())) } diff --git a/crates/tranquil-pds/src/api/backup.rs b/crates/tranquil-pds/src/api/backup.rs index d605e6d..237888d 100644 --- a/crates/tranquil-pds/src/api/backup.rs +++ b/crates/tranquil-pds/src/api/backup.rs @@ -1,6 +1,6 @@ use crate::api::error::ApiError; use crate::api::{EmptyResponse, EnabledResponse}; -use crate::auth::BearerAuth; +use crate::auth::{Active, Auth}; use crate::scheduled::generate_full_backup; use crate::state::AppState; use crate::storage::{BackupStorage, backup_retention_count}; @@ -35,24 +35,27 @@ pub struct ListBackupsOutput { pub backup_enabled: bool, } -pub async fn list_backups(State(state): State, auth: BearerAuth) -> Response { - let (user_id, backup_enabled) = - match state.backup_repo.get_user_backup_status(&auth.0.did).await { - Ok(Some(status)) => status, - Ok(None) => { - return ApiError::AccountNotFound.into_response(); - } - Err(e) => { - error!("DB error fetching user: {:?}", e); - return ApiError::InternalError(None).into_response(); - } - }; +pub async fn list_backups( + State(state): State, + auth: Auth, +) -> Result { + let (user_id, backup_enabled) = match state.backup_repo.get_user_backup_status(&auth.did).await + { + Ok(Some(status)) => status, + Ok(None) => { + return Ok(ApiError::AccountNotFound.into_response()); + } + Err(e) => { + error!("DB error fetching user: {:?}", e); + return Ok(ApiError::InternalError(None).into_response()); + } + }; let backups = match state.backup_repo.list_backups_for_user(user_id).await { Ok(rows) => rows, Err(e) => { error!("DB error fetching backups: {:?}", e); - return ApiError::InternalError(None).into_response(); + return Ok(ApiError::InternalError(None).into_response()); } }; @@ -68,14 +71,14 @@ pub async fn list_backups(State(state): State, auth: BearerAuth) -> Re }) .collect(); - ( + Ok(( StatusCode::OK, Json(ListBackupsOutput { backups: backup_list, backup_enabled, }), ) - .into_response() + .into_response()) } #[derive(Deserialize)] @@ -85,35 +88,35 @@ pub struct GetBackupQuery { pub async fn get_backup( State(state): State, - auth: BearerAuth, + auth: Auth, Query(query): Query, -) -> Response { +) -> Result { let backup_id = match uuid::Uuid::parse_str(&query.id) { Ok(id) => id, Err(_) => { - return ApiError::InvalidRequest("Invalid backup ID".into()).into_response(); + return Ok(ApiError::InvalidRequest("Invalid backup ID".into()).into_response()); } }; let backup_info = match state .backup_repo - .get_backup_storage_info(backup_id, &auth.0.did) + .get_backup_storage_info(backup_id, &auth.did) .await { Ok(Some(b)) => b, Ok(None) => { - return ApiError::BackupNotFound.into_response(); + return Ok(ApiError::BackupNotFound.into_response()); } Err(e) => { error!("DB error fetching backup: {:?}", e); - return ApiError::InternalError(None).into_response(); + return Ok(ApiError::InternalError(None).into_response()); } }; let backup_storage = match state.backup_storage.as_ref() { Some(storage) => storage, None => { - return ApiError::BackupsDisabled.into_response(); + return Ok(ApiError::BackupsDisabled.into_response()); } }; @@ -121,12 +124,13 @@ pub async fn get_backup( Ok(bytes) => bytes, Err(e) => { error!("Failed to fetch backup from storage: {:?}", e); - return ApiError::InternalError(Some("Failed to retrieve backup".into())) - .into_response(); + return Ok( + ApiError::InternalError(Some("Failed to retrieve backup".into())).into_response(), + ); } }; - ( + Ok(( StatusCode::OK, [ (axum::http::header::CONTENT_TYPE, "application/vnd.ipld.car"), @@ -137,7 +141,7 @@ pub async fn get_backup( ], car_bytes, ) - .into_response() + .into_response()) } #[derive(Serialize)] @@ -149,40 +153,45 @@ pub struct CreateBackupOutput { pub block_count: i32, } -pub async fn create_backup(State(state): State, auth: BearerAuth) -> Response { +pub async fn create_backup( + State(state): State, + auth: Auth, +) -> Result { let backup_storage = match state.backup_storage.as_ref() { Some(storage) => storage, None => { - return ApiError::BackupsDisabled.into_response(); + return Ok(ApiError::BackupsDisabled.into_response()); } }; - let user = match state.backup_repo.get_user_for_backup(&auth.0.did).await { + let user = match state.backup_repo.get_user_for_backup(&auth.did).await { Ok(Some(u)) => u, Ok(None) => { - return ApiError::AccountNotFound.into_response(); + return Ok(ApiError::AccountNotFound.into_response()); } Err(e) => { error!("DB error fetching user: {:?}", e); - return ApiError::InternalError(None).into_response(); + return Ok(ApiError::InternalError(None).into_response()); } }; if user.deactivated_at.is_some() { - return ApiError::AccountDeactivated.into_response(); + return Ok(ApiError::AccountDeactivated.into_response()); } let repo_rev = match &user.repo_rev { Some(rev) => rev.clone(), None => { - return ApiError::RepoNotReady.into_response(); + return Ok(ApiError::RepoNotReady.into_response()); } }; let head_cid = match Cid::from_str(&user.repo_root_cid) { Ok(c) => c, Err(_) => { - return ApiError::InternalError(Some("Invalid repo root CID".into())).into_response(); + return Ok( + ApiError::InternalError(Some("Invalid repo root CID".into())).into_response(), + ); } }; @@ -197,8 +206,9 @@ pub async fn create_backup(State(state): State, auth: BearerAuth) -> R Ok(bytes) => bytes, Err(e) => { error!("Failed to generate CAR: {:?}", e); - return ApiError::InternalError(Some("Failed to generate backup".into())) - .into_response(); + return Ok( + ApiError::InternalError(Some("Failed to generate backup".into())).into_response(), + ); } }; @@ -212,7 +222,9 @@ pub async fn create_backup(State(state): State, auth: BearerAuth) -> R Ok(key) => key, Err(e) => { error!("Failed to upload backup: {:?}", e); - return ApiError::InternalError(Some("Failed to store backup".into())).into_response(); + return Ok( + ApiError::InternalError(Some("Failed to store backup".into())).into_response(), + ); } }; @@ -238,7 +250,9 @@ pub async fn create_backup(State(state): State, auth: BearerAuth) -> R "Failed to rollback orphaned backup from S3" ); } - return ApiError::InternalError(Some("Failed to record backup".into())).into_response(); + return Ok( + ApiError::InternalError(Some("Failed to record backup".into())).into_response(), + ); } }; @@ -261,7 +275,7 @@ pub async fn create_backup(State(state): State, auth: BearerAuth) -> R warn!(did = %user.did, error = %e, "Failed to cleanup old backups after manual backup"); } - ( + Ok(( StatusCode::OK, Json(CreateBackupOutput { id: backup_id.to_string(), @@ -270,7 +284,7 @@ pub async fn create_backup(State(state): State, auth: BearerAuth) -> R block_count, }), ) - .into_response() + .into_response()) } async fn cleanup_old_backups( @@ -310,33 +324,33 @@ pub struct DeleteBackupQuery { pub async fn delete_backup( State(state): State, - auth: BearerAuth, + auth: Auth, Query(query): Query, -) -> Response { +) -> Result { let backup_id = match uuid::Uuid::parse_str(&query.id) { Ok(id) => id, Err(_) => { - return ApiError::InvalidRequest("Invalid backup ID".into()).into_response(); + return Ok(ApiError::InvalidRequest("Invalid backup ID".into()).into_response()); } }; let backup = match state .backup_repo - .get_backup_for_deletion(backup_id, &auth.0.did) + .get_backup_for_deletion(backup_id, &auth.did) .await { Ok(Some(b)) => b, Ok(None) => { - return ApiError::BackupNotFound.into_response(); + return Ok(ApiError::BackupNotFound.into_response()); } Err(e) => { error!("DB error fetching backup: {:?}", e); - return ApiError::InternalError(None).into_response(); + return Ok(ApiError::InternalError(None).into_response()); } }; if backup.deactivated_at.is_some() { - return ApiError::AccountDeactivated.into_response(); + return Ok(ApiError::AccountDeactivated.into_response()); } if let Some(backup_storage) = state.backup_storage.as_ref() @@ -351,12 +365,12 @@ pub async fn delete_backup( if let Err(e) = state.backup_repo.delete_backup(backup.id).await { error!("DB error deleting backup: {:?}", e); - return ApiError::InternalError(Some("Failed to delete backup".into())).into_response(); + return Ok(ApiError::InternalError(Some("Failed to delete backup".into())).into_response()); } - info!(did = %auth.0.did, backup_id = %backup_id, "Deleted backup"); + info!(did = %auth.did, backup_id = %backup_id, "Deleted backup"); - EmptyResponse::ok().into_response() + Ok(EmptyResponse::ok().into_response()) } #[derive(Deserialize)] @@ -367,51 +381,56 @@ pub struct SetBackupEnabledInput { pub async fn set_backup_enabled( State(state): State, - auth: BearerAuth, + auth: Auth, Json(input): Json, -) -> Response { +) -> Result { let deactivated_at = match state .backup_repo - .get_user_deactivated_status(&auth.0.did) + .get_user_deactivated_status(&auth.did) .await { Ok(Some(status)) => status, Ok(None) => { - return ApiError::AccountNotFound.into_response(); + return Ok(ApiError::AccountNotFound.into_response()); } Err(e) => { error!("DB error fetching user: {:?}", e); - return ApiError::InternalError(None).into_response(); + return Ok(ApiError::InternalError(None).into_response()); } }; if deactivated_at.is_some() { - return ApiError::AccountDeactivated.into_response(); + return Ok(ApiError::AccountDeactivated.into_response()); } if let Err(e) = state .backup_repo - .update_backup_enabled(&auth.0.did, input.enabled) + .update_backup_enabled(&auth.did, input.enabled) .await { error!("DB error updating backup_enabled: {:?}", e); - return ApiError::InternalError(Some("Failed to update setting".into())).into_response(); + return Ok( + ApiError::InternalError(Some("Failed to update setting".into())).into_response(), + ); } - info!(did = %auth.0.did, enabled = input.enabled, "Updated backup_enabled setting"); + info!(did = %auth.did, enabled = input.enabled, "Updated backup_enabled setting"); - EnabledResponse::response(input.enabled).into_response() + Ok(EnabledResponse::response(input.enabled).into_response()) } -pub async fn export_blobs(State(state): State, auth: BearerAuth) -> Response { - let user_id = match state.backup_repo.get_user_id_by_did(&auth.0.did).await { +pub async fn export_blobs( + State(state): State, + auth: Auth, +) -> Result { + let user_id = match state.backup_repo.get_user_id_by_did(&auth.did).await { Ok(Some(id)) => id, Ok(None) => { - return ApiError::AccountNotFound.into_response(); + return Ok(ApiError::AccountNotFound.into_response()); } Err(e) => { error!("DB error fetching user: {:?}", e); - return ApiError::InternalError(None).into_response(); + return Ok(ApiError::InternalError(None).into_response()); } }; @@ -419,12 +438,12 @@ pub async fn export_blobs(State(state): State, auth: BearerAuth) -> Re Ok(rows) => rows, Err(e) => { error!("DB error fetching blobs: {:?}", e); - return ApiError::InternalError(None).into_response(); + return Ok(ApiError::InternalError(None).into_response()); } }; if blobs.is_empty() { - return ( + return Ok(( StatusCode::OK, [ (axum::http::header::CONTENT_TYPE, "application/zip"), @@ -435,7 +454,7 @@ pub async fn export_blobs(State(state): State, auth: BearerAuth) -> Re ], Vec::::new(), ) - .into_response(); + .into_response()); } let mut zip_buffer = std::io::Cursor::new(Vec::new()); @@ -513,16 +532,17 @@ pub async fn export_blobs(State(state): State, auth: BearerAuth) -> Re if let Err(e) = zip.finish() { error!("Failed to finish zip: {:?}", e); - return ApiError::InternalError(Some("Failed to create zip file".into())) - .into_response(); + return Ok( + ApiError::InternalError(Some("Failed to create zip file".into())).into_response(), + ); } } let zip_bytes = zip_buffer.into_inner(); - info!(did = %auth.0.did, blob_count = blobs.len(), size_bytes = zip_bytes.len(), "Exported blobs"); + info!(did = %auth.did, blob_count = blobs.len(), size_bytes = zip_bytes.len(), "Exported blobs"); - ( + Ok(( StatusCode::OK, [ (axum::http::header::CONTENT_TYPE, "application/zip"), @@ -533,7 +553,7 @@ pub async fn export_blobs(State(state): State, auth: BearerAuth) -> Re ], zip_bytes, ) - .into_response() + .into_response()) } fn mime_to_extension(mime_type: &str) -> &'static str { diff --git a/crates/tranquil-pds/src/api/delegation.rs b/crates/tranquil-pds/src/api/delegation.rs index 24f2b50..7d48aa4 100644 --- a/crates/tranquil-pds/src/api/delegation.rs +++ b/crates/tranquil-pds/src/api/delegation.rs @@ -1,6 +1,6 @@ use crate::api::error::ApiError; use crate::api::repo::record::utils::create_signed_commit; -use crate::auth::BearerAuth; +use crate::auth::{Active, Auth}; use crate::delegation::{DelegationActionType, SCOPE_PRESETS, scopes}; use crate::state::{AppState, RateLimitKind}; use crate::types::{Did, Handle, Nsid, Rkey}; @@ -33,21 +33,25 @@ pub struct ListControllersResponse { pub controllers: Vec, } -pub async fn list_controllers(State(state): State, auth: BearerAuth) -> Response { +pub async fn list_controllers( + State(state): State, + auth: Auth, +) -> Result { let controllers = match state .delegation_repo - .get_delegations_for_account(&auth.0.did) + .get_delegations_for_account(&auth.did) .await { Ok(c) => c, Err(e) => { tracing::error!("Failed to list controllers: {:?}", e); - return ApiError::InternalError(Some("Failed to list controllers".into())) - .into_response(); + return Ok( + ApiError::InternalError(Some("Failed to list controllers".into())).into_response(), + ); } }; - Json(ListControllersResponse { + Ok(Json(ListControllersResponse { controllers: controllers .into_iter() .map(|c| ControllerInfo { @@ -59,7 +63,7 @@ pub async fn list_controllers(State(state): State, auth: BearerAuth) - }) .collect(), }) - .into_response() + .into_response()) } #[derive(Debug, Deserialize)] @@ -70,11 +74,11 @@ pub struct AddControllerInput { pub async fn add_controller( State(state): State, - auth: BearerAuth, + auth: Auth, Json(input): Json, -) -> Response { +) -> Result { if let Err(e) = scopes::validate_delegation_scopes(&input.granted_scopes) { - return ApiError::InvalidScopes(e).into_response(); + return Ok(ApiError::InvalidScopes(e).into_response()); } let controller_exists = state @@ -86,24 +90,22 @@ pub async fn add_controller( .is_some(); if !controller_exists { - return ApiError::ControllerNotFound.into_response(); + return Ok(ApiError::ControllerNotFound.into_response()); } - match state - .delegation_repo - .controls_any_accounts(&auth.0.did) - .await - { + match state.delegation_repo.controls_any_accounts(&auth.did).await { Ok(true) => { - return ApiError::InvalidDelegation( + return Ok(ApiError::InvalidDelegation( "Cannot add controllers to an account that controls other accounts".into(), ) - .into_response(); + .into_response()); } Err(e) => { tracing::error!("Failed to check delegation status: {:?}", e); - return ApiError::InternalError(Some("Failed to verify delegation status".into())) - .into_response(); + return Ok( + ApiError::InternalError(Some("Failed to verify delegation status".into())) + .into_response(), + ); } Ok(false) => {} } @@ -114,15 +116,17 @@ pub async fn add_controller( .await { Ok(true) => { - return ApiError::InvalidDelegation( + return Ok(ApiError::InvalidDelegation( "Cannot add a controlled account as a controller".into(), ) - .into_response(); + .into_response()); } Err(e) => { tracing::error!("Failed to check controller status: {:?}", e); - return ApiError::InternalError(Some("Failed to verify controller status".into())) - .into_response(); + return Ok( + ApiError::InternalError(Some("Failed to verify controller status".into())) + .into_response(), + ); } Ok(false) => {} } @@ -130,10 +134,10 @@ pub async fn add_controller( match state .delegation_repo .create_delegation( - &auth.0.did, + &auth.did, &input.controller_did, &input.granted_scopes, - &auth.0.did, + &auth.did, ) .await { @@ -141,8 +145,8 @@ pub async fn add_controller( let _ = state .delegation_repo .log_delegation_action( - &auth.0.did, - &auth.0.did, + &auth.did, + &auth.did, Some(&input.controller_did), DelegationActionType::GrantCreated, Some(serde_json::json!({ @@ -153,17 +157,17 @@ pub async fn add_controller( ) .await; - ( + Ok(( StatusCode::OK, Json(serde_json::json!({ "success": true })), ) - .into_response() + .into_response()) } Err(e) => { tracing::error!("Failed to add controller: {:?}", e); - ApiError::InternalError(Some("Failed to add controller".into())).into_response() + Ok(ApiError::InternalError(Some("Failed to add controller".into())).into_response()) } } } @@ -175,32 +179,32 @@ pub struct RemoveControllerInput { pub async fn remove_controller( State(state): State, - auth: BearerAuth, + auth: Auth, Json(input): Json, -) -> Response { +) -> Result { match state .delegation_repo - .revoke_delegation(&auth.0.did, &input.controller_did, &auth.0.did) + .revoke_delegation(&auth.did, &input.controller_did, &auth.did) .await { Ok(true) => { let revoked_app_passwords = state .session_repo - .delete_app_passwords_by_controller(&auth.0.did, &input.controller_did) + .delete_app_passwords_by_controller(&auth.did, &input.controller_did) .await .unwrap_or(0) as usize; let revoked_oauth_tokens = state .oauth_repo - .revoke_tokens_for_controller(&auth.0.did, &input.controller_did) + .revoke_tokens_for_controller(&auth.did, &input.controller_did) .await .unwrap_or(0); let _ = state .delegation_repo .log_delegation_action( - &auth.0.did, - &auth.0.did, + &auth.did, + &auth.did, Some(&input.controller_did), DelegationActionType::GrantRevoked, Some(serde_json::json!({ @@ -212,18 +216,18 @@ pub async fn remove_controller( ) .await; - ( + Ok(( StatusCode::OK, Json(serde_json::json!({ "success": true })), ) - .into_response() + .into_response()) } - Ok(false) => ApiError::DelegationNotFound.into_response(), + Ok(false) => Ok(ApiError::DelegationNotFound.into_response()), Err(e) => { tracing::error!("Failed to remove controller: {:?}", e); - ApiError::InternalError(Some("Failed to remove controller".into())).into_response() + Ok(ApiError::InternalError(Some("Failed to remove controller".into())).into_response()) } } } @@ -236,24 +240,24 @@ pub struct UpdateControllerScopesInput { pub async fn update_controller_scopes( State(state): State, - auth: BearerAuth, + auth: Auth, Json(input): Json, -) -> Response { +) -> Result { if let Err(e) = scopes::validate_delegation_scopes(&input.granted_scopes) { - return ApiError::InvalidScopes(e).into_response(); + return Ok(ApiError::InvalidScopes(e).into_response()); } match state .delegation_repo - .update_delegation_scopes(&auth.0.did, &input.controller_did, &input.granted_scopes) + .update_delegation_scopes(&auth.did, &input.controller_did, &input.granted_scopes) .await { Ok(true) => { let _ = state .delegation_repo .log_delegation_action( - &auth.0.did, - &auth.0.did, + &auth.did, + &auth.did, Some(&input.controller_did), DelegationActionType::ScopesModified, Some(serde_json::json!({ @@ -264,19 +268,21 @@ pub async fn update_controller_scopes( ) .await; - ( + Ok(( StatusCode::OK, Json(serde_json::json!({ "success": true })), ) - .into_response() + .into_response()) } - Ok(false) => ApiError::DelegationNotFound.into_response(), + Ok(false) => Ok(ApiError::DelegationNotFound.into_response()), Err(e) => { tracing::error!("Failed to update controller scopes: {:?}", e); - ApiError::InternalError(Some("Failed to update controller scopes".into())) - .into_response() + Ok( + ApiError::InternalError(Some("Failed to update controller scopes".into())) + .into_response(), + ) } } } @@ -295,21 +301,26 @@ pub struct ListControlledAccountsResponse { pub accounts: Vec, } -pub async fn list_controlled_accounts(State(state): State, auth: BearerAuth) -> Response { +pub async fn list_controlled_accounts( + State(state): State, + auth: Auth, +) -> Result { let accounts = match state .delegation_repo - .get_accounts_controlled_by(&auth.0.did) + .get_accounts_controlled_by(&auth.did) .await { Ok(a) => a, Err(e) => { tracing::error!("Failed to list controlled accounts: {:?}", e); - return ApiError::InternalError(Some("Failed to list controlled accounts".into())) - .into_response(); + return Ok( + ApiError::InternalError(Some("Failed to list controlled accounts".into())) + .into_response(), + ); } }; - Json(ListControlledAccountsResponse { + Ok(Json(ListControlledAccountsResponse { accounts: accounts .into_iter() .map(|a| DelegatedAccountInfo { @@ -320,7 +331,7 @@ pub async fn list_controlled_accounts(State(state): State, auth: Beare }) .collect(), }) - .into_response() + .into_response()) } #[derive(Debug, Deserialize)] @@ -355,31 +366,33 @@ pub struct GetAuditLogResponse { pub async fn get_audit_log( State(state): State, - auth: BearerAuth, + auth: Auth, Query(params): Query, -) -> Response { +) -> Result { let limit = params.limit.clamp(1, 100); let offset = params.offset.max(0); let entries = match state .delegation_repo - .get_audit_log_for_account(&auth.0.did, limit, offset) + .get_audit_log_for_account(&auth.did, limit, offset) .await { Ok(e) => e, Err(e) => { tracing::error!("Failed to get audit log: {:?}", e); - return ApiError::InternalError(Some("Failed to get audit log".into())).into_response(); + return Ok( + ApiError::InternalError(Some("Failed to get audit log".into())).into_response(), + ); } }; let total = state .delegation_repo - .count_audit_log_entries(&auth.0.did) + .count_audit_log_entries(&auth.did) .await .unwrap_or_default(); - Json(GetAuditLogResponse { + Ok(Json(GetAuditLogResponse { entries: entries .into_iter() .map(|e| AuditLogEntry { @@ -394,7 +407,7 @@ pub async fn get_audit_log( .collect(), total, }) - .into_response() + .into_response()) } #[derive(Debug, Serialize)] @@ -444,36 +457,38 @@ pub struct CreateDelegatedAccountResponse { pub async fn create_delegated_account( State(state): State, headers: HeaderMap, - auth: BearerAuth, + auth: Auth, Json(input): Json, -) -> Response { +) -> Result { let client_ip = extract_client_ip(&headers); if !state .check_rate_limit(RateLimitKind::AccountCreation, &client_ip) .await { warn!(ip = %client_ip, "Delegated account creation rate limit exceeded"); - return ApiError::RateLimitExceeded(Some( + return Ok(ApiError::RateLimitExceeded(Some( "Too many account creation attempts. Please try again later.".into(), )) - .into_response(); + .into_response()); } if let Err(e) = scopes::validate_delegation_scopes(&input.controller_scopes) { - return ApiError::InvalidScopes(e).into_response(); + return Ok(ApiError::InvalidScopes(e).into_response()); } - match state.delegation_repo.has_any_controllers(&auth.0.did).await { + match state.delegation_repo.has_any_controllers(&auth.did).await { Ok(true) => { - return ApiError::InvalidDelegation( + return Ok(ApiError::InvalidDelegation( "Cannot create delegated accounts from a controlled account".into(), ) - .into_response(); + .into_response()); } Err(e) => { tracing::error!("Failed to check controller status: {:?}", e); - return ApiError::InternalError(Some("Failed to verify controller status".into())) - .into_response(); + return Ok( + ApiError::InternalError(Some("Failed to verify controller status".into())) + .into_response(), + ); } Ok(false) => {} } @@ -494,7 +509,7 @@ pub async fn create_delegated_account( match crate::api::validation::validate_short_handle(handle_to_validate) { Ok(h) => format!("{}.{}", h, hostname_for_handles), Err(e) => { - return ApiError::InvalidRequest(e.to_string()).into_response(); + return Ok(ApiError::InvalidRequest(e.to_string()).into_response()); } } } else { @@ -509,7 +524,7 @@ pub async fn create_delegated_account( if let Some(ref email) = email && !crate::api::validation::is_valid_email(email) { - return ApiError::InvalidEmail.into_response(); + return Ok(ApiError::InvalidEmail.into_response()); } if let Some(ref code) = input.invite_code { @@ -520,14 +535,14 @@ pub async fn create_delegated_account( .unwrap_or(false); if !valid { - return ApiError::InvalidInviteCode.into_response(); + return Ok(ApiError::InvalidInviteCode.into_response()); } } else { let invite_required = std::env::var("INVITE_CODE_REQUIRED") .map(|v| v == "true" || v == "1") .unwrap_or(false); if invite_required { - return ApiError::InviteCodeRequired.into_response(); + return Ok(ApiError::InviteCodeRequired.into_response()); } } @@ -542,7 +557,7 @@ pub async fn create_delegated_account( Ok(k) => k, Err(e) => { error!("Error creating signing key: {:?}", e); - return ApiError::InternalError(None).into_response(); + return Ok(ApiError::InternalError(None).into_response()); } }; @@ -558,8 +573,10 @@ pub async fn create_delegated_account( Ok(r) => r, Err(e) => { error!("Error creating PLC genesis operation: {:?}", e); - return ApiError::InternalError(Some("Failed to create PLC operation".into())) - .into_response(); + return Ok( + ApiError::InternalError(Some("Failed to create PLC operation".into())) + .into_response(), + ); } }; @@ -569,22 +586,22 @@ pub async fn create_delegated_account( .await { error!("Failed to submit PLC genesis operation: {:?}", e); - return ApiError::UpstreamErrorMsg(format!( + return Ok(ApiError::UpstreamErrorMsg(format!( "Failed to register DID with PLC directory: {}", e )) - .into_response(); + .into_response()); } let did = Did::new_unchecked(&genesis_result.did); let handle = Handle::new_unchecked(&handle); - info!(did = %did, handle = %handle, controller = %&auth.0.did, "Created DID for delegated account"); + info!(did = %did, handle = %handle, controller = %&auth.did, "Created DID for delegated account"); let encrypted_key_bytes = match crate::config::encrypt_key(&secret_key_bytes) { Ok(bytes) => bytes, Err(e) => { error!("Error encrypting signing key: {:?}", e); - return ApiError::InternalError(None).into_response(); + return Ok(ApiError::InternalError(None).into_response()); } }; @@ -593,7 +610,7 @@ pub async fn create_delegated_account( Ok(c) => c, Err(e) => { error!("Error persisting MST: {:?}", e); - return ApiError::InternalError(None).into_response(); + return Ok(ApiError::InternalError(None).into_response()); } }; let rev = Tid::now(LimitedU32::MIN); @@ -602,14 +619,14 @@ pub async fn create_delegated_account( Ok(result) => result, Err(e) => { error!("Error creating genesis commit: {:?}", e); - return ApiError::InternalError(None).into_response(); + return Ok(ApiError::InternalError(None).into_response()); } }; let commit_cid: cid::Cid = match state.block_store.put(&commit_bytes).await { Ok(c) => c, Err(e) => { error!("Error saving genesis commit: {:?}", e); - return ApiError::InternalError(None).into_response(); + return Ok(ApiError::InternalError(None).into_response()); } }; let genesis_block_cids = vec![mst_root.to_bytes(), commit_cid.to_bytes()]; @@ -618,7 +635,7 @@ pub async fn create_delegated_account( handle: handle.clone(), email: email.clone(), did: did.clone(), - controller_did: auth.0.did.clone(), + controller_did: auth.did.clone(), controller_scopes: input.controller_scopes.clone(), encrypted_key_bytes, encryption_version: crate::config::ENCRYPTION_VERSION, @@ -635,14 +652,14 @@ pub async fn create_delegated_account( { Ok(id) => id, Err(tranquil_db_traits::CreateAccountError::HandleTaken) => { - return ApiError::HandleNotAvailable(None).into_response(); + return Ok(ApiError::HandleNotAvailable(None).into_response()); } Err(tranquil_db_traits::CreateAccountError::EmailTaken) => { - return ApiError::EmailTaken.into_response(); + return Ok(ApiError::EmailTaken.into_response()); } Err(e) => { error!("Error creating delegated account: {:?}", e); - return ApiError::InternalError(None).into_response(); + return Ok(ApiError::InternalError(None).into_response()); } }; @@ -678,8 +695,8 @@ pub async fn create_delegated_account( .delegation_repo .log_delegation_action( &did, - &auth.0.did, - Some(&auth.0.did), + &auth.did, + Some(&auth.did), DelegationActionType::GrantCreated, Some(json!({ "account_created": true, @@ -690,7 +707,7 @@ pub async fn create_delegated_account( ) .await; - info!(did = %did, handle = %handle, controller = %&auth.0.did, "Delegated account created"); + info!(did = %did, handle = %handle, controller = %&auth.did, "Delegated account created"); - Json(CreateDelegatedAccountResponse { did, handle }).into_response() + Ok(Json(CreateDelegatedAccountResponse { did, handle }).into_response()) } diff --git a/crates/tranquil-pds/src/api/error.rs b/crates/tranquil-pds/src/api/error.rs index 82cf3ec..3afe821 100644 --- a/crates/tranquil-pds/src/api/error.rs +++ b/crates/tranquil-pds/src/api/error.rs @@ -543,6 +543,13 @@ impl From for ApiError { crate::auth::extractor::AuthError::AccountDeactivated => Self::AccountDeactivated, crate::auth::extractor::AuthError::AccountTakedown => Self::AccountTakedown, crate::auth::extractor::AuthError::AdminRequired => Self::AdminRequired, + crate::auth::extractor::AuthError::ServiceAuthNotAllowed => Self::AuthenticationFailed( + Some("Service authentication not allowed for this endpoint".to_string()), + ), + crate::auth::extractor::AuthError::SigningKeyRequired => Self::InvalidSigningKey, + crate::auth::extractor::AuthError::InsufficientScope(msg) => { + Self::InsufficientScope(Some(msg)) + } crate::auth::extractor::AuthError::OAuthExpiredToken(msg) => { Self::OAuthExpiredToken(Some(msg)) } diff --git a/crates/tranquil-pds/src/api/identity/did.rs b/crates/tranquil-pds/src/api/identity/did.rs index af22777..3919fd7 100644 --- a/crates/tranquil-pds/src/api/identity/did.rs +++ b/crates/tranquil-pds/src/api/identity/did.rs @@ -1,5 +1,5 @@ use crate::api::{ApiError, DidResponse, EmptyResponse}; -use crate::auth::BearerAuthAllowDeactivated; +use crate::auth::{Auth, NotTakendown}; use crate::plc::signing_key_to_did_key; use crate::state::AppState; use crate::types::Handle; @@ -518,31 +518,25 @@ pub struct AtprotoPds { pub async fn get_recommended_did_credentials( State(state): State, - auth: BearerAuthAllowDeactivated, -) -> Response { - let auth_user = auth.0; - let handle = match state.user_repo.get_handle_by_did(&auth_user.did).await { - Ok(Some(h)) => h, - Ok(None) => return ApiError::InternalError(None).into_response(), - Err(_) => return ApiError::InternalError(None).into_response(), - }; - let key_bytes = match auth_user.key_bytes { - Some(kb) => kb, - None => { - return ApiError::AuthenticationFailed(Some( - "OAuth tokens cannot get DID credentials".into(), - )) - .into_response(); - } - }; + auth: Auth, +) -> Result { + let handle = state + .user_repo + .get_handle_by_did(&auth.did) + .await + .map_err(|_| ApiError::InternalError(None))? + .ok_or(ApiError::InternalError(None))?; + + let key_bytes = auth.key_bytes.clone().ok_or_else(|| { + ApiError::AuthenticationFailed(Some("OAuth tokens cannot get DID credentials".into())) + })?; + let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); let pds_endpoint = format!("https://{}", hostname); - let signing_key = match k256::ecdsa::SigningKey::from_slice(&key_bytes) { - Ok(k) => k, - Err(_) => return ApiError::InternalError(None).into_response(), - }; + let signing_key = k256::ecdsa::SigningKey::from_slice(&key_bytes) + .map_err(|_| ApiError::InternalError(None))?; let did_key = signing_key_to_did_key(&signing_key); - let rotation_keys = if auth_user.did.starts_with("did:web:") { + let rotation_keys = if auth.did.starts_with("did:web:") { vec![] } else { let server_rotation_key = match std::env::var("PLC_ROTATION_KEY") { @@ -556,7 +550,7 @@ pub async fn get_recommended_did_credentials( }; vec![server_rotation_key] }; - ( + Ok(( StatusCode::OK, Json(GetRecommendedDidCredentialsOutput { rotation_keys, @@ -570,7 +564,7 @@ pub async fn get_recommended_did_credentials( }, }), ) - .into_response() + .into_response()) } #[derive(Deserialize)] @@ -580,68 +574,70 @@ pub struct UpdateHandleInput { pub async fn update_handle( State(state): State, - auth: BearerAuthAllowDeactivated, + auth: Auth, Json(input): Json, -) -> Response { - let auth_user = auth.0; +) -> Result { if let Err(e) = crate::auth::scope_check::check_identity_scope( - auth_user.is_oauth, - auth_user.scope.as_deref(), + auth.is_oauth(), + auth.scope.as_deref(), crate::oauth::scopes::IdentityAttr::Handle, ) { - return e; + return Ok(e); } - let did = auth_user.did; + let did = auth.did.clone(); if !state .check_rate_limit(crate::state::RateLimitKind::HandleUpdate, &did) .await { - return ApiError::RateLimitExceeded(Some( + return Err(ApiError::RateLimitExceeded(Some( "Too many handle updates. Try again later.".into(), - )) - .into_response(); + ))); } if !state .check_rate_limit(crate::state::RateLimitKind::HandleUpdateDaily, &did) .await { - return ApiError::RateLimitExceeded(Some("Daily handle update limit exceeded.".into())) - .into_response(); + return Err(ApiError::RateLimitExceeded(Some( + "Daily handle update limit exceeded.".into(), + ))); } - let user_row = match state.user_repo.get_id_and_handle_by_did(&did).await { - Ok(Some(row)) => row, - Ok(None) => return ApiError::InternalError(None).into_response(), - Err(_) => return ApiError::InternalError(None).into_response(), - }; + let user_row = state + .user_repo + .get_id_and_handle_by_did(&did) + .await + .map_err(|_| ApiError::InternalError(None))? + .ok_or(ApiError::InternalError(None))?; let user_id = user_row.id; let current_handle = user_row.handle; let new_handle = input.handle.trim().to_ascii_lowercase(); if new_handle.is_empty() { - return ApiError::InvalidRequest("handle is required".into()).into_response(); + return Err(ApiError::InvalidRequest("handle is required".into())); } if !new_handle .chars() .all(|c| c.is_ascii_alphanumeric() || c == '.' || c == '-') { - return ApiError::InvalidHandle(Some("Handle contains invalid characters".into())) - .into_response(); + return Err(ApiError::InvalidHandle(Some( + "Handle contains invalid characters".into(), + ))); } if new_handle.split('.').any(|segment| segment.is_empty()) { - return ApiError::InvalidHandle(Some("Handle contains empty segment".into())) - .into_response(); + return Err(ApiError::InvalidHandle(Some( + "Handle contains empty segment".into(), + ))); } if new_handle .split('.') .any(|segment| segment.starts_with('-') || segment.ends_with('-')) { - return ApiError::InvalidHandle(Some( + return Err(ApiError::InvalidHandle(Some( "Handle segment cannot start or end with hyphen".into(), - )) - .into_response(); + ))); } if crate::moderation::has_explicit_slur(&new_handle) { - return ApiError::InvalidHandle(Some("Inappropriate language in handle".into())) - .into_response(); + return Err(ApiError::InvalidHandle(Some( + "Inappropriate language in handle".into(), + ))); } let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); let hostname_for_handles = hostname.split(':').next().unwrap_or(&hostname); @@ -667,19 +663,18 @@ pub async fn update_handle( { warn!("Failed to sequence identity event for handle update: {}", e); } - return EmptyResponse::ok().into_response(); + return Ok(EmptyResponse::ok().into_response()); } if short_part.contains('.') { - return ApiError::InvalidHandle(Some( + return Err(ApiError::InvalidHandle(Some( "Nested subdomains are not allowed. Use a simple handle without dots.".into(), - )) - .into_response(); + ))); } if short_part.len() < 3 { - return ApiError::InvalidHandle(Some("Handle too short".into())).into_response(); + return Err(ApiError::InvalidHandle(Some("Handle too short".into()))); } if short_part.len() > 18 { - return ApiError::InvalidHandle(Some("Handle too long".into())).into_response(); + return Err(ApiError::InvalidHandle(Some("Handle too long".into()))); } full_handle } else { @@ -691,74 +686,65 @@ pub async fn update_handle( { warn!("Failed to sequence identity event for handle update: {}", e); } - return EmptyResponse::ok().into_response(); + return Ok(EmptyResponse::ok().into_response()); } match crate::handle::verify_handle_ownership(&new_handle, &did).await { Ok(()) => {} Err(crate::handle::HandleResolutionError::NotFound) => { - return ApiError::HandleNotAvailable(None).into_response(); + return Err(ApiError::HandleNotAvailable(None)); } Err(crate::handle::HandleResolutionError::DidMismatch { expected, actual }) => { - return ApiError::HandleNotAvailable(Some(format!( + return Err(ApiError::HandleNotAvailable(Some(format!( "Handle points to different DID. Expected {}, got {}", expected, actual - ))) - .into_response(); + )))); } Err(e) => { warn!("Handle verification failed: {}", e); - return ApiError::HandleNotAvailable(Some(format!( + return Err(ApiError::HandleNotAvailable(Some(format!( "Handle verification failed: {}", e - ))) - .into_response(); + )))); } } new_handle.clone() }; - let handle_typed: Handle = match handle.parse() { - Ok(h) => h, - Err(_) => { - return ApiError::InvalidHandle(Some("Invalid handle format".into())).into_response(); - } - }; - let handle_exists = match state + let handle_typed: Handle = handle + .parse() + .map_err(|_| ApiError::InvalidHandle(Some("Invalid handle format".into())))?; + let handle_exists = state .user_repo .check_handle_exists(&handle_typed, user_id) .await - { - Ok(exists) => exists, - Err(_) => return ApiError::InternalError(None).into_response(), - }; + .map_err(|_| ApiError::InternalError(None))?; if handle_exists { - return ApiError::HandleTaken.into_response(); + return Err(ApiError::HandleTaken); } - let result = state.user_repo.update_handle(user_id, &handle_typed).await; - match result { - Ok(_) => { - if !current_handle.is_empty() { - let _ = state - .cache - .delete(&format!("handle:{}", current_handle)) - .await; - } - let _ = state.cache.delete(&format!("handle:{}", handle)).await; - if let Err(e) = - crate::api::repo::record::sequence_identity_event(&state, &did, Some(&handle_typed)) - .await - { - warn!("Failed to sequence identity event for handle update: {}", e); - } - if let Err(e) = update_plc_handle(&state, &did, &handle_typed).await { - warn!("Failed to update PLC handle: {}", e); - } - EmptyResponse::ok().into_response() - } - Err(e) => { + state + .user_repo + .update_handle(user_id, &handle_typed) + .await + .map_err(|e| { error!("DB error updating handle: {:?}", e); - ApiError::InternalError(None).into_response() - } + ApiError::InternalError(None) + })?; + + if !current_handle.is_empty() { + let _ = state + .cache + .delete(&format!("handle:{}", current_handle)) + .await; } + let _ = state.cache.delete(&format!("handle:{}", handle)).await; + if let Err(e) = + crate::api::repo::record::sequence_identity_event(&state, &did, Some(&handle_typed)).await + { + warn!("Failed to sequence identity event for handle update: {}", e); + } + if let Err(e) = update_plc_handle(&state, &did, &handle_typed).await { + warn!("Failed to update PLC handle: {}", e); + } + Ok(EmptyResponse::ok().into_response()) } pub async fn update_plc_handle( diff --git a/crates/tranquil-pds/src/api/identity/plc/request.rs b/crates/tranquil-pds/src/api/identity/plc/request.rs index 4700d9f..9bba943 100644 --- a/crates/tranquil-pds/src/api/identity/plc/request.rs +++ b/crates/tranquil-pds/src/api/identity/plc/request.rs @@ -1,6 +1,6 @@ use crate::api::EmptyResponse; use crate::api::error::ApiError; -use crate::auth::BearerAuthAllowDeactivated; +use crate::auth::{Auth, NotTakendown}; use crate::state::AppState; use axum::{ extract::State, @@ -15,35 +15,37 @@ fn generate_plc_token() -> String { pub async fn request_plc_operation_signature( State(state): State, - auth: BearerAuthAllowDeactivated, -) -> Response { - let auth_user = auth.0; + auth: Auth, +) -> Result { if let Err(e) = crate::auth::scope_check::check_identity_scope( - auth_user.is_oauth, - auth_user.scope.as_deref(), + auth.is_oauth(), + auth.scope.as_deref(), crate::oauth::scopes::IdentityAttr::Wildcard, ) { - return e; + return Ok(e); } - let user_id = match state.user_repo.get_id_by_did(&auth_user.did).await { - Ok(Some(id)) => id, - Ok(None) => return ApiError::AccountNotFound.into_response(), - Err(e) => { + let user_id = state + .user_repo + .get_id_by_did(&auth.did) + .await + .map_err(|e| { error!("DB error: {:?}", e); - return ApiError::InternalError(None).into_response(); - } - }; + ApiError::InternalError(None) + })? + .ok_or(ApiError::AccountNotFound)?; + let _ = state.infra_repo.delete_plc_tokens_for_user(user_id).await; let plc_token = generate_plc_token(); let expires_at = Utc::now() + Duration::minutes(10); - if let Err(e) = state + state .infra_repo .insert_plc_token(user_id, &plc_token, expires_at) .await - { - error!("Failed to create PLC token: {:?}", e); - return ApiError::InternalError(None).into_response(); - } + .map_err(|e| { + error!("Failed to create PLC token: {:?}", e); + ApiError::InternalError(None) + })?; + let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); if let Err(e) = crate::comms::comms_repo::enqueue_plc_operation( state.user_repo.as_ref(), @@ -56,9 +58,6 @@ pub async fn request_plc_operation_signature( { warn!("Failed to enqueue PLC operation notification: {:?}", e); } - info!( - "PLC operation signature requested for user {}", - auth_user.did - ); - EmptyResponse::ok().into_response() + info!("PLC operation signature requested for user {}", auth.did); + Ok(EmptyResponse::ok().into_response()) } diff --git a/crates/tranquil-pds/src/api/identity/plc/sign.rs b/crates/tranquil-pds/src/api/identity/plc/sign.rs index 4bbace7..ed13e89 100644 --- a/crates/tranquil-pds/src/api/identity/plc/sign.rs +++ b/crates/tranquil-pds/src/api/identity/plc/sign.rs @@ -1,5 +1,5 @@ use crate::api::ApiError; -use crate::auth::BearerAuthAllowDeactivated; +use crate::auth::{Auth, NotTakendown}; use crate::circuit_breaker::with_circuit_breaker; use crate::plc::{PlcClient, PlcError, PlcService, create_update_op, sign_operation}; use crate::state::AppState; @@ -40,93 +40,81 @@ pub struct SignPlcOperationOutput { pub async fn sign_plc_operation( State(state): State, - auth: BearerAuthAllowDeactivated, + auth: Auth, Json(input): Json, -) -> Response { - let auth_user = auth.0; +) -> Result { if let Err(e) = crate::auth::scope_check::check_identity_scope( - auth_user.is_oauth, - auth_user.scope.as_deref(), + auth.is_oauth(), + auth.scope.as_deref(), crate::oauth::scopes::IdentityAttr::Wildcard, ) { - return e; + return Ok(e); } - let did = &auth_user.did; + let did = &auth.did; if did.starts_with("did:web:") { - return ApiError::InvalidRequest( + return Err(ApiError::InvalidRequest( "PLC operations are only valid for did:plc identities".into(), - ) - .into_response(); + )); } - let token = match &input.token { - Some(t) => t, - None => { - return ApiError::InvalidRequest( - "Email confirmation token required to sign PLC operations".into(), - ) - .into_response(); - } - }; - let user_id = match state.user_repo.get_id_by_did(did).await { - Ok(Some(id)) => id, - Ok(None) => return ApiError::AccountNotFound.into_response(), - Err(e) => { + let token = input.token.as_ref().ok_or_else(|| { + ApiError::InvalidRequest("Email confirmation token required to sign PLC operations".into()) + })?; + + let user_id = state + .user_repo + .get_id_by_did(did) + .await + .map_err(|e| { error!("DB error: {:?}", e); - return ApiError::InternalError(None).into_response(); - } - }; - let token_expiry = match state.infra_repo.get_plc_token_expiry(user_id, token).await { - Ok(Some(expiry)) => expiry, - Ok(None) => { - return ApiError::InvalidToken(Some("Invalid or expired token".into())).into_response(); - } - Err(e) => { + ApiError::InternalError(None) + })? + .ok_or(ApiError::AccountNotFound)?; + + let token_expiry = state + .infra_repo + .get_plc_token_expiry(user_id, token) + .await + .map_err(|e| { error!("DB error: {:?}", e); - return ApiError::InternalError(None).into_response(); - } - }; + ApiError::InternalError(None) + })? + .ok_or_else(|| ApiError::InvalidToken(Some("Invalid or expired token".into())))?; + if Utc::now() > token_expiry { let _ = state.infra_repo.delete_plc_token(user_id, token).await; - return ApiError::ExpiredToken(Some("Token has expired".into())).into_response(); + return Err(ApiError::ExpiredToken(Some("Token has expired".into()))); } - let key_row = match state.user_repo.get_user_key_by_id(user_id).await { - Ok(Some(row)) => row, - Ok(None) => { - return ApiError::InternalError(Some("User signing key not found".into())) - .into_response(); - } - Err(e) => { + let key_row = state + .user_repo + .get_user_key_by_id(user_id) + .await + .map_err(|e| { error!("DB error: {:?}", e); - return ApiError::InternalError(None).into_response(); - } - }; - let key_bytes = match crate::config::decrypt_key(&key_row.key_bytes, key_row.encryption_version) - { - Ok(k) => k, - Err(e) => { + ApiError::InternalError(None) + })? + .ok_or_else(|| ApiError::InternalError(Some("User signing key not found".into())))?; + + let key_bytes = crate::config::decrypt_key(&key_row.key_bytes, key_row.encryption_version) + .map_err(|e| { error!("Failed to decrypt user key: {}", e); - return ApiError::InternalError(None).into_response(); - } - }; - let signing_key = match SigningKey::from_slice(&key_bytes) { - Ok(k) => k, - Err(e) => { - error!("Failed to create signing key: {:?}", e); - return ApiError::InternalError(None).into_response(); - } - }; + ApiError::InternalError(None) + })?; + + let signing_key = SigningKey::from_slice(&key_bytes).map_err(|e| { + error!("Failed to create signing key: {:?}", e); + ApiError::InternalError(None) + })?; + let plc_client = PlcClient::with_cache(None, Some(state.cache.clone())); let did_clone = did.clone(); - let last_op = match with_circuit_breaker(&state.circuit_breakers.plc_directory, || async { + let last_op = with_circuit_breaker(&state.circuit_breakers.plc_directory, || async { plc_client.get_last_op(&did_clone).await }) .await - { - Ok(op) => op, - Err(e) => return ApiError::from(e).into_response(), - }; + .map_err(ApiError::from)?; + if last_op.is_tombstone() { - return ApiError::from(PlcError::Tombstoned).into_response(); + return Err(ApiError::from(PlcError::Tombstoned)); } let services = input.services.map(|s| { s.into_iter() @@ -141,36 +129,33 @@ pub async fn sign_plc_operation( }) .collect() }); - let unsigned_op = match create_update_op( + let unsigned_op = create_update_op( &last_op, input.rotation_keys, input.verification_methods, input.also_known_as, services, - ) { - Ok(op) => op, - Err(PlcError::Tombstoned) => { - return ApiError::InvalidRequest("Cannot update tombstoned DID".into()).into_response(); - } - Err(e) => { + ) + .map_err(|e| match e { + PlcError::Tombstoned => ApiError::InvalidRequest("Cannot update tombstoned DID".into()), + _ => { error!("Failed to create PLC operation: {:?}", e); - return ApiError::InternalError(None).into_response(); + ApiError::InternalError(None) } - }; - let signed_op = match sign_operation(&unsigned_op, &signing_key) { - Ok(op) => op, - Err(e) => { - error!("Failed to sign PLC operation: {:?}", e); - return ApiError::InternalError(None).into_response(); - } - }; + })?; + + let signed_op = sign_operation(&unsigned_op, &signing_key).map_err(|e| { + error!("Failed to sign PLC operation: {:?}", e); + ApiError::InternalError(None) + })?; + let _ = state.infra_repo.delete_plc_token(user_id, token).await; info!("Signed PLC operation for user {}", did); - ( + Ok(( StatusCode::OK, Json(SignPlcOperationOutput { operation: signed_op, }), ) - .into_response() + .into_response()) } diff --git a/crates/tranquil-pds/src/api/identity/plc/submit.rs b/crates/tranquil-pds/src/api/identity/plc/submit.rs index f238945..0d432fb 100644 --- a/crates/tranquil-pds/src/api/identity/plc/submit.rs +++ b/crates/tranquil-pds/src/api/identity/plc/submit.rs @@ -1,5 +1,5 @@ use crate::api::{ApiError, EmptyResponse}; -use crate::auth::BearerAuthAllowDeactivated; +use crate::auth::{Auth, NotTakendown}; use crate::circuit_breaker::with_circuit_breaker; use crate::plc::{PlcClient, signing_key_to_did_key, validate_plc_operation}; use crate::state::AppState; @@ -20,64 +20,59 @@ pub struct SubmitPlcOperationInput { pub async fn submit_plc_operation( State(state): State, - auth: BearerAuthAllowDeactivated, + auth: Auth, Json(input): Json, -) -> Response { - let auth_user = auth.0; +) -> Result { if let Err(e) = crate::auth::scope_check::check_identity_scope( - auth_user.is_oauth, - auth_user.scope.as_deref(), + auth.is_oauth(), + auth.scope.as_deref(), crate::oauth::scopes::IdentityAttr::Wildcard, ) { - return e; + return Ok(e); } - let did = &auth_user.did; + let did = &auth.did; if did.starts_with("did:web:") { - return ApiError::InvalidRequest( + return Err(ApiError::InvalidRequest( "PLC operations are only valid for did:plc identities".into(), - ) - .into_response(); - } - if let Err(e) = validate_plc_operation(&input.operation) { - return ApiError::InvalidRequest(format!("Invalid operation: {}", e)).into_response(); + )); } + validate_plc_operation(&input.operation) + .map_err(|e| ApiError::InvalidRequest(format!("Invalid operation: {}", e)))?; + let op = &input.operation; let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); let public_url = format!("https://{}", hostname); - let user = match state.user_repo.get_id_and_handle_by_did(did).await { - Ok(Some(u)) => u, - Ok(None) => return ApiError::AccountNotFound.into_response(), - Err(e) => { + let user = state + .user_repo + .get_id_and_handle_by_did(did) + .await + .map_err(|e| { error!("DB error: {:?}", e); - return ApiError::InternalError(None).into_response(); - } - }; - let key_row = match state.user_repo.get_user_key_by_id(user.id).await { - Ok(Some(row)) => row, - Ok(None) => { - return ApiError::InternalError(Some("User signing key not found".into())) - .into_response(); - } - Err(e) => { + ApiError::InternalError(None) + })? + .ok_or(ApiError::AccountNotFound)?; + + let key_row = state + .user_repo + .get_user_key_by_id(user.id) + .await + .map_err(|e| { error!("DB error: {:?}", e); - return ApiError::InternalError(None).into_response(); - } - }; - let key_bytes = match crate::config::decrypt_key(&key_row.key_bytes, key_row.encryption_version) - { - Ok(k) => k, - Err(e) => { + ApiError::InternalError(None) + })? + .ok_or_else(|| ApiError::InternalError(Some("User signing key not found".into())))?; + + let key_bytes = crate::config::decrypt_key(&key_row.key_bytes, key_row.encryption_version) + .map_err(|e| { error!("Failed to decrypt user key: {}", e); - return ApiError::InternalError(None).into_response(); - } - }; - let signing_key = match SigningKey::from_slice(&key_bytes) { - Ok(k) => k, - Err(e) => { - error!("Failed to create signing key: {:?}", e); - return ApiError::InternalError(None).into_response(); - } - }; + ApiError::InternalError(None) + })?; + + let signing_key = SigningKey::from_slice(&key_bytes).map_err(|e| { + error!("Failed to create signing key: {:?}", e); + ApiError::InternalError(None) + })?; + let user_did_key = signing_key_to_did_key(&signing_key); let server_rotation_key = std::env::var("PLC_ROTATION_KEY").unwrap_or_else(|_| user_did_key.clone()); @@ -86,10 +81,9 @@ pub async fn submit_plc_operation( .iter() .any(|k| k.as_str() == Some(&server_rotation_key)); if !has_server_key { - return ApiError::InvalidRequest( + return Err(ApiError::InvalidRequest( "Rotation keys do not include server's rotation key".into(), - ) - .into_response(); + )); } } if let Some(services) = op.get("services").and_then(|v| v.as_object()) @@ -98,20 +92,23 @@ pub async fn submit_plc_operation( let service_type = pds.get("type").and_then(|v| v.as_str()); let endpoint = pds.get("endpoint").and_then(|v| v.as_str()); if service_type != Some("AtprotoPersonalDataServer") { - return ApiError::InvalidRequest("Incorrect type on atproto_pds service".into()) - .into_response(); + return Err(ApiError::InvalidRequest( + "Incorrect type on atproto_pds service".into(), + )); } if endpoint != Some(&public_url) { - return ApiError::InvalidRequest("Incorrect endpoint on atproto_pds service".into()) - .into_response(); + return Err(ApiError::InvalidRequest( + "Incorrect endpoint on atproto_pds service".into(), + )); } } if let Some(verification_methods) = op.get("verificationMethods").and_then(|v| v.as_object()) && let Some(atproto_key) = verification_methods.get("atproto").and_then(|v| v.as_str()) && atproto_key != user_did_key { - return ApiError::InvalidRequest("Incorrect signing key in verificationMethods".into()) - .into_response(); + return Err(ApiError::InvalidRequest( + "Incorrect signing key in verificationMethods".into(), + )); } if let Some(also_known_as) = (!user.handle.is_empty()) .then(|| op.get("alsoKnownAs").and_then(|v| v.as_array())) @@ -120,22 +117,22 @@ pub async fn submit_plc_operation( let expected_handle = format!("at://{}", user.handle); let first_aka = also_known_as.first().and_then(|v| v.as_str()); if first_aka != Some(&expected_handle) { - return ApiError::InvalidRequest("Incorrect handle in alsoKnownAs".into()) - .into_response(); + return Err(ApiError::InvalidRequest( + "Incorrect handle in alsoKnownAs".into(), + )); } } let plc_client = PlcClient::with_cache(None, Some(state.cache.clone())); let operation_clone = input.operation.clone(); let did_clone = did.clone(); - if let Err(e) = with_circuit_breaker(&state.circuit_breakers.plc_directory, || async { + with_circuit_breaker(&state.circuit_breakers.plc_directory, || async { plc_client .send_operation(&did_clone, &operation_clone) .await }) .await - { - return ApiError::from(e).into_response(); - } + .map_err(ApiError::from)?; + match state .repo_repo .insert_identity_event(did, Some(&user.handle)) @@ -157,5 +154,5 @@ pub async fn submit_plc_operation( warn!(did = %did, "Failed to refresh DID cache after PLC update"); } info!(did = %did, "PLC operation submitted successfully"); - EmptyResponse::ok().into_response() + Ok(EmptyResponse::ok().into_response()) } diff --git a/crates/tranquil-pds/src/api/moderation/mod.rs b/crates/tranquil-pds/src/api/moderation/mod.rs index 87f56a0..2cbe719 100644 --- a/crates/tranquil-pds/src/api/moderation/mod.rs +++ b/crates/tranquil-pds/src/api/moderation/mod.rs @@ -1,6 +1,6 @@ use crate::api::ApiError; use crate::api::proxy_client::{is_ssrf_safe, proxy_client}; -use crate::auth::extractor::BearerAuthAllowTakendown; +use crate::auth::{AnyUser, Auth}; use crate::state::AppState; use axum::{ Json, @@ -42,18 +42,16 @@ fn get_report_service_config() -> Option<(String, String)> { pub async fn create_report( State(state): State, - auth: BearerAuthAllowTakendown, + auth: Auth, Json(input): Json, ) -> Response { - let auth_user = auth.0; - let did = &auth_user.did; + let did = &auth.did; if let Some((service_url, service_did)) = get_report_service_config() { - return proxy_to_report_service(&state, &auth_user, &service_url, &service_did, &input) - .await; + return proxy_to_report_service(&state, &auth, &service_url, &service_did, &input).await; } - create_report_locally(&state, did, auth_user.is_takendown(), input).await + create_report_locally(&state, did, auth.status.is_takendown(), input).await } async fn proxy_to_report_service( diff --git a/crates/tranquil-pds/src/api/notification_prefs.rs b/crates/tranquil-pds/src/api/notification_prefs.rs index 3dd3e95..a82f3ce 100644 --- a/crates/tranquil-pds/src/api/notification_prefs.rs +++ b/crates/tranquil-pds/src/api/notification_prefs.rs @@ -1,5 +1,5 @@ use crate::api::error::ApiError; -use crate::auth::BearerAuth; +use crate::auth::{Active, Auth}; use crate::state::AppState; use axum::{ Json, @@ -23,16 +23,17 @@ pub struct NotificationPrefsResponse { pub signal_verified: bool, } -pub async fn get_notification_prefs(State(state): State, auth: BearerAuth) -> Response { - let user = auth.0; - let prefs = match state.user_repo.get_notification_prefs(&user.did).await { - Ok(Some(p)) => p, - Ok(None) => return ApiError::AccountNotFound.into_response(), - Err(e) => { - return ApiError::InternalError(Some(format!("Database error: {}", e))).into_response(); - } - }; - Json(NotificationPrefsResponse { +pub async fn get_notification_prefs( + State(state): State, + auth: Auth, +) -> Result { + let prefs = state + .user_repo + .get_notification_prefs(&auth.did) + .await + .map_err(|e| ApiError::InternalError(Some(format!("Database error: {}", e))))? + .ok_or(ApiError::AccountNotFound)?; + Ok(Json(NotificationPrefsResponse { preferred_channel: prefs.preferred_channel, email: prefs.email, discord_id: prefs.discord_id, @@ -42,7 +43,7 @@ pub async fn get_notification_prefs(State(state): State, auth: BearerA signal_number: prefs.signal_number, signal_verified: prefs.signal_verified, }) - .into_response() + .into_response()) } #[derive(Serialize)] @@ -62,23 +63,22 @@ pub struct GetNotificationHistoryResponse { pub notifications: Vec, } -pub async fn get_notification_history(State(state): State, auth: BearerAuth) -> Response { - let user = auth.0; +pub async fn get_notification_history( + State(state): State, + auth: Auth, +) -> Result { + let user_id = state + .user_repo + .get_id_by_did(&auth.did) + .await + .map_err(|e| ApiError::InternalError(Some(format!("Database error: {}", e))))? + .ok_or(ApiError::AccountNotFound)?; - let user_id: uuid::Uuid = match state.user_repo.get_id_by_did(&user.did).await { - Ok(Some(id)) => id, - Ok(None) => return ApiError::AccountNotFound.into_response(), - Err(e) => { - return ApiError::InternalError(Some(format!("Database error: {}", e))).into_response(); - } - }; - - let rows = match state.infra_repo.get_notification_history(user_id, 50).await { - Ok(r) => r, - Err(e) => { - return ApiError::InternalError(Some(format!("Database error: {}", e))).into_response(); - } - }; + let rows = state + .infra_repo + .get_notification_history(user_id, 50) + .await + .map_err(|e| ApiError::InternalError(Some(format!("Database error: {}", e))))?; let sensitive_types = [ "email_verification", @@ -111,7 +111,7 @@ pub async fn get_notification_history(State(state): State, auth: Beare }) .collect(); - Json(GetNotificationHistoryResponse { notifications }).into_response() + Ok(Json(GetNotificationHistoryResponse { notifications }).into_response()) } #[derive(Deserialize)] @@ -184,18 +184,15 @@ pub async fn request_channel_verification( pub async fn update_notification_prefs( State(state): State, - auth: BearerAuth, + auth: Auth, Json(input): Json, -) -> Response { - let user = auth.0; - - let user_row = match state.user_repo.get_id_handle_email_by_did(&user.did).await { - Ok(Some(row)) => row, - Ok(None) => return ApiError::AccountNotFound.into_response(), - Err(e) => { - return ApiError::InternalError(Some(format!("Database error: {}", e))).into_response(); - } - }; +) -> Result { + let user_row = state + .user_repo + .get_id_handle_email_by_did(&auth.did) + .await + .map_err(|e| ApiError::InternalError(Some(format!("Database error: {}", e))))? + .ok_or(ApiError::AccountNotFound)?; let user_id = user_row.id; let handle = user_row.handle; @@ -206,119 +203,106 @@ pub async fn update_notification_prefs( if let Some(ref channel) = input.preferred_channel { let valid_channels = ["email", "discord", "telegram", "signal"]; if !valid_channels.contains(&channel.as_str()) { - return ApiError::InvalidRequest( + return Err(ApiError::InvalidRequest( "Invalid channel. Must be one of: email, discord, telegram, signal".into(), - ) - .into_response(); + )); } - if let Err(e) = state + state .user_repo - .update_preferred_comms_channel(&user.did, channel) + .update_preferred_comms_channel(&auth.did, channel) .await - { - return ApiError::InternalError(Some(format!("Database error: {}", e))).into_response(); - } - info!(did = %user.did, channel = %channel, "Updated preferred notification channel"); + .map_err(|e| ApiError::InternalError(Some(format!("Database error: {}", e))))?; + info!(did = %auth.did, channel = %channel, "Updated preferred notification channel"); } if let Some(ref new_email) = input.email { let email_clean = new_email.trim().to_lowercase(); if email_clean.is_empty() { - return ApiError::InvalidRequest("Email cannot be empty".into()).into_response(); + return Err(ApiError::InvalidRequest("Email cannot be empty".into())); } if !crate::api::validation::is_valid_email(&email_clean) { - return ApiError::InvalidEmail.into_response(); + return Err(ApiError::InvalidEmail); } - if current_email.as_ref().map(|e| e.to_lowercase()) == Some(email_clean.clone()) { - info!(did = %user.did, "Email unchanged, skipping"); - } else { - if let Err(e) = request_channel_verification( + if current_email.as_ref().map(|e| e.to_lowercase()) != Some(email_clean.clone()) { + request_channel_verification( &state, user_id, - &user.did, + &auth.did, "email", &email_clean, Some(&handle), ) .await - { - return ApiError::InternalError(Some(e)).into_response(); - } + .map_err(|e| ApiError::InternalError(Some(e)))?; verification_required.push("email".to_string()); - info!(did = %user.did, "Requested email verification"); + info!(did = %auth.did, "Requested email verification"); } } if let Some(ref discord_id) = input.discord_id { if discord_id.is_empty() { - if let Err(e) = state.user_repo.clear_discord(user_id).await { - return ApiError::InternalError(Some(format!("Database error: {}", e))) - .into_response(); - } - info!(did = %user.did, "Cleared Discord ID"); + state + .user_repo + .clear_discord(user_id) + .await + .map_err(|e| ApiError::InternalError(Some(format!("Database error: {}", e))))?; + info!(did = %auth.did, "Cleared Discord ID"); } else { - if let Err(e) = request_channel_verification( - &state, user_id, &user.did, "discord", discord_id, None, - ) - .await - { - return ApiError::InternalError(Some(e)).into_response(); - } + request_channel_verification(&state, user_id, &auth.did, "discord", discord_id, None) + .await + .map_err(|e| ApiError::InternalError(Some(e)))?; verification_required.push("discord".to_string()); - info!(did = %user.did, "Requested Discord verification"); + info!(did = %auth.did, "Requested Discord verification"); } } if let Some(ref telegram) = input.telegram_username { let telegram_clean = telegram.trim_start_matches('@'); if telegram_clean.is_empty() { - if let Err(e) = state.user_repo.clear_telegram(user_id).await { - return ApiError::InternalError(Some(format!("Database error: {}", e))) - .into_response(); - } - info!(did = %user.did, "Cleared Telegram username"); + state + .user_repo + .clear_telegram(user_id) + .await + .map_err(|e| ApiError::InternalError(Some(format!("Database error: {}", e))))?; + info!(did = %auth.did, "Cleared Telegram username"); } else { - if let Err(e) = request_channel_verification( + request_channel_verification( &state, user_id, - &user.did, + &auth.did, "telegram", telegram_clean, None, ) .await - { - return ApiError::InternalError(Some(e)).into_response(); - } + .map_err(|e| ApiError::InternalError(Some(e)))?; verification_required.push("telegram".to_string()); - info!(did = %user.did, "Requested Telegram verification"); + info!(did = %auth.did, "Requested Telegram verification"); } } if let Some(ref signal) = input.signal_number { if signal.is_empty() { - if let Err(e) = state.user_repo.clear_signal(user_id).await { - return ApiError::InternalError(Some(format!("Database error: {}", e))) - .into_response(); - } - info!(did = %user.did, "Cleared Signal number"); + state + .user_repo + .clear_signal(user_id) + .await + .map_err(|e| ApiError::InternalError(Some(format!("Database error: {}", e))))?; + info!(did = %auth.did, "Cleared Signal number"); } else { - if let Err(e) = - request_channel_verification(&state, user_id, &user.did, "signal", signal, None) - .await - { - return ApiError::InternalError(Some(e)).into_response(); - } + request_channel_verification(&state, user_id, &auth.did, "signal", signal, None) + .await + .map_err(|e| ApiError::InternalError(Some(e)))?; verification_required.push("signal".to_string()); - info!(did = %user.did, "Requested Signal verification"); + info!(did = %auth.did, "Requested Signal verification"); } } - Json(UpdateNotificationPrefsResponse { + Ok(Json(UpdateNotificationPrefsResponse { success: true, verification_required, }) - .into_response() + .into_response()) } diff --git a/crates/tranquil-pds/src/api/proxy.rs b/crates/tranquil-pds/src/api/proxy.rs index e5dba33..5e4f229 100644 --- a/crates/tranquil-pds/src/api/proxy.rs +++ b/crates/tranquil-pds/src/api/proxy.rs @@ -238,7 +238,7 @@ async fn proxy_handler( { Ok(auth_user) => { if let Err(e) = crate::auth::scope_check::check_rpc_scope( - auth_user.is_oauth, + auth_user.is_oauth(), auth_user.scope.as_deref(), &resolved.did, method, diff --git a/crates/tranquil-pds/src/api/repo/blob.rs b/crates/tranquil-pds/src/api/repo/blob.rs index a9c2ac3..7b6e05d 100644 --- a/crates/tranquil-pds/src/api/repo/blob.rs +++ b/crates/tranquil-pds/src/api/repo/blob.rs @@ -1,5 +1,5 @@ use crate::api::error::ApiError; -use crate::auth::{BearerAuthAllowDeactivated, BlobAuth, BlobAuthResult}; +use crate::auth::{Auth, AuthAny, NotTakendown, Permissive}; use crate::delegation::DelegationActionType; use crate::state::AppState; use crate::types::{CidLink, Did}; @@ -44,25 +44,30 @@ fn detect_mime_type(data: &[u8], client_hint: &str) -> String { pub async fn upload_blob( State(state): State, headers: axum::http::HeaderMap, - auth: BlobAuth, + auth: AuthAny, body: Body, -) -> Response { - let (did, controller_did): (Did, Option) = match auth.0 { - BlobAuthResult::Service { did } => (did, None), - BlobAuthResult::User(auth_user) => { +) -> Result { + let (did, controller_did): (Did, Option) = match &auth { + AuthAny::Service(service) => { + service.require_lxm("com.atproto.repo.uploadBlob")?; + (service.did.clone(), None) + } + AuthAny::User(user) => { + if user.status.is_takendown() { + return Err(ApiError::AccountTakedown); + } let mime_type_for_check = headers .get("content-type") .and_then(|h| h.to_str().ok()) .unwrap_or("application/octet-stream"); if let Err(e) = crate::auth::scope_check::check_blob_scope( - auth_user.is_oauth, - auth_user.scope.as_deref(), + user.is_oauth(), + user.scope.as_deref(), mime_type_for_check, ) { - return e; + return Ok(e); } - let ctrl_did = auth_user.controller_did.clone(); - (auth_user.did, ctrl_did) + (user.did.clone(), user.controller_did.clone()) } }; @@ -72,7 +77,7 @@ pub async fn upload_blob( .await .unwrap_or(false) { - return ApiError::Forbidden.into_response(); + return Err(ApiError::Forbidden); } let client_mime_hint = headers @@ -80,12 +85,13 @@ pub async fn upload_blob( .and_then(|h| h.to_str().ok()) .unwrap_or("application/octet-stream"); - let user_id = match state.user_repo.get_id_by_did(&did).await { - Ok(Some(id)) => id, - _ => { - return ApiError::InternalError(None).into_response(); - } - }; + let user_id = state + .user_repo + .get_id_by_did(&did) + .await + .ok() + .flatten() + .ok_or(ApiError::InternalError(None))?; let temp_key = format!("temp/{}", uuid::Uuid::new_v4()); let max_size = get_max_blob_size() as u64; @@ -98,22 +104,22 @@ pub async fn upload_blob( info!("Starting streaming blob upload to temp key: {}", temp_key); - let upload_result = match state.blob_store.put_stream(&temp_key, pinned_stream).await { - Ok(result) => result, - Err(e) => { + let upload_result = state + .blob_store + .put_stream(&temp_key, pinned_stream) + .await + .map_err(|e| { error!("Failed to stream blob to storage: {:?}", e); - return ApiError::InternalError(Some("Failed to store blob".into())).into_response(); - } - }; + ApiError::InternalError(Some("Failed to store blob".into())) + })?; let size = upload_result.size; if size > max_size { let _ = state.blob_store.delete(&temp_key).await; - return ApiError::InvalidRequest(format!( + return Err(ApiError::InvalidRequest(format!( "Blob size {} exceeds maximum of {} bytes", size, max_size - )) - .into_response(); + ))); } let mime_type = match state.blob_store.get_head(&temp_key, 8192).await { @@ -129,7 +135,7 @@ pub async fn upload_blob( Err(e) => { let _ = state.blob_store.delete(&temp_key).await; error!("Failed to create multihash for blob: {:?}", e); - return ApiError::InternalError(Some("Failed to hash blob".into())).into_response(); + return Err(ApiError::InternalError(Some("Failed to hash blob".into()))); } }; let cid = Cid::new_v1(0x55, multihash); @@ -152,14 +158,14 @@ pub async fn upload_blob( Err(e) => { let _ = state.blob_store.delete(&temp_key).await; error!("Failed to insert blob record: {:?}", e); - return ApiError::InternalError(None).into_response(); + return Err(ApiError::InternalError(None)); } }; if was_inserted && let Err(e) = state.blob_store.copy(&temp_key, &storage_key).await { let _ = state.blob_store.delete(&temp_key).await; error!("Failed to copy blob to final location: {:?}", e); - return ApiError::InternalError(Some("Failed to store blob".into())).into_response(); + return Err(ApiError::InternalError(Some("Failed to store blob".into()))); } let _ = state.blob_store.delete(&temp_key).await; @@ -183,7 +189,7 @@ pub async fn upload_blob( .await; } - Json(json!({ + Ok(Json(json!({ "blob": { "$type": "blob", "ref": { @@ -193,7 +199,7 @@ pub async fn upload_blob( "size": size } })) - .into_response() + .into_response()) } #[derive(Deserialize)] @@ -218,32 +224,31 @@ pub struct ListMissingBlobsOutput { pub async fn list_missing_blobs( State(state): State, - auth: BearerAuthAllowDeactivated, + auth: Auth, Query(params): Query, -) -> Response { - let auth_user = auth.0; - let did = &auth_user.did; - let user = match state.user_repo.get_by_did(did).await { - Ok(Some(u)) => u, - Ok(None) => return ApiError::InternalError(None).into_response(), - Err(e) => { +) -> Result { + let did = &auth.did; + let user = state + .user_repo + .get_by_did(did) + .await + .map_err(|e| { error!("DB error fetching user: {:?}", e); - return ApiError::InternalError(None).into_response(); - } - }; + ApiError::InternalError(None) + })? + .ok_or(ApiError::InternalError(None))?; + let limit = params.limit.unwrap_or(500).clamp(1, 1000); let cursor = params.cursor.as_deref(); - let missing = match state + let missing = state .blob_repo .list_missing_blobs(user.id, cursor, limit + 1) .await - { - Ok(m) => m, - Err(e) => { + .map_err(|e| { error!("DB error fetching missing blobs: {:?}", e); - return ApiError::InternalError(None).into_response(); - } - }; + ApiError::InternalError(None) + })?; + let has_more = missing.len() > limit as usize; let blobs: Vec = missing .into_iter() @@ -258,12 +263,12 @@ pub async fn list_missing_blobs( } else { None }; - ( + Ok(( StatusCode::OK, Json(ListMissingBlobsOutput { cursor: next_cursor, blobs, }), ) - .into_response() + .into_response()) } diff --git a/crates/tranquil-pds/src/api/repo/import.rs b/crates/tranquil-pds/src/api/repo/import.rs index ee3af17..32fa141 100644 --- a/crates/tranquil-pds/src/api/repo/import.rs +++ b/crates/tranquil-pds/src/api/repo/import.rs @@ -1,7 +1,7 @@ use crate::api::EmptyResponse; use crate::api::error::ApiError; use crate::api::repo::record::create_signed_commit; -use crate::auth::BearerAuthAllowDeactivated; +use crate::auth::{Auth, NotTakendown}; use crate::state::AppState; use crate::sync::import::{ImportError, apply_import, parse_car}; use crate::sync::verify::CarVerifier; @@ -23,56 +23,57 @@ const DEFAULT_MAX_BLOCKS: usize = 500000; pub async fn import_repo( State(state): State, - auth: BearerAuthAllowDeactivated, + auth: Auth, body: Bytes, -) -> Response { +) -> Result { let accepting_imports = std::env::var("ACCEPTING_REPO_IMPORTS") .map(|v| v != "false" && v != "0") .unwrap_or(true); if !accepting_imports { - return ApiError::InvalidRequest("Service is not accepting repo imports".into()) - .into_response(); + return Err(ApiError::InvalidRequest( + "Service is not accepting repo imports".into(), + )); } let max_size: usize = std::env::var("MAX_IMPORT_SIZE") .ok() .and_then(|s| s.parse().ok()) .unwrap_or(DEFAULT_MAX_IMPORT_SIZE); if body.len() > max_size { - return ApiError::PayloadTooLarge(format!( + return Err(ApiError::PayloadTooLarge(format!( "Import size exceeds limit of {} bytes", max_size - )) - .into_response(); + ))); } - let auth_user = auth.0; - let did = &auth_user.did; - let user = match state.user_repo.get_by_did(did).await { - Ok(Some(row)) => row, - Ok(None) => { - return ApiError::AccountNotFound.into_response(); - } - Err(e) => { + let did = &auth.did; + let user = state + .user_repo + .get_by_did(did) + .await + .map_err(|e| { error!("DB error fetching user: {:?}", e); - return ApiError::InternalError(None).into_response(); - } - }; + ApiError::InternalError(None) + })? + .ok_or(ApiError::AccountNotFound)?; if user.takedown_ref.is_some() { - return ApiError::AccountTakedown.into_response(); + return Err(ApiError::AccountTakedown); } let user_id = user.id; let (root, blocks) = match parse_car(&body).await { Ok((r, b)) => (r, b), Err(ImportError::InvalidRootCount) => { - return ApiError::InvalidRequest("Expected exactly one root in CAR file".into()) - .into_response(); + return Err(ApiError::InvalidRequest( + "Expected exactly one root in CAR file".into(), + )); } Err(ImportError::CarParse(msg)) => { - return ApiError::InvalidRequest(format!("Failed to parse CAR file: {}", msg)) - .into_response(); + return Err(ApiError::InvalidRequest(format!( + "Failed to parse CAR file: {}", + msg + ))); } Err(e) => { error!("CAR parsing error: {:?}", e); - return ApiError::InvalidRequest(format!("Invalid CAR file: {}", e)).into_response(); + return Err(ApiError::InvalidRequest(format!("Invalid CAR file: {}", e))); } }; info!( @@ -82,20 +83,21 @@ pub async fn import_repo( root ); let Some(root_block) = blocks.get(&root) else { - return ApiError::InvalidRequest("Root block not found in CAR file".into()).into_response(); + return Err(ApiError::InvalidRequest( + "Root block not found in CAR file".into(), + )); }; let commit_did = match jacquard_repo::commit::Commit::from_cbor(root_block) { Ok(commit) => commit.did().to_string(), Err(e) => { - return ApiError::InvalidRequest(format!("Invalid commit: {}", e)).into_response(); + return Err(ApiError::InvalidRequest(format!("Invalid commit: {}", e))); } }; if commit_did != *did { - return ApiError::InvalidRepo(format!( + return Err(ApiError::InvalidRepo(format!( "CAR file is for DID {} but you are authenticated as {}", commit_did, did - )) - .into_response(); + ))); } let skip_verification = std::env::var("SKIP_IMPORT_VERIFICATION") .map(|v| v == "true" || v == "1") @@ -117,20 +119,23 @@ pub async fn import_repo( commit_did, expected_did, }) => { - return ApiError::InvalidRepo(format!( + return Err(ApiError::InvalidRepo(format!( "CAR file is for DID {} but you are authenticated as {}", commit_did, expected_did - )) - .into_response(); + ))); } Err(crate::sync::verify::VerifyError::MstValidationFailed(msg)) => { - return ApiError::InvalidRequest(format!("MST validation failed: {}", msg)) - .into_response(); + return Err(ApiError::InvalidRequest(format!( + "MST validation failed: {}", + msg + ))); } Err(e) => { error!("CAR structure verification error: {:?}", e); - return ApiError::InvalidRequest(format!("CAR verification failed: {}", e)) - .into_response(); + return Err(ApiError::InvalidRequest(format!( + "CAR verification failed: {}", + e + ))); } } } else { @@ -147,37 +152,40 @@ pub async fn import_repo( commit_did, expected_did, }) => { - return ApiError::InvalidRepo(format!( + return Err(ApiError::InvalidRepo(format!( "CAR file is for DID {} but you are authenticated as {}", commit_did, expected_did - )) - .into_response(); + ))); } Err(crate::sync::verify::VerifyError::InvalidSignature) => { - return ApiError::InvalidRequest( + return Err(ApiError::InvalidRequest( "CAR file commit signature verification failed".into(), - ) - .into_response(); + )); } Err(crate::sync::verify::VerifyError::DidResolutionFailed(msg)) => { warn!("DID resolution failed during import verification: {}", msg); - return ApiError::InvalidRequest(format!("Failed to verify DID: {}", msg)) - .into_response(); + return Err(ApiError::InvalidRequest(format!( + "Failed to verify DID: {}", + msg + ))); } Err(crate::sync::verify::VerifyError::NoSigningKey) => { - return ApiError::InvalidRequest( + return Err(ApiError::InvalidRequest( "DID document does not contain a signing key".into(), - ) - .into_response(); + )); } Err(crate::sync::verify::VerifyError::MstValidationFailed(msg)) => { - return ApiError::InvalidRequest(format!("MST validation failed: {}", msg)) - .into_response(); + return Err(ApiError::InvalidRequest(format!( + "MST validation failed: {}", + msg + ))); } Err(e) => { error!("CAR verification error: {:?}", e); - return ApiError::InvalidRequest(format!("CAR verification failed: {}", e)) - .into_response(); + return Err(ApiError::InvalidRequest(format!( + "CAR verification failed: {}", + e + ))); } } } @@ -227,74 +235,65 @@ pub async fn import_repo( } } } - let key_row = match state.user_repo.get_user_with_key_by_did(did).await { - Ok(Some(row)) => row, - Ok(None) => { - error!("No signing key found for user {}", did); - return ApiError::InternalError(Some("Signing key not found".into())) - .into_response(); - } - Err(e) => { + let key_row = state + .user_repo + .get_user_with_key_by_did(did) + .await + .map_err(|e| { error!("DB error fetching signing key: {:?}", e); - return ApiError::InternalError(None).into_response(); - } - }; + ApiError::InternalError(None) + })? + .ok_or_else(|| { + error!("No signing key found for user {}", did); + ApiError::InternalError(Some("Signing key not found".into())) + })?; let key_bytes = - match crate::config::decrypt_key(&key_row.key_bytes, key_row.encryption_version) { - Ok(k) => k, - Err(e) => { + crate::config::decrypt_key(&key_row.key_bytes, key_row.encryption_version) + .map_err(|e| { error!("Failed to decrypt signing key: {}", e); - return ApiError::InternalError(None).into_response(); - } - }; - let signing_key = match SigningKey::from_slice(&key_bytes) { - Ok(k) => k, - Err(e) => { - error!("Invalid signing key: {:?}", e); - return ApiError::InternalError(None).into_response(); - } - }; + ApiError::InternalError(None) + })?; + let signing_key = SigningKey::from_slice(&key_bytes).map_err(|e| { + error!("Invalid signing key: {:?}", e); + ApiError::InternalError(None) + })?; let new_rev = Tid::now(LimitedU32::MIN); let new_rev_str = new_rev.to_string(); - let (commit_bytes, _sig) = match create_signed_commit( + let (commit_bytes, _sig) = create_signed_commit( did, import_result.data_cid, &new_rev_str, None, &signing_key, - ) { - Ok(result) => result, - Err(e) => { - error!("Failed to create new commit: {}", e); - return ApiError::InternalError(None).into_response(); - } - }; - let new_root_cid: cid::Cid = match state.block_store.put(&commit_bytes).await { - Ok(cid) => cid, - Err(e) => { + ) + .map_err(|e| { + error!("Failed to create new commit: {}", e); + ApiError::InternalError(None) + })?; + let new_root_cid: cid::Cid = + state.block_store.put(&commit_bytes).await.map_err(|e| { error!("Failed to store new commit block: {:?}", e); - return ApiError::InternalError(None).into_response(); - } - }; + ApiError::InternalError(None) + })?; let new_root_cid_link = CidLink::new_unchecked(new_root_cid.to_string()); - if let Err(e) = state + state .repo_repo .update_repo_root(user_id, &new_root_cid_link, &new_rev_str) .await - { - error!("Failed to update repo root: {:?}", e); - return ApiError::InternalError(None).into_response(); - } + .map_err(|e| { + error!("Failed to update repo root: {:?}", e); + ApiError::InternalError(None) + })?; let mut all_block_cids: Vec> = blocks.keys().map(|c| c.to_bytes()).collect(); all_block_cids.push(new_root_cid.to_bytes()); - if let Err(e) = state + state .repo_repo .insert_user_blocks(user_id, &all_block_cids, &new_rev_str) .await - { - error!("Failed to insert user_blocks: {:?}", e); - return ApiError::InternalError(None).into_response(); - } + .map_err(|e| { + error!("Failed to insert user_blocks: {:?}", e); + ApiError::InternalError(None) + })?; let new_root_str = new_root_cid.to_string(); info!( "Created new commit for imported repo: cid={}, rev={}", @@ -324,41 +323,40 @@ pub async fn import_repo( ); } } - EmptyResponse::ok().into_response() + Ok(EmptyResponse::ok().into_response()) } - Err(ImportError::SizeLimitExceeded) => { - ApiError::PayloadTooLarge(format!("Import exceeds block limit of {}", max_blocks)) - .into_response() - } - Err(ImportError::RepoNotFound) => { - ApiError::RepoNotFound(Some("Repository not initialized for this account".into())) - .into_response() - } - Err(ImportError::InvalidCbor(msg)) => { - ApiError::InvalidRequest(format!("Invalid CBOR data: {}", msg)).into_response() - } - Err(ImportError::InvalidCommit(msg)) => { - ApiError::InvalidRequest(format!("Invalid commit structure: {}", msg)).into_response() - } - Err(ImportError::BlockNotFound(cid)) => { - ApiError::InvalidRequest(format!("Referenced block not found in CAR: {}", cid)) - .into_response() - } - Err(ImportError::ConcurrentModification) => ApiError::InvalidSwap(Some( + Err(ImportError::SizeLimitExceeded) => Err(ApiError::PayloadTooLarge(format!( + "Import exceeds block limit of {}", + max_blocks + ))), + Err(ImportError::RepoNotFound) => Err(ApiError::RepoNotFound(Some( + "Repository not initialized for this account".into(), + ))), + Err(ImportError::InvalidCbor(msg)) => Err(ApiError::InvalidRequest(format!( + "Invalid CBOR data: {}", + msg + ))), + Err(ImportError::InvalidCommit(msg)) => Err(ApiError::InvalidRequest(format!( + "Invalid commit structure: {}", + msg + ))), + Err(ImportError::BlockNotFound(cid)) => Err(ApiError::InvalidRequest(format!( + "Referenced block not found in CAR: {}", + cid + ))), + Err(ImportError::ConcurrentModification) => Err(ApiError::InvalidSwap(Some( "Repository is being modified by another operation, please retry".into(), - )) - .into_response(), - Err(ImportError::VerificationFailed(ve)) => { - ApiError::InvalidRequest(format!("CAR verification failed: {}", ve)).into_response() - } - Err(ImportError::DidMismatch { car_did, auth_did }) => ApiError::InvalidRequest(format!( - "CAR is for {} but authenticated as {}", - car_did, auth_did - )) - .into_response(), + ))), + Err(ImportError::VerificationFailed(ve)) => Err(ApiError::InvalidRequest(format!( + "CAR verification failed: {}", + ve + ))), + Err(ImportError::DidMismatch { car_did, auth_did }) => Err(ApiError::InvalidRequest( + format!("CAR is for {} but authenticated as {}", car_did, auth_did), + )), Err(e) => { error!("Import error: {:?}", e); - ApiError::InternalError(None).into_response() + Err(ApiError::InternalError(None)) } } } diff --git a/crates/tranquil-pds/src/api/repo/record/batch.rs b/crates/tranquil-pds/src/api/repo/record/batch.rs index af4d300..2f8b7b7 100644 --- a/crates/tranquil-pds/src/api/repo/record/batch.rs +++ b/crates/tranquil-pds/src/api/repo/record/batch.rs @@ -1,7 +1,7 @@ use super::validation::validate_record_with_status; use crate::api::error::ApiError; use crate::api::repo::record::utils::{CommitParams, RecordOp, commit_and_log, extract_blob_cids}; -use crate::auth::BearerAuth; +use crate::auth::{Active, Auth}; use crate::delegation::DelegationActionType; use crate::repo::tracking::TrackingBlockStore; use crate::state::AppState; @@ -262,22 +262,22 @@ pub struct CommitInfo { pub async fn apply_writes( State(state): State, - auth: BearerAuth, + auth: Auth, Json(input): Json, -) -> Response { +) -> Result { info!( "apply_writes called: repo={}, writes={}", input.repo, input.writes.len() ); - let auth_user = auth.0; - let did = auth_user.did.clone(); - let is_oauth = auth_user.is_oauth; - let scope = auth_user.scope; - let controller_did = auth_user.controller_did.clone(); + let did = auth.did.clone(); + let is_oauth = auth.is_oauth(); + let scope = auth.scope.clone(); + let controller_did = auth.controller_did.clone(); if input.repo.as_str() != did { - return ApiError::InvalidRepo("Repo does not match authenticated user".into()) - .into_response(); + return Err(ApiError::InvalidRepo( + "Repo does not match authenticated user".into(), + )); } if state .user_repo @@ -285,7 +285,7 @@ pub async fn apply_writes( .await .unwrap_or(false) { - return ApiError::AccountMigrated.into_response(); + return Err(ApiError::AccountMigrated); } let is_verified = state .user_repo @@ -298,14 +298,16 @@ pub async fn apply_writes( .await .unwrap_or(false); if !is_verified && !is_delegated { - return ApiError::AccountNotVerified.into_response(); + return Err(ApiError::AccountNotVerified); } if input.writes.is_empty() { - return ApiError::InvalidRequest("writes array is empty".into()).into_response(); + return Err(ApiError::InvalidRequest("writes array is empty".into())); } if input.writes.len() > MAX_BATCH_WRITES { - return ApiError::InvalidRequest(format!("Too many writes (max {})", MAX_BATCH_WRITES)) - .into_response(); + return Err(ApiError::InvalidRequest(format!( + "Too many writes (max {})", + MAX_BATCH_WRITES + ))); } let has_custom_scope = scope @@ -374,38 +376,40 @@ pub async fn apply_writes( }) .next() { - return err; + return Ok(err); } } - let user_id: uuid::Uuid = match state.user_repo.get_id_by_did(&did).await { - Ok(Some(id)) => id, - _ => return ApiError::InternalError(Some("User not found".into())).into_response(), - }; - let root_cid_str = match state.repo_repo.get_repo_root_cid_by_user_id(user_id).await { - Ok(Some(cid_str)) => cid_str, - _ => return ApiError::InternalError(Some("Repo root not found".into())).into_response(), - }; - let current_root_cid = match Cid::from_str(&root_cid_str) { - Ok(c) => c, - Err(_) => { - return ApiError::InternalError(Some("Invalid repo root CID".into())).into_response(); - } - }; + let user_id: uuid::Uuid = state + .user_repo + .get_id_by_did(&did) + .await + .ok() + .flatten() + .ok_or_else(|| ApiError::InternalError(Some("User not found".into())))?; + let root_cid_str = state + .repo_repo + .get_repo_root_cid_by_user_id(user_id) + .await + .ok() + .flatten() + .ok_or_else(|| ApiError::InternalError(Some("Repo root not found".into())))?; + let current_root_cid = Cid::from_str(&root_cid_str) + .map_err(|_| ApiError::InternalError(Some("Invalid repo root CID".into())))?; if let Some(swap_commit) = &input.swap_commit && Cid::from_str(swap_commit).ok() != Some(current_root_cid) { - return ApiError::InvalidSwap(Some("Repo has been modified".into())).into_response(); + return Err(ApiError::InvalidSwap(Some("Repo has been modified".into()))); } let tracking_store = TrackingBlockStore::new(state.block_store.clone()); - let commit_bytes = match tracking_store.get(¤t_root_cid).await { - Ok(Some(b)) => b, - _ => return ApiError::InternalError(Some("Commit block not found".into())).into_response(), - }; - let commit = match Commit::from_cbor(&commit_bytes) { - Ok(c) => c, - _ => return ApiError::InternalError(Some("Failed to parse commit".into())).into_response(), - }; + let commit_bytes = tracking_store + .get(¤t_root_cid) + .await + .ok() + .flatten() + .ok_or_else(|| ApiError::InternalError(Some("Commit block not found".into())))?; + let commit = Commit::from_cbor(&commit_bytes) + .map_err(|_| ApiError::InternalError(Some("Failed to parse commit".into())))?; let original_mst = Mst::load(Arc::new(tracking_store.clone()), commit.data, None); let initial_mst = Mst::load(Arc::new(tracking_store.clone()), commit.data, None); let WriteAccumulator { @@ -424,34 +428,27 @@ pub async fn apply_writes( .await { Ok(acc) => acc, - Err(response) => return response, - }; - let new_mst_root = match mst.persist().await { - Ok(c) => c, - Err(_) => { - return ApiError::InternalError(Some("Failed to persist MST".into())).into_response(); - } + Err(response) => return Ok(response), }; + let new_mst_root = mst + .persist() + .await + .map_err(|_| ApiError::InternalError(Some("Failed to persist MST".into())))?; let (new_mst_blocks, old_mst_blocks) = { let mut new_blocks = std::collections::BTreeMap::new(); let mut old_blocks = std::collections::BTreeMap::new(); for key in &modified_keys { - if mst.blocks_for_path(key, &mut new_blocks).await.is_err() { - return ApiError::InternalError(Some( - "Failed to get new MST blocks for path".into(), - )) - .into_response(); - } - if original_mst + mst.blocks_for_path(key, &mut new_blocks) + .await + .map_err(|_| { + ApiError::InternalError(Some("Failed to get new MST blocks for path".into())) + })?; + original_mst .blocks_for_path(key, &mut old_blocks) .await - .is_err() - { - return ApiError::InternalError(Some( - "Failed to get old MST blocks for path".into(), - )) - .into_response(); - } + .map_err(|_| { + ApiError::InternalError(Some("Failed to get old MST blocks for path".into())) + })?; } (new_blocks, old_blocks) }; @@ -503,12 +500,13 @@ pub async fn apply_writes( { Ok(res) => res, Err(e) if e.contains("ConcurrentModification") => { - return ApiError::InvalidSwap(Some("Repo has been modified".into())).into_response(); + return Err(ApiError::InvalidSwap(Some("Repo has been modified".into()))); } Err(e) => { error!("Commit failed: {}", e); - return ApiError::InternalError(Some("Failed to commit changes".into())) - .into_response(); + return Err(ApiError::InternalError(Some( + "Failed to commit changes".into(), + ))); } }; @@ -557,7 +555,7 @@ pub async fn apply_writes( .await; } - ( + Ok(( StatusCode::OK, Json(ApplyWritesOutput { commit: CommitInfo { @@ -567,5 +565,5 @@ pub async fn apply_writes( results, }), ) - .into_response() + .into_response()) } diff --git a/crates/tranquil-pds/src/api/repo/record/delete.rs b/crates/tranquil-pds/src/api/repo/record/delete.rs index 1ba38b3..2662016 100644 --- a/crates/tranquil-pds/src/api/repo/record/delete.rs +++ b/crates/tranquil-pds/src/api/repo/record/delete.rs @@ -1,7 +1,7 @@ use crate::api::error::ApiError; use crate::api::repo::record::utils::{CommitParams, RecordOp, commit_and_log}; use crate::api::repo::record::write::{CommitInfo, prepare_repo_write}; -use crate::auth::BearerAuth; +use crate::auth::{Active, Auth}; use crate::delegation::DelegationActionType; use crate::repo::tracking::TrackingBlockStore; use crate::state::AppState; @@ -40,41 +40,49 @@ pub struct DeleteRecordOutput { pub async fn delete_record( State(state): State, - auth: BearerAuth, + auth: Auth, Json(input): Json, -) -> Response { - let auth = match prepare_repo_write(&state, auth.0, &input.repo).await { +) -> Result { + let repo_auth = match prepare_repo_write(&state, &auth, &input.repo).await { Ok(res) => res, - Err(err_res) => return err_res, + Err(err_res) => return Ok(err_res), }; if let Err(e) = crate::auth::scope_check::check_repo_scope( - auth.is_oauth, - auth.scope.as_deref(), + repo_auth.is_oauth, + repo_auth.scope.as_deref(), crate::oauth::RepoAction::Delete, &input.collection, ) { - return e; + return Ok(e); } - let did = auth.did; - let user_id = auth.user_id; - let current_root_cid = auth.current_root_cid; - let controller_did = auth.controller_did; + let did = repo_auth.did; + let user_id = repo_auth.user_id; + let current_root_cid = repo_auth.current_root_cid; + let controller_did = repo_auth.controller_did; if let Some(swap_commit) = &input.swap_commit && Cid::from_str(swap_commit).ok() != Some(current_root_cid) { - return ApiError::InvalidSwap(Some("Repo has been modified".into())).into_response(); + return Ok(ApiError::InvalidSwap(Some("Repo has been modified".into())).into_response()); } let tracking_store = TrackingBlockStore::new(state.block_store.clone()); let commit_bytes = match tracking_store.get(¤t_root_cid).await { Ok(Some(b)) => b, - _ => return ApiError::InternalError(Some("Commit block not found".into())).into_response(), + _ => { + return Ok( + ApiError::InternalError(Some("Commit block not found".into())).into_response(), + ); + } }; let commit = match Commit::from_cbor(&commit_bytes) { Ok(c) => c, - _ => return ApiError::InternalError(Some("Failed to parse commit".into())).into_response(), + _ => { + return Ok( + ApiError::InternalError(Some("Failed to parse commit".into())).into_response(), + ); + } }; let mst = Mst::load(Arc::new(tracking_store.clone()), commit.data, None); let key = format!("{}/{}", input.collection, input.rkey); @@ -82,29 +90,34 @@ pub async fn delete_record( let expected_cid = Cid::from_str(swap_record_str).ok(); let actual_cid = mst.get(&key).await.ok().flatten(); if expected_cid != actual_cid { - return ApiError::InvalidSwap(Some( + return Ok(ApiError::InvalidSwap(Some( "Record has been modified or does not exist".into(), )) - .into_response(); + .into_response()); } } let prev_record_cid = mst.get(&key).await.ok().flatten(); if prev_record_cid.is_none() { - return (StatusCode::OK, Json(DeleteRecordOutput { commit: None })).into_response(); + return Ok((StatusCode::OK, Json(DeleteRecordOutput { commit: None })).into_response()); } let new_mst = match mst.delete(&key).await { Ok(m) => m, Err(e) => { error!("Failed to delete from MST: {:?}", e); - return ApiError::InternalError(Some(format!("Failed to delete from MST: {:?}", e))) - .into_response(); + return Ok(ApiError::InternalError(Some(format!( + "Failed to delete from MST: {:?}", + e + ))) + .into_response()); } }; let new_mst_root = match new_mst.persist().await { Ok(c) => c, Err(e) => { error!("Failed to persist MST: {:?}", e); - return ApiError::InternalError(Some("Failed to persist MST".into())).into_response(); + return Ok( + ApiError::InternalError(Some("Failed to persist MST".into())).into_response(), + ); } }; let collection_for_audit = input.collection.to_string(); @@ -121,16 +134,20 @@ pub async fn delete_record( .await .is_err() { - return ApiError::InternalError(Some("Failed to get new MST blocks for path".into())) - .into_response(); + return Ok( + ApiError::InternalError(Some("Failed to get new MST blocks for path".into())) + .into_response(), + ); } if mst .blocks_for_path(&key, &mut old_mst_blocks) .await .is_err() { - return ApiError::InternalError(Some("Failed to get old MST blocks for path".into())) - .into_response(); + return Ok( + ApiError::InternalError(Some("Failed to get old MST blocks for path".into())) + .into_response(), + ); } let mut relevant_blocks = new_mst_blocks.clone(); relevant_blocks.extend(old_mst_blocks.iter().map(|(k, v)| (*k, v.clone()))); @@ -169,9 +186,9 @@ pub async fn delete_record( { Ok(res) => res, Err(e) if e.contains("ConcurrentModification") => { - return ApiError::InvalidSwap(Some("Repo has been modified".into())).into_response(); + return Ok(ApiError::InvalidSwap(Some("Repo has been modified".into())).into_response()); } - Err(e) => return ApiError::InternalError(Some(e)).into_response(), + Err(e) => return Ok(ApiError::InternalError(Some(e)).into_response()), }; if let Some(ref controller) = controller_did { @@ -202,7 +219,7 @@ pub async fn delete_record( error!("Failed to remove backlinks for {}: {}", deleted_uri, e); } - ( + Ok(( StatusCode::OK, Json(DeleteRecordOutput { commit: Some(CommitInfo { @@ -211,7 +228,7 @@ pub async fn delete_record( }), }), ) - .into_response() + .into_response()) } use crate::types::Did; diff --git a/crates/tranquil-pds/src/api/repo/record/write.rs b/crates/tranquil-pds/src/api/repo/record/write.rs index 8580721..968c6cb 100644 --- a/crates/tranquil-pds/src/api/repo/record/write.rs +++ b/crates/tranquil-pds/src/api/repo/record/write.rs @@ -3,7 +3,7 @@ use crate::api::error::ApiError; use crate::api::repo::record::utils::{ CommitParams, RecordOp, commit_and_log, extract_backlinks, extract_blob_cids, }; -use crate::auth::{AuthenticatedUser, BearerAuth}; +use crate::auth::{Active, Auth}; use crate::delegation::DelegationActionType; use crate::repo::tracking::TrackingBlockStore; use crate::state::AppState; @@ -34,7 +34,7 @@ pub struct RepoWriteAuth { pub async fn prepare_repo_write( state: &AppState, - auth_user: AuthenticatedUser, + auth_user: &crate::auth::AuthenticatedUser, repo: &AtIdentifier, ) -> Result { if repo.as_str() != auth_user.did.as_str() { @@ -90,8 +90,8 @@ pub async fn prepare_repo_write( did: auth_user.did.clone(), user_id, current_root_cid, - is_oauth: auth_user.is_oauth, - scope: auth_user.scope, + is_oauth: auth_user.is_oauth(), + scope: auth_user.scope.clone(), controller_did: auth_user.controller_did.clone(), }) } @@ -124,32 +124,32 @@ pub struct CreateRecordOutput { } pub async fn create_record( State(state): State, - auth: BearerAuth, + auth: Auth, Json(input): Json, -) -> Response { - let auth = match prepare_repo_write(&state, auth.0, &input.repo).await { +) -> Result { + let repo_auth = match prepare_repo_write(&state, &auth, &input.repo).await { Ok(res) => res, - Err(err_res) => return err_res, + Err(err_res) => return Ok(err_res), }; if let Err(e) = crate::auth::scope_check::check_repo_scope( - auth.is_oauth, - auth.scope.as_deref(), + repo_auth.is_oauth, + repo_auth.scope.as_deref(), crate::oauth::RepoAction::Create, &input.collection, ) { - return e; + return Ok(e); } - let did = auth.did; - let user_id = auth.user_id; - let current_root_cid = auth.current_root_cid; - let controller_did = auth.controller_did; + let did = repo_auth.did; + let user_id = repo_auth.user_id; + let current_root_cid = repo_auth.current_root_cid; + let controller_did = repo_auth.controller_did; if let Some(swap_commit) = &input.swap_commit && Cid::from_str(swap_commit).ok() != Some(current_root_cid) { - return ApiError::InvalidSwap(Some("Repo has been modified".into())).into_response(); + return Ok(ApiError::InvalidSwap(Some("Repo has been modified".into())).into_response()); } let validation_status = if input.validate == Some(false) { @@ -163,7 +163,7 @@ pub async fn create_record( require_lexicon, ) { Ok(status) => Some(status), - Err(err_response) => return *err_response, + Err(err_response) => return Ok(*err_response), } }; let rkey = input.rkey.unwrap_or_else(Rkey::generate); @@ -171,11 +171,19 @@ pub async fn create_record( let tracking_store = TrackingBlockStore::new(state.block_store.clone()); let commit_bytes = match tracking_store.get(¤t_root_cid).await { Ok(Some(b)) => b, - _ => return ApiError::InternalError(Some("Commit block not found".into())).into_response(), + _ => { + return Ok( + ApiError::InternalError(Some("Commit block not found".into())).into_response(), + ); + } }; let commit = match Commit::from_cbor(&commit_bytes) { Ok(c) => c, - _ => return ApiError::InternalError(Some("Failed to parse commit".into())).into_response(), + _ => { + return Ok( + ApiError::InternalError(Some("Failed to parse commit".into())).into_response(), + ); + } }; let mut mst = Mst::load(Arc::new(tracking_store.clone()), commit.data, None); let initial_mst_root = commit.data; @@ -197,7 +205,7 @@ pub async fn create_record( Ok(c) => c, Err(e) => { error!("Failed to check backlink conflicts: {}", e); - return ApiError::InternalError(None).into_response(); + return Ok(ApiError::InternalError(None).into_response()); } }; @@ -250,13 +258,14 @@ pub async fn create_record( let record_ipld = crate::util::json_to_ipld(&input.record); let mut record_bytes = Vec::new(); if serde_ipld_dagcbor::to_writer(&mut record_bytes, &record_ipld).is_err() { - return ApiError::InvalidRecord("Failed to serialize record".into()).into_response(); + return Ok(ApiError::InvalidRecord("Failed to serialize record".into()).into_response()); } let record_cid = match tracking_store.put(&record_bytes).await { Ok(c) => c, _ => { - return ApiError::InternalError(Some("Failed to save record block".into())) - .into_response(); + return Ok( + ApiError::InternalError(Some("Failed to save record block".into())).into_response(), + ); } }; let key = format!("{}/{}", input.collection, rkey); @@ -271,11 +280,17 @@ pub async fn create_record( let new_mst = match mst.add(&key, record_cid).await { Ok(m) => m, - _ => return ApiError::InternalError(Some("Failed to add to MST".into())).into_response(), + _ => { + return Ok(ApiError::InternalError(Some("Failed to add to MST".into())).into_response()); + } }; let new_mst_root = match new_mst.persist().await { Ok(c) => c, - _ => return ApiError::InternalError(Some("Failed to persist MST".into())).into_response(), + _ => { + return Ok( + ApiError::InternalError(Some("Failed to persist MST".into())).into_response(), + ); + } }; ops.push(RecordOp::Create { @@ -290,8 +305,10 @@ pub async fn create_record( .await .is_err() { - return ApiError::InternalError(Some("Failed to get new MST blocks for path".into())) - .into_response(); + return Ok( + ApiError::InternalError(Some("Failed to get new MST blocks for path".into())) + .into_response(), + ); } let mut relevant_blocks = new_mst_blocks.clone(); @@ -333,9 +350,9 @@ pub async fn create_record( { Ok(res) => res, Err(e) if e.contains("ConcurrentModification") => { - return ApiError::InvalidSwap(Some("Repo has been modified".into())).into_response(); + return Ok(ApiError::InvalidSwap(Some("Repo has been modified".into())).into_response()); } - Err(e) => return ApiError::InternalError(Some(e)).into_response(), + Err(e) => return Ok(ApiError::InternalError(Some(e)).into_response()), }; for conflict_uri in conflict_uris_to_cleanup { @@ -375,7 +392,7 @@ pub async fn create_record( error!("Failed to add backlinks for {}: {}", created_uri, e); } - ( + Ok(( StatusCode::OK, Json(CreateRecordOutput { uri: created_uri, @@ -387,7 +404,7 @@ pub async fn create_record( validation_status: validation_status.map(|s| s.to_string()), }), ) - .into_response() + .into_response()) } #[derive(Deserialize)] #[allow(dead_code)] @@ -414,49 +431,57 @@ pub struct PutRecordOutput { } pub async fn put_record( State(state): State, - auth: BearerAuth, + auth: Auth, Json(input): Json, -) -> Response { - let auth = match prepare_repo_write(&state, auth.0, &input.repo).await { +) -> Result { + let repo_auth = match prepare_repo_write(&state, &auth, &input.repo).await { Ok(res) => res, - Err(err_res) => return err_res, + Err(err_res) => return Ok(err_res), }; if let Err(e) = crate::auth::scope_check::check_repo_scope( - auth.is_oauth, - auth.scope.as_deref(), + repo_auth.is_oauth, + repo_auth.scope.as_deref(), crate::oauth::RepoAction::Create, &input.collection, ) { - return e; + return Ok(e); } if let Err(e) = crate::auth::scope_check::check_repo_scope( - auth.is_oauth, - auth.scope.as_deref(), + repo_auth.is_oauth, + repo_auth.scope.as_deref(), crate::oauth::RepoAction::Update, &input.collection, ) { - return e; + return Ok(e); } - let did = auth.did; - let user_id = auth.user_id; - let current_root_cid = auth.current_root_cid; - let controller_did = auth.controller_did; + let did = repo_auth.did; + let user_id = repo_auth.user_id; + let current_root_cid = repo_auth.current_root_cid; + let controller_did = repo_auth.controller_did; if let Some(swap_commit) = &input.swap_commit && Cid::from_str(swap_commit).ok() != Some(current_root_cid) { - return ApiError::InvalidSwap(Some("Repo has been modified".into())).into_response(); + return Ok(ApiError::InvalidSwap(Some("Repo has been modified".into())).into_response()); } let tracking_store = TrackingBlockStore::new(state.block_store.clone()); let commit_bytes = match tracking_store.get(¤t_root_cid).await { Ok(Some(b)) => b, - _ => return ApiError::InternalError(Some("Commit block not found".into())).into_response(), + _ => { + return Ok( + ApiError::InternalError(Some("Commit block not found".into())).into_response(), + ); + } }; let commit = match Commit::from_cbor(&commit_bytes) { Ok(c) => c, - _ => return ApiError::InternalError(Some("Failed to parse commit".into())).into_response(), + _ => { + return Ok( + ApiError::InternalError(Some("Failed to parse commit".into())).into_response(), + ); + } }; let mst = Mst::load(Arc::new(tracking_store.clone()), commit.data, None); let key = format!("{}/{}", input.collection, input.rkey); @@ -471,34 +496,35 @@ pub async fn put_record( require_lexicon, ) { Ok(status) => Some(status), - Err(err_response) => return *err_response, + Err(err_response) => return Ok(*err_response), } }; if let Some(swap_record_str) = &input.swap_record { let expected_cid = Cid::from_str(swap_record_str).ok(); let actual_cid = mst.get(&key).await.ok().flatten(); if expected_cid != actual_cid { - return ApiError::InvalidSwap(Some( + return Ok(ApiError::InvalidSwap(Some( "Record has been modified or does not exist".into(), )) - .into_response(); + .into_response()); } } let existing_cid = mst.get(&key).await.ok().flatten(); let record_ipld = crate::util::json_to_ipld(&input.record); let mut record_bytes = Vec::new(); if serde_ipld_dagcbor::to_writer(&mut record_bytes, &record_ipld).is_err() { - return ApiError::InvalidRecord("Failed to serialize record".into()).into_response(); + return Ok(ApiError::InvalidRecord("Failed to serialize record".into()).into_response()); } let record_cid = match tracking_store.put(&record_bytes).await { Ok(c) => c, _ => { - return ApiError::InternalError(Some("Failed to save record block".into())) - .into_response(); + return Ok( + ApiError::InternalError(Some("Failed to save record block".into())).into_response(), + ); } }; if existing_cid == Some(record_cid) { - return ( + return Ok(( StatusCode::OK, Json(PutRecordOutput { uri: AtUri::from_parts(&did, &input.collection, &input.rkey), @@ -507,29 +533,32 @@ pub async fn put_record( validation_status: validation_status.map(|s| s.to_string()), }), ) - .into_response(); + .into_response()); } - let new_mst = if existing_cid.is_some() { - match mst.update(&key, record_cid).await { - Ok(m) => m, - Err(_) => { - return ApiError::InternalError(Some("Failed to update MST".into())) - .into_response(); + let new_mst = + if existing_cid.is_some() { + match mst.update(&key, record_cid).await { + Ok(m) => m, + Err(_) => { + return Ok(ApiError::InternalError(Some("Failed to update MST".into())) + .into_response()); + } } - } - } else { - match mst.add(&key, record_cid).await { - Ok(m) => m, - Err(_) => { - return ApiError::InternalError(Some("Failed to add to MST".into())) - .into_response(); + } else { + match mst.add(&key, record_cid).await { + Ok(m) => m, + Err(_) => { + return Ok(ApiError::InternalError(Some("Failed to add to MST".into())) + .into_response()); + } } - } - }; + }; let new_mst_root = match new_mst.persist().await { Ok(c) => c, Err(_) => { - return ApiError::InternalError(Some("Failed to persist MST".into())).into_response(); + return Ok( + ApiError::InternalError(Some("Failed to persist MST".into())).into_response(), + ); } }; let op = if existing_cid.is_some() { @@ -553,16 +582,20 @@ pub async fn put_record( .await .is_err() { - return ApiError::InternalError(Some("Failed to get new MST blocks for path".into())) - .into_response(); + return Ok( + ApiError::InternalError(Some("Failed to get new MST blocks for path".into())) + .into_response(), + ); } if mst .blocks_for_path(&key, &mut old_mst_blocks) .await .is_err() { - return ApiError::InternalError(Some("Failed to get old MST blocks for path".into())) - .into_response(); + return Ok( + ApiError::InternalError(Some("Failed to get old MST blocks for path".into())) + .into_response(), + ); } let mut relevant_blocks = new_mst_blocks.clone(); relevant_blocks.extend(old_mst_blocks.iter().map(|(k, v)| (*k, v.clone()))); @@ -604,9 +637,9 @@ pub async fn put_record( { Ok(res) => res, Err(e) if e.contains("ConcurrentModification") => { - return ApiError::InvalidSwap(Some("Repo has been modified".into())).into_response(); + return Ok(ApiError::InvalidSwap(Some("Repo has been modified".into())).into_response()); } - Err(e) => return ApiError::InternalError(Some(e)).into_response(), + Err(e) => return Ok(ApiError::InternalError(Some(e)).into_response()), }; if let Some(ref controller) = controller_did { @@ -628,7 +661,7 @@ pub async fn put_record( .await; } - ( + Ok(( StatusCode::OK, Json(PutRecordOutput { uri: AtUri::from_parts(&did, &input.collection, &input.rkey), @@ -640,5 +673,5 @@ pub async fn put_record( validation_status: validation_status.map(|s| s.to_string()), }), ) - .into_response() + .into_response()) } diff --git a/crates/tranquil-pds/src/api/server/account_status.rs b/crates/tranquil-pds/src/api/server/account_status.rs index 3b158ec..98ea29c 100644 --- a/crates/tranquil-pds/src/api/server/account_status.rs +++ b/crates/tranquil-pds/src/api/server/account_status.rs @@ -1,5 +1,6 @@ use crate::api::EmptyResponse; use crate::api::error::ApiError; +use crate::auth::{Active, Auth, NotTakendown}; use crate::cache::Cache; use crate::plc::PlcClient; use crate::state::AppState; @@ -40,18 +41,18 @@ pub struct CheckAccountStatusOutput { pub async fn check_account_status( State(state): State, - auth: crate::auth::BearerAuthAllowDeactivated, -) -> Response { - let did = auth.0.did; - let user_id = match state.user_repo.get_id_by_did(&did).await { - Ok(Some(id)) => id, - _ => { - return ApiError::InternalError(None).into_response(); - } - }; + auth: Auth, +) -> Result { + let did = &auth.did; + let user_id = state + .user_repo + .get_id_by_did(did) + .await + .map_err(|_| ApiError::InternalError(None))? + .ok_or(ApiError::InternalError(None))?; let is_active = state .user_repo - .is_account_active_by_did(&did) + .is_account_active_by_did(did) .await .ok() .flatten() @@ -95,8 +96,8 @@ pub async fn check_account_status( .await .unwrap_or(0); let valid_did = - is_valid_did_for_service(state.user_repo.as_ref(), state.cache.clone(), &did).await; - ( + is_valid_did_for_service(state.user_repo.as_ref(), state.cache.clone(), did).await; + Ok(( StatusCode::OK, Json(CheckAccountStatusOutput { activated: is_active, @@ -110,7 +111,7 @@ pub async fn check_account_status( imported_blobs, }), ) - .into_response() + .into_response()) } async fn is_valid_did_for_service( @@ -305,26 +306,25 @@ async fn assert_valid_did_document_for_service( pub async fn activate_account( State(state): State, - auth: crate::auth::BearerAuthAllowDeactivated, -) -> Response { + auth: Auth, +) -> Result { info!("[MIGRATION] activateAccount called"); - let auth_user = auth.0; info!( "[MIGRATION] activateAccount: Authenticated user did={}", - auth_user.did + auth.did ); if let Err(e) = crate::auth::scope_check::check_account_scope( - auth_user.is_oauth, - auth_user.scope.as_deref(), + auth.is_oauth(), + auth.scope.as_deref(), crate::oauth::scopes::AccountAttr::Repo, crate::oauth::scopes::AccountAction::Manage, ) { info!("[MIGRATION] activateAccount: Scope check failed"); - return e; + return Ok(e); } - let did = auth_user.did; + let did = auth.did.clone(); info!( "[MIGRATION] activateAccount: Validating DID document for did={}", @@ -344,7 +344,7 @@ pub async fn activate_account( did, did_validation_start.elapsed() ); - return e.into_response(); + return Err(e); } info!( "[MIGRATION] activateAccount: DID document validation SUCCESS for {} (took {:?})", @@ -450,14 +450,14 @@ pub async fn activate_account( ); } info!("[MIGRATION] activateAccount: SUCCESS for did={}", did); - EmptyResponse::ok().into_response() + Ok(EmptyResponse::ok().into_response()) } Err(e) => { error!( "[MIGRATION] activateAccount: DB error activating account: {:?}", e ); - ApiError::InternalError(None).into_response() + Err(ApiError::InternalError(None)) } } } @@ -470,18 +470,16 @@ pub struct DeactivateAccountInput { pub async fn deactivate_account( State(state): State, - auth: crate::auth::BearerAuth, + auth: Auth, Json(input): Json, -) -> Response { - let auth_user = auth.0; - +) -> Result { if let Err(e) = crate::auth::scope_check::check_account_scope( - auth_user.is_oauth, - auth_user.scope.as_deref(), + auth.is_oauth(), + auth.scope.as_deref(), crate::oauth::scopes::AccountAttr::Repo, crate::oauth::scopes::AccountAction::Manage, ) { - return e; + return Ok(e); } let delete_after: Option> = input @@ -490,7 +488,7 @@ pub async fn deactivate_account( .and_then(|s| chrono::DateTime::parse_from_rfc3339(s).ok()) .map(|dt| dt.with_timezone(&chrono::Utc)); - let did = auth_user.did; + let did = auth.did.clone(); let handle = state.user_repo.get_handle_by_did(&did).await.ok().flatten(); @@ -511,47 +509,48 @@ pub async fn deactivate_account( { warn!("Failed to sequence account deactivated event: {}", e); } - EmptyResponse::ok().into_response() + Ok(EmptyResponse::ok().into_response()) } - Ok(false) => EmptyResponse::ok().into_response(), + Ok(false) => Ok(EmptyResponse::ok().into_response()), Err(e) => { error!("DB error deactivating account: {:?}", e); - ApiError::InternalError(None).into_response() + Err(ApiError::InternalError(None)) } } } pub async fn request_account_delete( State(state): State, - auth: crate::auth::BearerAuthAllowDeactivated, -) -> Response { - let did = &auth.0.did; + auth: Auth, +) -> Result { + let did = &auth.did; if !crate::api::server::reauth::check_legacy_session_mfa(&*state.session_repo, did).await { - return crate::api::server::reauth::legacy_mfa_required_response( + return Ok(crate::api::server::reauth::legacy_mfa_required_response( &*state.user_repo, &*state.session_repo, did, ) - .await; + .await); } - let user_id = match state.user_repo.get_id_by_did(did).await { - Ok(Some(id)) => id, - _ => { - return ApiError::InternalError(None).into_response(); - } - }; + let user_id = state + .user_repo + .get_id_by_did(did) + .await + .ok() + .flatten() + .ok_or(ApiError::InternalError(None))?; let confirmation_token = Uuid::new_v4().to_string(); let expires_at = Utc::now() + Duration::minutes(15); - if let Err(e) = state + state .infra_repo .create_deletion_request(&confirmation_token, did, expires_at) .await - { - error!("DB error creating deletion token: {:?}", e); - return ApiError::InternalError(None).into_response(); - } + .map_err(|e| { + error!("DB error creating deletion token: {:?}", e); + ApiError::InternalError(None) + })?; let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); if let Err(e) = crate::comms::comms_repo::enqueue_account_deletion( state.user_repo.as_ref(), @@ -565,7 +564,7 @@ pub async fn request_account_delete( warn!("Failed to enqueue account deletion notification: {:?}", e); } info!("Account deletion requested for user {}", did); - EmptyResponse::ok().into_response() + Ok(EmptyResponse::ok().into_response()) } #[derive(Deserialize)] diff --git a/crates/tranquil-pds/src/api/server/app_password.rs b/crates/tranquil-pds/src/api/server/app_password.rs index edb3f71..a374ac8 100644 --- a/crates/tranquil-pds/src/api/server/app_password.rs +++ b/crates/tranquil-pds/src/api/server/app_password.rs @@ -1,6 +1,6 @@ use crate::api::EmptyResponse; use crate::api::error::ApiError; -use crate::auth::{BearerAuth, generate_app_password}; +use crate::auth::{Active, Auth, generate_app_password}; use crate::delegation::{DelegationActionType, intersect_scopes}; use crate::state::{AppState, RateLimitKind}; use axum::{ @@ -33,39 +33,40 @@ pub struct ListAppPasswordsOutput { pub async fn list_app_passwords( State(state): State, - BearerAuth(auth_user): BearerAuth, -) -> Response { - let user = match state.user_repo.get_by_did(&auth_user.did).await { - Ok(Some(u)) => u, - Ok(None) => return ApiError::AccountNotFound.into_response(), - Err(e) => { + auth: Auth, +) -> Result { + let user = state + .user_repo + .get_by_did(&auth.did) + .await + .map_err(|e| { error!("DB error getting user: {:?}", e); - return ApiError::InternalError(None).into_response(); - } - }; + ApiError::InternalError(None) + })? + .ok_or(ApiError::AccountNotFound)?; - match state.session_repo.list_app_passwords(user.id).await { - Ok(rows) => { - let passwords: Vec = rows - .iter() - .map(|row| AppPassword { - name: row.name.clone(), - created_at: row.created_at.to_rfc3339(), - privileged: row.privileged, - scopes: row.scopes.clone(), - created_by_controller: row - .created_by_controller_did - .as_ref() - .map(|d| d.to_string()), - }) - .collect(); - Json(ListAppPasswordsOutput { passwords }).into_response() - } - Err(e) => { + let rows = state + .session_repo + .list_app_passwords(user.id) + .await + .map_err(|e| { error!("DB error listing app passwords: {:?}", e); - ApiError::InternalError(None).into_response() - } - } + ApiError::InternalError(None) + })?; + let passwords: Vec = rows + .iter() + .map(|row| AppPassword { + name: row.name.clone(), + created_at: row.created_at.to_rfc3339(), + privileged: row.privileged, + scopes: row.scopes.clone(), + created_by_controller: row + .created_by_controller_did + .as_ref() + .map(|d| d.to_string()), + }) + .collect(); + Ok(Json(ListAppPasswordsOutput { passwords }).into_response()) } #[derive(Deserialize)] @@ -89,49 +90,50 @@ pub struct CreateAppPasswordOutput { pub async fn create_app_password( State(state): State, headers: HeaderMap, - BearerAuth(auth_user): BearerAuth, + auth: Auth, Json(input): Json, -) -> Response { +) -> Result { let client_ip = crate::rate_limit::extract_client_ip(&headers, None); if !state .check_rate_limit(RateLimitKind::AppPassword, &client_ip) .await { warn!(ip = %client_ip, "App password creation rate limit exceeded"); - return ApiError::RateLimitExceeded(None).into_response(); + return Err(ApiError::RateLimitExceeded(None)); } - let user = match state.user_repo.get_by_did(&auth_user.did).await { - Ok(Some(u)) => u, - Ok(None) => return ApiError::AccountNotFound.into_response(), - Err(e) => { + let user = state + .user_repo + .get_by_did(&auth.did) + .await + .map_err(|e| { error!("DB error getting user: {:?}", e); - return ApiError::InternalError(None).into_response(); - } - }; + ApiError::InternalError(None) + })? + .ok_or(ApiError::AccountNotFound)?; let name = input.name.trim(); if name.is_empty() { - return ApiError::InvalidRequest("name is required".into()).into_response(); + return Err(ApiError::InvalidRequest("name is required".into())); } - match state + if state .session_repo .get_app_password_by_name(user.id, name) .await - { - Ok(Some(_)) => return ApiError::DuplicateAppPassword.into_response(), - Err(e) => { + .map_err(|e| { error!("DB error checking app password: {:?}", e); - return ApiError::InternalError(None).into_response(); - } - Ok(None) => {} + ApiError::InternalError(None) + })? + .is_some() + { + return Err(ApiError::DuplicateAppPassword); } - let (final_scopes, controller_did) = if let Some(ref controller) = auth_user.controller_did { + let (final_scopes, controller_did) = if let Some(ref controller) = auth.controller_did { let grant = state .delegation_repo - .get_delegation(&auth_user.did, controller) + .get_delegation(&auth.did, controller) .await .ok() .flatten(); @@ -141,7 +143,7 @@ pub async fn create_app_password( let intersected = intersect_scopes(requested, &granted_scopes); if intersected.is_empty() && !granted_scopes.is_empty() { - return ApiError::InsufficientScope(None).into_response(); + return Err(ApiError::InsufficientScope(None)); } let scope_result = if intersected.is_empty() { @@ -157,21 +159,17 @@ pub async fn create_app_password( let password = generate_app_password(); let password_clone = password.clone(); - let password_hash = match tokio::task::spawn_blocking(move || { - bcrypt::hash(&password_clone, bcrypt::DEFAULT_COST) - }) - .await - { - Ok(Ok(h)) => h, - Ok(Err(e)) => { - error!("Failed to hash password: {:?}", e); - return ApiError::InternalError(None).into_response(); - } - Err(e) => { - error!("Failed to spawn blocking task: {:?}", e); - return ApiError::InternalError(None).into_response(); - } - }; + let password_hash = + tokio::task::spawn_blocking(move || bcrypt::hash(&password_clone, bcrypt::DEFAULT_COST)) + .await + .map_err(|e| { + error!("Failed to spawn blocking task: {:?}", e); + ApiError::InternalError(None) + })? + .map_err(|e| { + error!("Failed to hash password: {:?}", e); + ApiError::InternalError(None) + })?; let privileged = input.privileged.unwrap_or(false); let created_at = chrono::Utc::now(); @@ -185,40 +183,41 @@ pub async fn create_app_password( created_by_controller_did: controller_did.clone(), }; - match state.session_repo.create_app_password(&create_data).await { - Ok(_) => { - if let Some(ref controller) = controller_did { - let _ = state - .delegation_repo - .log_delegation_action( - &auth_user.did, - controller, - Some(controller), - DelegationActionType::AccountAction, - Some(json!({ - "action": "create_app_password", - "name": name, - "scopes": final_scopes - })), - None, - None, - ) - .await; - } - Json(CreateAppPasswordOutput { - name: name.to_string(), - password, - created_at: created_at.to_rfc3339(), - privileged, - scopes: final_scopes, - }) - .into_response() - } - Err(e) => { + state + .session_repo + .create_app_password(&create_data) + .await + .map_err(|e| { error!("DB error creating app password: {:?}", e); - ApiError::InternalError(None).into_response() - } + ApiError::InternalError(None) + })?; + + if let Some(ref controller) = controller_did { + let _ = state + .delegation_repo + .log_delegation_action( + &auth.did, + controller, + Some(controller), + DelegationActionType::AccountAction, + Some(json!({ + "action": "create_app_password", + "name": name, + "scopes": final_scopes + })), + None, + None, + ) + .await; } + Ok(Json(CreateAppPasswordOutput { + name: name.to_string(), + password, + created_at: created_at.to_rfc3339(), + privileged, + scopes: final_scopes, + }) + .into_response()) } #[derive(Deserialize)] @@ -228,40 +227,41 @@ pub struct RevokeAppPasswordInput { pub async fn revoke_app_password( State(state): State, - BearerAuth(auth_user): BearerAuth, + auth: Auth, Json(input): Json, -) -> Response { - let user = match state.user_repo.get_by_did(&auth_user.did).await { - Ok(Some(u)) => u, - Ok(None) => return ApiError::AccountNotFound.into_response(), - Err(e) => { +) -> Result { + let user = state + .user_repo + .get_by_did(&auth.did) + .await + .map_err(|e| { error!("DB error getting user: {:?}", e); - return ApiError::InternalError(None).into_response(); - } - }; + ApiError::InternalError(None) + })? + .ok_or(ApiError::AccountNotFound)?; let name = input.name.trim(); if name.is_empty() { - return ApiError::InvalidRequest("name is required".into()).into_response(); + return Err(ApiError::InvalidRequest("name is required".into())); } let sessions_to_invalidate = state .session_repo - .get_session_jtis_by_app_password(&auth_user.did, name) + .get_session_jtis_by_app_password(&auth.did, name) .await .unwrap_or_default(); - if let Err(e) = state + state .session_repo - .delete_sessions_by_app_password(&auth_user.did, name) + .delete_sessions_by_app_password(&auth.did, name) .await - { - error!("DB error revoking sessions for app password: {:?}", e); - return ApiError::InternalError(None).into_response(); - } + .map_err(|e| { + error!("DB error revoking sessions for app password: {:?}", e); + ApiError::InternalError(None) + })?; futures::future::join_all(sessions_to_invalidate.iter().map(|jti| { - let cache_key = format!("auth:session:{}:{}", &auth_user.did, jti); + let cache_key = format!("auth:session:{}:{}", &auth.did, jti); let cache = state.cache.clone(); async move { let _ = cache.delete(&cache_key).await; @@ -269,10 +269,14 @@ pub async fn revoke_app_password( })) .await; - if let Err(e) = state.session_repo.delete_app_password(user.id, name).await { - error!("DB error revoking app password: {:?}", e); - return ApiError::InternalError(None).into_response(); - } + state + .session_repo + .delete_app_password(user.id, name) + .await + .map_err(|e| { + error!("DB error revoking app password: {:?}", e); + ApiError::InternalError(None) + })?; - EmptyResponse::ok().into_response() + Ok(EmptyResponse::ok().into_response()) } diff --git a/crates/tranquil-pds/src/api/server/email.rs b/crates/tranquil-pds/src/api/server/email.rs index 5720bec..3403715 100644 --- a/crates/tranquil-pds/src/api/server/email.rs +++ b/crates/tranquil-pds/src/api/server/email.rs @@ -1,6 +1,6 @@ use crate::api::error::ApiError; use crate::api::{EmptyResponse, TokenRequiredResponse, VerifiedResponse}; -use crate::auth::BearerAuth; +use crate::auth::{Active, Auth}; use crate::state::{AppState, RateLimitKind}; use axum::{ Json, @@ -45,48 +45,48 @@ pub struct RequestEmailUpdateInput { pub async fn request_email_update( State(state): State, headers: axum::http::HeaderMap, - auth: BearerAuth, + auth: Auth, input: Option>, -) -> Response { +) -> Result { let client_ip = crate::rate_limit::extract_client_ip(&headers, None); if !state .check_rate_limit(RateLimitKind::EmailUpdate, &client_ip) .await { warn!(ip = %client_ip, "Email update rate limit exceeded"); - return ApiError::RateLimitExceeded(None).into_response(); + return Err(ApiError::RateLimitExceeded(None)); } if let Err(e) = crate::auth::scope_check::check_account_scope( - auth.0.is_oauth, - auth.0.scope.as_deref(), + auth.is_oauth(), + auth.scope.as_deref(), crate::oauth::scopes::AccountAttr::Email, crate::oauth::scopes::AccountAction::Manage, ) { - return e; + return Ok(e); } - let user = match state.user_repo.get_email_info_by_did(&auth.0.did).await { - Ok(Some(row)) => row, - Ok(None) => { - return ApiError::AccountNotFound.into_response(); - } - Err(e) => { + let user = state + .user_repo + .get_email_info_by_did(&auth.did) + .await + .map_err(|e| { error!("DB error: {:?}", e); - return ApiError::InternalError(None).into_response(); - } - }; + ApiError::InternalError(None) + })? + .ok_or(ApiError::AccountNotFound)?; let Some(current_email) = user.email else { - return ApiError::InvalidRequest("account does not have an email address".into()) - .into_response(); + return Err(ApiError::InvalidRequest( + "account does not have an email address".into(), + )); }; let token_required = user.email_verified; if token_required { let code = crate::auth::verification_token::generate_channel_update_token( - &auth.0.did, + &auth.did, "email_update", ¤t_email.to_lowercase(), ); @@ -103,7 +103,7 @@ pub async fn request_email_update( authorized: false, }; if let Ok(json) = serde_json::to_string(&pending) { - let cache_key = email_update_cache_key(&auth.0.did); + let cache_key = email_update_cache_key(&auth.did); if let Err(e) = state.cache.set(&cache_key, &json, EMAIL_UPDATE_TTL).await { warn!("Failed to cache pending email update: {:?}", e); } @@ -127,7 +127,7 @@ pub async fn request_email_update( } info!("Email update requested for user {}", user.id); - TokenRequiredResponse::response(token_required).into_response() + Ok(TokenRequiredResponse::response(token_required).into_response()) } #[derive(Deserialize)] @@ -140,51 +140,50 @@ pub struct ConfirmEmailInput { pub async fn confirm_email( State(state): State, headers: axum::http::HeaderMap, - auth: BearerAuth, + auth: Auth, Json(input): Json, -) -> Response { +) -> Result { let client_ip = crate::rate_limit::extract_client_ip(&headers, None); if !state .check_rate_limit(RateLimitKind::EmailUpdate, &client_ip) .await { warn!(ip = %client_ip, "Confirm email rate limit exceeded"); - return ApiError::RateLimitExceeded(None).into_response(); + return Err(ApiError::RateLimitExceeded(None)); } if let Err(e) = crate::auth::scope_check::check_account_scope( - auth.0.is_oauth, - auth.0.scope.as_deref(), + auth.is_oauth(), + auth.scope.as_deref(), crate::oauth::scopes::AccountAttr::Email, crate::oauth::scopes::AccountAction::Manage, ) { - return e; + return Ok(e); } - let did = &auth.0.did; - let user = match state.user_repo.get_email_info_by_did(did).await { - Ok(Some(row)) => row, - Ok(None) => { - return ApiError::AccountNotFound.into_response(); - } - Err(e) => { + let did = &auth.did; + let user = state + .user_repo + .get_email_info_by_did(did) + .await + .map_err(|e| { error!("DB error: {:?}", e); - return ApiError::InternalError(None).into_response(); - } - }; + ApiError::InternalError(None) + })? + .ok_or(ApiError::AccountNotFound)?; let Some(ref email) = user.email else { - return ApiError::InvalidEmail.into_response(); + return Err(ApiError::InvalidEmail); }; let current_email = email.to_lowercase(); let provided_email = input.email.trim().to_lowercase(); if provided_email != current_email { - return ApiError::InvalidEmail.into_response(); + return Err(ApiError::InvalidEmail); } if user.email_verified { - return EmptyResponse::ok().into_response(); + return Ok(EmptyResponse::ok().into_response()); } let confirmation_code = @@ -199,24 +198,28 @@ pub async fn confirm_email( match verified { Ok(token_data) => { if token_data.did != did.as_str() { - return ApiError::InvalidToken(None).into_response(); + return Err(ApiError::InvalidToken(None)); } } Err(crate::auth::verification_token::VerifyError::Expired) => { - return ApiError::ExpiredToken(None).into_response(); + return Err(ApiError::ExpiredToken(None)); } Err(_) => { - return ApiError::InvalidToken(None).into_response(); + return Err(ApiError::InvalidToken(None)); } } - if let Err(e) = state.user_repo.set_email_verified(user.id, true).await { - error!("DB error confirming email: {:?}", e); - return ApiError::InternalError(None).into_response(); - } + state + .user_repo + .set_email_verified(user.id, true) + .await + .map_err(|e| { + error!("DB error confirming email: {:?}", e); + ApiError::InternalError(None) + })?; info!("Email confirmed for user {}", user.id); - EmptyResponse::ok().into_response() + Ok(EmptyResponse::ok().into_response()) } #[derive(Deserialize)] @@ -230,31 +233,28 @@ pub struct UpdateEmailInput { pub async fn update_email( State(state): State, - auth: BearerAuth, + auth: Auth, Json(input): Json, -) -> Response { - let auth_user = auth.0; - +) -> Result { if let Err(e) = crate::auth::scope_check::check_account_scope( - auth_user.is_oauth, - auth_user.scope.as_deref(), + auth.is_oauth(), + auth.scope.as_deref(), crate::oauth::scopes::AccountAttr::Email, crate::oauth::scopes::AccountAction::Manage, ) { - return e; + return Ok(e); } - let did = &auth_user.did; - let user = match state.user_repo.get_email_info_by_did(did).await { - Ok(Some(row)) => row, - Ok(None) => { - return ApiError::AccountNotFound.into_response(); - } - Err(e) => { + let did = &auth.did; + let user = state + .user_repo + .get_email_info_by_did(did) + .await + .map_err(|e| { error!("DB error: {:?}", e); - return ApiError::InternalError(None).into_response(); - } - }; + ApiError::InternalError(None) + })? + .ok_or(ApiError::AccountNotFound)?; let user_id = user.id; let current_email = user.email.clone(); @@ -262,16 +262,15 @@ pub async fn update_email( let new_email = input.email.trim().to_lowercase(); if !crate::api::validation::is_valid_email(&new_email) { - return ApiError::InvalidRequest( + return Err(ApiError::InvalidRequest( "This email address is not supported, please use a different email.".into(), - ) - .into_response(); + )); } if let Some(ref current) = current_email && new_email == current.to_lowercase() { - return EmptyResponse::ok().into_response(); + return Ok(EmptyResponse::ok().into_response()); } if email_verified { @@ -290,7 +289,7 @@ pub async fn update_email( if !authorized_via_link { let Some(ref t) = input.token else { - return ApiError::TokenRequired.into_response(); + return Err(ApiError::TokenRequired); }; let confirmation_token = crate::auth::verification_token::normalize_token_input(t.trim()); @@ -309,23 +308,27 @@ pub async fn update_email( match verified { Ok(token_data) => { if token_data.did != did.as_str() { - return ApiError::InvalidToken(None).into_response(); + return Err(ApiError::InvalidToken(None)); } } Err(crate::auth::verification_token::VerifyError::Expired) => { - return ApiError::ExpiredToken(None).into_response(); + return Err(ApiError::ExpiredToken(None)); } Err(_) => { - return ApiError::InvalidToken(None).into_response(); + return Err(ApiError::InvalidToken(None)); } } } } - if let Err(e) = state.user_repo.update_email(user_id, &new_email).await { - error!("DB error updating email: {:?}", e); - return ApiError::InternalError(None).into_response(); - } + state + .user_repo + .update_email(user_id, &new_email) + .await + .map_err(|e| { + error!("DB error updating email: {:?}", e); + ApiError::InternalError(None) + })?; let verification_token = crate::auth::verification_token::generate_signup_token(did, "email", &new_email); @@ -358,7 +361,7 @@ pub async fn update_email( } info!("Email updated for user {}", user_id); - EmptyResponse::ok().into_response() + Ok(EmptyResponse::ok().into_response()) } #[derive(Deserialize)] @@ -497,46 +500,46 @@ pub async fn authorize_email_update( pub async fn check_email_update_status( State(state): State, headers: axum::http::HeaderMap, - auth: BearerAuth, -) -> Response { + auth: Auth, +) -> Result { let client_ip = crate::rate_limit::extract_client_ip(&headers, None); if !state .check_rate_limit(RateLimitKind::VerificationCheck, &client_ip) .await { - return ApiError::RateLimitExceeded(None).into_response(); + return Err(ApiError::RateLimitExceeded(None)); } if let Err(e) = crate::auth::scope_check::check_account_scope( - auth.0.is_oauth, - auth.0.scope.as_deref(), + auth.is_oauth(), + auth.scope.as_deref(), crate::oauth::scopes::AccountAttr::Email, crate::oauth::scopes::AccountAction::Read, ) { - return e; + return Ok(e); } - let cache_key = email_update_cache_key(&auth.0.did); + let cache_key = email_update_cache_key(&auth.did); let pending_json = match state.cache.get(&cache_key).await { Some(json) => json, None => { - return Json(json!({ "pending": false, "authorized": false })).into_response(); + return Ok(Json(json!({ "pending": false, "authorized": false })).into_response()); } }; let pending: PendingEmailUpdate = match serde_json::from_str(&pending_json) { Ok(p) => p, Err(_) => { - return Json(json!({ "pending": false, "authorized": false })).into_response(); + return Ok(Json(json!({ "pending": false, "authorized": false })).into_response()); } }; - Json(json!({ + Ok(Json(json!({ "pending": true, "authorized": pending.authorized, "newEmail": pending.new_email, })) - .into_response() + .into_response()) } #[derive(Deserialize)] diff --git a/crates/tranquil-pds/src/api/server/invite.rs b/crates/tranquil-pds/src/api/server/invite.rs index d4c50cb..8b00719 100644 --- a/crates/tranquil-pds/src/api/server/invite.rs +++ b/crates/tranquil-pds/src/api/server/invite.rs @@ -1,6 +1,5 @@ use crate::api::ApiError; -use crate::auth::BearerAuth; -use crate::auth::extractor::BearerAuthAdmin; +use crate::auth::{Active, Admin, Auth}; use crate::state::AppState; use crate::types::Did; use axum::{ @@ -44,19 +43,20 @@ pub struct CreateInviteCodeOutput { pub async fn create_invite_code( State(state): State, - BearerAuthAdmin(auth_user): BearerAuthAdmin, + auth: Auth, Json(input): Json, -) -> Response { +) -> Result { if input.use_count < 1 { - return ApiError::InvalidRequest("useCount must be at least 1".into()).into_response(); + return Err(ApiError::InvalidRequest( + "useCount must be at least 1".into(), + )); } let for_account: Did = match &input.for_account { - Some(acct) => match acct.parse() { - Ok(d) => d, - Err(_) => return ApiError::InvalidDid("Invalid DID format".into()).into_response(), - }, - None => auth_user.did.clone(), + Some(acct) => acct + .parse() + .map_err(|_| ApiError::InvalidDid("Invalid DID format".into()))?, + None => auth.did.clone(), }; let code = gen_invite_code(); @@ -65,14 +65,14 @@ pub async fn create_invite_code( .create_invite_code(&code, input.use_count, Some(&for_account)) .await { - Ok(true) => Json(CreateInviteCodeOutput { code }).into_response(), + Ok(true) => Ok(Json(CreateInviteCodeOutput { code }).into_response()), Ok(false) => { error!("No admin user found to create invite code"); - ApiError::InternalError(None).into_response() + Err(ApiError::InternalError(None)) } Err(e) => { error!("DB error creating invite code: {:?}", e); - ApiError::InternalError(None).into_response() + Err(ApiError::InternalError(None)) } } } @@ -98,36 +98,37 @@ pub struct AccountCodes { pub async fn create_invite_codes( State(state): State, - BearerAuthAdmin(auth_user): BearerAuthAdmin, + auth: Auth, Json(input): Json, -) -> Response { +) -> Result { if input.use_count < 1 { - return ApiError::InvalidRequest("useCount must be at least 1".into()).into_response(); + return Err(ApiError::InvalidRequest( + "useCount must be at least 1".into(), + )); } let code_count = input.code_count.unwrap_or(1).max(1); let for_accounts: Vec = match &input.for_accounts { - Some(accounts) if !accounts.is_empty() => { - let parsed: Result, _> = accounts.iter().map(|a| a.parse()).collect(); - match parsed { - Ok(dids) => dids, - Err(_) => return ApiError::InvalidDid("Invalid DID format".into()).into_response(), - } - } - _ => vec![auth_user.did.clone()], + Some(accounts) if !accounts.is_empty() => accounts + .iter() + .map(|a| a.parse()) + .collect::, _>>() + .map_err(|_| ApiError::InvalidDid("Invalid DID format".into()))?, + _ => vec![auth.did.clone()], }; - let admin_user_id = match state.user_repo.get_any_admin_user_id().await { - Ok(Some(id)) => id, - Ok(None) => { - error!("No admin user found to create invite codes"); - return ApiError::InternalError(None).into_response(); - } - Err(e) => { + let admin_user_id = state + .user_repo + .get_any_admin_user_id() + .await + .map_err(|e| { error!("DB error looking up admin user: {:?}", e); - return ApiError::InternalError(None).into_response(); - } - }; + ApiError::InternalError(None) + })? + .ok_or_else(|| { + error!("No admin user found to create invite codes"); + ApiError::InternalError(None) + })?; let result = futures::future::try_join_all(for_accounts.into_iter().map(|account| { let infra_repo = state.infra_repo.clone(); @@ -146,13 +147,13 @@ pub async fn create_invite_codes( .await; match result { - Ok(result_codes) => Json(CreateInviteCodesOutput { + Ok(result_codes) => Ok(Json(CreateInviteCodesOutput { codes: result_codes, }) - .into_response(), + .into_response()), Err(e) => { error!("DB error creating invite codes: {:?}", e); - ApiError::InternalError(None).into_response() + Err(ApiError::InternalError(None)) } } } @@ -192,22 +193,19 @@ pub struct GetAccountInviteCodesOutput { pub async fn get_account_invite_codes( State(state): State, - BearerAuth(auth_user): BearerAuth, + auth: Auth, axum::extract::Query(params): axum::extract::Query, -) -> Response { +) -> Result { let include_used = params.include_used.unwrap_or(true); - let codes_info = match state + let codes_info = state .infra_repo - .get_invite_codes_for_account(&auth_user.did) + .get_invite_codes_for_account(&auth.did) .await - { - Ok(info) => info, - Err(e) => { + .map_err(|e| { error!("DB error fetching invite codes: {:?}", e); - return ApiError::InternalError(None).into_response(); - } - }; + ApiError::InternalError(None) + })?; let filtered_codes: Vec<_> = codes_info .into_iter() @@ -254,5 +252,5 @@ pub async fn get_account_invite_codes( .await; let codes: Vec = codes.into_iter().flatten().collect(); - Json(GetAccountInviteCodesOutput { codes }).into_response() + Ok(Json(GetAccountInviteCodesOutput { codes }).into_response()) } diff --git a/crates/tranquil-pds/src/api/server/migration.rs b/crates/tranquil-pds/src/api/server/migration.rs index f69c8b4..0eaec3f 100644 --- a/crates/tranquil-pds/src/api/server/migration.rs +++ b/crates/tranquil-pds/src/api/server/migration.rs @@ -1,5 +1,5 @@ use crate::api::ApiError; -use crate::auth::BearerAuth; +use crate::auth::{Active, Auth}; use crate::state::AppState; use axum::{ Json, @@ -36,35 +36,30 @@ pub struct UpdateDidDocumentOutput { pub async fn update_did_document( State(state): State, - auth: BearerAuth, + auth: Auth, Json(input): Json, -) -> Response { - let auth_user = auth.0; - - if !auth_user.did.starts_with("did:web:") { - return ApiError::InvalidRequest( +) -> Result { + if !auth.did.starts_with("did:web:") { + return Err(ApiError::InvalidRequest( "DID document updates are only available for did:web accounts".into(), - ) - .into_response(); + )); } - let user = match state.user_repo.get_user_for_did_doc(&auth_user.did).await { - Ok(Some(u)) => u, - Ok(None) => return ApiError::AccountNotFound.into_response(), - Err(e) => { + let user = state + .user_repo + .get_user_for_did_doc(&auth.did) + .await + .map_err(|e| { tracing::error!("DB error getting user: {:?}", e); - return ApiError::InternalError(None).into_response(); - } - }; - - if user.deactivated_at.is_some() { - return ApiError::AccountDeactivated.into_response(); - } + ApiError::InternalError(None) + })? + .ok_or(ApiError::AccountNotFound)?; if let Some(ref methods) = input.verification_methods { if methods.is_empty() { - return ApiError::InvalidRequest("verification_methods cannot be empty".into()) - .into_response(); + return Err(ApiError::InvalidRequest( + "verification_methods cannot be empty".into(), + )); } let validation_error = methods.iter().find_map(|method| { if method.id.is_empty() { @@ -80,22 +75,24 @@ pub async fn update_did_document( } }); if let Some(err) = validation_error { - return ApiError::InvalidRequest(err.into()).into_response(); + return Err(ApiError::InvalidRequest(err.into())); } } if let Some(ref handles) = input.also_known_as && handles.iter().any(|h| !h.starts_with("at://")) { - return ApiError::InvalidRequest("alsoKnownAs entries must be at:// URIs".into()) - .into_response(); + return Err(ApiError::InvalidRequest( + "alsoKnownAs entries must be at:// URIs".into(), + )); } if let Some(ref endpoint) = input.service_endpoint { let endpoint = endpoint.trim(); if !endpoint.starts_with("https://") { - return ApiError::InvalidRequest("serviceEndpoint must start with https://".into()) - .into_response(); + return Err(ApiError::InvalidRequest( + "serviceEndpoint must start with https://".into(), + )); } } @@ -106,54 +103,54 @@ pub async fn update_did_document( let also_known_as: Option> = input.also_known_as.clone(); - if let Err(e) = state + state .user_repo .upsert_did_web_overrides(user.id, verification_methods_json, also_known_as) .await - { - tracing::error!("DB error upserting did_web_overrides: {:?}", e); - return ApiError::InternalError(None).into_response(); - } + .map_err(|e| { + tracing::error!("DB error upserting did_web_overrides: {:?}", e); + ApiError::InternalError(None) + })?; if let Some(ref endpoint) = input.service_endpoint { let endpoint_clean = endpoint.trim().trim_end_matches('/'); - if let Err(e) = state + state .user_repo - .update_migrated_to_pds(&auth_user.did, endpoint_clean) + .update_migrated_to_pds(&auth.did, endpoint_clean) .await - { - tracing::error!("DB error updating service endpoint: {:?}", e); - return ApiError::InternalError(None).into_response(); - } + .map_err(|e| { + tracing::error!("DB error updating service endpoint: {:?}", e); + ApiError::InternalError(None) + })?; } - let did_doc = build_did_document(&state, &auth_user.did).await; + let did_doc = build_did_document(&state, &auth.did).await; - tracing::info!("Updated DID document for {}", &auth_user.did); + tracing::info!("Updated DID document for {}", &auth.did); - ( + Ok(( StatusCode::OK, Json(UpdateDidDocumentOutput { success: true, did_document: did_doc, }), ) - .into_response() + .into_response()) } -pub async fn get_did_document(State(state): State, auth: BearerAuth) -> Response { - let auth_user = auth.0; - - if !auth_user.did.starts_with("did:web:") { - return ApiError::InvalidRequest( +pub async fn get_did_document( + State(state): State, + auth: Auth, +) -> Result { + if !auth.did.starts_with("did:web:") { + return Err(ApiError::InvalidRequest( "This endpoint is only available for did:web accounts".into(), - ) - .into_response(); + )); } - let did_doc = build_did_document(&state, &auth_user.did).await; + let did_doc = build_did_document(&state, &auth.did).await; - (StatusCode::OK, Json(json!({ "didDocument": did_doc }))).into_response() + Ok((StatusCode::OK, Json(json!({ "didDocument": did_doc }))).into_response()) } async fn build_did_document(state: &AppState, did: &crate::types::Did) -> serde_json::Value { diff --git a/crates/tranquil-pds/src/api/server/passkeys.rs b/crates/tranquil-pds/src/api/server/passkeys.rs index 32ef39d..3b80511 100644 --- a/crates/tranquil-pds/src/api/server/passkeys.rs +++ b/crates/tranquil-pds/src/api/server/passkeys.rs @@ -1,7 +1,7 @@ use crate::api::EmptyResponse; use crate::api::error::ApiError; -use crate::auth::BearerAuth; use crate::auth::webauthn::WebAuthnConfig; +use crate::auth::{Active, Auth}; use crate::state::AppState; use axum::{ Json, @@ -34,32 +34,29 @@ pub struct StartRegistrationResponse { pub async fn start_passkey_registration( State(state): State, - auth: BearerAuth, + auth: Auth, Json(input): Json, -) -> Response { - let webauthn = match get_webauthn() { - Ok(w) => w, - Err(e) => return e.into_response(), - }; +) -> Result { + let webauthn = get_webauthn()?; - let handle = match state.user_repo.get_handle_by_did(&auth.0.did).await { - Ok(Some(h)) => h, - Ok(None) => { - return ApiError::AccountNotFound.into_response(); - } - Err(e) => { + let handle = state + .user_repo + .get_handle_by_did(&auth.did) + .await + .map_err(|e| { error!("DB error fetching user: {:?}", e); - return ApiError::InternalError(None).into_response(); - } - }; + ApiError::InternalError(None) + })? + .ok_or(ApiError::AccountNotFound)?; - let existing_passkeys = match state.user_repo.get_passkeys_for_user(&auth.0.did).await { - Ok(passkeys) => passkeys, - Err(e) => { + let existing_passkeys = state + .user_repo + .get_passkeys_for_user(&auth.did) + .await + .map_err(|e| { error!("DB error fetching existing passkeys: {:?}", e); - return ApiError::InternalError(None).into_response(); - } - }; + ApiError::InternalError(None) + })?; let exclude_credentials: Vec = existing_passkeys .iter() @@ -68,42 +65,32 @@ pub async fn start_passkey_registration( let display_name = input.friendly_name.as_deref().unwrap_or(&handle); - let (ccr, reg_state) = match webauthn.start_registration( - &auth.0.did, - &handle, - display_name, - exclude_credentials, - ) { - Ok(result) => result, - Err(e) => { + let (ccr, reg_state) = webauthn + .start_registration(&auth.did, &handle, display_name, exclude_credentials) + .map_err(|e| { error!("Failed to start passkey registration: {}", e); - return ApiError::InternalError(Some("Failed to start registration".into())) - .into_response(); - } - }; + ApiError::InternalError(Some("Failed to start registration".into())) + })?; - let state_json = match serde_json::to_string(®_state) { - Ok(s) => s, - Err(e) => { - error!("Failed to serialize registration state: {:?}", e); - return ApiError::InternalError(None).into_response(); - } - }; + let state_json = serde_json::to_string(®_state).map_err(|e| { + error!("Failed to serialize registration state: {:?}", e); + ApiError::InternalError(None) + })?; - if let Err(e) = state + state .user_repo - .save_webauthn_challenge(&auth.0.did, "registration", &state_json) + .save_webauthn_challenge(&auth.did, "registration", &state_json) .await - { - error!("Failed to save registration state: {:?}", e); - return ApiError::InternalError(None).into_response(); - } + .map_err(|e| { + error!("Failed to save registration state: {:?}", e); + ApiError::InternalError(None) + })?; let options = serde_json::to_value(&ccr).unwrap_or(serde_json::json!({})); - info!(did = %auth.0.did, "Passkey registration started"); + info!(did = %auth.did, "Passkey registration started"); - Json(StartRegistrationResponse { options }).into_response() + Ok(Json(StartRegistrationResponse { options }).into_response()) } #[derive(Deserialize)] @@ -122,81 +109,62 @@ pub struct FinishRegistrationResponse { pub async fn finish_passkey_registration( State(state): State, - auth: BearerAuth, + auth: Auth, Json(input): Json, -) -> Response { - let webauthn = match get_webauthn() { - Ok(w) => w, - Err(e) => return e.into_response(), - }; +) -> Result { + let webauthn = get_webauthn()?; - let reg_state_json = match state + let reg_state_json = state .user_repo - .load_webauthn_challenge(&auth.0.did, "registration") + .load_webauthn_challenge(&auth.did, "registration") .await - { - Ok(Some(json)) => json, - Ok(None) => { - return ApiError::NoRegistrationInProgress.into_response(); - } - Err(e) => { + .map_err(|e| { error!("DB error loading registration state: {:?}", e); - return ApiError::InternalError(None).into_response(); - } - }; + ApiError::InternalError(None) + })? + .ok_or(ApiError::NoRegistrationInProgress)?; - let reg_state: SecurityKeyRegistration = match serde_json::from_str(®_state_json) { - Ok(s) => s, - Err(e) => { + let reg_state: SecurityKeyRegistration = + serde_json::from_str(®_state_json).map_err(|e| { error!("Failed to deserialize registration state: {:?}", e); - return ApiError::InternalError(None).into_response(); - } - }; + ApiError::InternalError(None) + })?; - let credential: RegisterPublicKeyCredential = match serde_json::from_value(input.credential) { - Ok(c) => c, - Err(e) => { + let credential: RegisterPublicKeyCredential = serde_json::from_value(input.credential) + .map_err(|e| { warn!("Failed to parse credential: {:?}", e); - return ApiError::InvalidCredential.into_response(); - } - }; + ApiError::InvalidCredential + })?; - let passkey = match webauthn.finish_registration(&credential, ®_state) { - Ok(pk) => pk, - Err(e) => { + let passkey = webauthn + .finish_registration(&credential, ®_state) + .map_err(|e| { warn!("Failed to finish passkey registration: {}", e); - return ApiError::RegistrationFailed.into_response(); - } - }; + ApiError::RegistrationFailed + })?; - let public_key = match serde_json::to_vec(&passkey) { - Ok(pk) => pk, - Err(e) => { - error!("Failed to serialize passkey: {:?}", e); - return ApiError::InternalError(None).into_response(); - } - }; + let public_key = serde_json::to_vec(&passkey).map_err(|e| { + error!("Failed to serialize passkey: {:?}", e); + ApiError::InternalError(None) + })?; - let passkey_id = match state + let passkey_id = state .user_repo .save_passkey( - &auth.0.did, + &auth.did, passkey.cred_id(), &public_key, input.friendly_name.as_deref(), ) .await - { - Ok(id) => id, - Err(e) => { + .map_err(|e| { error!("Failed to save passkey: {:?}", e); - return ApiError::InternalError(None).into_response(); - } - }; + ApiError::InternalError(None) + })?; if let Err(e) = state .user_repo - .delete_webauthn_challenge(&auth.0.did, "registration") + .delete_webauthn_challenge(&auth.did, "registration") .await { warn!("Failed to delete registration state: {:?}", e); @@ -207,13 +175,13 @@ pub async fn finish_passkey_registration( passkey.cred_id(), ); - info!(did = %auth.0.did, passkey_id = %passkey_id, "Passkey registered"); + info!(did = %auth.did, passkey_id = %passkey_id, "Passkey registered"); - Json(FinishRegistrationResponse { + Ok(Json(FinishRegistrationResponse { id: passkey_id.to_string(), credential_id: credential_id_base64, }) - .into_response() + .into_response()) } #[derive(Serialize)] @@ -232,14 +200,18 @@ pub struct ListPasskeysResponse { pub passkeys: Vec, } -pub async fn list_passkeys(State(state): State, auth: BearerAuth) -> Response { - let passkeys = match state.user_repo.get_passkeys_for_user(&auth.0.did).await { - Ok(pks) => pks, - Err(e) => { +pub async fn list_passkeys( + State(state): State, + auth: Auth, +) -> Result { + let passkeys = state + .user_repo + .get_passkeys_for_user(&auth.did) + .await + .map_err(|e| { error!("DB error fetching passkeys: {:?}", e); - return ApiError::InternalError(None).into_response(); - } - }; + ApiError::InternalError(None) + })?; let passkey_infos: Vec = passkeys .into_iter() @@ -252,10 +224,10 @@ pub async fn list_passkeys(State(state): State, auth: BearerAuth) -> R }) .collect(); - Json(ListPasskeysResponse { + Ok(Json(ListPasskeysResponse { passkeys: passkey_infos, }) - .into_response() + .into_response()) } #[derive(Deserialize)] @@ -266,45 +238,39 @@ pub struct DeletePasskeyInput { pub async fn delete_passkey( State(state): State, - auth: BearerAuth, + auth: Auth, Json(input): Json, -) -> Response { - if !crate::api::server::reauth::check_legacy_session_mfa(&*state.session_repo, &auth.0.did) - .await +) -> Result { + if !crate::api::server::reauth::check_legacy_session_mfa(&*state.session_repo, &auth.did).await { - return crate::api::server::reauth::legacy_mfa_required_response( + return Ok(crate::api::server::reauth::legacy_mfa_required_response( &*state.user_repo, &*state.session_repo, - &auth.0.did, + &auth.did, ) - .await; + .await); } - if crate::api::server::reauth::check_reauth_required(&*state.session_repo, &auth.0.did).await { - return crate::api::server::reauth::reauth_required_response( + if crate::api::server::reauth::check_reauth_required(&*state.session_repo, &auth.did).await { + return Ok(crate::api::server::reauth::reauth_required_response( &*state.user_repo, &*state.session_repo, - &auth.0.did, + &auth.did, ) - .await; + .await); } - let id: uuid::Uuid = match input.id.parse() { - Ok(id) => id, - Err(_) => { - return ApiError::InvalidId.into_response(); - } - }; + let id: uuid::Uuid = input.id.parse().map_err(|_| ApiError::InvalidId)?; - match state.user_repo.delete_passkey(id, &auth.0.did).await { + match state.user_repo.delete_passkey(id, &auth.did).await { Ok(true) => { - info!(did = %auth.0.did, passkey_id = %id, "Passkey deleted"); - EmptyResponse::ok().into_response() + info!(did = %auth.did, passkey_id = %id, "Passkey deleted"); + Ok(EmptyResponse::ok().into_response()) } - Ok(false) => ApiError::PasskeyNotFound.into_response(), + Ok(false) => Err(ApiError::PasskeyNotFound), Err(e) => { error!("DB error deleting passkey: {:?}", e); - ApiError::InternalError(None).into_response() + Err(ApiError::InternalError(None)) } } } @@ -318,29 +284,24 @@ pub struct UpdatePasskeyInput { pub async fn update_passkey( State(state): State, - auth: BearerAuth, + auth: Auth, Json(input): Json, -) -> Response { - let id: uuid::Uuid = match input.id.parse() { - Ok(id) => id, - Err(_) => { - return ApiError::InvalidId.into_response(); - } - }; +) -> Result { + let id: uuid::Uuid = input.id.parse().map_err(|_| ApiError::InvalidId)?; match state .user_repo - .update_passkey_name(id, &auth.0.did, &input.friendly_name) + .update_passkey_name(id, &auth.did, &input.friendly_name) .await { Ok(true) => { - info!(did = %auth.0.did, passkey_id = %id, "Passkey renamed"); - EmptyResponse::ok().into_response() + info!(did = %auth.did, passkey_id = %id, "Passkey renamed"); + Ok(EmptyResponse::ok().into_response()) } - Ok(false) => ApiError::PasskeyNotFound.into_response(), + Ok(false) => Err(ApiError::PasskeyNotFound), Err(e) => { error!("DB error updating passkey: {:?}", e); - ApiError::InternalError(None).into_response() + Err(ApiError::InternalError(None)) } } } diff --git a/crates/tranquil-pds/src/api/server/password.rs b/crates/tranquil-pds/src/api/server/password.rs index d27f9d7..5fc78bf 100644 --- a/crates/tranquil-pds/src/api/server/password.rs +++ b/crates/tranquil-pds/src/api/server/password.rs @@ -1,6 +1,6 @@ use crate::api::error::ApiError; use crate::api::{EmptyResponse, HasPasswordResponse, SuccessResponse}; -use crate::auth::BearerAuth; +use crate::auth::{Active, Auth}; use crate::state::{AppState, RateLimitKind}; use crate::types::PlainPassword; use crate::validation::validate_password; @@ -227,153 +227,158 @@ pub struct ChangePasswordInput { pub async fn change_password( State(state): State, - auth: BearerAuth, + auth: Auth, Json(input): Json, -) -> Response { - if !crate::api::server::reauth::check_legacy_session_mfa(&*state.session_repo, &auth.0.did) - .await +) -> Result { + if !crate::api::server::reauth::check_legacy_session_mfa(&*state.session_repo, &auth.did).await { - return crate::api::server::reauth::legacy_mfa_required_response( + return Ok(crate::api::server::reauth::legacy_mfa_required_response( &*state.user_repo, &*state.session_repo, - &auth.0.did, + &auth.did, ) - .await; + .await); } let current_password = &input.current_password; let new_password = &input.new_password; if current_password.is_empty() { - return ApiError::InvalidRequest("currentPassword is required".into()).into_response(); + return Err(ApiError::InvalidRequest( + "currentPassword is required".into(), + )); } if new_password.is_empty() { - return ApiError::InvalidRequest("newPassword is required".into()).into_response(); + return Err(ApiError::InvalidRequest("newPassword is required".into())); } if let Err(e) = validate_password(new_password) { - return ApiError::InvalidRequest(e.to_string()).into_response(); + return Err(ApiError::InvalidRequest(e.to_string())); } - let user = match state + let user = state .user_repo - .get_id_and_password_hash_by_did(&auth.0.did) + .get_id_and_password_hash_by_did(&auth.did) .await - { - Ok(Some(u)) => u, - Ok(None) => { - return ApiError::AccountNotFound.into_response(); - } - Err(e) => { + .map_err(|e| { error!("DB error in change_password: {:?}", e); - return ApiError::InternalError(None).into_response(); - } - }; + ApiError::InternalError(None) + })? + .ok_or(ApiError::AccountNotFound)?; + let (user_id, password_hash) = (user.id, user.password_hash); - let valid = match verify(current_password, &password_hash) { - Ok(v) => v, - Err(e) => { - error!("Password verification error: {:?}", e); - return ApiError::InternalError(None).into_response(); - } - }; + let valid = verify(current_password, &password_hash).map_err(|e| { + error!("Password verification error: {:?}", e); + ApiError::InternalError(None) + })?; if !valid { - return ApiError::InvalidPassword("Current password is incorrect".into()).into_response(); + return Err(ApiError::InvalidPassword( + "Current password is incorrect".into(), + )); } let new_password_clone = new_password.to_string(); - let new_hash = - match tokio::task::spawn_blocking(move || hash(new_password_clone, DEFAULT_COST)).await { - Ok(Ok(h)) => h, - Ok(Err(e)) => { - error!("Failed to hash password: {:?}", e); - return ApiError::InternalError(None).into_response(); - } - Err(e) => { - error!("Failed to spawn blocking task: {:?}", e); - return ApiError::InternalError(None).into_response(); - } - }; - if let Err(e) = state + let new_hash = tokio::task::spawn_blocking(move || hash(new_password_clone, DEFAULT_COST)) + .await + .map_err(|e| { + error!("Failed to spawn blocking task: {:?}", e); + ApiError::InternalError(None) + })? + .map_err(|e| { + error!("Failed to hash password: {:?}", e); + ApiError::InternalError(None) + })?; + + state .user_repo .update_password_hash(user_id, &new_hash) .await - { - error!("DB error updating password: {:?}", e); - return ApiError::InternalError(None).into_response(); - } - info!(did = %&auth.0.did, "Password changed successfully"); - EmptyResponse::ok().into_response() + .map_err(|e| { + error!("DB error updating password: {:?}", e); + ApiError::InternalError(None) + })?; + + info!(did = %&auth.did, "Password changed successfully"); + Ok(EmptyResponse::ok().into_response()) } -pub async fn get_password_status(State(state): State, auth: BearerAuth) -> Response { - match state.user_repo.has_password_by_did(&auth.0.did).await { - Ok(Some(has)) => HasPasswordResponse::response(has).into_response(), - Ok(None) => ApiError::AccountNotFound.into_response(), +pub async fn get_password_status( + State(state): State, + auth: Auth, +) -> Result { + match state.user_repo.has_password_by_did(&auth.did).await { + Ok(Some(has)) => Ok(HasPasswordResponse::response(has).into_response()), + Ok(None) => Err(ApiError::AccountNotFound), Err(e) => { error!("DB error: {:?}", e); - ApiError::InternalError(None).into_response() + Err(ApiError::InternalError(None)) } } } -pub async fn remove_password(State(state): State, auth: BearerAuth) -> Response { - if !crate::api::server::reauth::check_legacy_session_mfa(&*state.session_repo, &auth.0.did) - .await +pub async fn remove_password( + State(state): State, + auth: Auth, +) -> Result { + if !crate::api::server::reauth::check_legacy_session_mfa(&*state.session_repo, &auth.did).await { - return crate::api::server::reauth::legacy_mfa_required_response( + return Ok(crate::api::server::reauth::legacy_mfa_required_response( &*state.user_repo, &*state.session_repo, - &auth.0.did, + &auth.did, ) - .await; + .await); } if crate::api::server::reauth::check_reauth_required_cached( &*state.session_repo, &state.cache, - &auth.0.did, + &auth.did, ) .await { - return crate::api::server::reauth::reauth_required_response( + return Ok(crate::api::server::reauth::reauth_required_response( &*state.user_repo, &*state.session_repo, - &auth.0.did, + &auth.did, ) - .await; + .await); } let has_passkeys = state .user_repo - .has_passkeys(&auth.0.did) + .has_passkeys(&auth.did) .await .unwrap_or(false); if !has_passkeys { - return ApiError::InvalidRequest( + return Err(ApiError::InvalidRequest( "You must have at least one passkey registered before removing your password".into(), - ) - .into_response(); + )); } - let user = match state.user_repo.get_password_info_by_did(&auth.0.did).await { - Ok(Some(u)) => u, - Ok(None) => { - return ApiError::AccountNotFound.into_response(); - } - Err(e) => { + let user = state + .user_repo + .get_password_info_by_did(&auth.did) + .await + .map_err(|e| { error!("DB error: {:?}", e); - return ApiError::InternalError(None).into_response(); - } - }; + ApiError::InternalError(None) + })? + .ok_or(ApiError::AccountNotFound)?; if user.password_hash.is_none() { - return ApiError::InvalidRequest("Account already has no password".into()).into_response(); + return Err(ApiError::InvalidRequest( + "Account already has no password".into(), + )); } - if let Err(e) = state.user_repo.remove_user_password(user.id).await { - error!("DB error removing password: {:?}", e); - return ApiError::InternalError(None).into_response(); - } + state + .user_repo + .remove_user_password(user.id) + .await + .map_err(|e| { + error!("DB error removing password: {:?}", e); + ApiError::InternalError(None) + })?; - info!(did = %&auth.0.did, "Password removed - account is now passkey-only"); - SuccessResponse::ok().into_response() + info!(did = %&auth.did, "Password removed - account is now passkey-only"); + Ok(SuccessResponse::ok().into_response()) } #[derive(Deserialize)] @@ -384,24 +389,24 @@ pub struct SetPasswordInput { pub async fn set_password( State(state): State, - auth: BearerAuth, + auth: Auth, Json(input): Json, -) -> Response { +) -> Result { let has_password = state .user_repo - .has_password_by_did(&auth.0.did) + .has_password_by_did(&auth.did) .await .ok() .flatten() .unwrap_or(false); let has_passkeys = state .user_repo - .has_passkeys(&auth.0.did) + .has_passkeys(&auth.did) .await .unwrap_or(false); let has_totp = state .user_repo - .has_totp_enabled(&auth.0.did) + .has_totp_enabled(&auth.did) .await .unwrap_or(false); @@ -411,67 +416,63 @@ pub async fn set_password( && crate::api::server::reauth::check_reauth_required_cached( &*state.session_repo, &state.cache, - &auth.0.did, + &auth.did, ) .await { - return crate::api::server::reauth::reauth_required_response( + return Ok(crate::api::server::reauth::reauth_required_response( &*state.user_repo, &*state.session_repo, - &auth.0.did, + &auth.did, ) - .await; + .await); } let new_password = &input.new_password; if new_password.is_empty() { - return ApiError::InvalidRequest("newPassword is required".into()).into_response(); + return Err(ApiError::InvalidRequest("newPassword is required".into())); } if let Err(e) = validate_password(new_password) { - return ApiError::InvalidRequest(e.to_string()).into_response(); + return Err(ApiError::InvalidRequest(e.to_string())); } - let user = match state.user_repo.get_password_info_by_did(&auth.0.did).await { - Ok(Some(u)) => u, - Ok(None) => { - return ApiError::AccountNotFound.into_response(); - } - Err(e) => { + let user = state + .user_repo + .get_password_info_by_did(&auth.did) + .await + .map_err(|e| { error!("DB error: {:?}", e); - return ApiError::InternalError(None).into_response(); - } - }; + ApiError::InternalError(None) + })? + .ok_or(ApiError::AccountNotFound)?; if user.password_hash.is_some() { - return ApiError::InvalidRequest( + return Err(ApiError::InvalidRequest( "Account already has a password. Use changePassword instead.".into(), - ) - .into_response(); + )); } let new_password_clone = new_password.to_string(); - let new_hash = - match tokio::task::spawn_blocking(move || hash(new_password_clone, DEFAULT_COST)).await { - Ok(Ok(h)) => h, - Ok(Err(e)) => { - error!("Failed to hash password: {:?}", e); - return ApiError::InternalError(None).into_response(); - } - Err(e) => { - error!("Failed to spawn blocking task: {:?}", e); - return ApiError::InternalError(None).into_response(); - } - }; + let new_hash = tokio::task::spawn_blocking(move || hash(new_password_clone, DEFAULT_COST)) + .await + .map_err(|e| { + error!("Failed to spawn blocking task: {:?}", e); + ApiError::InternalError(None) + })? + .map_err(|e| { + error!("Failed to hash password: {:?}", e); + ApiError::InternalError(None) + })?; - if let Err(e) = state + state .user_repo .set_new_user_password(user.id, &new_hash) .await - { - error!("DB error setting password: {:?}", e); - return ApiError::InternalError(None).into_response(); - } + .map_err(|e| { + error!("DB error setting password: {:?}", e); + ApiError::InternalError(None) + })?; - info!(did = %&auth.0.did, "Password set for passkey-only account"); - SuccessResponse::ok().into_response() + info!(did = %&auth.did, "Password set for passkey-only account"); + Ok(SuccessResponse::ok().into_response()) } diff --git a/crates/tranquil-pds/src/api/server/reauth.rs b/crates/tranquil-pds/src/api/server/reauth.rs index 82a52f4..c06f10e 100644 --- a/crates/tranquil-pds/src/api/server/reauth.rs +++ b/crates/tranquil-pds/src/api/server/reauth.rs @@ -10,7 +10,7 @@ use serde::{Deserialize, Serialize}; use tracing::{error, info, warn}; use tranquil_db_traits::{SessionRepository, UserRepository}; -use crate::auth::BearerAuth; +use crate::auth::{Active, Auth}; use crate::state::{AppState, RateLimitKind}; use crate::types::PlainPassword; @@ -24,25 +24,29 @@ pub struct ReauthStatusResponse { pub available_methods: Vec, } -pub async fn get_reauth_status(State(state): State, auth: BearerAuth) -> Response { - let last_reauth_at = match state.session_repo.get_last_reauth_at(&auth.0.did).await { - Ok(t) => t, - Err(e) => { +pub async fn get_reauth_status( + State(state): State, + auth: Auth, +) -> Result { + let last_reauth_at = state + .session_repo + .get_last_reauth_at(&auth.did) + .await + .map_err(|e| { error!("DB error: {:?}", e); - return ApiError::InternalError(None).into_response(); - } - }; + ApiError::InternalError(None) + })?; let reauth_required = is_reauth_required(last_reauth_at); let available_methods = - get_available_reauth_methods(&*state.user_repo, &*state.session_repo, &auth.0.did).await; + get_available_reauth_methods(&*state.user_repo, &*state.session_repo, &auth.did).await; - Json(ReauthStatusResponse { + Ok(Json(ReauthStatusResponse { last_reauth_at, reauth_required, available_methods, }) - .into_response() + .into_response()) } #[derive(Deserialize)] @@ -59,26 +63,25 @@ pub struct ReauthResponse { pub async fn reauth_password( State(state): State, - auth: BearerAuth, + auth: Auth, Json(input): Json, -) -> Response { - let password_hash = match state.user_repo.get_password_hash_by_did(&auth.0.did).await { - Ok(Some(hash)) => hash, - Ok(None) => { - return ApiError::AccountNotFound.into_response(); - } - Err(e) => { +) -> Result { + let password_hash = state + .user_repo + .get_password_hash_by_did(&auth.did) + .await + .map_err(|e| { error!("DB error: {:?}", e); - return ApiError::InternalError(None).into_response(); - } - }; + ApiError::InternalError(None) + })? + .ok_or(ApiError::AccountNotFound)?; let password_valid = bcrypt::verify(&input.password, &password_hash).unwrap_or(false); if !password_valid { let app_password_hashes = state .session_repo - .get_app_password_hashes_by_did(&auth.0.did) + .get_app_password_hashes_by_did(&auth.did) .await .unwrap_or_default(); @@ -87,21 +90,20 @@ pub async fn reauth_password( }); if !app_password_valid { - warn!(did = %&auth.0.did, "Re-auth failed: invalid password"); - return ApiError::InvalidPassword("Password is incorrect".into()).into_response(); + warn!(did = %&auth.did, "Re-auth failed: invalid password"); + return Err(ApiError::InvalidPassword("Password is incorrect".into())); } } - match update_last_reauth_cached(&*state.session_repo, &state.cache, &auth.0.did).await { - Ok(reauthed_at) => { - info!(did = %&auth.0.did, "Re-auth successful via password"); - Json(ReauthResponse { reauthed_at }).into_response() - } - Err(e) => { + let reauthed_at = update_last_reauth_cached(&*state.session_repo, &state.cache, &auth.did) + .await + .map_err(|e| { error!("DB error updating reauth: {:?}", e); - ApiError::InternalError(None).into_response() - } - } + ApiError::InternalError(None) + })?; + + info!(did = %&auth.did, "Re-auth successful via password"); + Ok(Json(ReauthResponse { reauthed_at }).into_response()) } #[derive(Deserialize)] @@ -112,39 +114,39 @@ pub struct TotpReauthInput { pub async fn reauth_totp( State(state): State, - auth: BearerAuth, + auth: Auth, Json(input): Json, -) -> Response { +) -> Result { if !state - .check_rate_limit(RateLimitKind::TotpVerify, &auth.0.did) + .check_rate_limit(RateLimitKind::TotpVerify, &auth.did) .await { - warn!(did = %&auth.0.did, "TOTP verification rate limit exceeded"); - return ApiError::RateLimitExceeded(Some( + warn!(did = %&auth.did, "TOTP verification rate limit exceeded"); + return Err(ApiError::RateLimitExceeded(Some( "Too many verification attempts. Please try again in a few minutes.".into(), - )) - .into_response(); + ))); } let valid = - crate::api::server::totp::verify_totp_or_backup_for_user(&state, &auth.0.did, &input.code) + crate::api::server::totp::verify_totp_or_backup_for_user(&state, &auth.did, &input.code) .await; if !valid { - warn!(did = %&auth.0.did, "Re-auth failed: invalid TOTP code"); - return ApiError::InvalidCode(Some("Invalid TOTP or backup code".into())).into_response(); + warn!(did = %&auth.did, "Re-auth failed: invalid TOTP code"); + return Err(ApiError::InvalidCode(Some( + "Invalid TOTP or backup code".into(), + ))); } - match update_last_reauth_cached(&*state.session_repo, &state.cache, &auth.0.did).await { - Ok(reauthed_at) => { - info!(did = %&auth.0.did, "Re-auth successful via TOTP"); - Json(ReauthResponse { reauthed_at }).into_response() - } - Err(e) => { + let reauthed_at = update_last_reauth_cached(&*state.session_repo, &state.cache, &auth.did) + .await + .map_err(|e| { error!("DB error updating reauth: {:?}", e); - ApiError::InternalError(None).into_response() - } - } + ApiError::InternalError(None) + })?; + + info!(did = %&auth.did, "Re-auth successful via TOTP"); + Ok(Json(ReauthResponse { reauthed_at }).into_response()) } #[derive(Serialize)] @@ -153,19 +155,23 @@ pub struct PasskeyReauthStartResponse { pub options: serde_json::Value, } -pub async fn reauth_passkey_start(State(state): State, auth: BearerAuth) -> Response { +pub async fn reauth_passkey_start( + State(state): State, + auth: Auth, +) -> Result { let pds_hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); - let stored_passkeys = match state.user_repo.get_passkeys_for_user(&auth.0.did).await { - Ok(pks) => pks, - Err(e) => { + let stored_passkeys = state + .user_repo + .get_passkeys_for_user(&auth.did) + .await + .map_err(|e| { error!("Failed to get passkeys: {:?}", e); - return ApiError::InternalError(None).into_response(); - } - }; + ApiError::InternalError(None) + })?; if stored_passkeys.is_empty() { - return ApiError::NoPasskeys.into_response(); + return Err(ApiError::NoPasskeys); } let passkeys: Vec = stored_passkeys @@ -174,44 +180,37 @@ pub async fn reauth_passkey_start(State(state): State, auth: BearerAut .collect(); if passkeys.is_empty() { - return ApiError::InternalError(Some("Failed to load passkeys".into())).into_response(); + return Err(ApiError::InternalError(Some( + "Failed to load passkeys".into(), + ))); } - let webauthn = match crate::auth::webauthn::WebAuthnConfig::new(&pds_hostname) { - Ok(w) => w, - Err(e) => { - error!("Failed to create WebAuthn config: {:?}", e); - return ApiError::InternalError(None).into_response(); - } - }; + let webauthn = crate::auth::webauthn::WebAuthnConfig::new(&pds_hostname).map_err(|e| { + error!("Failed to create WebAuthn config: {:?}", e); + ApiError::InternalError(None) + })?; - let (rcr, auth_state) = match webauthn.start_authentication(passkeys) { - Ok(result) => result, - Err(e) => { - error!("Failed to start passkey authentication: {:?}", e); - return ApiError::InternalError(None).into_response(); - } - }; + let (rcr, auth_state) = webauthn.start_authentication(passkeys).map_err(|e| { + error!("Failed to start passkey authentication: {:?}", e); + ApiError::InternalError(None) + })?; - let state_json = match serde_json::to_string(&auth_state) { - Ok(s) => s, - Err(e) => { - error!("Failed to serialize authentication state: {:?}", e); - return ApiError::InternalError(None).into_response(); - } - }; + let state_json = serde_json::to_string(&auth_state).map_err(|e| { + error!("Failed to serialize authentication state: {:?}", e); + ApiError::InternalError(None) + })?; - if let Err(e) = state + state .user_repo - .save_webauthn_challenge(&auth.0.did, "authentication", &state_json) + .save_webauthn_challenge(&auth.did, "authentication", &state_json) .await - { - error!("Failed to save authentication state: {:?}", e); - return ApiError::InternalError(None).into_response(); - } + .map_err(|e| { + error!("Failed to save authentication state: {:?}", e); + ApiError::InternalError(None) + })?; let options = serde_json::to_value(&rcr).unwrap_or(serde_json::json!({})); - Json(PasskeyReauthStartResponse { options }).into_response() + Ok(Json(PasskeyReauthStartResponse { options }).into_response()) } #[derive(Deserialize)] @@ -222,60 +221,44 @@ pub struct PasskeyReauthFinishInput { pub async fn reauth_passkey_finish( State(state): State, - auth: BearerAuth, + auth: Auth, Json(input): Json, -) -> Response { +) -> Result { let pds_hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); - let auth_state_json = match state + let auth_state_json = state .user_repo - .load_webauthn_challenge(&auth.0.did, "authentication") + .load_webauthn_challenge(&auth.did, "authentication") .await - { - Ok(Some(json)) => json, - Ok(None) => { - return ApiError::NoChallengeInProgress.into_response(); - } - Err(e) => { + .map_err(|e| { error!("Failed to load authentication state: {:?}", e); - return ApiError::InternalError(None).into_response(); - } - }; + ApiError::InternalError(None) + })? + .ok_or(ApiError::NoChallengeInProgress)?; let auth_state: webauthn_rs::prelude::SecurityKeyAuthentication = - match serde_json::from_str(&auth_state_json) { - Ok(s) => s, - Err(e) => { - error!("Failed to deserialize authentication state: {:?}", e); - return ApiError::InternalError(None).into_response(); - } - }; + serde_json::from_str(&auth_state_json).map_err(|e| { + error!("Failed to deserialize authentication state: {:?}", e); + ApiError::InternalError(None) + })?; let credential: webauthn_rs::prelude::PublicKeyCredential = - match serde_json::from_value(input.credential) { - Ok(c) => c, - Err(e) => { - warn!("Failed to parse credential: {:?}", e); - return ApiError::InvalidCredential.into_response(); - } - }; + serde_json::from_value(input.credential).map_err(|e| { + warn!("Failed to parse credential: {:?}", e); + ApiError::InvalidCredential + })?; - let webauthn = match crate::auth::webauthn::WebAuthnConfig::new(&pds_hostname) { - Ok(w) => w, - Err(e) => { - error!("Failed to create WebAuthn config: {:?}", e); - return ApiError::InternalError(None).into_response(); - } - }; + let webauthn = crate::auth::webauthn::WebAuthnConfig::new(&pds_hostname).map_err(|e| { + error!("Failed to create WebAuthn config: {:?}", e); + ApiError::InternalError(None) + })?; - let auth_result = match webauthn.finish_authentication(&credential, &auth_state) { - Ok(r) => r, - Err(e) => { - warn!(did = %&auth.0.did, "Passkey re-auth failed: {:?}", e); - return ApiError::AuthenticationFailed(Some("Passkey authentication failed".into())) - .into_response(); - } - }; + let auth_result = webauthn + .finish_authentication(&credential, &auth_state) + .map_err(|e| { + warn!(did = %&auth.did, "Passkey re-auth failed: {:?}", e); + ApiError::AuthenticationFailed(Some("Passkey authentication failed".into())) + })?; let cred_id_bytes = auth_result.cred_id().as_ref(); match state @@ -284,12 +267,12 @@ pub async fn reauth_passkey_finish( .await { Ok(false) => { - warn!(did = %&auth.0.did, "Passkey counter anomaly detected - possible cloned key"); + warn!(did = %&auth.did, "Passkey counter anomaly detected - possible cloned key"); let _ = state .user_repo - .delete_webauthn_challenge(&auth.0.did, "authentication") + .delete_webauthn_challenge(&auth.did, "authentication") .await; - return ApiError::PasskeyCounterAnomaly.into_response(); + return Err(ApiError::PasskeyCounterAnomaly); } Err(e) => { error!("Failed to update passkey counter: {:?}", e); @@ -299,19 +282,18 @@ pub async fn reauth_passkey_finish( let _ = state .user_repo - .delete_webauthn_challenge(&auth.0.did, "authentication") + .delete_webauthn_challenge(&auth.did, "authentication") .await; - match update_last_reauth_cached(&*state.session_repo, &state.cache, &auth.0.did).await { - Ok(reauthed_at) => { - info!(did = %&auth.0.did, "Re-auth successful via passkey"); - Json(ReauthResponse { reauthed_at }).into_response() - } - Err(e) => { + let reauthed_at = update_last_reauth_cached(&*state.session_repo, &state.cache, &auth.did) + .await + .map_err(|e| { error!("DB error updating reauth: {:?}", e); - ApiError::InternalError(None).into_response() - } - } + ApiError::InternalError(None) + })?; + + info!(did = %&auth.did, "Re-auth successful via passkey"); + Ok(Json(ReauthResponse { reauthed_at }).into_response()) } pub async fn update_last_reauth_cached( diff --git a/crates/tranquil-pds/src/api/server/service_auth.rs b/crates/tranquil-pds/src/api/server/service_auth.rs index 4b8b13a..1b78a04 100644 --- a/crates/tranquil-pds/src/api/server/service_auth.rs +++ b/crates/tranquil-pds/src/api/server/service_auth.rs @@ -95,12 +95,12 @@ pub async fn get_service_auth( { Ok(result) => crate::auth::AuthenticatedUser { did: Did::new_unchecked(result.did), - is_oauth: true, is_admin: false, status: AccountStatus::Active, scope: result.scope, key_bytes: None, controller_did: None, + auth_source: crate::auth::AuthSource::OAuth, }, Err(crate::oauth::OAuthError::UseDpopNonce(nonce)) => { return ( @@ -131,7 +131,7 @@ pub async fn get_service_auth( }; info!( did = %&auth_user.did, - is_oauth = auth_user.is_oauth, + is_oauth = auth_user.is_oauth(), has_key = auth_user.key_bytes.is_some(), "getServiceAuth auth validated" ); @@ -180,14 +180,14 @@ pub async fn get_service_auth( if let Some(method) = lxm { if let Err(e) = crate::auth::scope_check::check_rpc_scope( - auth_user.is_oauth, + auth_user.is_oauth(), auth_user.scope.as_deref(), ¶ms.aud, method, ) { return e; } - } else if auth_user.is_oauth { + } else if auth_user.is_oauth() { let permissions = auth_user.permissions(); if !permissions.has_full_access() { return ApiError::InvalidRequest( diff --git a/crates/tranquil-pds/src/api/server/session.rs b/crates/tranquil-pds/src/api/server/session.rs index dd3707e..3dc5e9b 100644 --- a/crates/tranquil-pds/src/api/server/session.rs +++ b/crates/tranquil-pds/src/api/server/session.rs @@ -1,6 +1,6 @@ use crate::api::error::ApiError; use crate::api::{EmptyResponse, SuccessResponse}; -use crate::auth::{BearerAuth, BearerAuthAllowDeactivated}; +use crate::auth::{Active, Auth, NotTakendown}; use crate::state::{AppState, RateLimitKind}; use crate::types::{AccountState, Did, Handle, PlainPassword}; use axum::{ @@ -279,15 +279,15 @@ pub async fn create_session( pub async fn get_session( State(state): State, - BearerAuthAllowDeactivated(auth_user): BearerAuthAllowDeactivated, -) -> Response { - let permissions = auth_user.permissions(); + auth: Auth, +) -> Result { + let permissions = auth.permissions(); let can_read_email = permissions.allows_email_read(); - let did_for_doc = auth_user.did.clone(); + let did_for_doc = auth.did.clone(); let did_resolver = state.did_resolver.clone(); let (db_result, did_doc) = tokio::join!( - state.user_repo.get_session_info_by_did(&auth_user.did), + state.user_repo.get_session_info_by_did(&auth.did), did_resolver.resolve_did_document(&did_for_doc) ); match db_result { @@ -316,7 +316,7 @@ pub async fn get_session( let email_confirmed_value = can_read_email && row.email_verified; let mut response = json!({ "handle": handle, - "did": &auth_user.did, + "did": &auth.did, "active": account_state.is_active(), "preferredChannel": preferred_channel, "preferredChannelVerified": preferred_channel_verified, @@ -337,12 +337,12 @@ pub async fn get_session( if let Some(doc) = did_doc { response["didDoc"] = doc; } - Json(response).into_response() + Ok(Json(response).into_response()) } - Ok(None) => ApiError::AuthenticationFailed(None).into_response(), + Ok(None) => Err(ApiError::AuthenticationFailed(None)), Err(e) => { error!("Database error in get_session: {:?}", e); - ApiError::InternalError(None).into_response() + Err(ApiError::InternalError(None)) } } } @@ -350,18 +350,14 @@ pub async fn get_session( pub async fn delete_session( State(state): State, headers: axum::http::HeaderMap, - _auth: BearerAuth, -) -> Response { - let extracted = match crate::auth::extract_auth_token_from_header( + _auth: Auth, +) -> Result { + let extracted = crate::auth::extract_auth_token_from_header( headers.get("Authorization").and_then(|h| h.to_str().ok()), - ) { - Some(t) => t, - None => return ApiError::AuthenticationRequired.into_response(), - }; - let jti = match crate::auth::get_jti_from_token(&extracted.token) { - Ok(jti) => jti, - Err(_) => return ApiError::AuthenticationFailed(None).into_response(), - }; + ) + .ok_or(ApiError::AuthenticationRequired)?; + let jti = crate::auth::get_jti_from_token(&extracted.token) + .map_err(|_| ApiError::AuthenticationFailed(None))?; let did = crate::auth::get_did_from_token(&extracted.token).ok(); match state.session_repo.delete_session_by_access_jti(&jti).await { Ok(rows) if rows > 0 => { @@ -369,10 +365,10 @@ pub async fn delete_session( let session_cache_key = format!("auth:session:{}:{}", did, jti); let _ = state.cache.delete(&session_cache_key).await; } - EmptyResponse::ok().into_response() + Ok(EmptyResponse::ok().into_response()) } - Ok(_) => ApiError::AuthenticationFailed(None).into_response(), - Err(_) => ApiError::AuthenticationFailed(None).into_response(), + Ok(_) => Err(ApiError::AuthenticationFailed(None)), + Err(_) => Err(ApiError::AuthenticationFailed(None)), } } @@ -796,29 +792,31 @@ pub struct ListSessionsOutput { pub async fn list_sessions( State(state): State, headers: HeaderMap, - auth: BearerAuth, -) -> Response { + auth: Auth, +) -> Result { let current_jti = headers .get("authorization") .and_then(|v| v.to_str().ok()) .and_then(|v| v.strip_prefix("Bearer ")) .and_then(|token| crate::auth::get_jti_from_token(token).ok()); - let jwt_rows = match state.session_repo.list_sessions_by_did(&auth.0.did).await { - Ok(rows) => rows, - Err(e) => { + let jwt_rows = state + .session_repo + .list_sessions_by_did(&auth.did) + .await + .map_err(|e| { error!("DB error fetching JWT sessions: {:?}", e); - return ApiError::InternalError(None).into_response(); - } - }; + ApiError::InternalError(None) + })?; - let oauth_rows = match state.oauth_repo.list_sessions_by_did(&auth.0.did).await { - Ok(rows) => rows, - Err(e) => { + let oauth_rows = state + .oauth_repo + .list_sessions_by_did(&auth.did) + .await + .map_err(|e| { error!("DB error fetching OAuth sessions: {:?}", e); - return ApiError::InternalError(None).into_response(); - } - }; + ApiError::InternalError(None) + })?; let jwt_sessions = jwt_rows.into_iter().map(|row| SessionInfo { id: format!("jwt:{}", row.id), @@ -829,7 +827,7 @@ pub async fn list_sessions( is_current: current_jti.as_ref() == Some(&row.access_jti), }); - let is_oauth = auth.0.is_oauth; + let is_oauth = auth.is_oauth(); let oauth_sessions = oauth_rows.into_iter().map(|row| { let client_name = extract_client_name(&row.client_id); let is_current_oauth = is_oauth && current_jti.as_deref() == Some(row.token_id.as_str()); @@ -846,7 +844,7 @@ pub async fn list_sessions( let mut sessions: Vec = jwt_sessions.chain(oauth_sessions).collect(); sessions.sort_by(|a, b| b.created_at.cmp(&a.created_at)); - (StatusCode::OK, Json(ListSessionsOutput { sessions })).into_response() + Ok((StatusCode::OK, Json(ListSessionsOutput { sessions })).into_response()) } fn extract_client_name(client_id: &str) -> String { @@ -867,106 +865,107 @@ pub struct RevokeSessionInput { pub async fn revoke_session( State(state): State, - auth: BearerAuth, + auth: Auth, Json(input): Json, -) -> Response { +) -> Result { if let Some(jwt_id) = input.session_id.strip_prefix("jwt:") { - let Ok(session_id) = jwt_id.parse::() else { - return ApiError::InvalidRequest("Invalid session ID".into()).into_response(); - }; - let access_jti = match state + let session_id: i32 = jwt_id + .parse() + .map_err(|_| ApiError::InvalidRequest("Invalid session ID".into()))?; + let access_jti = state .session_repo - .get_session_access_jti_by_id(session_id, &auth.0.did) + .get_session_access_jti_by_id(session_id, &auth.did) .await - { - Ok(Some(jti)) => jti, - Ok(None) => { - return ApiError::SessionNotFound.into_response(); - } - Err(e) => { + .map_err(|e| { error!("DB error in revoke_session: {:?}", e); - return ApiError::InternalError(None).into_response(); - } - }; - if let Err(e) = state.session_repo.delete_session_by_id(session_id).await { - error!("DB error deleting session: {:?}", e); - return ApiError::InternalError(None).into_response(); - } - let cache_key = format!("auth:session:{}:{}", &auth.0.did, access_jti); + ApiError::InternalError(None) + })? + .ok_or(ApiError::SessionNotFound)?; + state + .session_repo + .delete_session_by_id(session_id) + .await + .map_err(|e| { + error!("DB error deleting session: {:?}", e); + ApiError::InternalError(None) + })?; + let cache_key = format!("auth:session:{}:{}", &auth.did, access_jti); if let Err(e) = state.cache.delete(&cache_key).await { warn!("Failed to invalidate session cache: {:?}", e); } - info!(did = %&auth.0.did, session_id = %session_id, "JWT session revoked"); + info!(did = %&auth.did, session_id = %session_id, "JWT session revoked"); } else if let Some(oauth_id) = input.session_id.strip_prefix("oauth:") { - let Ok(session_id) = oauth_id.parse::() else { - return ApiError::InvalidRequest("Invalid session ID".into()).into_response(); - }; - match state + let session_id: i32 = oauth_id + .parse() + .map_err(|_| ApiError::InvalidRequest("Invalid session ID".into()))?; + let deleted = state .oauth_repo - .delete_session_by_id(session_id, &auth.0.did) + .delete_session_by_id(session_id, &auth.did) .await - { - Ok(0) => { - return ApiError::SessionNotFound.into_response(); - } - Err(e) => { + .map_err(|e| { error!("DB error deleting OAuth session: {:?}", e); - return ApiError::InternalError(None).into_response(); - } - _ => {} + ApiError::InternalError(None) + })?; + if deleted == 0 { + return Err(ApiError::SessionNotFound); } - info!(did = %&auth.0.did, session_id = %session_id, "OAuth session revoked"); + info!(did = %&auth.did, session_id = %session_id, "OAuth session revoked"); } else { - return ApiError::InvalidRequest("Invalid session ID format".into()).into_response(); + return Err(ApiError::InvalidRequest("Invalid session ID format".into())); } - EmptyResponse::ok().into_response() + Ok(EmptyResponse::ok().into_response()) } pub async fn revoke_all_sessions( State(state): State, headers: HeaderMap, - auth: BearerAuth, -) -> Response { - let current_jti = crate::auth::extract_auth_token_from_header( + auth: Auth, +) -> Result { + let jti = crate::auth::extract_auth_token_from_header( headers.get("authorization").and_then(|v| v.to_str().ok()), ) - .and_then(|extracted| crate::auth::get_jti_from_token(&extracted.token).ok()); + .and_then(|extracted| crate::auth::get_jti_from_token(&extracted.token).ok()) + .ok_or(ApiError::InvalidToken(None))?; - let Some(ref jti) = current_jti else { - return ApiError::InvalidToken(None).into_response(); - }; - - if auth.0.is_oauth { - if let Err(e) = state.session_repo.delete_sessions_by_did(&auth.0.did).await { - error!("DB error revoking JWT sessions: {:?}", e); - return ApiError::InternalError(None).into_response(); - } - let jti_typed = TokenId::from(jti.clone()); - if let Err(e) = state - .oauth_repo - .delete_sessions_by_did_except(&auth.0.did, &jti_typed) - .await - { - error!("DB error revoking OAuth sessions: {:?}", e); - return ApiError::InternalError(None).into_response(); - } - } else { - if let Err(e) = state + if auth.is_oauth() { + state .session_repo - .delete_sessions_by_did_except_jti(&auth.0.did, jti) + .delete_sessions_by_did(&auth.did) .await - { - error!("DB error revoking JWT sessions: {:?}", e); - return ApiError::InternalError(None).into_response(); - } - if let Err(e) = state.oauth_repo.delete_sessions_by_did(&auth.0.did).await { - error!("DB error revoking OAuth sessions: {:?}", e); - return ApiError::InternalError(None).into_response(); - } + .map_err(|e| { + error!("DB error revoking JWT sessions: {:?}", e); + ApiError::InternalError(None) + })?; + let jti_typed = TokenId::from(jti.clone()); + state + .oauth_repo + .delete_sessions_by_did_except(&auth.did, &jti_typed) + .await + .map_err(|e| { + error!("DB error revoking OAuth sessions: {:?}", e); + ApiError::InternalError(None) + })?; + } else { + state + .session_repo + .delete_sessions_by_did_except_jti(&auth.did, &jti) + .await + .map_err(|e| { + error!("DB error revoking JWT sessions: {:?}", e); + ApiError::InternalError(None) + })?; + state + .oauth_repo + .delete_sessions_by_did(&auth.did) + .await + .map_err(|e| { + error!("DB error revoking OAuth sessions: {:?}", e); + ApiError::InternalError(None) + })?; } - info!(did = %&auth.0.did, "All other sessions revoked"); - SuccessResponse::ok().into_response() + info!(did = %&auth.did, "All other sessions revoked"); + Ok(SuccessResponse::ok().into_response()) } #[derive(Serialize)] @@ -978,20 +977,22 @@ pub struct LegacyLoginPreferenceOutput { pub async fn get_legacy_login_preference( State(state): State, - auth: BearerAuth, -) -> Response { - match state.user_repo.get_legacy_login_pref(&auth.0.did).await { - Ok(Some(pref)) => Json(LegacyLoginPreferenceOutput { - allow_legacy_login: pref.allow_legacy_login, - has_mfa: pref.has_mfa, - }) - .into_response(), - Ok(None) => ApiError::AccountNotFound.into_response(), - Err(e) => { + auth: Auth, +) -> Result { + let pref = state + .user_repo + .get_legacy_login_pref(&auth.did) + .await + .map_err(|e| { error!("DB error: {:?}", e); - ApiError::InternalError(None).into_response() - } - } + ApiError::InternalError(None) + })? + .ok_or(ApiError::AccountNotFound)?; + Ok(Json(LegacyLoginPreferenceOutput { + allow_legacy_login: pref.allow_legacy_login, + has_mfa: pref.has_mfa, + }) + .into_response()) } #[derive(Deserialize)] @@ -1002,51 +1003,48 @@ pub struct UpdateLegacyLoginInput { pub async fn update_legacy_login_preference( State(state): State, - auth: BearerAuth, + auth: Auth, Json(input): Json, -) -> Response { - if !crate::api::server::reauth::check_legacy_session_mfa(&*state.session_repo, &auth.0.did) - .await +) -> Result { + if !crate::api::server::reauth::check_legacy_session_mfa(&*state.session_repo, &auth.did).await { - return crate::api::server::reauth::legacy_mfa_required_response( + return Ok(crate::api::server::reauth::legacy_mfa_required_response( &*state.user_repo, &*state.session_repo, - &auth.0.did, + &auth.did, ) - .await; + .await); } - if crate::api::server::reauth::check_reauth_required(&*state.session_repo, &auth.0.did).await { - return crate::api::server::reauth::reauth_required_response( + if crate::api::server::reauth::check_reauth_required(&*state.session_repo, &auth.did).await { + return Ok(crate::api::server::reauth::reauth_required_response( &*state.user_repo, &*state.session_repo, - &auth.0.did, + &auth.did, ) - .await; + .await); } - match state + let updated = state .user_repo - .update_legacy_login(&auth.0.did, input.allow_legacy_login) + .update_legacy_login(&auth.did, input.allow_legacy_login) .await - { - Ok(true) => { - info!( - did = %&auth.0.did, - allow_legacy_login = input.allow_legacy_login, - "Legacy login preference updated" - ); - Json(json!({ - "allowLegacyLogin": input.allow_legacy_login - })) - .into_response() - } - Ok(false) => ApiError::AccountNotFound.into_response(), - Err(e) => { + .map_err(|e| { error!("DB error: {:?}", e); - ApiError::InternalError(None).into_response() - } + ApiError::InternalError(None) + })?; + if !updated { + return Err(ApiError::AccountNotFound); } + info!( + did = %&auth.did, + allow_legacy_login = input.allow_legacy_login, + "Legacy login preference updated" + ); + Ok(Json(json!({ + "allowLegacyLogin": input.allow_legacy_login + })) + .into_response()) } use crate::comms::VALID_LOCALES; @@ -1059,37 +1057,34 @@ pub struct UpdateLocaleInput { pub async fn update_locale( State(state): State, - auth: BearerAuth, + auth: Auth, Json(input): Json, -) -> Response { +) -> Result { if !VALID_LOCALES.contains(&input.preferred_locale.as_str()) { - return ApiError::InvalidRequest(format!( + return Err(ApiError::InvalidRequest(format!( "Invalid locale. Valid options: {}", VALID_LOCALES.join(", ") - )) - .into_response(); + ))); } - match state + let updated = state .user_repo - .update_locale(&auth.0.did, &input.preferred_locale) + .update_locale(&auth.did, &input.preferred_locale) .await - { - Ok(true) => { - info!( - did = %&auth.0.did, - locale = %input.preferred_locale, - "User locale preference updated" - ); - Json(json!({ - "preferredLocale": input.preferred_locale - })) - .into_response() - } - Ok(false) => ApiError::AccountNotFound.into_response(), - Err(e) => { + .map_err(|e| { error!("DB error updating locale: {:?}", e); - ApiError::InternalError(None).into_response() - } + ApiError::InternalError(None) + })?; + if !updated { + return Err(ApiError::AccountNotFound); } + info!( + did = %&auth.did, + locale = %input.preferred_locale, + "User locale preference updated" + ); + Ok(Json(json!({ + "preferredLocale": input.preferred_locale + })) + .into_response()) } diff --git a/crates/tranquil-pds/src/api/server/totp.rs b/crates/tranquil-pds/src/api/server/totp.rs index 1dddc3a..0f5cd44 100644 --- a/crates/tranquil-pds/src/api/server/totp.rs +++ b/crates/tranquil-pds/src/api/server/totp.rs @@ -1,6 +1,6 @@ use crate::api::EmptyResponse; use crate::api::error::ApiError; -use crate::auth::BearerAuth; +use crate::auth::{Active, Auth}; use crate::auth::{ decrypt_totp_secret, encrypt_totp_secret, generate_backup_codes, generate_qr_png_base64, generate_totp_secret, generate_totp_uri, hash_backup_code, is_backup_code_format, @@ -26,66 +26,63 @@ pub struct CreateTotpSecretResponse { pub qr_base64: String, } -pub async fn create_totp_secret(State(state): State, auth: BearerAuth) -> Response { - match state.user_repo.get_totp_record(&auth.0.did).await { - Ok(Some(record)) if record.verified => return ApiError::TotpAlreadyEnabled.into_response(), +pub async fn create_totp_secret( + State(state): State, + auth: Auth, +) -> Result { + match state.user_repo.get_totp_record(&auth.did).await { + Ok(Some(record)) if record.verified => return Err(ApiError::TotpAlreadyEnabled), Ok(_) => {} Err(e) => { error!("DB error checking TOTP: {:?}", e); - return ApiError::InternalError(None).into_response(); + return Err(ApiError::InternalError(None)); } } let secret = generate_totp_secret(); - let handle = match state.user_repo.get_handle_by_did(&auth.0.did).await { - Ok(Some(h)) => h, - Ok(None) => return ApiError::AccountNotFound.into_response(), - Err(e) => { + let handle = state + .user_repo + .get_handle_by_did(&auth.did) + .await + .map_err(|e| { error!("DB error fetching handle: {:?}", e); - return ApiError::InternalError(None).into_response(); - } - }; + ApiError::InternalError(None) + })? + .ok_or(ApiError::AccountNotFound)?; let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); let uri = generate_totp_uri(&secret, &handle, &hostname); - let qr_code = match generate_qr_png_base64(&secret, &handle, &hostname) { - Ok(qr) => qr, - Err(e) => { - error!("Failed to generate QR code: {:?}", e); - return ApiError::InternalError(Some("Failed to generate QR code".into())) - .into_response(); - } - }; + let qr_code = generate_qr_png_base64(&secret, &handle, &hostname).map_err(|e| { + error!("Failed to generate QR code: {:?}", e); + ApiError::InternalError(Some("Failed to generate QR code".into())) + })?; - let encrypted_secret = match encrypt_totp_secret(&secret) { - Ok(enc) => enc, - Err(e) => { - error!("Failed to encrypt TOTP secret: {:?}", e); - return ApiError::InternalError(None).into_response(); - } - }; + let encrypted_secret = encrypt_totp_secret(&secret).map_err(|e| { + error!("Failed to encrypt TOTP secret: {:?}", e); + ApiError::InternalError(None) + })?; - if let Err(e) = state + state .user_repo - .upsert_totp_secret(&auth.0.did, &encrypted_secret, ENCRYPTION_VERSION) + .upsert_totp_secret(&auth.did, &encrypted_secret, ENCRYPTION_VERSION) .await - { - error!("Failed to store TOTP secret: {:?}", e); - return ApiError::InternalError(None).into_response(); - } + .map_err(|e| { + error!("Failed to store TOTP secret: {:?}", e); + ApiError::InternalError(None) + })?; let secret_base32 = base32::encode(base32::Alphabet::Rfc4648 { padding: false }, &secret); - info!(did = %&auth.0.did, "TOTP secret created (pending verification)"); + info!(did = %&auth.did, "TOTP secret created (pending verification)"); - Json(CreateTotpSecretResponse { + Ok(Json(CreateTotpSecretResponse { secret: secret_base32, uri, qr_base64: qr_code, }) - .into_response() + .into_response()) } #[derive(Deserialize)] @@ -101,69 +98,68 @@ pub struct EnableTotpResponse { pub async fn enable_totp( State(state): State, - auth: BearerAuth, + auth: Auth, Json(input): Json, -) -> Response { +) -> Result { if !state - .check_rate_limit(RateLimitKind::TotpVerify, &auth.0.did) + .check_rate_limit(RateLimitKind::TotpVerify, &auth.did) .await { - warn!(did = %&auth.0.did, "TOTP verification rate limit exceeded"); - return ApiError::RateLimitExceeded(None).into_response(); + warn!(did = %&auth.did, "TOTP verification rate limit exceeded"); + return Err(ApiError::RateLimitExceeded(None)); } - let totp_record = match state.user_repo.get_totp_record(&auth.0.did).await { + let totp_record = match state.user_repo.get_totp_record(&auth.did).await { Ok(Some(row)) => row, - Ok(None) => return ApiError::TotpNotEnabled.into_response(), + Ok(None) => return Err(ApiError::TotpNotEnabled), Err(e) => { error!("DB error fetching TOTP: {:?}", e); - return ApiError::InternalError(None).into_response(); + return Err(ApiError::InternalError(None)); } }; if totp_record.verified { - return ApiError::TotpAlreadyEnabled.into_response(); + return Err(ApiError::TotpAlreadyEnabled); } - let secret = match decrypt_totp_secret( + let secret = decrypt_totp_secret( &totp_record.secret_encrypted, totp_record.encryption_version, - ) { - Ok(s) => s, - Err(e) => { - error!("Failed to decrypt TOTP secret: {:?}", e); - return ApiError::InternalError(None).into_response(); - } - }; + ) + .map_err(|e| { + error!("Failed to decrypt TOTP secret: {:?}", e); + ApiError::InternalError(None) + })?; let code = input.code.trim(); if !verify_totp_code(&secret, code) { - return ApiError::InvalidCode(Some("Invalid verification code".into())).into_response(); + return Err(ApiError::InvalidCode(Some( + "Invalid verification code".into(), + ))); } let backup_codes = generate_backup_codes(); - let backup_hashes: Result, _> = - backup_codes.iter().map(|c| hash_backup_code(c)).collect(); - let backup_hashes = match backup_hashes { - Ok(hashes) => hashes, - Err(e) => { + let backup_hashes: Vec<_> = backup_codes + .iter() + .map(|c| hash_backup_code(c)) + .collect::, _>>() + .map_err(|e| { error!("Failed to hash backup code: {:?}", e); - return ApiError::InternalError(None).into_response(); - } - }; + ApiError::InternalError(None) + })?; - if let Err(e) = state + state .user_repo - .enable_totp_with_backup_codes(&auth.0.did, &backup_hashes) + .enable_totp_with_backup_codes(&auth.did, &backup_hashes) .await - { - error!("Failed to enable TOTP: {:?}", e); - return ApiError::InternalError(None).into_response(); - } + .map_err(|e| { + error!("Failed to enable TOTP: {:?}", e); + ApiError::InternalError(None) + })?; - info!(did = %&auth.0.did, "TOTP enabled with {} backup codes", backup_codes.len()); + info!(did = %&auth.did, "TOTP enabled with {} backup codes", backup_codes.len()); - Json(EnableTotpResponse { backup_codes }).into_response() + Ok(Json(EnableTotpResponse { backup_codes }).into_response()) } #[derive(Deserialize)] @@ -174,84 +170,84 @@ pub struct DisableTotpInput { pub async fn disable_totp( State(state): State, - auth: BearerAuth, + auth: Auth, Json(input): Json, -) -> Response { - if !crate::api::server::reauth::check_legacy_session_mfa(&*state.session_repo, &auth.0.did) - .await +) -> Result { + if !crate::api::server::reauth::check_legacy_session_mfa(&*state.session_repo, &auth.did).await { - return crate::api::server::reauth::legacy_mfa_required_response( + return Ok(crate::api::server::reauth::legacy_mfa_required_response( &*state.user_repo, &*state.session_repo, - &auth.0.did, + &auth.did, ) - .await; + .await); } if !state - .check_rate_limit(RateLimitKind::TotpVerify, &auth.0.did) + .check_rate_limit(RateLimitKind::TotpVerify, &auth.did) .await { - warn!(did = %&auth.0.did, "TOTP verification rate limit exceeded"); - return ApiError::RateLimitExceeded(None).into_response(); + warn!(did = %&auth.did, "TOTP verification rate limit exceeded"); + return Err(ApiError::RateLimitExceeded(None)); } - let password_hash = match state.user_repo.get_password_hash_by_did(&auth.0.did).await { - Ok(Some(hash)) => hash, - Ok(None) => return ApiError::AccountNotFound.into_response(), - Err(e) => { + let password_hash = state + .user_repo + .get_password_hash_by_did(&auth.did) + .await + .map_err(|e| { error!("DB error fetching user: {:?}", e); - return ApiError::InternalError(None).into_response(); - } - }; + ApiError::InternalError(None) + })? + .ok_or(ApiError::AccountNotFound)?; let password_valid = bcrypt::verify(&input.password, &password_hash).unwrap_or(false); if !password_valid { - return ApiError::InvalidPassword("Password is incorrect".into()).into_response(); + return Err(ApiError::InvalidPassword("Password is incorrect".into())); } - let totp_record = match state.user_repo.get_totp_record(&auth.0.did).await { + let totp_record = match state.user_repo.get_totp_record(&auth.did).await { Ok(Some(row)) if row.verified => row, - Ok(Some(_)) | Ok(None) => return ApiError::TotpNotEnabled.into_response(), + Ok(Some(_)) | Ok(None) => return Err(ApiError::TotpNotEnabled), Err(e) => { error!("DB error fetching TOTP: {:?}", e); - return ApiError::InternalError(None).into_response(); + return Err(ApiError::InternalError(None)); } }; let code = input.code.trim(); let code_valid = if is_backup_code_format(code) { - verify_backup_code_for_user(&state, &auth.0.did, code).await + verify_backup_code_for_user(&state, &auth.did, code).await } else { - let secret = match decrypt_totp_secret( + let secret = decrypt_totp_secret( &totp_record.secret_encrypted, totp_record.encryption_version, - ) { - Ok(s) => s, - Err(e) => { - error!("Failed to decrypt TOTP secret: {:?}", e); - return ApiError::InternalError(None).into_response(); - } - }; + ) + .map_err(|e| { + error!("Failed to decrypt TOTP secret: {:?}", e); + ApiError::InternalError(None) + })?; verify_totp_code(&secret, code) }; if !code_valid { - return ApiError::InvalidCode(Some("Invalid verification code".into())).into_response(); + return Err(ApiError::InvalidCode(Some( + "Invalid verification code".into(), + ))); } - if let Err(e) = state + state .user_repo - .delete_totp_and_backup_codes(&auth.0.did) + .delete_totp_and_backup_codes(&auth.did) .await - { - error!("Failed to delete TOTP: {:?}", e); - return ApiError::InternalError(None).into_response(); - } + .map_err(|e| { + error!("Failed to delete TOTP: {:?}", e); + ApiError::InternalError(None) + })?; - info!(did = %&auth.0.did, "TOTP disabled"); + info!(did = %&auth.did, "TOTP disabled"); - EmptyResponse::ok().into_response() + Ok(EmptyResponse::ok().into_response()) } #[derive(Serialize)] @@ -262,30 +258,34 @@ pub struct GetTotpStatusResponse { pub backup_codes_remaining: i64, } -pub async fn get_totp_status(State(state): State, auth: BearerAuth) -> Response { - let enabled = match state.user_repo.get_totp_record(&auth.0.did).await { +pub async fn get_totp_status( + State(state): State, + auth: Auth, +) -> Result { + let enabled = match state.user_repo.get_totp_record(&auth.did).await { Ok(Some(row)) => row.verified, Ok(None) => false, Err(e) => { error!("DB error fetching TOTP status: {:?}", e); - return ApiError::InternalError(None).into_response(); + return Err(ApiError::InternalError(None)); } }; - let backup_count = match state.user_repo.count_unused_backup_codes(&auth.0.did).await { - Ok(count) => count, - Err(e) => { + let backup_count = state + .user_repo + .count_unused_backup_codes(&auth.did) + .await + .map_err(|e| { error!("DB error counting backup codes: {:?}", e); - return ApiError::InternalError(None).into_response(); - } - }; + ApiError::InternalError(None) + })?; - Json(GetTotpStatusResponse { + Ok(Json(GetTotpStatusResponse { enabled, has_backup_codes: backup_count > 0, backup_codes_remaining: backup_count, }) - .into_response() + .into_response()) } #[derive(Deserialize)] @@ -302,79 +302,79 @@ pub struct RegenerateBackupCodesResponse { pub async fn regenerate_backup_codes( State(state): State, - auth: BearerAuth, + auth: Auth, Json(input): Json, -) -> Response { +) -> Result { if !state - .check_rate_limit(RateLimitKind::TotpVerify, &auth.0.did) + .check_rate_limit(RateLimitKind::TotpVerify, &auth.did) .await { - warn!(did = %&auth.0.did, "TOTP verification rate limit exceeded"); - return ApiError::RateLimitExceeded(None).into_response(); + warn!(did = %&auth.did, "TOTP verification rate limit exceeded"); + return Err(ApiError::RateLimitExceeded(None)); } - let password_hash = match state.user_repo.get_password_hash_by_did(&auth.0.did).await { - Ok(Some(hash)) => hash, - Ok(None) => return ApiError::AccountNotFound.into_response(), - Err(e) => { + let password_hash = state + .user_repo + .get_password_hash_by_did(&auth.did) + .await + .map_err(|e| { error!("DB error fetching user: {:?}", e); - return ApiError::InternalError(None).into_response(); - } - }; + ApiError::InternalError(None) + })? + .ok_or(ApiError::AccountNotFound)?; let password_valid = bcrypt::verify(&input.password, &password_hash).unwrap_or(false); if !password_valid { - return ApiError::InvalidPassword("Password is incorrect".into()).into_response(); + return Err(ApiError::InvalidPassword("Password is incorrect".into())); } - let totp_record = match state.user_repo.get_totp_record(&auth.0.did).await { + let totp_record = match state.user_repo.get_totp_record(&auth.did).await { Ok(Some(row)) if row.verified => row, - Ok(Some(_)) | Ok(None) => return ApiError::TotpNotEnabled.into_response(), + Ok(Some(_)) | Ok(None) => return Err(ApiError::TotpNotEnabled), Err(e) => { error!("DB error fetching TOTP: {:?}", e); - return ApiError::InternalError(None).into_response(); + return Err(ApiError::InternalError(None)); } }; - let secret = match decrypt_totp_secret( + let secret = decrypt_totp_secret( &totp_record.secret_encrypted, totp_record.encryption_version, - ) { - Ok(s) => s, - Err(e) => { - error!("Failed to decrypt TOTP secret: {:?}", e); - return ApiError::InternalError(None).into_response(); - } - }; + ) + .map_err(|e| { + error!("Failed to decrypt TOTP secret: {:?}", e); + ApiError::InternalError(None) + })?; let code = input.code.trim(); if !verify_totp_code(&secret, code) { - return ApiError::InvalidCode(Some("Invalid verification code".into())).into_response(); + return Err(ApiError::InvalidCode(Some( + "Invalid verification code".into(), + ))); } let backup_codes = generate_backup_codes(); - let backup_hashes: Result, _> = - backup_codes.iter().map(|c| hash_backup_code(c)).collect(); - let backup_hashes = match backup_hashes { - Ok(hashes) => hashes, - Err(e) => { + let backup_hashes: Vec<_> = backup_codes + .iter() + .map(|c| hash_backup_code(c)) + .collect::, _>>() + .map_err(|e| { error!("Failed to hash backup code: {:?}", e); - return ApiError::InternalError(None).into_response(); - } - }; + ApiError::InternalError(None) + })?; - if let Err(e) = state + state .user_repo - .replace_backup_codes(&auth.0.did, &backup_hashes) + .replace_backup_codes(&auth.did, &backup_hashes) .await - { - error!("Failed to regenerate backup codes: {:?}", e); - return ApiError::InternalError(None).into_response(); - } + .map_err(|e| { + error!("Failed to regenerate backup codes: {:?}", e); + ApiError::InternalError(None) + })?; - info!(did = %&auth.0.did, "Backup codes regenerated"); + info!(did = %&auth.did, "Backup codes regenerated"); - Json(RegenerateBackupCodesResponse { backup_codes }).into_response() + Ok(Json(RegenerateBackupCodesResponse { backup_codes }).into_response()) } async fn verify_backup_code_for_user( diff --git a/crates/tranquil-pds/src/api/server/trusted_devices.rs b/crates/tranquil-pds/src/api/server/trusted_devices.rs index 148624d..0ea9835 100644 --- a/crates/tranquil-pds/src/api/server/trusted_devices.rs +++ b/crates/tranquil-pds/src/api/server/trusted_devices.rs @@ -11,7 +11,7 @@ use tracing::{error, info}; use tranquil_db_traits::OAuthRepository; use tranquil_types::DeviceId; -use crate::auth::BearerAuth; +use crate::auth::{Active, Auth}; use crate::state::AppState; const TRUST_DURATION_DAYS: i64 = 30; @@ -71,32 +71,36 @@ pub struct ListTrustedDevicesResponse { pub devices: Vec, } -pub async fn list_trusted_devices(State(state): State, auth: BearerAuth) -> Response { - match state.oauth_repo.list_trusted_devices(&auth.0.did).await { - Ok(rows) => { - let devices = rows - .into_iter() - .map(|row| { - let trust_state = - DeviceTrustState::from_timestamps(row.trusted_at, row.trusted_until); - TrustedDevice { - id: row.id, - user_agent: row.user_agent, - friendly_name: row.friendly_name, - trusted_at: row.trusted_at, - trusted_until: row.trusted_until, - last_seen_at: row.last_seen_at, - trust_state, - } - }) - .collect(); - Json(ListTrustedDevicesResponse { devices }).into_response() - } - Err(e) => { +pub async fn list_trusted_devices( + State(state): State, + auth: Auth, +) -> Result { + let rows = state + .oauth_repo + .list_trusted_devices(&auth.did) + .await + .map_err(|e| { error!("DB error: {:?}", e); - ApiError::InternalError(None).into_response() - } - } + ApiError::InternalError(None) + })?; + + let devices = rows + .into_iter() + .map(|row| { + let trust_state = DeviceTrustState::from_timestamps(row.trusted_at, row.trusted_until); + TrustedDevice { + id: row.id, + user_agent: row.user_agent, + friendly_name: row.friendly_name, + trusted_at: row.trusted_at, + trusted_until: row.trusted_until, + last_seen_at: row.last_seen_at, + trust_state, + } + }) + .collect(); + + Ok(Json(ListTrustedDevicesResponse { devices }).into_response()) } #[derive(Deserialize)] @@ -107,35 +111,36 @@ pub struct RevokeTrustedDeviceInput { pub async fn revoke_trusted_device( State(state): State, - auth: BearerAuth, + auth: Auth, Json(input): Json, -) -> Response { +) -> Result { let device_id = DeviceId::from(input.device_id.clone()); match state .oauth_repo - .device_belongs_to_user(&device_id, &auth.0.did) + .device_belongs_to_user(&device_id, &auth.did) .await { Ok(true) => {} Ok(false) => { - return ApiError::DeviceNotFound.into_response(); + return Err(ApiError::DeviceNotFound); } Err(e) => { error!("DB error: {:?}", e); - return ApiError::InternalError(None).into_response(); + return Err(ApiError::InternalError(None)); } } - match state.oauth_repo.revoke_device_trust(&device_id).await { - Ok(()) => { - info!(did = %&auth.0.did, device_id = %input.device_id, "Trusted device revoked"); - SuccessResponse::ok().into_response() - } - Err(e) => { + state + .oauth_repo + .revoke_device_trust(&device_id) + .await + .map_err(|e| { error!("DB error: {:?}", e); - ApiError::InternalError(None).into_response() - } - } + ApiError::InternalError(None) + })?; + + info!(did = %&auth.did, device_id = %input.device_id, "Trusted device revoked"); + Ok(SuccessResponse::ok().into_response()) } #[derive(Deserialize)] @@ -147,39 +152,36 @@ pub struct UpdateTrustedDeviceInput { pub async fn update_trusted_device( State(state): State, - auth: BearerAuth, + auth: Auth, Json(input): Json, -) -> Response { +) -> Result { let device_id = DeviceId::from(input.device_id.clone()); match state .oauth_repo - .device_belongs_to_user(&device_id, &auth.0.did) + .device_belongs_to_user(&device_id, &auth.did) .await { Ok(true) => {} Ok(false) => { - return ApiError::DeviceNotFound.into_response(); + return Err(ApiError::DeviceNotFound); } Err(e) => { error!("DB error: {:?}", e); - return ApiError::InternalError(None).into_response(); + return Err(ApiError::InternalError(None)); } } - match state + state .oauth_repo .update_device_friendly_name(&device_id, input.friendly_name.as_deref()) .await - { - Ok(()) => { - info!(did = %auth.0.did, device_id = %input.device_id, "Trusted device updated"); - SuccessResponse::ok().into_response() - } - Err(e) => { + .map_err(|e| { error!("DB error: {:?}", e); - ApiError::InternalError(None).into_response() - } - } + ApiError::InternalError(None) + })?; + + info!(did = %auth.did, device_id = %input.device_id, "Trusted device updated"); + Ok(SuccessResponse::ok().into_response()) } pub async fn get_device_trust_state( diff --git a/crates/tranquil-pds/src/api/temp.rs b/crates/tranquil-pds/src/api/temp.rs index e10f09f..c46e493 100644 --- a/crates/tranquil-pds/src/api/temp.rs +++ b/crates/tranquil-pds/src/api/temp.rs @@ -1,5 +1,5 @@ use crate::api::error::ApiError; -use crate::auth::{BearerAuth, OptionalBearerAuth}; +use crate::auth::{Active, Auth, Permissive}; use crate::state::AppState; use axum::{ Json, @@ -21,9 +21,9 @@ pub struct CheckSignupQueueOutput { pub estimated_time_ms: Option, } -pub async fn check_signup_queue(auth: OptionalBearerAuth) -> Response { - if let Some(user) = auth.0 - && user.is_oauth +pub async fn check_signup_queue(auth: Option>) -> Response { + if let Some(ref user) = auth + && user.is_oauth() { return ApiError::Forbidden.into_response(); } @@ -49,11 +49,9 @@ pub struct DereferenceScopeOutput { pub async fn dereference_scope( State(state): State, - auth: BearerAuth, + _auth: Auth, Json(input): Json, -) -> Response { - let _ = auth; - +) -> Result { let scope_parts: Vec<&str> = input.scope.split_whitespace().collect(); let mut resolved_scopes: Vec = Vec::new(); @@ -118,8 +116,8 @@ pub async fn dereference_scope( } } - Json(DereferenceScopeOutput { + Ok(Json(DereferenceScopeOutput { scope: resolved_scopes.join(" "), }) - .into_response() + .into_response()) } diff --git a/crates/tranquil-pds/src/auth/auth_extractor.rs b/crates/tranquil-pds/src/auth/auth_extractor.rs index c7988ec..7f849bb 100644 --- a/crates/tranquil-pds/src/auth/auth_extractor.rs +++ b/crates/tranquil-pds/src/auth/auth_extractor.rs @@ -161,7 +161,7 @@ async fn test_oauth_token_works_with_bearer_auth() { .await .unwrap(); - assert_eq!(res.status(), StatusCode::OK, "OAuth token should work with BearerAuth extractor"); + assert_eq!(res.status(), StatusCode::OK, "OAuth token should work with RequiredAuth extractor"); let body: Value = res.json().await.unwrap(); assert_eq!(body["did"].as_str().unwrap(), did); } diff --git a/crates/tranquil-pds/src/auth/extractor.rs b/crates/tranquil-pds/src/auth/extractor.rs index 09745cd..9ea40ff 100644 --- a/crates/tranquil-pds/src/auth/extractor.rs +++ b/crates/tranquil-pds/src/auth/extractor.rs @@ -1,22 +1,22 @@ +use std::marker::PhantomData; + use axum::{ - extract::FromRequestParts, + extract::{FromRequestParts, OptionalFromRequestParts}, http::{StatusCode, header::AUTHORIZATION, request::Parts}, response::{IntoResponse, Response}, }; use tracing::{debug, error, info}; use super::{ - AccountStatus, AuthenticatedUser, ServiceTokenClaims, ServiceTokenVerifier, is_service_token, - validate_bearer_token, validate_bearer_token_allow_deactivated, - validate_bearer_token_allow_takendown, + AccountStatus, AuthSource, AuthenticatedUser, ServiceTokenClaims, ServiceTokenVerifier, + is_service_token, validate_bearer_token_for_service_auth, }; use crate::api::error::ApiError; +use crate::oauth::scopes::{RepoAction, ScopePermissions}; use crate::state::AppState; use crate::types::Did; use crate::util::build_full_url; -pub struct BearerAuth(pub AuthenticatedUser); - #[derive(Debug)] pub enum AuthError { MissingToken, @@ -26,6 +26,9 @@ pub enum AuthError { AccountDeactivated, AccountTakedown, AdminRequired, + ServiceAuthNotAllowed, + SigningKeyRequired, + InsufficientScope(String), OAuthExpiredToken(String), UseDpopNonce(String), InvalidDpopProof(String), @@ -56,30 +59,15 @@ impl IntoResponse for AuthError { })), ) .into_response(), + Self::InsufficientScope(msg) => ApiError::InsufficientScope(Some(msg)).into_response(), other => ApiError::from(other).into_response(), } } } -#[cfg(test)] -fn extract_bearer_token(auth_header: &str) -> Result<&str, AuthError> { - let auth_header = auth_header.trim(); - - if auth_header.len() < 8 { - return Err(AuthError::InvalidFormat); - } - - let prefix = &auth_header[..7]; - if !prefix.eq_ignore_ascii_case("bearer ") { - return Err(AuthError::InvalidFormat); - } - - let token = auth_header[7..].trim(); - if token.is_empty() { - return Err(AuthError::InvalidFormat); - } - - Ok(token) +pub struct ExtractedToken { + pub token: String, + pub is_dpop: bool, } pub fn extract_bearer_token_from_header(auth_header: Option<&str>) -> Option { @@ -102,11 +90,6 @@ pub fn extract_bearer_token_from_header(auth_header: Option<&str>) -> Option) -> Option { let header = auth_header?; let header = header.trim(); @@ -136,10 +119,92 @@ pub fn extract_auth_token_from_header(auth_header: Option<&str>) -> Option Result<(), AuthError>; +} + +pub struct Permissive; + +impl AuthPolicy for Permissive { + fn validate(_user: &AuthenticatedUser) -> Result<(), AuthError> { + Ok(()) + } +} + +pub struct Active; + +impl AuthPolicy for Active { + fn validate(user: &AuthenticatedUser) -> Result<(), AuthError> { + if user.status.is_deactivated() { + return Err(AuthError::AccountDeactivated); + } + if user.status.is_takendown() { + return Err(AuthError::AccountTakedown); + } + Ok(()) + } +} + +pub struct NotTakendown; + +impl AuthPolicy for NotTakendown { + fn validate(user: &AuthenticatedUser) -> Result<(), AuthError> { + if user.status.is_takendown() { + return Err(AuthError::AccountTakedown); + } + Ok(()) + } +} + +pub struct AnyUser; + +impl AuthPolicy for AnyUser { + fn validate(_user: &AuthenticatedUser) -> Result<(), AuthError> { + Ok(()) + } +} + +pub struct Admin; + +impl AuthPolicy for Admin { + fn validate(user: &AuthenticatedUser) -> Result<(), AuthError> { + if user.status.is_deactivated() { + return Err(AuthError::AccountDeactivated); + } + if user.status.is_takendown() { + return Err(AuthError::AccountTakedown); + } + if !user.is_admin { + return Err(AuthError::AdminRequired); + } + Ok(()) + } +} + +impl AuthenticatedUser { + pub fn require_active(&self) -> Result<&Self, ApiError> { + if self.status.is_deactivated() { + return Err(ApiError::AccountDeactivated); + } + if self.status.is_takendown() { + return Err(ApiError::AccountTakedown); + } + Ok(self) + } + + pub fn require_not_takendown(&self) -> Result<&Self, ApiError> { + if self.status.is_takendown() { + return Err(ApiError::AccountTakedown); + } + Ok(self) + } + + pub fn require_admin(&self) -> Result<&Self, ApiError> { + if !self.is_admin { + return Err(ApiError::AdminRequired); + } + Ok(self) + } } async fn verify_oauth_token_and_build_user( @@ -148,7 +213,6 @@ async fn verify_oauth_token_and_build_user( dpop_proof: Option<&str>, method: &str, uri: &str, - flags: StatusCheckFlags, ) -> Result { match crate::oauth::verify::verify_oauth_access_token( state.oauth_repo.as_ref(), @@ -171,22 +235,16 @@ async fn verify_oauth_token_and_build_user( user_info.takedown_ref.as_deref(), user_info.deactivated_at, ); - if !flags.allow_deactivated && status.is_deactivated() { - return Err(AuthError::AccountDeactivated); - } - if !flags.allow_takendown && status.is_takendown() { - return Err(AuthError::AccountTakedown); - } Ok(AuthenticatedUser { did: result.did, key_bytes: user_info.key_bytes.and_then(|kb| { crate::config::decrypt_key(&kb, user_info.encryption_version).ok() }), - is_oauth: true, is_admin: user_info.is_admin, status, scope: result.scope, controller_did: None, + auth_source: AuthSource::OAuth, }) } Err(crate::oauth::OAuthError::ExpiredToken(msg)) => Err(AuthError::OAuthExpiredToken(msg)), @@ -198,302 +256,158 @@ async fn verify_oauth_token_and_build_user( } } -impl FromRequestParts for BearerAuth { - type Rejection = AuthError; - - async fn from_request_parts( - parts: &mut Parts, - state: &AppState, - ) -> Result { - let auth_header = parts - .headers - .get(AUTHORIZATION) - .ok_or(AuthError::MissingToken)? - .to_str() - .map_err(|_| AuthError::InvalidFormat)?; - - let extracted = - extract_auth_token_from_header(Some(auth_header)).ok_or(AuthError::InvalidFormat)?; - - let dpop_proof = parts.headers.get("DPoP").and_then(|h| h.to_str().ok()); - let method = parts.method.as_str(); - let uri = build_full_url(&parts.uri.to_string()); - - match validate_bearer_token(state.user_repo.as_ref(), &extracted.token).await { - Ok(user) if !user.is_oauth => { - return if user.status.is_deactivated() { - Err(AuthError::AccountDeactivated) - } else if user.status.is_takendown() { - Err(AuthError::AccountTakedown) - } else { - Ok(BearerAuth(user)) - }; - } - Ok(_) => {} - Err(super::TokenValidationError::AccountDeactivated) => { - return Err(AuthError::AccountDeactivated); - } - Err(super::TokenValidationError::AccountTakedown) => { - return Err(AuthError::AccountTakedown); - } - Err(super::TokenValidationError::TokenExpired) => { - info!("JWT access token expired in BearerAuth, returning ExpiredToken"); - return Err(AuthError::TokenExpired); - } - Err(_) => {} - } - - verify_oauth_token_and_build_user( - state, - &extracted.token, - dpop_proof, - method, - &uri, - StatusCheckFlags::default(), - ) +async fn verify_service_token(token: &str) -> Result { + let verifier = ServiceTokenVerifier::new(); + let claims = verifier + .verify_service_token(token, None) .await - .map(BearerAuth) - } + .map_err(|e| { + error!("Service token verification failed: {:?}", e); + AuthError::AuthenticationFailed + })?; + + debug!("Service token verified for DID: {}", claims.iss); + Ok(claims) } -pub struct BearerAuthAllowDeactivated(pub AuthenticatedUser); - -impl FromRequestParts for BearerAuthAllowDeactivated { - type Rejection = AuthError; - - async fn from_request_parts( - parts: &mut Parts, - state: &AppState, - ) -> Result { - let auth_header = parts - .headers - .get(AUTHORIZATION) - .ok_or(AuthError::MissingToken)? - .to_str() - .map_err(|_| AuthError::InvalidFormat)?; - - let extracted = - extract_auth_token_from_header(Some(auth_header)).ok_or(AuthError::InvalidFormat)?; - - let dpop_proof = parts.headers.get("DPoP").and_then(|h| h.to_str().ok()); - let method = parts.method.as_str(); - let uri = build_full_url(&parts.uri.to_string()); - - match validate_bearer_token_allow_deactivated(state.user_repo.as_ref(), &extracted.token) - .await - { - Ok(user) if !user.is_oauth => { - return if user.status.is_takendown() { - Err(AuthError::AccountTakedown) - } else { - Ok(BearerAuthAllowDeactivated(user)) - }; - } - Ok(_) => {} - Err(super::TokenValidationError::AccountTakedown) => { - return Err(AuthError::AccountTakedown); - } - Err(super::TokenValidationError::TokenExpired) => { - return Err(AuthError::TokenExpired); - } - Err(_) => {} - } - - verify_oauth_token_and_build_user( - state, - &extracted.token, - dpop_proof, - method, - &uri, - StatusCheckFlags { - allow_deactivated: true, - allow_takendown: false, - }, - ) - .await - .map(BearerAuthAllowDeactivated) - } +enum ExtractedAuth { + User(AuthenticatedUser), + Service(ServiceTokenClaims), } -pub struct BearerAuthAllowTakendown(pub AuthenticatedUser); +async fn extract_auth_internal( + parts: &mut Parts, + state: &AppState, +) -> Result { + let auth_header = parts + .headers + .get(AUTHORIZATION) + .ok_or(AuthError::MissingToken)? + .to_str() + .map_err(|_| AuthError::InvalidFormat)?; -impl FromRequestParts for BearerAuthAllowTakendown { - type Rejection = AuthError; + let extracted = + extract_auth_token_from_header(Some(auth_header)).ok_or(AuthError::InvalidFormat)?; - async fn from_request_parts( - parts: &mut Parts, - state: &AppState, - ) -> Result { - let auth_header = parts - .headers - .get(AUTHORIZATION) - .ok_or(AuthError::MissingToken)? - .to_str() - .map_err(|_| AuthError::InvalidFormat)?; - - let extracted = - extract_auth_token_from_header(Some(auth_header)).ok_or(AuthError::InvalidFormat)?; - - let dpop_proof = parts.headers.get("DPoP").and_then(|h| h.to_str().ok()); - let method = parts.method.as_str(); - let uri = build_full_url(&parts.uri.to_string()); - - match validate_bearer_token_allow_takendown(state.user_repo.as_ref(), &extracted.token) - .await - { - Ok(user) if !user.is_oauth => { - return if user.status.is_deactivated() { - Err(AuthError::AccountDeactivated) - } else { - Ok(BearerAuthAllowTakendown(user)) - }; - } - Ok(_) => {} - Err(super::TokenValidationError::AccountDeactivated) => { - return Err(AuthError::AccountDeactivated); - } - Err(super::TokenValidationError::TokenExpired) => { - return Err(AuthError::TokenExpired); - } - Err(_) => {} - } - - verify_oauth_token_and_build_user( - state, - &extracted.token, - dpop_proof, - method, - &uri, - StatusCheckFlags { - allow_deactivated: false, - allow_takendown: true, - }, - ) - .await - .map(BearerAuthAllowTakendown) + if is_service_token(&extracted.token) { + let claims = verify_service_token(&extracted.token).await?; + return Ok(ExtractedAuth::Service(claims)); } -} -pub struct BearerAuthAdmin(pub AuthenticatedUser); + let dpop_proof = parts.headers.get("DPoP").and_then(|h| h.to_str().ok()); + let method = parts.method.as_str(); + let uri = build_full_url(&parts.uri.to_string()); -impl FromRequestParts for BearerAuthAdmin { - type Rejection = AuthError; - - async fn from_request_parts( - parts: &mut Parts, - state: &AppState, - ) -> Result { - let auth_header = parts - .headers - .get(AUTHORIZATION) - .ok_or(AuthError::MissingToken)? - .to_str() - .map_err(|_| AuthError::InvalidFormat)?; - - let extracted = - extract_auth_token_from_header(Some(auth_header)).ok_or(AuthError::InvalidFormat)?; - - let dpop_proof = parts.headers.get("DPoP").and_then(|h| h.to_str().ok()); - let method = parts.method.as_str(); - let uri = build_full_url(&parts.uri.to_string()); - - match validate_bearer_token(state.user_repo.as_ref(), &extracted.token).await { - Ok(user) if !user.is_oauth => { - if user.status.is_deactivated() { - return Err(AuthError::AccountDeactivated); - } - if user.status.is_takendown() { - return Err(AuthError::AccountTakedown); - } - if !user.is_admin { - return Err(AuthError::AdminRequired); - } - return Ok(BearerAuthAdmin(user)); - } - Ok(_) => {} - Err(super::TokenValidationError::AccountDeactivated) => { - return Err(AuthError::AccountDeactivated); - } - Err(super::TokenValidationError::AccountTakedown) => { - return Err(AuthError::AccountTakedown); - } - Err(super::TokenValidationError::TokenExpired) => { - return Err(AuthError::TokenExpired); - } - Err(_) => {} + match validate_bearer_token_for_service_auth(state.user_repo.as_ref(), &extracted.token).await { + Ok(user) if !user.auth_source.is_oauth() => { + return Ok(ExtractedAuth::User(user)); } + Ok(_) => {} + Err(super::TokenValidationError::TokenExpired) => { + info!("JWT access token expired, returning ExpiredToken"); + return Err(AuthError::TokenExpired); + } + Err(_) => {} + } - let user = verify_oauth_token_and_build_user( - state, - &extracted.token, - dpop_proof, - method, - &uri, - StatusCheckFlags::default(), - ) + let user = verify_oauth_token_and_build_user(state, &extracted.token, dpop_proof, method, &uri) .await?; + Ok(ExtractedAuth::User(user)) +} - if !user.is_admin { - return Err(AuthError::AdminRequired); - } - Ok(BearerAuthAdmin(user)) +async fn extract_user_auth_internal( + parts: &mut Parts, + state: &AppState, +) -> Result { + match extract_auth_internal(parts, state).await? { + ExtractedAuth::User(user) => Ok(user), + ExtractedAuth::Service(_) => Err(AuthError::ServiceAuthNotAllowed), } } -pub struct OptionalBearerAuth(pub Option); +pub struct Auth(pub AuthenticatedUser, PhantomData

); -impl FromRequestParts for OptionalBearerAuth { +impl Auth

{ + pub fn into_inner(self) -> AuthenticatedUser { + self.0 + } + + pub fn needs_scope_check(&self) -> bool { + self.0.is_oauth() + } + + pub fn permissions(&self) -> ScopePermissions { + self.0.permissions() + } + + #[allow(clippy::result_large_err)] + pub fn check_repo_scope(&self, action: RepoAction, collection: &str) -> Result<(), Response> { + if !self.needs_scope_check() { + return Ok(()); + } + self.permissions() + .assert_repo(action, collection) + .map_err(|e| ApiError::InsufficientScope(Some(e.to_string())).into_response()) + } +} + +impl std::ops::Deref for Auth

{ + type Target = AuthenticatedUser; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl FromRequestParts for Auth

{ type Rejection = AuthError; async fn from_request_parts( parts: &mut Parts, state: &AppState, ) -> Result { - let auth_header = match parts.headers.get(AUTHORIZATION) { - Some(h) => match h.to_str() { - Ok(s) => s, - Err(_) => return Ok(OptionalBearerAuth(None)), - }, - None => return Ok(OptionalBearerAuth(None)), - }; + let user = extract_user_auth_internal(parts, state).await?; + P::validate(&user)?; + Ok(Auth(user, PhantomData)) + } +} - let extracted = match extract_auth_token_from_header(Some(auth_header)) { - Some(e) => e, - None => return Ok(OptionalBearerAuth(None)), - }; +impl OptionalFromRequestParts for Auth

{ + type Rejection = AuthError; - let dpop_proof = parts.headers.get("DPoP").and_then(|h| h.to_str().ok()); - let method = parts.method.as_str(); - let uri = build_full_url(&parts.uri.to_string()); - - if let Ok(user) = validate_bearer_token(state.user_repo.as_ref(), &extracted.token).await - && !user.is_oauth - { - return if user.status.is_deactivated() || user.status.is_takendown() { - Ok(OptionalBearerAuth(None)) - } else { - Ok(OptionalBearerAuth(Some(user))) - }; + async fn from_request_parts( + parts: &mut Parts, + state: &AppState, + ) -> Result, Self::Rejection> { + match extract_user_auth_internal(parts, state).await { + Ok(user) => { + P::validate(&user)?; + Ok(Some(Auth(user, PhantomData))) + } + Err(AuthError::MissingToken) => Ok(None), + Err(e) => Err(e), } - - Ok(OptionalBearerAuth( - verify_oauth_token_and_build_user( - state, - &extracted.token, - dpop_proof, - method, - &uri, - StatusCheckFlags::default(), - ) - .await - .ok(), - )) } } pub struct ServiceAuth { - pub claims: ServiceTokenClaims, pub did: Did, + pub claims: ServiceTokenClaims, +} + +impl ServiceAuth { + pub fn require_lxm(&self, expected_lxm: &str) -> Result<(), ApiError> { + match &self.claims.lxm { + Some(lxm) if lxm == "*" || lxm == expected_lxm => Ok(()), + Some(lxm) => Err(ApiError::AuthorizationError(format!( + "Token lxm '{}' does not permit '{}'", + lxm, expected_lxm + ))), + None => Err(ApiError::AuthorizationError( + "Token missing lxm claim".to_string(), + )), + } + } } impl FromRequestParts for ServiceAuth { @@ -501,157 +415,209 @@ impl FromRequestParts for ServiceAuth { async fn from_request_parts( parts: &mut Parts, - _state: &AppState, + state: &AppState, ) -> Result { - let auth_header = parts - .headers - .get(AUTHORIZATION) - .ok_or(AuthError::MissingToken)? - .to_str() - .map_err(|_| AuthError::InvalidFormat)?; - - let extracted = - extract_auth_token_from_header(Some(auth_header)).ok_or(AuthError::InvalidFormat)?; - - if !is_service_token(&extracted.token) { - return Err(AuthError::InvalidFormat); - } - - let verifier = ServiceTokenVerifier::new(); - let claims = verifier - .verify_service_token(&extracted.token, None) - .await - .map_err(|e| { - error!("Service token verification failed: {:?}", e); - AuthError::AuthenticationFailed - })?; - - let did: Did = claims - .iss - .parse() - .map_err(|_| AuthError::AuthenticationFailed)?; - - debug!("Service token verified for DID: {}", did); - - Ok(ServiceAuth { claims, did }) - } -} - -pub struct OptionalServiceAuth(pub Option); - -impl FromRequestParts for OptionalServiceAuth { - type Rejection = std::convert::Infallible; - - async fn from_request_parts( - parts: &mut Parts, - _state: &AppState, - ) -> Result { - let auth_header = match parts.headers.get(AUTHORIZATION) { - Some(h) => match h.to_str() { - Ok(s) => s, - Err(_) => return Ok(OptionalServiceAuth(None)), - }, - None => return Ok(OptionalServiceAuth(None)), - }; - - let extracted = match extract_auth_token_from_header(Some(auth_header)) { - Some(e) => e, - None => return Ok(OptionalServiceAuth(None)), - }; - - if !is_service_token(&extracted.token) { - return Ok(OptionalServiceAuth(None)); - } - - let verifier = ServiceTokenVerifier::new(); - match verifier.verify_service_token(&extracted.token, None).await { - Ok(claims) => { - debug!("Service token verified for DID: {}", claims.iss); - Ok(OptionalServiceAuth(Some(claims))) - } - Err(e) => { - debug!("Service token verification failed (optional): {:?}", e); - Ok(OptionalServiceAuth(None)) + match extract_auth_internal(parts, state).await? { + ExtractedAuth::Service(claims) => { + let did: Did = claims + .iss + .parse() + .map_err(|_| AuthError::AuthenticationFailed)?; + Ok(ServiceAuth { did, claims }) } + ExtractedAuth::User(_) => Err(AuthError::AuthenticationFailed), } } } -pub enum BlobAuthResult { - Service { did: Did }, - User(AuthenticatedUser), +pub enum AuthAny { + User(Auth

), + Service(ServiceAuth), } -pub struct BlobAuth(pub BlobAuthResult); +impl AuthAny

{ + pub fn did(&self) -> &Did { + match self { + Self::User(auth) => &auth.did, + Self::Service(auth) => &auth.did, + } + } -impl FromRequestParts for BlobAuth { + pub fn as_user(&self) -> Option<&Auth

> { + match self { + Self::User(auth) => Some(auth), + Self::Service(_) => None, + } + } + + pub fn as_service(&self) -> Option<&ServiceAuth> { + match self { + Self::User(_) => None, + Self::Service(auth) => Some(auth), + } + } + + pub fn is_service(&self) -> bool { + matches!(self, Self::Service(_)) + } + + pub fn require_lxm(&self, expected_lxm: &str) -> Result<(), ApiError> { + match self { + Self::User(_) => Ok(()), + Self::Service(auth) => auth.require_lxm(expected_lxm), + } + } +} + +impl FromRequestParts for AuthAny

{ type Rejection = AuthError; async fn from_request_parts( parts: &mut Parts, state: &AppState, ) -> Result { - let auth_header = parts - .headers - .get(AUTHORIZATION) - .ok_or(AuthError::MissingToken)? - .to_str() - .map_err(|_| AuthError::InvalidFormat)?; - - let extracted = - extract_auth_token_from_header(Some(auth_header)).ok_or(AuthError::InvalidFormat)?; - - if is_service_token(&extracted.token) { - debug!("Verifying service token for blob upload"); - let verifier = ServiceTokenVerifier::new(); - let claims = verifier - .verify_service_token(&extracted.token, Some("com.atproto.repo.uploadBlob")) - .await - .map_err(|e| { - error!("Service token verification failed: {:?}", e); - AuthError::AuthenticationFailed - })?; - - let did: Did = claims - .iss - .parse() - .map_err(|_| AuthError::AuthenticationFailed)?; - - debug!("Service token verified for DID: {}", did); - return Ok(BlobAuth(BlobAuthResult::Service { did })); + match extract_auth_internal(parts, state).await? { + ExtractedAuth::User(user) => { + P::validate(&user)?; + Ok(AuthAny::User(Auth(user, PhantomData))) + } + ExtractedAuth::Service(claims) => { + let did: Did = claims + .iss + .parse() + .map_err(|_| AuthError::AuthenticationFailed)?; + Ok(AuthAny::Service(ServiceAuth { did, claims })) + } } - - let dpop_proof = parts.headers.get("DPoP").and_then(|h| h.to_str().ok()); - let uri = build_full_url("/xrpc/com.atproto.repo.uploadBlob"); - - if let Ok(user) = - validate_bearer_token_allow_deactivated(state.user_repo.as_ref(), &extracted.token) - .await - && !user.is_oauth - { - return if user.status.is_takendown() { - Err(AuthError::AccountTakedown) - } else { - Ok(BlobAuth(BlobAuthResult::User(user))) - }; - } - - verify_oauth_token_and_build_user( - state, - &extracted.token, - dpop_proof, - "POST", - &uri, - StatusCheckFlags { - allow_deactivated: true, - allow_takendown: false, - }, - ) - .await - .map(|user| BlobAuth(BlobAuthResult::User(user))) } } +impl OptionalFromRequestParts for AuthAny

{ + type Rejection = AuthError; + + async fn from_request_parts( + parts: &mut Parts, + state: &AppState, + ) -> Result, Self::Rejection> { + match extract_auth_internal(parts, state).await { + Ok(ExtractedAuth::User(user)) => { + P::validate(&user)?; + Ok(Some(AuthAny::User(Auth(user, PhantomData)))) + } + Ok(ExtractedAuth::Service(claims)) => { + let did: Did = claims + .iss + .parse() + .map_err(|_| AuthError::AuthenticationFailed)?; + Ok(Some(AuthAny::Service(ServiceAuth { did, claims }))) + } + Err(AuthError::MissingToken) => Ok(None), + Err(e) => Err(e), + } + } +} + +pub struct SigningAuth { + pub did: Did, + pub key_bytes: Vec, + pub is_admin: bool, + pub status: AccountStatus, + pub scope: Option, + pub controller_did: Option, + is_oauth: bool, + _policy: PhantomData

, +} + +impl SigningAuth

{ + pub fn needs_scope_check(&self) -> bool { + self.is_oauth + } + + pub fn permissions(&self) -> ScopePermissions { + if let Some(ref scope) = self.scope + && scope != super::SCOPE_ACCESS + { + return ScopePermissions::from_scope_string(Some(scope)); + } + if !self.is_oauth { + return ScopePermissions::from_scope_string(Some("atproto")); + } + ScopePermissions::from_scope_string(self.scope.as_deref()) + } + + #[allow(clippy::result_large_err)] + pub fn check_repo_scope(&self, action: RepoAction, collection: &str) -> Result<(), Response> { + if !self.needs_scope_check() { + return Ok(()); + } + self.permissions() + .assert_repo(action, collection) + .map_err(|e| ApiError::InsufficientScope(Some(e.to_string())).into_response()) + } +} + +impl FromRequestParts for SigningAuth

{ + type Rejection = AuthError; + + async fn from_request_parts( + parts: &mut Parts, + state: &AppState, + ) -> Result { + let user = extract_user_auth_internal(parts, state).await?; + P::validate(&user)?; + + let key_bytes = match user.key_bytes { + Some(kb) => kb, + None => { + let user_with_key = state + .user_repo + .get_with_key_by_did(&user.did) + .await + .ok() + .flatten() + .ok_or(AuthError::SigningKeyRequired)?; + crate::config::decrypt_key( + &user_with_key.key_bytes, + user_with_key.encryption_version, + ) + .map_err(|_| AuthError::SigningKeyRequired)? + } + }; + + Ok(SigningAuth { + did: user.did, + key_bytes, + is_admin: user.is_admin, + status: user.status, + scope: user.scope, + controller_did: user.controller_did, + is_oauth: user.auth_source.is_oauth(), + _policy: PhantomData, + }) + } +} + +#[cfg(test)] +fn extract_bearer_token(auth_header: &str) -> Result<&str, AuthError> { + let auth_header = auth_header.trim(); + + if auth_header.len() < 8 { + return Err(AuthError::InvalidFormat); + } + + let prefix = &auth_header[..7]; + if !prefix.eq_ignore_ascii_case("bearer ") { + return Err(AuthError::InvalidFormat); + } + + let token = auth_header[7..].trim(); + if token.is_empty() { + return Err(AuthError::InvalidFormat); + } + + Ok(token) +} + #[cfg(test)] mod tests { use super::*; diff --git a/crates/tranquil-pds/src/auth/mod.rs b/crates/tranquil-pds/src/auth/mod.rs index 6f6348c..1f07eb3 100644 --- a/crates/tranquil-pds/src/auth/mod.rs +++ b/crates/tranquil-pds/src/auth/mod.rs @@ -3,6 +3,7 @@ use std::fmt; use std::time::Duration; use crate::AccountStatus; +use crate::api::ApiError; use crate::cache::Cache; use crate::oauth::scopes::ScopePermissions; use crate::types::Did; @@ -16,9 +17,9 @@ pub mod verification_token; pub mod webauthn; pub use extractor::{ - AuthError, BearerAuth, BearerAuthAdmin, BearerAuthAllowDeactivated, BlobAuth, BlobAuthResult, - ExtractedToken, OptionalBearerAuth, OptionalServiceAuth, ServiceAuth, - extract_auth_token_from_header, extract_bearer_token_from_header, + Active, Admin, AnyUser, Auth, AuthAny, AuthError, AuthPolicy, ExtractedToken, NotTakendown, + Permissive, ServiceAuth, SigningAuth, extract_auth_token_from_header, + extract_bearer_token_from_header, }; pub use service::{ServiceTokenClaims, ServiceTokenVerifier, is_service_token}; @@ -94,14 +95,80 @@ impl fmt::Display for TokenValidationError { } } +pub enum AuthSource { + Session, + OAuth, + Service { claims: ServiceTokenClaims }, +} + +impl AuthSource { + pub fn is_oauth(&self) -> bool { + matches!(self, Self::OAuth) + } + + pub fn is_service(&self) -> bool { + matches!(self, Self::Service { .. }) + } + + pub fn service_claims(&self) -> Option<&ServiceTokenClaims> { + match self { + Self::Service { claims } => Some(claims), + _ => None, + } + } +} + pub struct AuthenticatedUser { pub did: Did, pub key_bytes: Option>, - pub is_oauth: bool, pub is_admin: bool, pub status: AccountStatus, pub scope: Option, pub controller_did: Option, + pub auth_source: AuthSource, +} + +impl AuthenticatedUser { + pub fn is_oauth(&self) -> bool { + self.auth_source.is_oauth() + } + + pub fn is_service(&self) -> bool { + self.auth_source.is_service() + } + + pub fn service_claims(&self) -> Option<&ServiceTokenClaims> { + self.auth_source.service_claims() + } + + pub fn require_lxm(&self, expected_lxm: &str) -> Result<(), ApiError> { + match self.auth_source.service_claims() { + Some(claims) => match &claims.lxm { + Some(lxm) if lxm == "*" || lxm == expected_lxm => Ok(()), + Some(lxm) => Err(ApiError::AuthorizationError(format!( + "Token lxm '{}' does not permit '{}'", + lxm, expected_lxm + ))), + None => Err(ApiError::AuthorizationError( + "Token missing lxm claim".to_string(), + )), + }, + None => Ok(()), + } + } + + pub fn require_user(&self) -> Result<&Self, ApiError> { + if self.is_service() { + return Err(ApiError::AuthenticationFailed(Some( + "User authentication required".to_string(), + ))); + } + Ok(self) + } + + pub fn as_user(&self) -> Option<&Self> { + if self.is_service() { None } else { Some(self) } + } } impl AuthenticatedUser { @@ -111,7 +178,7 @@ impl AuthenticatedUser { { return ScopePermissions::from_scope_string(Some(scope)); } - if !self.is_oauth { + if !self.is_oauth() { return ScopePermissions::from_scope_string(Some("atproto")); } ScopePermissions::from_scope_string(self.scope.as_deref()) @@ -349,11 +416,11 @@ async fn validate_bearer_token_with_options_internal( return Ok(AuthenticatedUser { did: did.clone(), key_bytes: Some(decrypted_key), - is_oauth: false, is_admin, status, scope: token_data.claims.scope.clone(), controller_did, + auth_source: AuthSource::Session, }); } } @@ -397,11 +464,11 @@ async fn validate_bearer_token_with_options_internal( return Ok(AuthenticatedUser { did: Did::new_unchecked(oauth_token.did), key_bytes, - is_oauth: true, is_admin: oauth_token.is_admin, status, scope: oauth_info.scope, controller_did: oauth_info.controller_did.map(Did::new_unchecked), + auth_source: AuthSource::OAuth, }); } else { return Err(TokenValidationError::TokenExpired); @@ -481,11 +548,11 @@ pub async fn validate_token_with_dpop( Ok(AuthenticatedUser { did: Did::new_unchecked(result.did), key_bytes, - is_oauth: true, is_admin: user_info.is_admin, status, scope: result.scope, controller_did: None, + auth_source: AuthSource::OAuth, }) } Err(crate::oauth::OAuthError::ExpiredToken(_)) => { diff --git a/crates/tranquil-pds/src/oauth/endpoints/authorize.rs b/crates/tranquil-pds/src/oauth/endpoints/authorize.rs index c6bfe3b..3b9463a 100644 --- a/crates/tranquil-pds/src/oauth/endpoints/authorize.rs +++ b/crates/tranquil-pds/src/oauth/endpoints/authorize.rs @@ -1,8 +1,7 @@ use crate::comms::{channel_display_name, comms_repo::enqueue_2fa_code}; use crate::oauth::{ AuthFlowState, ClientMetadataCache, Code, DeviceData, DeviceId, OAuthError, SessionId, - db::should_show_consent, - scopes::expand_include_scopes, + db::should_show_consent, scopes::expand_include_scopes, }; use crate::state::{AppState, RateLimitKind}; use crate::types::{Did, Handle, PlainPassword}; @@ -3645,9 +3644,9 @@ pub async fn register_complete( pub async fn establish_session( State(state): State, headers: HeaderMap, - auth: crate::auth::BearerAuth, + auth: crate::auth::Auth, ) -> Response { - let did = &auth.0.did; + let did = &auth.did; let existing_device = extract_device_cookie(&headers); @@ -3670,7 +3669,11 @@ pub async fn establish_session( }; let device_typed = DeviceIdType::from(new_id.0.clone()); - if let Err(e) = state.oauth_repo.create_device(&device_typed, &device_data).await { + if let Err(e) = state + .oauth_repo + .create_device(&device_typed, &device_data) + .await + { tracing::error!(error = ?e, "Failed to create device"); return ( StatusCode::INTERNAL_SERVER_ERROR, @@ -3682,7 +3685,11 @@ pub async fn establish_session( .into_response(); } - if let Err(e) = state.oauth_repo.upsert_account_device(did, &device_typed).await { + if let Err(e) = state + .oauth_repo + .upsert_account_device(did, &device_typed) + .await + { tracing::error!(error = ?e, "Failed to link device to account"); return ( StatusCode::INTERNAL_SERVER_ERROR, diff --git a/crates/tranquil-pds/src/oauth/endpoints/delegation.rs b/crates/tranquil-pds/src/oauth/endpoints/delegation.rs index 9df8afd..7d36a52 100644 --- a/crates/tranquil-pds/src/oauth/endpoints/delegation.rs +++ b/crates/tranquil-pds/src/oauth/endpoints/delegation.rs @@ -1,4 +1,4 @@ -use crate::auth::BearerAuth; +use crate::auth::{Active, Auth}; use crate::delegation::DelegationActionType; use crate::state::{AppState, RateLimitKind}; use crate::types::PlainPassword; @@ -463,10 +463,10 @@ pub struct DelegationTokenAuthSubmit { pub async fn delegation_auth_token( State(state): State, headers: HeaderMap, - auth: BearerAuth, + auth: Auth, Json(form): Json, ) -> Response { - let controller_did = auth.0.did; + let controller_did = &auth.did; let delegated_did: Did = match form.delegated_did.parse() { Ok(d) => d, @@ -510,7 +510,7 @@ pub async fn delegation_auth_token( let grant = match state .delegation_repo - .get_delegation(&delegated_did, &controller_did) + .get_delegation(&delegated_did, controller_did) .await { Ok(Some(g)) => g, @@ -551,7 +551,7 @@ pub async fn delegation_auth_token( if state .oauth_repo - .set_controller_did(&request_id, &controller_did) + .set_controller_did(&request_id, controller_did) .await .is_err() { @@ -574,8 +574,8 @@ pub async fn delegation_auth_token( .delegation_repo .log_delegation_action( &delegated_did, - &controller_did, - Some(&controller_did), + controller_did, + Some(controller_did), DelegationActionType::TokenIssued, Some(serde_json::json!({ "client_id": request.client_id, diff --git a/crates/tranquil-pds/src/oauth/verify.rs b/crates/tranquil-pds/src/oauth/verify.rs index 7a660d5..39ae2f7 100644 --- a/crates/tranquil-pds/src/oauth/verify.rs +++ b/crates/tranquil-pds/src/oauth/verify.rs @@ -396,7 +396,7 @@ async fn try_legacy_auth( token: &str, ) -> Result { match crate::auth::validate_bearer_token(user_repo, token).await { - Ok(user) if !user.is_oauth => Ok(LegacyAuthResult { did: user.did }), + Ok(user) if !user.is_oauth() => Ok(LegacyAuthResult { did: user.did }), _ => Err(()), } } diff --git a/crates/tranquil-pds/src/sso/endpoints.rs b/crates/tranquil-pds/src/sso/endpoints.rs index af64a2c..38ec1ea 100644 --- a/crates/tranquil-pds/src/sso/endpoints.rs +++ b/crates/tranquil-pds/src/sso/endpoints.rs @@ -644,7 +644,7 @@ pub struct LinkedAccountsResponse { pub async fn get_linked_accounts( State(state): State, - crate::auth::extractor::BearerAuth(auth): crate::auth::extractor::BearerAuth, + auth: crate::auth::Auth, ) -> Result, ApiError> { let identities = state .sso_repo @@ -679,7 +679,7 @@ pub struct UnlinkAccountResponse { pub async fn unlink_account( State(state): State, - crate::auth::extractor::BearerAuth(auth): crate::auth::extractor::BearerAuth, + auth: crate::auth::Auth, Json(input): Json, ) -> Result, ApiError> { if !state diff --git a/crates/tranquil-scopes/src/permission_set.rs b/crates/tranquil-scopes/src/permission_set.rs index 29cf6d2..6e254f4 100644 --- a/crates/tranquil-scopes/src/permission_set.rs +++ b/crates/tranquil-scopes/src/permission_set.rs @@ -149,7 +149,12 @@ async fn fetch_lexicon_via_atproto(nsid: &str) -> Result { return Err(format!("Invalid NSID format: {}", nsid)); } - let authority = parts[..2].iter().rev().cloned().collect::>().join("."); + let authority = parts[..2] + .iter() + .rev() + .cloned() + .collect::>() + .join("."); debug!(nsid, authority = %authority, "Resolving lexicon DID authority via DNS"); let did = resolve_lexicon_did_authority(&authority).await?; @@ -279,40 +284,42 @@ fn build_expanded_scopes( ) -> String { let mut scopes: Vec = Vec::new(); - permissions.iter().for_each(|perm| match perm.resource.as_str() { - "repo" => { - if let Some(collections) = &perm.collection { - let actions: Vec<&str> = perm - .action - .as_ref() - .map(|a| a.iter().map(String::as_str).collect()) - .unwrap_or_else(|| DEFAULT_ACTIONS.to_vec()); + permissions + .iter() + .for_each(|perm| match perm.resource.as_str() { + "repo" => { + if let Some(collections) = &perm.collection { + let actions: Vec<&str> = perm + .action + .as_ref() + .map(|a| a.iter().map(String::as_str).collect()) + .unwrap_or_else(|| DEFAULT_ACTIONS.to_vec()); - collections - .iter() - .filter(|coll| is_under_authority(coll, namespace_authority)) - .for_each(|coll| { - actions.iter().for_each(|action| { - scopes.push(format!("repo:{}?action={}", coll, action)); + collections + .iter() + .filter(|coll| is_under_authority(coll, namespace_authority)) + .for_each(|coll| { + actions.iter().for_each(|action| { + scopes.push(format!("repo:{}?action={}", coll, action)); + }); }); - }); + } } - } - "rpc" => { - if let Some(lxms) = &perm.lxm { - let perm_aud = perm.aud.as_deref().or(default_aud); + "rpc" => { + if let Some(lxms) = &perm.lxm { + let perm_aud = perm.aud.as_deref().or(default_aud); - lxms.iter().for_each(|lxm| { - let scope = match perm_aud { - Some(aud) => format!("rpc:{}?aud={}", lxm, aud), - None => format!("rpc:{}", lxm), - }; - scopes.push(scope); - }); + lxms.iter().for_each(|lxm| { + let scope = match perm_aud { + Some(aud) => format!("rpc:{}?aud={}", lxm, aud), + None => format!("rpc:{}", lxm), + }; + scopes.push(scope); + }); + } } - } - _ => {} - }); + _ => {} + }); scopes.join(" ") } @@ -334,7 +341,8 @@ mod tests { #[test] fn test_parse_include_scope_with_multiple_params() { - let (nsid, aud) = parse_include_scope("io.atcr.authFullApp?foo=bar&aud=did:web:example.com&baz=qux"); + let (nsid, aud) = + parse_include_scope("io.atcr.authFullApp?foo=bar&aud=did:web:example.com&baz=qux"); assert_eq!(nsid, "io.atcr.authFullApp"); assert_eq!(aud, Some("did:web:example.com")); } @@ -443,7 +451,8 @@ mod tests { aud: None, }]; - let expanded = build_expanded_scopes(&permissions, Some("did:web:api.example.com"), "io.atcr"); + let expanded = + build_expanded_scopes(&permissions, Some("did:web:api.example.com"), "io.atcr"); assert!(expanded.contains("rpc:io.atcr.getManifest?aud=did:web:api.example.com")); } @@ -583,7 +592,8 @@ mod tests { cache_key.to_string(), CachedLexicon { expanded_scope: "old_value".to_string(), - cached_at: std::time::Instant::now() - std::time::Duration::from_secs(CACHE_TTL_SECS + 1), + cached_at: std::time::Instant::now() + - std::time::Duration::from_secs(CACHE_TTL_SECS + 1), }, ); } @@ -601,12 +611,22 @@ mod tests { fn test_nsid_authority_extraction_for_dns() { let nsid = "io.atcr.authFullApp"; let parts: Vec<&str> = nsid.split('.').collect(); - let authority = parts[..2].iter().rev().cloned().collect::>().join("."); + let authority = parts[..2] + .iter() + .rev() + .cloned() + .collect::>() + .join("."); assert_eq!(authority, "atcr.io"); let nsid2 = "app.bsky.feed.post"; let parts2: Vec<&str> = nsid2.split('.').collect(); - let authority2 = parts2[..2].iter().rev().cloned().collect::>().join("."); + let authority2 = parts2[..2] + .iter() + .rev() + .cloned() + .collect::>() + .join("."); assert_eq!(authority2, "bsky.app"); } } diff --git a/crates/tranquil-scopes/src/permissions.rs b/crates/tranquil-scopes/src/permissions.rs index de3bebc..8a9f88c 100644 --- a/crates/tranquil-scopes/src/permissions.rs +++ b/crates/tranquil-scopes/src/permissions.rs @@ -556,5 +556,4 @@ mod tests { "app.bsky.feed.getAuthorFeed" )); } - }