Microservice to bring 2FA to self hosted PDSes
at feature/admin-rbac 109 lines 3.8 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 crate::AppState; 10 11use super::rbac::RbacConfig; 12use super::session; 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 40 .can_access_endpoint(did, "com.atproto.admin.getAccountInfo") 41 || rbac.can_access_endpoint(did, "com.atproto.admin.getAccountInfos"), 42 can_manage_takedowns: rbac 43 .can_access_endpoint(did, "com.atproto.admin.updateSubjectStatus"), 44 can_delete_account: rbac 45 .can_access_endpoint(did, "com.atproto.admin.deleteAccount"), 46 can_reset_password: rbac 47 .can_access_endpoint(did, "com.atproto.admin.updateAccountPassword"), 48 can_create_account: rbac 49 .can_access_endpoint(did, "com.atproto.server.createAccount"), 50 can_manage_invites: rbac 51 .can_access_endpoint(did, "com.atproto.admin.getInviteCodes"), 52 can_create_invite: rbac 53 .can_access_endpoint(did, "com.atproto.server.createInviteCode"), 54 can_send_email: rbac.can_access_endpoint(did, "com.atproto.admin.sendEmail"), 55 can_request_crawl: rbac 56 .can_access_endpoint(did, "com.atproto.sync.requestCrawl"), 57 } 58 } 59} 60 61/// Middleware that checks for a valid admin session cookie. 62/// If valid, injects AdminSession and AdminPermissions into request extensions. 63/// If invalid or missing, redirects to /admin/login. 64pub async fn admin_auth_middleware( 65 State(state): State<AppState>, 66 jar: SignedCookieJar, 67 mut req: Request, 68 next: Next, 69) -> Response { 70 let rbac = match &state.admin_rbac_config { 71 Some(rbac) => rbac, 72 None => return StatusCode::NOT_FOUND.into_response(), 73 }; 74 75 // Extract session ID from signed cookie 76 let session_id = match jar.get("__gatekeeper_admin_session") { 77 Some(cookie) => cookie.value().to_string(), 78 None => return Redirect::to("/admin/login").into_response(), 79 }; 80 81 // Look up session in database 82 let session_row = match session::get_session(&state.pds_gatekeeper_pool, &session_id).await { 83 Ok(Some(row)) => row, 84 Ok(None) => return Redirect::to("/admin/login").into_response(), 85 Err(e) => { 86 tracing::error!("Failed to look up admin session: {}", e); 87 return Redirect::to("/admin/login").into_response(); 88 } 89 }; 90 91 // Verify the DID is still a valid member 92 if !rbac.is_member(&session_row.did) { 93 return Redirect::to("/admin/login").into_response(); 94 } 95 96 let roles = rbac.get_member_roles(&session_row.did); 97 let permissions = AdminPermissions::compute(rbac, &session_row.did); 98 99 let admin_session = AdminSession { 100 did: session_row.did, 101 handle: session_row.handle, 102 roles, 103 }; 104 105 req.extensions_mut().insert(admin_session); 106 req.extensions_mut().insert(permissions); 107 108 next.run(req).await 109}