Dashboard endpoint and page aggregating inventory, vulnerabilities, cluster health, backups and recent jobs. Security headers (CSP, nosniff, DENY, referrer policy), 1 MB body limit, configurable login rate limit, audit steps in CI. Installer script, systemd unit, install/architecture docs. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
73 lines
2.4 KiB
Rust
73 lines
2.4 KiB
Rust
//! WP-41: security headers and cookie hardening.
|
|
mod common;
|
|
|
|
use axum::http::StatusCode;
|
|
use common::{get, post, test_app_with_admin};
|
|
use serde_json::json;
|
|
|
|
#[tokio::test]
|
|
async fn responses_carry_security_headers() {
|
|
let app = test_app_with_admin().await;
|
|
let res = get(&app, "/healthz", None).await;
|
|
let h = |k: &str| {
|
|
res.headers
|
|
.get(k)
|
|
.and_then(|v| v.to_str().ok())
|
|
.unwrap_or("")
|
|
.to_string()
|
|
};
|
|
assert_eq!(h("x-content-type-options"), "nosniff");
|
|
assert_eq!(h("x-frame-options"), "DENY");
|
|
assert_eq!(h("referrer-policy"), "same-origin");
|
|
let csp = h("content-security-policy");
|
|
assert!(csp.contains("default-src 'self'"), "{csp}");
|
|
assert!(csp.contains("frame-ancestors 'none'"), "{csp}");
|
|
assert_eq!(h("cache-control"), "no-store");
|
|
assert!(res.headers.get("server").is_none());
|
|
|
|
// API errors are JSON, not HTML, and still carry the headers
|
|
let res = get(&app, "/api/users", None).await;
|
|
assert_eq!(res.status, StatusCode::UNAUTHORIZED);
|
|
assert_eq!(res.json["error"], "unauthorized");
|
|
assert_eq!(
|
|
res.headers.get("x-content-type-options").unwrap(),
|
|
"nosniff"
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn refresh_cookie_is_strict_and_scoped() {
|
|
let app = test_app_with_admin().await;
|
|
let res = post(
|
|
&app,
|
|
"/api/auth/login",
|
|
json!({"email": "admin@example.com", "password": "admin-password-123"}),
|
|
None,
|
|
)
|
|
.await;
|
|
let cookie = res.headers.get("set-cookie").unwrap().to_str().unwrap();
|
|
assert!(
|
|
cookie.contains("HttpOnly")
|
|
&& cookie.contains("SameSite=Strict")
|
|
&& cookie.contains("Path=/api/auth")
|
|
);
|
|
// cross-site style request without the cookie cannot refresh
|
|
assert_eq!(
|
|
post(&app, "/api/auth/refresh", json!({}), None)
|
|
.await
|
|
.status,
|
|
StatusCode::UNAUTHORIZED
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn oversized_json_bodies_are_rejected() {
|
|
let app = test_app_with_admin().await;
|
|
let token = common::login(&app, "admin@example.com", "admin-password-123")
|
|
.await
|
|
.access;
|
|
let big = "x".repeat(2 * 1024 * 1024);
|
|
let res = post(&app, "/api/users", json!({"email": "a@b.de", "display_name": big, "password": "user-password-123", "role": "user"}), Some(&token)).await;
|
|
assert_eq!(res.status, StatusCode::PAYLOAD_TOO_LARGE);
|
|
}
|