mirror of
https://tangled.org/tranquil.farm/tranquil-pds
synced 2026-09-26 04:04:14 +00:00
153 lines
5.4 KiB
Rust
153 lines
5.4 KiB
Rust
use axum::{Json, extract::State};
|
|
use k256::ecdsa::SigningKey;
|
|
use serde::Deserialize;
|
|
use serde_json::Value;
|
|
use tracing::{error, info, warn};
|
|
use tranquil_pds::api::error::DbResultExt;
|
|
use tranquil_pds::api::{ApiError, EmptyResponse};
|
|
use tranquil_pds::auth::{Auth, Permissive};
|
|
use tranquil_pds::circuit_breaker::with_circuit_breaker;
|
|
use tranquil_pds::plc::{signing_key_to_did_key, validate_plc_operation};
|
|
use tranquil_pds::state::AppState;
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
pub struct SubmitPlcOperationInput {
|
|
pub operation: Value,
|
|
}
|
|
|
|
pub async fn submit_plc_operation(
|
|
State(state): State<AppState>,
|
|
auth: Auth<Permissive>,
|
|
Json(input): Json<SubmitPlcOperationInput>,
|
|
) -> Result<Json<EmptyResponse>, ApiError> {
|
|
tranquil_pds::auth::scope_check::check_identity_scope(
|
|
&auth.auth_source,
|
|
auth.scope.as_deref(),
|
|
tranquil_pds::oauth::scopes::IdentityAttr::Wildcard,
|
|
)?;
|
|
let did = &auth.did;
|
|
if did.starts_with("did:web:") {
|
|
return Err(ApiError::InvalidRequest(
|
|
"PLC operations are only valid for did:plc identities".into(),
|
|
));
|
|
}
|
|
validate_plc_operation(&input.operation)
|
|
.map_err(|e| ApiError::InvalidRequest(format!("Invalid operation: {}", e)))?;
|
|
|
|
let op = &input.operation;
|
|
let hostname = &tranquil_config::get().server.hostname;
|
|
let public_url = format!("https://{}", hostname);
|
|
let user = state
|
|
.repos
|
|
.user
|
|
.get_id_and_handle_by_did(did)
|
|
.await
|
|
.log_db_err("fetching user")?
|
|
.ok_or(ApiError::AccountNotFound)?;
|
|
|
|
let key_row = state
|
|
.repos
|
|
.user
|
|
.get_user_key_by_id(user.id)
|
|
.await
|
|
.log_db_err("fetching user key")?
|
|
.ok_or_else(|| ApiError::InternalError(Some("User signing key not found".into())))?;
|
|
|
|
let key_bytes =
|
|
tranquil_pds::config::decrypt_key(&key_row.key_bytes, key_row.encryption_version).map_err(
|
|
|e| {
|
|
error!("Failed to decrypt user key: {}", e);
|
|
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);
|
|
if let Some(rotation_keys) = op.get("rotationKeys").and_then(Value::as_array) {
|
|
let rotation_key_strs: Vec<&str> = rotation_keys.iter().filter_map(Value::as_str).collect();
|
|
if let Some(missing) = tranquil_pds::plc::missing_required_rotation_key(
|
|
&rotation_key_strs,
|
|
&user_did_key,
|
|
tranquil_config::get().secrets.plc_rotation_key.as_deref(),
|
|
) {
|
|
return Err(ApiError::InvalidRequest(missing.message().into()));
|
|
}
|
|
}
|
|
if let Some(services) = op.get("services").and_then(Value::as_object)
|
|
&& let Some(pds) = services.get("atproto_pds").and_then(Value::as_object)
|
|
{
|
|
let service_type = pds.get("type").and_then(Value::as_str);
|
|
let endpoint = pds.get("endpoint").and_then(Value::as_str);
|
|
if service_type != Some(tranquil_pds::plc::ServiceType::Pds.as_str()) {
|
|
return Err(ApiError::InvalidRequest(
|
|
"Incorrect type on atproto_pds service".into(),
|
|
));
|
|
}
|
|
if endpoint != Some(&public_url) {
|
|
return Err(ApiError::InvalidRequest(
|
|
"Incorrect endpoint on atproto_pds service".into(),
|
|
));
|
|
}
|
|
}
|
|
if let Some(verification_methods) = op.get("verificationMethods").and_then(Value::as_object)
|
|
&& let Some(atproto_key) = verification_methods.get("atproto").and_then(Value::as_str)
|
|
&& atproto_key != user_did_key
|
|
{
|
|
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(Value::as_array))
|
|
.flatten()
|
|
{
|
|
let expected_handle = format!("at://{}", user.handle);
|
|
let first_aka = also_known_as.first().and_then(Value::as_str);
|
|
if first_aka != Some(&expected_handle) {
|
|
return Err(ApiError::InvalidRequest(
|
|
"Incorrect handle in alsoKnownAs".into(),
|
|
));
|
|
}
|
|
}
|
|
let plc_client = state.plc_client();
|
|
let operation_clone = input.operation.clone();
|
|
let did_clone = did.clone();
|
|
with_circuit_breaker(&state.circuit_breakers.plc_directory, || async {
|
|
plc_client
|
|
.send_operation(&did_clone, &operation_clone)
|
|
.await
|
|
})
|
|
.await
|
|
.map_err(ApiError::from)?;
|
|
|
|
if let Err(e) = state
|
|
.repos
|
|
.repo
|
|
.insert_identity_event(did, Some(&user.handle))
|
|
.await
|
|
{
|
|
warn!("Failed to sequence identity event: {:?}", e);
|
|
}
|
|
let _ = state
|
|
.cache
|
|
.delete(&tranquil_pds::cache_keys::handle_key(&user.handle))
|
|
.await;
|
|
let _ = state
|
|
.cache
|
|
.delete(&tranquil_pds::cache_keys::plc_doc_key(did))
|
|
.await;
|
|
let _ = state
|
|
.cache
|
|
.delete(&tranquil_pds::cache_keys::plc_data_key(did))
|
|
.await;
|
|
if state.did_resolver.refresh_did(did).await.is_err() {
|
|
warn!(did = %did, "Failed to refresh DID cache after PLC update");
|
|
}
|
|
info!(did = %did, "PLC operation submitted successfully");
|
|
Ok(Json(EmptyResponse {}))
|
|
}
|