Files
tranquil-pds/src/api/delegation.rs
T
2026-01-05 00:48:18 +02:00

774 lines
23 KiB
Rust

use crate::api::error::ApiError;
use crate::api::repo::record::utils::create_signed_commit;
use crate::auth::BearerAuth;
use crate::delegation::{self, DelegationActionType};
use crate::oauth::db as oauth_db;
use crate::state::{AppState, RateLimitKind};
use crate::types::{Did, Handle};
use crate::util::extract_client_ip;
use axum::{
Json,
extract::{Query, State},
http::{HeaderMap, StatusCode},
response::{IntoResponse, Response},
};
use jacquard::types::{integer::LimitedU32, string::Tid};
use jacquard_repo::{mst::Mst, storage::BlockStore};
use serde::{Deserialize, Serialize};
use serde_json::json;
use std::sync::Arc;
use tracing::{error, info, warn};
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ControllerInfo {
pub did: Did,
pub handle: Handle,
pub granted_scopes: String,
pub granted_at: chrono::DateTime<chrono::Utc>,
pub is_active: bool,
}
#[derive(Debug, Serialize)]
pub struct ListControllersResponse {
pub controllers: Vec<ControllerInfo>,
}
pub async fn list_controllers(State(state): State<AppState>, auth: BearerAuth) -> Response {
let controllers = match delegation::get_delegations_for_account(&state.db, &auth.0.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();
}
};
Json(ListControllersResponse {
controllers: controllers
.into_iter()
.map(|c| ControllerInfo {
did: c.did.into(),
handle: c.handle,
granted_scopes: c.granted_scopes,
granted_at: c.granted_at,
is_active: c.is_active,
})
.collect(),
})
.into_response()
}
#[derive(Debug, Deserialize)]
pub struct AddControllerInput {
pub controller_did: Did,
pub granted_scopes: String,
}
pub async fn add_controller(
State(state): State<AppState>,
auth: BearerAuth,
Json(input): Json<AddControllerInput>,
) -> Response {
if let Err(e) = delegation::scopes::validate_delegation_scopes(&input.granted_scopes) {
return ApiError::InvalidScopes(e).into_response();
}
let controller_exists: bool = sqlx::query_scalar!(
r#"SELECT EXISTS(SELECT 1 FROM users WHERE did = $1) as "exists!""#,
input.controller_did.as_str()
)
.fetch_one(&state.db)
.await
.unwrap_or(false);
if !controller_exists {
return ApiError::ControllerNotFound.into_response();
}
match delegation::controls_any_accounts(&state.db, &auth.0.did).await {
Ok(true) => {
return ApiError::InvalidDelegation(
"Cannot add controllers to an account that controls other accounts".into(),
)
.into_response();
}
Err(e) => {
tracing::error!("Failed to check delegation status: {:?}", e);
return ApiError::InternalError(Some("Failed to verify delegation status".into()))
.into_response();
}
Ok(false) => {}
}
match delegation::has_any_controllers(&state.db, &input.controller_did).await {
Ok(true) => {
return ApiError::InvalidDelegation(
"Cannot add a controlled account as a controller".into(),
)
.into_response();
}
Err(e) => {
tracing::error!("Failed to check controller status: {:?}", e);
return ApiError::InternalError(Some("Failed to verify controller status".into()))
.into_response();
}
Ok(false) => {}
}
match delegation::create_delegation(
&state.db,
&auth.0.did,
&input.controller_did,
&input.granted_scopes,
&auth.0.did,
)
.await
{
Ok(_) => {
let _ = delegation::log_delegation_action(
&state.db,
&auth.0.did,
&auth.0.did,
Some(&input.controller_did),
DelegationActionType::GrantCreated,
Some(serde_json::json!({
"granted_scopes": input.granted_scopes
})),
None,
None,
)
.await;
(
StatusCode::OK,
Json(serde_json::json!({
"success": true
})),
)
.into_response()
}
Err(e) => {
tracing::error!("Failed to add controller: {:?}", e);
ApiError::InternalError(Some("Failed to add controller".into())).into_response()
}
}
}
#[derive(Debug, Deserialize)]
pub struct RemoveControllerInput {
pub controller_did: Did,
}
pub async fn remove_controller(
State(state): State<AppState>,
auth: BearerAuth,
Json(input): Json<RemoveControllerInput>,
) -> Response {
match delegation::revoke_delegation(&state.db, &auth.0.did, &input.controller_did, &auth.0.did)
.await
{
Ok(true) => {
let revoked_app_passwords = sqlx::query_scalar!(
r#"DELETE FROM app_passwords
WHERE user_id = (SELECT id FROM users WHERE did = $1)
AND created_by_controller_did = $2
RETURNING id"#,
&auth.0.did,
input.controller_did.as_str()
)
.fetch_all(&state.db)
.await
.map(|r| r.len())
.unwrap_or(0);
let revoked_oauth_tokens = oauth_db::revoke_tokens_for_controller(
&state.db,
&auth.0.did,
&input.controller_did,
)
.await
.unwrap_or(0);
let _ = delegation::log_delegation_action(
&state.db,
&auth.0.did,
&auth.0.did,
Some(&input.controller_did),
DelegationActionType::GrantRevoked,
Some(serde_json::json!({
"revoked_app_passwords": revoked_app_passwords,
"revoked_oauth_tokens": revoked_oauth_tokens
})),
None,
None,
)
.await;
(
StatusCode::OK,
Json(serde_json::json!({
"success": true
})),
)
.into_response()
}
Ok(false) => ApiError::DelegationNotFound.into_response(),
Err(e) => {
tracing::error!("Failed to remove controller: {:?}", e);
ApiError::InternalError(Some("Failed to remove controller".into())).into_response()
}
}
}
#[derive(Debug, Deserialize)]
pub struct UpdateControllerScopesInput {
pub controller_did: Did,
pub granted_scopes: String,
}
pub async fn update_controller_scopes(
State(state): State<AppState>,
auth: BearerAuth,
Json(input): Json<UpdateControllerScopesInput>,
) -> Response {
if let Err(e) = delegation::scopes::validate_delegation_scopes(&input.granted_scopes) {
return ApiError::InvalidScopes(e).into_response();
}
match delegation::update_delegation_scopes(
&state.db,
&auth.0.did,
&input.controller_did,
&input.granted_scopes,
)
.await
{
Ok(true) => {
let _ = delegation::log_delegation_action(
&state.db,
&auth.0.did,
&auth.0.did,
Some(&input.controller_did),
DelegationActionType::ScopesModified,
Some(serde_json::json!({
"new_scopes": input.granted_scopes
})),
None,
None,
)
.await;
(
StatusCode::OK,
Json(serde_json::json!({
"success": true
})),
)
.into_response()
}
Ok(false) => 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()
}
}
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct DelegatedAccountInfo {
pub did: Did,
pub handle: Handle,
pub granted_scopes: String,
pub granted_at: chrono::DateTime<chrono::Utc>,
}
#[derive(Debug, Serialize)]
pub struct ListControlledAccountsResponse {
pub accounts: Vec<DelegatedAccountInfo>,
}
pub async fn list_controlled_accounts(State(state): State<AppState>, auth: BearerAuth) -> Response {
let accounts = match delegation::get_accounts_controlled_by(&state.db, &auth.0.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();
}
};
Json(ListControlledAccountsResponse {
accounts: accounts
.into_iter()
.map(|a| DelegatedAccountInfo {
did: a.did.into(),
handle: a.handle,
granted_scopes: a.granted_scopes,
granted_at: a.granted_at,
})
.collect(),
})
.into_response()
}
#[derive(Debug, Deserialize)]
pub struct AuditLogParams {
#[serde(default = "default_limit")]
pub limit: i64,
#[serde(default)]
pub offset: i64,
}
fn default_limit() -> i64 {
50
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct AuditLogEntry {
pub id: String,
pub delegated_did: Did,
pub actor_did: Did,
pub controller_did: Option<Did>,
pub action_type: String,
pub action_details: Option<serde_json::Value>,
pub created_at: chrono::DateTime<chrono::Utc>,
}
#[derive(Debug, Serialize)]
pub struct GetAuditLogResponse {
pub entries: Vec<AuditLogEntry>,
pub total: i64,
}
pub async fn get_audit_log(
State(state): State<AppState>,
auth: BearerAuth,
Query(params): Query<AuditLogParams>,
) -> Response {
let limit = params.limit.clamp(1, 100);
let offset = params.offset.max(0);
let entries =
match delegation::audit::get_audit_log_for_account(&state.db, &auth.0.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();
}
};
let total = delegation::audit::count_audit_log_entries(&state.db, &auth.0.did)
.await
.unwrap_or_default();
Json(GetAuditLogResponse {
entries: entries
.into_iter()
.map(|e| AuditLogEntry {
id: e.id.to_string(),
delegated_did: e.delegated_did.into(),
actor_did: e.actor_did.into(),
controller_did: e.controller_did.map(Into::into),
action_type: format!("{:?}", e.action_type),
action_details: e.action_details,
created_at: e.created_at,
})
.collect(),
total,
})
.into_response()
}
#[derive(Debug, Serialize)]
pub struct ScopePresetInfo {
pub name: &'static str,
pub label: &'static str,
pub description: &'static str,
pub scopes: &'static str,
}
#[derive(Debug, Serialize)]
pub struct GetScopePresetsResponse {
pub presets: Vec<ScopePresetInfo>,
}
pub async fn get_scope_presets() -> Response {
Json(GetScopePresetsResponse {
presets: delegation::SCOPE_PRESETS
.iter()
.map(|p| ScopePresetInfo {
name: p.name,
label: p.label,
description: p.description,
scopes: p.scopes,
})
.collect(),
})
.into_response()
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CreateDelegatedAccountInput {
pub handle: String,
pub email: Option<String>,
pub controller_scopes: String,
pub invite_code: Option<String>,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct CreateDelegatedAccountResponse {
pub did: Did,
pub handle: Handle,
}
pub async fn create_delegated_account(
State(state): State<AppState>,
headers: HeaderMap,
auth: BearerAuth,
Json(input): Json<CreateDelegatedAccountInput>,
) -> Response {
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(
"Too many account creation attempts. Please try again later.".into(),
))
.into_response();
}
if let Err(e) = delegation::scopes::validate_delegation_scopes(&input.controller_scopes) {
return ApiError::InvalidScopes(e).into_response();
}
match delegation::has_any_controllers(&state.db, &auth.0.did).await {
Ok(true) => {
return ApiError::InvalidDelegation(
"Cannot create delegated accounts from a controlled account".into(),
)
.into_response();
}
Err(e) => {
tracing::error!("Failed to check controller status: {:?}", e);
return ApiError::InternalError(Some("Failed to verify controller status".into()))
.into_response();
}
Ok(false) => {}
}
let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string());
let pds_suffix = format!(".{}", hostname);
let handle = if !input.handle.contains('.') || input.handle.ends_with(&pds_suffix) {
let handle_to_validate = if input.handle.ends_with(&pds_suffix) {
input
.handle
.strip_suffix(&pds_suffix)
.unwrap_or(&input.handle)
} else {
&input.handle
};
match crate::api::validation::validate_short_handle(handle_to_validate) {
Ok(h) => format!("{}.{}", h, hostname),
Err(e) => {
return ApiError::InvalidRequest(e.to_string()).into_response();
}
}
} else {
input.handle.to_lowercase()
};
let email = input
.email
.as_ref()
.map(|e| e.trim().to_string())
.filter(|e| !e.is_empty());
if let Some(ref email) = email
&& !crate::api::validation::is_valid_email(email)
{
return ApiError::InvalidEmail.into_response();
}
if let Some(ref code) = input.invite_code {
let valid = sqlx::query_scalar!(
"SELECT available_uses > 0 AND NOT disabled FROM invite_codes WHERE code = $1",
code
)
.fetch_optional(&state.db)
.await
.ok()
.flatten()
.unwrap_or(Some(false));
if valid != Some(true) {
return 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();
}
}
use k256::ecdsa::SigningKey;
use rand::rngs::OsRng;
let pds_endpoint = format!("https://{}", hostname);
let secret_key = k256::SecretKey::random(&mut OsRng);
let secret_key_bytes = secret_key.to_bytes().to_vec();
let signing_key = match SigningKey::from_slice(&secret_key_bytes) {
Ok(k) => k,
Err(e) => {
error!("Error creating signing key: {:?}", e);
return ApiError::InternalError(None).into_response();
}
};
let rotation_key = std::env::var("PLC_ROTATION_KEY")
.unwrap_or_else(|_| crate::plc::signing_key_to_did_key(&signing_key));
let genesis_result = match crate::plc::create_genesis_operation(
&signing_key,
&rotation_key,
&handle,
&pds_endpoint,
) {
Ok(r) => r,
Err(e) => {
error!("Error creating PLC genesis operation: {:?}", e);
return ApiError::InternalError(Some("Failed to create PLC operation".into()))
.into_response();
}
};
let plc_client = crate::plc::PlcClient::with_cache(None, Some(state.cache.clone()));
if let Err(e) = plc_client
.send_operation(&genesis_result.did, &genesis_result.signed_operation)
.await
{
error!("Failed to submit PLC genesis operation: {:?}", e);
return ApiError::UpstreamErrorMsg(format!(
"Failed to register DID with PLC directory: {}",
e
))
.into_response();
}
let did = genesis_result.did;
info!(did = %did, handle = %handle, controller = %&auth.0.did, "Created DID for delegated account");
let mut tx = match state.db.begin().await {
Ok(tx) => tx,
Err(e) => {
error!("Error starting transaction: {:?}", e);
return ApiError::InternalError(None).into_response();
}
};
let user_insert: Result<(uuid::Uuid,), _> = sqlx::query_as(
r#"INSERT INTO users (
handle, email, did, password_hash, password_required,
account_type, preferred_comms_channel
) VALUES ($1, $2, $3, NULL, FALSE, 'delegated'::account_type, 'email'::comms_channel) RETURNING id"#,
)
.bind(&handle)
.bind(&email)
.bind(&did)
.fetch_one(&mut *tx)
.await;
let user_id = match user_insert {
Ok((id,)) => id,
Err(e) => {
if let Some(db_err) = e.as_database_error()
&& db_err.code().as_deref() == Some("23505")
{
let constraint = db_err.constraint().unwrap_or("");
if constraint.contains("handle") {
return ApiError::HandleNotAvailable(None).into_response();
} else if constraint.contains("email") {
return ApiError::EmailTaken.into_response();
}
}
error!("Error inserting user: {:?}", e);
return ApiError::InternalError(None).into_response();
}
};
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();
}
};
if let Err(e) = sqlx::query!(
"INSERT INTO user_keys (user_id, key_bytes, encryption_version, encrypted_at) VALUES ($1, $2, $3, NOW())",
user_id,
&encrypted_key_bytes[..],
crate::config::ENCRYPTION_VERSION
)
.execute(&mut *tx)
.await
{
error!("Error inserting user key: {:?}", e);
return ApiError::InternalError(None).into_response();
}
if let Err(e) = sqlx::query!(
r#"INSERT INTO account_delegations (delegated_did, controller_did, granted_scopes, granted_by)
VALUES ($1, $2, $3, $4)"#,
did,
&auth.0.did,
input.controller_scopes,
&auth.0.did
)
.execute(&mut *tx)
.await
{
error!("Error creating initial delegation: {:?}", e);
return ApiError::InternalError(None).into_response();
}
let mst = Mst::new(Arc::new(state.block_store.clone()));
let mst_root = match mst.persist().await {
Ok(c) => c,
Err(e) => {
error!("Error persisting MST: {:?}", e);
return ApiError::InternalError(None).into_response();
}
};
let rev = Tid::now(LimitedU32::MIN);
let (commit_bytes, _sig) =
match create_signed_commit(&did, mst_root, rev.as_ref(), None, &signing_key) {
Ok(result) => result,
Err(e) => {
error!("Error creating genesis commit: {:?}", e);
return 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();
}
};
let commit_cid_str = commit_cid.to_string();
let rev_str = rev.as_ref().to_string();
if let Err(e) = sqlx::query!(
"INSERT INTO repos (user_id, repo_root_cid, repo_rev) VALUES ($1, $2, $3)",
user_id,
commit_cid_str,
rev_str
)
.execute(&mut *tx)
.await
{
error!("Error inserting repo: {:?}", e);
return ApiError::InternalError(None).into_response();
}
let genesis_block_cids = vec![mst_root.to_bytes(), commit_cid.to_bytes()];
if let Err(e) = sqlx::query!(
r#"
INSERT INTO user_blocks (user_id, block_cid)
SELECT $1, block_cid FROM UNNEST($2::bytea[]) AS t(block_cid)
ON CONFLICT (user_id, block_cid) DO NOTHING
"#,
user_id,
&genesis_block_cids
)
.execute(&mut *tx)
.await
{
error!("Error inserting user_blocks: {:?}", e);
return ApiError::InternalError(None).into_response();
}
if let Some(ref code) = input.invite_code {
let _ = sqlx::query!(
"UPDATE invite_codes SET available_uses = available_uses - 1 WHERE code = $1",
code
)
.execute(&mut *tx)
.await;
let _ = sqlx::query!(
"INSERT INTO invite_code_uses (code, used_by_user) VALUES ($1, $2)",
code,
user_id
)
.execute(&mut *tx)
.await;
}
if let Err(e) = tx.commit().await {
error!("Error committing transaction: {:?}", e);
return ApiError::InternalError(None).into_response();
}
if let Err(e) =
crate::api::repo::record::sequence_identity_event(&state, &did, Some(&handle)).await
{
warn!("Failed to sequence identity event for {}: {}", did, e);
}
if let Err(e) = crate::api::repo::record::sequence_account_event(&state, &did, true, None).await
{
warn!("Failed to sequence account event for {}: {}", did, e);
}
let profile_record = json!({
"$type": "app.bsky.actor.profile",
"displayName": handle
});
if let Err(e) = crate::api::repo::record::create_record_internal(
&state,
&did,
"app.bsky.actor.profile",
"self",
&profile_record,
)
.await
{
warn!("Failed to create default profile for {}: {}", did, e);
}
let _ = delegation::log_delegation_action(
&state.db,
&did,
&auth.0.did,
Some(&auth.0.did),
DelegationActionType::GrantCreated,
Some(json!({
"account_created": true,
"granted_scopes": input.controller_scopes
})),
None,
None,
)
.await;
info!(did = %did, handle = %handle, controller = %&auth.0.did, "Delegated account created");
Json(CreateDelegatedAccountResponse {
did: did.into(),
handle: handle.into(),
})
.into_response()
}