Microservice to bring 2FA to self hosted PDSes
at feature/admin-rbac 120 lines 4.4 kB view raw
1use axum::{ 2 extract::{Request, State}, 3 http::StatusCode, 4 middleware::Next, 5 response::{IntoResponse, Redirect, Response}, 6}; 7use axum_extra::extract::cookie::SignedCookieJar; 8 9use super::rbac::RbacConfig; 10use super::session; 11use crate::AppState; 12use jacquard_common::types::did::Did; 13 14/// Admin session data injected into request extensions. 15#[derive(Debug, Clone)] 16pub struct AdminSession { 17 pub did: String, 18 pub handle: String, 19 pub roles: Vec<String>, 20} 21 22/// Pre-computed permission flags for template rendering and quick checks. 23#[derive(Debug, Clone)] 24pub struct AdminPermissions { 25 pub can_view_accounts: bool, 26 pub can_manage_takedowns: bool, 27 pub can_delete_account: bool, 28 pub can_reset_password: bool, 29 pub can_create_account: bool, 30 pub can_manage_invites: bool, 31 pub can_create_invite: bool, 32 pub can_send_email: bool, 33 pub can_request_crawl: bool, 34} 35 36impl AdminPermissions { 37 pub fn compute(rbac: &RbacConfig, did: &str) -> Self { 38 Self { 39 can_view_accounts: rbac.can_access_endpoint(did, "com.atproto.admin.getAccountInfo") 40 || rbac.can_access_endpoint(did, "com.atproto.admin.getAccountInfos"), 41 can_manage_takedowns: rbac 42 .can_access_endpoint(did, "com.atproto.admin.updateSubjectStatus"), 43 can_delete_account: rbac.can_access_endpoint(did, "com.atproto.admin.deleteAccount"), 44 can_reset_password: rbac 45 .can_access_endpoint(did, "com.atproto.admin.updateAccountPassword"), 46 can_create_account: rbac.can_access_endpoint(did, "com.atproto.server.createAccount"), 47 can_manage_invites: rbac.can_access_endpoint(did, "com.atproto.admin.getInviteCodes"), 48 can_create_invite: rbac.can_access_endpoint(did, "com.atproto.server.createInviteCode"), 49 can_send_email: rbac.can_access_endpoint(did, "com.atproto.admin.sendEmail"), 50 can_request_crawl: rbac.can_access_endpoint(did, "com.atproto.sync.requestCrawl"), 51 } 52 } 53} 54 55/// Middleware that checks for a valid admin session cookie. 56/// If valid, injects AdminSession and AdminPermissions into request extensions. 57/// If invalid or missing, redirects to /admin/login. 58pub async fn admin_auth_middleware( 59 State(state): State<AppState>, 60 jar: SignedCookieJar, 61 mut req: Request, 62 next: Next, 63) -> Response { 64 let rbac = match &state.admin_rbac_config { 65 Some(rbac) => rbac, 66 None => return StatusCode::NOT_FOUND.into_response(), 67 }; 68 69 // Extract session ID from signed cookie 70 let session_id = match jar.get("__gatekeeper_admin_session") { 71 Some(cookie) => cookie.value().to_string(), 72 None => return Redirect::to("/admin/login").into_response(), 73 }; 74 75 // Look up session in database 76 let session_row = match session::get_session(&state.pds_gatekeeper_pool, &session_id).await { 77 Ok(Some(row)) => row, 78 Ok(None) => return Redirect::to("/admin/login").into_response(), 79 Err(e) => { 80 tracing::error!("Failed to look up admin session: {}", e); 81 return Redirect::to("/admin/login").into_response(); 82 } 83 }; 84 85 // Verify the DID is still a valid member 86 if !rbac.is_member(&session_row.did) { 87 return Redirect::to("/admin/login").into_response(); 88 } 89 90 let oauth_client = if let Some(client) = &state.admin_oauth_client { 91 client 92 } else { 93 return Redirect::to("/admin/login").into_response(); 94 }; 95 96 let did: Did = session_row.did.clone().into(); 97 let oauth_session_id = session_row.oauth_session_id.clone(); 98 match oauth_client.restore(&did, oauth_session_id.as_str()).await { 99 Ok(_) => {} 100 Err(e) => { 101 tracing::error!("Failed to restore admin session: {}", e); 102 let error_msg = e.to_string(); 103 return Redirect::to(&format!("/admin/login?error={}", error_msg)).into_response(); 104 } 105 } 106 107 let roles = rbac.get_member_roles(&session_row.did); 108 let permissions = AdminPermissions::compute(rbac, &session_row.did); 109 110 let admin_session = AdminSession { 111 did: session_row.did, 112 handle: session_row.handle, 113 roles, 114 }; 115 116 req.extensions_mut().insert(admin_session); 117 req.extensions_mut().insert(permissions); 118 119 next.run(req).await 120}