fix: consolidate auth extractors & standardize usage

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