A lexicon-driven AppView for ATProto.
Something went wrong. Try again.
Rust
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483use axum::Json;use axum::extract::{Path, State};use axum::http::StatusCode;use serde_json::Value;use uuid::Uuid;
use crate::AppState;use crate::db::{adapt_sql, now_rfc3339};use crate::error::AppError;use crate::event_log::{EventLog, Severity, log_event};
use super::auth::UserAuth;use super::permissions::Permission;use super::types::{CreateUserBody, TransferSuperBody, UpdatePermissionsBody, UserSummary};
/// POST /admin/users — create a new user with template or explicit permissions.pub(super) async fn create_user( State(state): State<AppState>, auth: UserAuth, Json(body): Json<CreateUserBody>,) -> Result<(StatusCode, Json<Value>), AppError> { auth.require(Permission::UsersCreate).await?;
// Determine permissions to grant let perms_to_grant: Vec<String> = if let Some(explicit) = &body.permissions { explicit.clone() } else if let Some(template) = &body.template { template .permissions() .iter() .map(|p| p.as_str().to_string()) .collect() } else { // Default to viewer super::permissions::Template::Viewer .permissions() .iter() .map(|p| p.as_str().to_string()) .collect() };
// Escalation guard: actor can only grant permissions they hold if !auth.is_super { for perm_str in &perms_to_grant { #[allow(clippy::collapsible_if)] if let Ok(p) = serde_json::from_value::<Permission>(serde_json::Value::String(perm_str.clone())) { if !auth.permissions.contains(&p) { return Err(AppError::Forbidden(format!( "Cannot grant permission you don't have: {perm_str}" ))); } } } }
let user_id = Uuid::new_v4().to_string(); let now = now_rfc3339(); let backend = state.db_backend;
let insert_sql = adapt_sql( "INSERT INTO users (id, did, is_super, created_at) VALUES (?, ?, ?, ?)", backend, );
sqlx::query(&insert_sql) .bind(&user_id) .bind(&body.did) .bind(0_i32) .bind(&now) .execute(&state.db) .await .map_err(|e| AppError::Internal(format!("failed to create user: {e}")))?;
let perm_sql = adapt_sql( "INSERT INTO user_permissions (user_id, permission, granted_by, granted_at) VALUES (?, ?, ?, ?) ON CONFLICT DO NOTHING", backend, );
for perm_str in &perms_to_grant { sqlx::query(&perm_sql) .bind(&user_id) .bind(perm_str) .bind(&auth.user_id) .bind(&now) .execute(&state.db) .await .map_err(|e| AppError::Internal(format!("failed to grant permission: {e}")))?; }
let template_name = body .template .as_ref() .map(|t| format!("{t:?}").to_lowercase());
log_event( &state.db, EventLog { event_type: "user.created".to_string(), severity: Severity::Info, actor_did: Some(auth.did.clone()), subject: Some(body.did.clone()), detail: serde_json::json!({ "template": template_name, "permissions": perms_to_grant, }), }, backend, ) .await;
Ok(( StatusCode::CREATED, Json(serde_json::json!({ "id": user_id, "did": body.did, })), ))}
/// GET /admin/users — list all users with their permissions.pub(super) async fn list_users( State(state): State<AppState>, auth: UserAuth,) -> Result<Json<Vec<UserSummary>>, AppError> { auth.require(Permission::UsersRead).await?;
let backend = state.db_backend;
let select_sql = adapt_sql( "SELECT id, did, is_super, created_at, last_used_at FROM users ORDER BY created_at", backend, );
let rows: Vec<(String, String, i32, String, Option<String>)> = sqlx::query_as(&select_sql) .fetch_all(&state.db) .await .map_err(|e| AppError::Internal(format!("failed to list users: {e}")))?;
let perm_sql = adapt_sql( "SELECT permission FROM user_permissions WHERE user_id = ? ORDER BY permission", backend, );
let mut users = Vec::new(); for (id, did, is_super_int, created_at, last_used_at) in rows { let perm_rows: Vec<(String,)> = sqlx::query_as(&perm_sql) .bind(&id) .fetch_all(&state.db) .await .map_err(|e| AppError::Internal(format!("failed to load permissions: {e}")))?;
users.push(UserSummary { id, did, is_super: is_super_int != 0, permissions: perm_rows.into_iter().map(|(p,)| p).collect(), created_at, last_used_at, }); }
Ok(Json(users))}
/// GET /admin/users/:id — get a single user with permissions.pub(super) async fn get_user( State(state): State<AppState>, auth: UserAuth, Path(id): Path<String>,) -> Result<Json<UserSummary>, AppError> { auth.require(Permission::UsersRead).await?;
let backend = state.db_backend;
let select_sql = adapt_sql( "SELECT id, did, is_super, created_at, last_used_at FROM users WHERE id = ?", backend, );
let found: Option<(String, String, i32, String, Option<String>)> = sqlx::query_as(&select_sql) .bind(&id) .fetch_optional(&state.db) .await .map_err(|e| AppError::Internal(format!("failed to get user: {e}")))?;
let Some((uid, did, is_super_int, created_at, last_used_at)) = found else { return Err(AppError::NotFound(format!("user '{id}' not found"))); };
let perm_sql = adapt_sql( "SELECT permission FROM user_permissions WHERE user_id = ? ORDER BY permission", backend, );
let perm_rows: Vec<(String,)> = sqlx::query_as(&perm_sql) .bind(&uid) .fetch_all(&state.db) .await .map_err(|e| AppError::Internal(format!("failed to load permissions: {e}")))?;
Ok(Json(UserSummary { id: uid, did, is_super: is_super_int != 0, permissions: perm_rows.into_iter().map(|(p,)| p).collect(), created_at, last_used_at, }))}
/// PATCH /admin/users/:id/permissions — grant/revoke permissions.pub(super) async fn update_permissions( State(state): State<AppState>, auth: UserAuth, Path(id): Path<String>, Json(body): Json<UpdatePermissionsBody>,) -> Result<StatusCode, AppError> { auth.require(Permission::UsersUpdate).await?;
let backend = state.db_backend;
// Self-modification guard if auth.user_id == id { return Err(AppError::Forbidden( "Cannot modify your own permissions".into(), )); }
// Cannot modify super user's permissions let select_sql = adapt_sql("SELECT is_super FROM users WHERE id = ?", backend); let target: Option<(i32,)> = sqlx::query_as(&select_sql) .bind(&id) .fetch_optional(&state.db) .await .map_err(|e| AppError::Internal(format!("user lookup failed: {e}")))?;
let Some((target_is_super,)) = target else { return Err(AppError::NotFound(format!("user '{id}' not found"))); };
if target_is_super != 0 { return Err(AppError::Forbidden( "Cannot modify super user's permissions".into(), )); }
// Validate all permission strings are recognized for perm_str in body.grant.iter().chain(body.revoke.iter()) { if serde_json::from_value::<Permission>(serde_json::Value::String(perm_str.clone())) .is_err() { return Err(AppError::BadRequest(format!( "Unrecognized permission: {perm_str}" ))); } }
// Escalation guard: can only grant permissions you hold if !auth.is_super { for perm_str in &body.grant { #[allow(clippy::collapsible_if)] if let Ok(p) = serde_json::from_value::<Permission>(serde_json::Value::String(perm_str.clone())) { if !auth.permissions.contains(&p) { return Err(AppError::Forbidden(format!( "Cannot grant permission you don't have: {perm_str}" ))); } } } }
let now = now_rfc3339();
let grant_sql = adapt_sql( "INSERT INTO user_permissions (user_id, permission, granted_by, granted_at) VALUES (?, ?, ?, ?) ON CONFLICT DO NOTHING", backend, );
for perm_str in &body.grant { sqlx::query(&grant_sql) .bind(&id) .bind(perm_str) .bind(&auth.user_id) .bind(&now) .execute(&state.db) .await .map_err(|e| AppError::Internal(format!("failed to grant permission: {e}")))?; }
let revoke_sql = adapt_sql( "DELETE FROM user_permissions WHERE user_id = ? AND permission = ?", backend, );
for perm_str in &body.revoke { sqlx::query(&revoke_sql) .bind(&id) .bind(perm_str) .execute(&state.db) .await .map_err(|e| AppError::Internal(format!("failed to revoke permission: {e}")))?; }
log_event( &state.db, EventLog { event_type: "user.permissions_updated".to_string(), severity: Severity::Info, actor_did: Some(auth.did.clone()), subject: Some(id.clone()), detail: serde_json::json!({ "granted": body.grant, "revoked": body.revoke, }), }, backend, ) .await;
Ok(StatusCode::NO_CONTENT)}
/// DELETE /admin/users/:id — remove a user.pub(super) async fn delete_user( State(state): State<AppState>, auth: UserAuth, Path(id): Path<String>,) -> Result<StatusCode, AppError> { auth.require(Permission::UsersDelete).await?;
let backend = state.db_backend;
// Self-deletion guard if auth.user_id == id { return Err(AppError::Forbidden("Cannot delete yourself".into())); }
// Cannot delete super user let select_sql = adapt_sql("SELECT is_super FROM users WHERE id = ?", backend); let target: Option<(i32,)> = sqlx::query_as(&select_sql) .bind(&id) .fetch_optional(&state.db) .await .map_err(|e| AppError::Internal(format!("user lookup failed: {e}")))?;
let Some((is_super,)) = target else { return Err(AppError::NotFound(format!("user '{id}' not found"))); };
if is_super != 0 { return Err(AppError::Forbidden("Cannot delete the super user".into())); }
// Revoke API keys and delete user let now = now_rfc3339();
let revoke_keys_sql = adapt_sql( "UPDATE api_keys SET revoked_at = ? WHERE user_id = ? AND revoked_at IS NULL", backend, );
sqlx::query(&revoke_keys_sql) .bind(&now) .bind(&id) .execute(&state.db) .await .map_err(|e| AppError::Internal(format!("failed to revoke api keys: {e}")))?;
let delete_sql = adapt_sql("DELETE FROM users WHERE id = ?", backend);
let result = sqlx::query(&delete_sql) .bind(&id) .execute(&state.db) .await .map_err(|e| AppError::Internal(format!("failed to delete user: {e}")))?;
if result.rows_affected() == 0 { return Err(AppError::NotFound(format!("user '{id}' not found"))); }
log_event( &state.db, EventLog { event_type: "user.deleted".to_string(), severity: Severity::Info, actor_did: Some(auth.did.clone()), subject: Some(id), detail: serde_json::json!({}), }, backend, ) .await;
Ok(StatusCode::NO_CONTENT)}
/// POST /admin/users/transfer-super — transfer super user status.pub(super) async fn transfer_super( State(state): State<AppState>, auth: UserAuth, Json(body): Json<TransferSuperBody>,) -> Result<StatusCode, AppError> { if !auth.is_super { return Err(AppError::Forbidden( "Only the super user can transfer super status".into(), )); }
let backend = state.db_backend; let now = now_rfc3339();
// Remove super from current user let update1_sql = adapt_sql("UPDATE users SET is_super = ? WHERE id = ?", backend); sqlx::query(&update1_sql) .bind(0_i32) .bind(&auth.user_id) .execute(&state.db) .await .map_err(|e| AppError::Internal(format!("failed to remove super: {e}")))?;
// Set super on target user let update2_sql = adapt_sql("UPDATE users SET is_super = ? WHERE id = ?", backend); let result = sqlx::query(&update2_sql) .bind(1_i32) .bind(&body.target_user_id) .execute(&state.db) .await .map_err(|e| AppError::Internal(format!("failed to set super: {e}")))?;
if result.rows_affected() == 0 { // Restore super on current user let restore_sql = adapt_sql("UPDATE users SET is_super = ? WHERE id = ?", backend); let _ = sqlx::query(&restore_sql) .bind(1_i32) .bind(&auth.user_id) .execute(&state.db) .await; return Err(AppError::NotFound(format!( "user '{}' not found", body.target_user_id ))); }
// Ensure target has all permissions let perm_sql = adapt_sql( "INSERT INTO user_permissions (user_id, permission, granted_by, granted_at) VALUES (?, ?, ?, ?) ON CONFLICT DO NOTHING", backend, );
for perm in Permission::all() { sqlx::query(&perm_sql) .bind(&body.target_user_id) .bind(perm.as_str()) .bind(&auth.user_id) .bind(&now) .execute(&state.db) .await .map_err(|e| AppError::Internal(format!("failed to grant permission: {e}")))?; }
log_event( &state.db, EventLog { event_type: "user.super_transferred".to_string(), severity: Severity::Warn, actor_did: Some(auth.did.clone()), subject: Some(body.target_user_id.clone()), detail: serde_json::json!({ "from_user_id": auth.user_id, "to_user_id": body.target_user_id, }), }, backend, ) .await;
Ok(StatusCode::NO_CONTENT)}