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

fix: oauth consolidation, include-scope improvements #4

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

None yet.

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

This file has not been changed.

.sqlx/query-5031b96c65078d6c54954ce6e57ff9cbba4c48dd8a7546882ab5647114ffab4a.json

This file has not been changed.

.sqlx/query-6258398accee69e0c5f455a3c0ecc273b3da6ef5bb4d8660adafe63d8e3cd2d4.json

This file has not been changed.

.sqlx/query-a4dc8fb22bd094d414c55b9da20b610f7b122b485ab0fd0d0646d68ae8e64fe6.json

This file has not been changed.

.sqlx/query-dec3a21a8e60cc8d2c5dad727750bc88f5535dedae244f7b6e4afa95769b8f1a.json

This file has not been changed.

Cargo.lock

This file has not been changed.

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

This file has not been changed.

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

This file has not been changed.

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

This file has not been changed.

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

This file has not been changed.

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

This file has not been changed.

crates/tranquil-pds/tests/oauth_security.rs

This file has not been changed.

crates/tranquil-scopes/Cargo.toml

This file has not been changed.

crates/tranquil-scopes/src/permission_set.rs

This file has not been changed.

crates/tranquil-scopes/src/permissions.rs

This file has not been changed.

crates/tranquil-storage/src/lib.rs

This file has not been changed.

frontend/src/lib/api.ts

This file has not been changed.

frontend/src/lib/auth.svelte.ts

This file has not been changed.

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

This file has not been changed.

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

This file has not been changed.

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

This file has not been changed.

frontend/src/lib/oauth.ts

This file has not been changed.

frontend/src/locales/en.json

This file has not been changed.

frontend/src/locales/fi.json

This file has not been changed.

frontend/src/locales/ja.json

This file has not been changed.

frontend/src/locales/ko.json

This file has not been changed.

frontend/src/locales/sv.json

This file has not been changed.

frontend/src/locales/zh.json

This file has not been changed.

frontend/src/routes/Migration.svelte

This file has not been changed.

frontend/src/routes/OAuthAccounts.svelte

This file has not been changed.

frontend/src/routes/OAuthConsent.svelte

This file has not been changed.

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

History

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

so three things:

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

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

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

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

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

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