Microservice to bring 2FA to self hosted PDSes
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}