Something went wrong. Try again.
A downstream fork of Tranquil PDS for use in margin.cafe
Something went wrong. Try again.
17 kB · 573 lines
Rust
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574use crate::identity::provision::{create_plc_did, init_genesis_repo};use axum::{ Json, extract::{Query, State},};use serde::{Deserialize, Serialize};use serde_json::json;use tracing::{error, info, warn};use tranquil_pds::api::error::ApiError;use tranquil_pds::api::{ AccountsOutput, AuditLogOutput, ControllersOutput, PresetsOutput, SuccessResponse,};use tranquil_pds::auth::{Active, Auth};use tranquil_pds::delegation::{ DelegationActionType, SCOPE_PRESETS, ValidatedDelegationScope, grant_can_delete_account, verify_can_add_controllers, verify_can_control_accounts,};use tranquil_pds::rate_limit::{AccountCreationLimit, RateLimited};use tranquil_pds::state::AppState;use tranquil_pds::types::{CidLink, Did, Handle};
pub async fn list_controllers( State(state): State<AppState>, auth: Auth<Active>,) -> Result<Json<ControllersOutput<Vec<tranquil_db_traits::ControllerInfo>>>, ApiError> { let controllers = state .repos .delegation .get_delegations_for_account(&auth.did) .await .map_err(|e| { tracing::error!("Failed to list controllers: {:?}", e); ApiError::InternalError(Some("Failed to list controllers".into())) })?;
let resolve_futures = controllers.into_iter().map(|mut c| { let did_resolver = state.did_resolver.clone(); async move { if c.handle.is_none() { c.handle = did_resolver .fetch_did_document(&c.did) .await .ok() .and_then(|doc| tranquil_types::did_doc::extract_handle(&doc)); } c } });
let controllers = futures::future::join_all(resolve_futures).await;
Ok(Json(ControllersOutput { controllers }))}
#[derive(Debug, Deserialize)]pub struct AddControllerInput { pub controller_did: Did, pub granted_scopes: ValidatedDelegationScope,}
pub async fn add_controller( State(state): State<AppState>, auth: Auth<Active>, Json(input): Json<AddControllerInput>,) -> Result<Json<SuccessResponse>, ApiError> { let resolved = tranquil_pds::delegation::resolve_identity(&state, &input.controller_did) .await .map_err(|_| ApiError::ControllerNotFound)?;
if !resolved.is_local && let Some(ref pds_url) = resolved.pds_url { if !pds_url.starts_with("https://") { return Err(ApiError::InvalidDelegation( "Controller PDS must use HTTPS".into(), )); } match state .cross_pds_oauth .check_remote_is_delegated(pds_url, &input.controller_did) .await { Some(true) => { return Err(ApiError::InvalidDelegation( "Cannot add a delegated account from another PDS as a controller".into(), )); } Some(false) => {} None => { warn!( controller = %input.controller_did, pds = %pds_url, "Could not verify remote controller delegation status" ); } } }
let can_add = verify_can_add_controllers(&state, &auth).await?;
if resolved.is_local && state .repos .delegation .is_delegated_account(&input.controller_did) .await .unwrap_or(false) { return Err(ApiError::InvalidDelegation( "Cannot add a controlled account as a controller".into(), )); }
match state .repos .delegation .create_delegation( can_add.did(), &input.controller_did, &input.granted_scopes, can_add.did(), ) .await { Ok(_) => { let _ = state .repos .delegation .log_delegation_action( can_add.did(), can_add.did(), Some(&input.controller_did), DelegationActionType::GrantCreated, Some(json!({ "granted_scopes": input.granted_scopes.as_str(), "is_local": resolved.is_local })), None, None, ) .await;
Ok(Json(SuccessResponse { success: true })) } Err(e) => { tracing::error!("Failed to add controller: {:?}", e); Err(ApiError::InternalError(Some( "Failed to add controller".into(), ))) } }}
#[derive(Debug, Deserialize)]pub struct RemoveControllerInput { pub controller_did: Did,}
pub async fn remove_controller( State(state): State<AppState>, auth: Auth<Active>, Json(input): Json<RemoveControllerInput>,) -> Result<Json<SuccessResponse>, ApiError> { match state .repos .delegation .revoke_delegation(&auth.did, &input.controller_did, &auth.did) .await { Ok(true) => { let revoked_app_passwords = state .repos .session .delete_app_passwords_by_controller(&auth.did, &input.controller_did) .await .unwrap_or(0);
let revoked_oauth_tokens = state .repos .oauth .revoke_tokens_for_controller(&auth.did, &input.controller_did) .await .unwrap_or(0);
let _ = state .repos .delegation .log_delegation_action( &auth.did, &auth.did, Some(&input.controller_did), DelegationActionType::GrantRevoked, Some(json!({ "revoked_app_passwords": revoked_app_passwords, "revoked_oauth_tokens": revoked_oauth_tokens })), None, None, ) .await;
Ok(Json(SuccessResponse { success: true })) } Ok(false) => Err(ApiError::DelegationNotFound), Err(e) => { tracing::error!("Failed to remove controller: {:?}", e); Err(ApiError::InternalError(Some( "Failed to remove controller".into(), ))) } }}
#[derive(Debug, Deserialize)]pub struct UpdateControllerScopesInput { pub controller_did: Did, pub granted_scopes: ValidatedDelegationScope,}
pub async fn update_controller_scopes( State(state): State<AppState>, auth: Auth<Active>, Json(input): Json<UpdateControllerScopesInput>,) -> Result<Json<SuccessResponse>, ApiError> { match state .repos .delegation .update_delegation_scopes(&auth.did, &input.controller_did, &input.granted_scopes) .await { Ok(true) => { let revoked_app_passwords = state .repos .session .delete_app_passwords_by_controller(&auth.did, &input.controller_did) .await .unwrap_or(0);
let revoked_oauth_tokens = state .repos .oauth .revoke_tokens_for_controller(&auth.did, &input.controller_did) .await .unwrap_or(0);
let _ = state .repos .delegation .log_delegation_action( &auth.did, &auth.did, Some(&input.controller_did), DelegationActionType::ScopesModified, Some(json!({ "new_scopes": input.granted_scopes.as_str(), "revoked_app_passwords": revoked_app_passwords, "revoked_oauth_tokens": revoked_oauth_tokens })), None, None, ) .await;
Ok(Json(SuccessResponse { success: true })) } Ok(false) => Err(ApiError::DelegationNotFound), Err(e) => { tracing::error!("Failed to update controller scopes: {:?}", e); Err(ApiError::InternalError(Some( "Failed to update controller scopes".into(), ))) } }}
pub async fn list_controlled_accounts( State(state): State<AppState>, auth: Auth<Active>,) -> Result<Json<AccountsOutput<Vec<tranquil_db_traits::DelegatedAccountInfo>>>, ApiError> { let accounts = state .repos .delegation .get_accounts_controlled_by(&auth.did) .await .map_err(|e| { tracing::error!("Failed to list controlled accounts: {:?}", e); ApiError::InternalError(Some("Failed to list controlled accounts".into())) })?;
Ok(Json(AccountsOutput { accounts }))}
#[derive(Debug, Deserialize)]pub struct AuditLogParams { #[serde(default = "default_limit")] pub limit: i64, #[serde(default)] pub offset: i64,}
fn default_limit() -> i64 { 50}
pub async fn get_audit_log( State(state): State<AppState>, auth: Auth<Active>, Query(params): Query<AuditLogParams>,) -> Result<Json<AuditLogOutput<Vec<tranquil_db_traits::AuditLogEntry>>>, ApiError> { let limit = params.limit.clamp(1, 100); let offset = params.offset.max(0);
let entries = state .repos .delegation .get_audit_log_for_account(&auth.did, limit, offset) .await .map_err(|e| { tracing::error!("Failed to get audit log: {:?}", e); ApiError::InternalError(Some("Failed to get audit log".into())) })?;
let total = state .repos .delegation .count_audit_log_entries(&auth.did) .await .unwrap_or_default();
Ok(Json(AuditLogOutput { entries, total }))}
pub async fn get_scope_presets()-> Json<PresetsOutput<&'static [tranquil_pds::delegation::ScopePreset]>> { Json(PresetsOutput { presets: SCOPE_PRESETS, })}
#[derive(Debug, Deserialize)]#[serde(rename_all = "camelCase")]pub struct CreateDelegatedAccountInput { pub handle: String, pub email: Option<String>, pub controller_scopes: ValidatedDelegationScope,}
#[derive(Debug, Serialize)]#[serde(rename_all = "camelCase")]pub struct CreateDelegatedAccountOutput { pub did: Did, pub handle: Handle,}
pub async fn create_delegated_account( State(state): State<AppState>, _rate_limit: RateLimited<AccountCreationLimit>, auth: Auth<Active>, Json(input): Json<CreateDelegatedAccountInput>,) -> Result<Json<CreateDelegatedAccountOutput>, ApiError> { let can_control = verify_can_control_accounts(&state, &auth).await?;
let handle = tranquil_pds::api::validation::resolve_handle_input(&input.handle) .map_err(|e| ApiError::InvalidRequest(e.to_string()))?;
let email = input .email .as_ref() .map(|e| e.trim().to_string()) .filter(|e| !e.is_empty()); if let Some(ref email) = email && !tranquil_pds::api::validation::is_valid_email(email) { return Err(ApiError::InvalidEmail); }
let plc = create_plc_did(&state, &handle).await.map_err(|e| { tracing::error!("PLC DID creation failed: {:?}", e); e })?; let did = plc.did; info!(did = %did, handle = %handle, controller = %can_control.did(), "Created DID for delegated account");
let repo = init_genesis_repo(&state, &did, &plc.signing_key, &plc.signing_key_bytes).await?; let repo_for_seq = repo.clone();
let create_input = tranquil_db_traits::CreateDelegatedAccountInput { handle: handle.clone(), email: email.clone(), did: did.clone(), controller_did: can_control.did().clone(), controller_scopes: input.controller_scopes.as_str().to_string(), encrypted_key_bytes: repo.encrypted_key_bytes, encryption_version: tranquil_pds::config::ENCRYPTION_VERSION, commit_cid: CidLink::from(&repo.commit_cid), repo_rev: repo.repo_rev.clone(), genesis_block_cids: repo.genesis_block_cids, };
match state .repos .user .create_delegated_account(&create_input) .await { Ok(_) => {} Err(tranquil_db_traits::CreateAccountError::HandleTaken) => { return Err(ApiError::HandleNotAvailable(None)); } Err(tranquil_db_traits::CreateAccountError::EmailTaken) => { return Err(ApiError::EmailTaken); } Err(e) => { error!("Error creating delegated account: {:?}", e); return Err(ApiError::InternalError(None)); } }
crate::identity::provision::sequence_new_account( &state, &did, &handle, &repo_for_seq, handle.as_str(), ) .await;
let _ = state .repos .delegation .log_delegation_action( &did, &auth.did, Some(&auth.did), DelegationActionType::GrantCreated, Some(json!({ "account_created": true, "granted_scopes": input.controller_scopes.as_str() })), None, None, ) .await;
info!(did = %did, handle = %handle, controller = %&auth.did, "Delegated account created");
Ok(Json(CreateDelegatedAccountOutput { did, handle }))}
#[derive(Debug, Deserialize)]#[serde(rename_all = "camelCase")]pub struct DeleteControlledAccountInput { pub did: Did,}
pub async fn delete_controlled_account( State(state): State<AppState>, auth: Auth<Active>, Json(input): Json<DeleteControlledAccountInput>,) -> Result<Json<SuccessResponse>, ApiError> { let grant = state .repos .delegation .get_delegation(&input.did, &auth.did) .await .map_err(|e| { error!("Failed to look up delegation for delete: {:?}", e); ApiError::InternalError(Some("Failed to look up delegation".into())) })? .ok_or(ApiError::DelegationNotFound)?;
if !grant_can_delete_account(grant.granted_scopes.as_str()) { return Err(ApiError::InvalidDelegation( "Owner access is required to delete a controlled account".into(), )); }
let (user_id, handle) = state .repos .user .get_id_and_handle_by_did(&input.did) .await .map_err(|e| { error!("Failed to look up controlled account: {:?}", e); ApiError::InternalError(Some("Failed to look up account".into())) })? .ok_or(ApiError::AccountNotFound) .map(|row| (row.id, row.handle))?;
state .repos .user .admin_delete_account_complete(user_id, &input.did) .await .map_err(|e| { error!("Failed to delete controlled account: {:?}", e); ApiError::InternalError(Some("Failed to delete account".into())) })?;
if let Err(e) = tranquil_pds::repo_ops::sequence_account_event( &state, &input.did, tranquil_db_traits::AccountStatus::Deleted, ) .await { warn!( "Failed to sequence controlled account deletion event for {}: {}", input.did, e ); } let _ = state .cache .delete(&tranquil_pds::cache_keys::handle_key(&handle)) .await; let _ = state .repos .delegation .log_delegation_action( &input.did, &auth.did, Some(&auth.did), DelegationActionType::AccountAction, Some(json!({ "deleted": true })), None, None, ) .await;
info!( did = %input.did, handle = %handle, controller = %auth.did, "Controlled account deleted" );
Ok(Json(SuccessResponse { success: true }))}
#[derive(Debug, Deserialize)]pub struct ResolveControllerParams { pub identifier: String,}
pub async fn resolve_controller( State(state): State<AppState>, Query(params): Query<ResolveControllerParams>,) -> Result<Json<tranquil_pds::delegation::ResolvedIdentity>, ApiError> { let identifier = params.identifier.trim().trim_start_matches('@');
let did: Did = if identifier.starts_with("did:") { identifier .parse() .map_err(|_| ApiError::ControllerNotFound)? } else { let handle: Handle = identifier .parse() .map_err(|_| ApiError::ControllerNotFound)?; let local_user = state.repos.user.get_by_handle(&handle).await.ok().flatten(); match local_user { Some(user) => user.did, None => tranquil_pds::handle::resolve_handle(&handle) .await .map_err(|_| ApiError::ControllerNotFound)?, } };
let resolved = tranquil_pds::delegation::resolve_identity(&state, &did) .await .map_err(|_| ApiError::ControllerNotFound)?;
Ok(Json(resolved))}