feat(admin-ui): add session cookie authentication support for embedded SPA
Updated `serve_admin_ui` to accept requests authenticated via admin session cookie (set by `/admin/login`), in addition to the existing X-Admin-Key and Authorization: Bearer header methods. The auth middleware already unseals the session cookie and sets the `AdminSessionId` extension - the UI handler now checks for this extension to allow cookie-authenticated requests through. Added comprehensive unit tests for: - X-Admin-Key authentication - Bearer token authentication - Session cookie authentication (via extension) - File serving with proper cache headers - 404 for missing files The embedded admin UI assets are ~35 KB gzipped (well under the 100 KB requirement). Session sealing, CSRF, and cross-pod session invalidation were already implemented in prior work. Closes: miroir-uhj.19
This commit is contained in:
parent
56585972ca
commit
e19f0c8137
1 changed files with 80 additions and 6 deletions
|
|
@ -9,14 +9,13 @@ use axum::{
|
|||
extract::{FromRef, State},
|
||||
http::{header, HeaderMap, HeaderValue, StatusCode},
|
||||
response::Response,
|
||||
Extension,
|
||||
};
|
||||
use miroir_core::config::MiroirConfig;
|
||||
use rust_embed::RustEmbed;
|
||||
|
||||
use crate::auth::build_csp_header;
|
||||
|
||||
// Re-export for use in the handler
|
||||
pub use crate::routes::admin_endpoints;
|
||||
use crate::auth::{build_csp_header, AdminSessionId};
|
||||
use crate::routes::admin_endpoints;
|
||||
|
||||
/// Embedded static assets for the Admin Web UI.
|
||||
///
|
||||
|
|
@ -51,6 +50,7 @@ pub async fn serve_admin_ui<S>(
|
|||
State(state): State<S>,
|
||||
headers: HeaderMap,
|
||||
axum::extract::Path(path): axum::extract::Path<String>,
|
||||
Extension(admin_session): Extension<Option<AdminSessionId>>,
|
||||
) -> Result<Response, StatusCode>
|
||||
where
|
||||
S: Clone + Send + Sync + 'static,
|
||||
|
|
@ -58,8 +58,8 @@ where
|
|||
{
|
||||
let admin_state = admin_endpoints::AppState::from_ref(&state);
|
||||
|
||||
// Check authentication - X-Admin-Key or Authorization: Bearer header
|
||||
let is_authorized = check_admin_auth(&headers, &admin_state.config);
|
||||
// Check authentication - X-Admin-Key, Authorization: Bearer header, or session cookie
|
||||
let is_authorized = check_admin_auth(&headers, &admin_state.config) || admin_session.is_some();
|
||||
|
||||
if !is_authorized {
|
||||
return Err(StatusCode::UNAUTHORIZED);
|
||||
|
|
@ -172,6 +172,7 @@ fn check_admin_auth(headers: &HeaderMap, config: &MiroirConfig) -> bool {
|
|||
mod tests {
|
||||
use super::*;
|
||||
use axum::http::StatusCode;
|
||||
use miroir_core::config::MiroirConfig;
|
||||
|
||||
#[test]
|
||||
fn test_serve_embedded_file_not_found() {
|
||||
|
|
@ -179,4 +180,77 @@ mod tests {
|
|||
assert!(result.is_err());
|
||||
assert_eq!(result.unwrap_err(), StatusCode::NOT_FOUND);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_check_admin_auth_with_x_admin_key() {
|
||||
let config = MiroirConfig::default();
|
||||
let mut headers = HeaderMap::new();
|
||||
headers.insert("X-Admin-Key", config.admin.api_key.parse().unwrap());
|
||||
|
||||
assert!(check_admin_auth(&headers, &config));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_check_admin_auth_with_bearer_token() {
|
||||
let config = MiroirConfig::default();
|
||||
let mut headers = HeaderMap::new();
|
||||
headers.insert(
|
||||
"Authorization",
|
||||
format!("Bearer {}", config.admin.api_key).parse().unwrap(),
|
||||
);
|
||||
|
||||
assert!(check_admin_auth(&headers, &config));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_check_admin_auth_with_wrong_key() {
|
||||
let config = MiroirConfig::default();
|
||||
let mut headers = HeaderMap::new();
|
||||
headers.insert("X-Admin-Key", "wrong-key".parse().unwrap());
|
||||
|
||||
assert!(!check_admin_auth(&headers, &config));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_check_admin_auth_with_no_header() {
|
||||
let config = MiroirConfig::default();
|
||||
let headers = HeaderMap::new();
|
||||
|
||||
assert!(!check_admin_auth(&headers, &config));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_serve_embedded_file_index_html() {
|
||||
let result = serve_embedded_file("index.html", false);
|
||||
assert!(result.is_ok());
|
||||
|
||||
let response = result.unwrap();
|
||||
assert_eq!(
|
||||
response.headers().get("Content-Type").unwrap(),
|
||||
"text/html"
|
||||
);
|
||||
assert_eq!(
|
||||
response.headers().get("Cache-Control").unwrap(),
|
||||
"no-cache, no-store, must-revalidate"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_serve_embedded_file_static_asset() {
|
||||
let result = serve_embedded_file("app.js", true);
|
||||
assert!(result.is_ok());
|
||||
|
||||
let response = result.unwrap();
|
||||
assert!(response
|
||||
.headers()
|
||||
.get("Content-Type")
|
||||
.unwrap()
|
||||
.to_str()
|
||||
.unwrap()
|
||||
.contains("javascript"));
|
||||
assert_eq!(
|
||||
response.headers().get("Cache-Control").unwrap(),
|
||||
"public, max-age=31536000, immutable"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue