Files
RustProxy/src/admin/tests/part_03.rs
T
2026-10-08 11:27:18 +02:00

433 lines
17 KiB
Rust

#[tokio::test]
async fn csrf_required_and_logout_revokes_cookie() {
let (router, store, _dir, secret, csrf) = fixture().await;
let cookie = format!("{}={secret}", auth::COOKIE_NAME);
let req = request("/api/auth/logout")
.method("POST")
.header("cookie", &cookie)
.header("origin", "http://127.0.0.1:9090")
.header("x-rustproxy-request", "1")
.body(Body::empty())
.unwrap();
assert_eq!(
router.clone().oneshot(req).await.unwrap().status(),
StatusCode::FORBIDDEN
);
let req = request("/api/auth/logout")
.method("POST")
.header("cookie", &cookie)
.header("origin", "http://127.0.0.1:9090")
.header("x-rustproxy-request", "1")
.header("x-csrf-token", csrf)
.body(Body::empty())
.unwrap();
assert_eq!(
router.clone().oneshot(req).await.unwrap().status(),
StatusCode::OK
);
let req = request("/api/status")
.header("cookie", &cookie)
.body(Body::empty())
.unwrap();
assert_eq!(
router.oneshot(req).await.unwrap().status(),
StatusCode::UNAUTHORIZED
);
store.close().await;
}
#[tokio::test]
async fn upload_and_scoped_queries_keep_control_plane_guards() {
let (router, store, _dir, secret, csrf) = fixture().await;
let cookie = format!("{}={secret}", auth::COOKIE_NAME);
for (path, body) in [
(
"/api/certificates/upload",
r#"{"cert_pem":"test","key_pem":"test","hosts":["app.localhost"]}"#,
),
(
"/api/certificates/deploy",
r#"{"certificate_id":"shared","cert_pem":"test","key_pem":"test"}"#,
),
] {
for (authenticated, valid_csrf, expected) in
[(false, false, 401), (true, false, 403), (true, true, 409)]
{
let mut req = request(path)
.method("POST")
.header("content-type", "application/json")
.header("origin", "http://127.0.0.1:9090")
.header("x-rustproxy-request", "1");
if authenticated {
req = req.header("cookie", &cookie);
}
if valid_csrf {
req = req.header("x-csrf-token", &csrf);
}
assert_eq!(
router
.clone()
.oneshot(req.body(Body::from(body)).unwrap())
.await
.unwrap()
.status()
.as_u16(),
expected
);
}
}
for (path, expected) in [
("/api/logs?vhost=demo&limit=8", 200),
("/api/logs?cache=BAD", 400),
("/api/file-logs", 200),
("/api/file-logs/query?source=all&limit=8", 200),
("/api/file-logs/query?limit=0", 400),
("/api/traffic", 200),
("/api/cache", 200),
("/api/transform", 200),
] {
let req = request(path)
.header("cookie", &cookie)
.body(Body::empty())
.unwrap();
assert_eq!(
router.clone().oneshot(req).await.unwrap().status().as_u16(),
expected
);
}
let req = request("/api/cache/purge?vhost=missing")
.method("POST")
.header("cookie", &cookie)
.header("origin", "http://127.0.0.1:9090")
.header("x-rustproxy-request", "1")
.header("x-csrf-token", &csrf)
.body(Body::empty())
.unwrap();
assert_eq!(
router.oneshot(req).await.unwrap().status(),
StatusCode::NOT_FOUND
);
store.close().await;
}
#[tokio::test]
async fn certificate_reload_keeps_configuration_revision_stable() {
let (router, store, _dir, secret, csrf) = fixture().await;
let cookie = format!("{}={secret}", auth::COOKIE_NAME);
let response = router
.clone()
.oneshot(
request("/api/certificates/reload")
.method("POST")
.header("content-type", "application/json")
.header("cookie", &cookie)
.header("origin", "http://127.0.0.1:9090")
.header("x-rustproxy-request", "1")
.header("x-csrf-token", &csrf)
.body(Body::from("{}"))
.unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::OK);
let payload = json_body(response).await;
assert_eq!(payload["reloaded"], true);
assert_eq!(payload["revision"], 0);
let configuration = router
.oneshot(
request("/api/config")
.header("cookie", &cookie)
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(json_body(configuration).await["revision"], 0);
store.close().await;
}
#[tokio::test]
async fn manual_certificate_can_be_reloaded_by_id_without_revision_change() {
let mut config = Config::default();
config.tls.certificates.push(crate::tls::Certificate {
issuer: None,
id: "manual-file".into(),
covered_hosts: Vec::new(),
hosts: Vec::new(),
cert_file: "default.pem".into(),
key_file: "default.key".into(),
});
let (router, store, _dir, secret, csrf) = fixture_with_certificate_storage(config).await;
let cookie = format!("{}={secret}", auth::COOKIE_NAME);
let response = router
.clone()
.oneshot(
request("/api/certificates/manual-file/reload")
.method("POST")
.header("cookie", &cookie)
.header("origin", "http://127.0.0.1:9090")
.header("x-rustproxy-request", "1")
.header("x-csrf-token", &csrf)
.header("content-type", "application/json")
.body(Body::from("{}"))
.unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::OK);
let payload = json_body(response).await;
assert_eq!(payload["certificate_id"], "manual-file");
assert_eq!(payload["revision"], 0);
assert_eq!(payload["reloaded"], true);
let api_token = "c".repeat(64);
store
.upsert_certificate_api_credential(
"file-renewer",
&crate::auth::secret_hash(&api_token),
"external file renewal hook",
false,
&["manual-file".to_owned()],
"owner",
)
.await
.unwrap();
let external = router
.clone()
.oneshot(
request("/api/certificates/manual-file/reload")
.method("POST")
.header("authorization", format!("Bearer {api_token}"))
.header("x-rustproxy-certificate-key", "file-renewer")
.header("content-type", "application/json")
.body(Body::from("{}"))
.unwrap(),
)
.await
.unwrap();
assert_eq!(external.status(), StatusCode::OK);
assert_eq!(json_body(external).await["certificate_id"], "manual-file");
store.close().await;
}
#[tokio::test]
async fn certificate_api_tokens_are_scoped_commented_and_individually_revocable() {
let (router, store, _dir, secret, csrf) = fixture().await;
let cookie = format!("{}={secret}", auth::COOKIE_NAME);
let token = "a".repeat(64);
let token_two = "b".repeat(64);
let invalid_key = router
.clone()
.oneshot(
request("/api/certificate-api")
.method("PUT")
.header("cookie", &cookie)
.header("origin", "http://127.0.0.1:9090")
.header("x-rustproxy-request", "1")
.header("x-csrf-token", &csrf)
.header("content-type", "application/json")
.body(Body::from(json!({"key_id":"x","token":token,"certificate_ids":["shared-cert"]}).to_string()))
.unwrap(),
)
.await
.unwrap();
assert_eq!(invalid_key.status(), StatusCode::UNPROCESSABLE_ENTITY);
for (key_id, value, comment) in [
("certbot-main", token.as_str(), "production certbot"),
("deploy-hook", token_two.as_str(), "secondary deploy hook"),
] {
let rotated = router
.clone()
.oneshot(
request("/api/certificate-api")
.method("PUT")
.header("cookie", &cookie)
.header("origin", "http://127.0.0.1:9090")
.header("x-rustproxy-request", "1")
.header("x-csrf-token", &csrf)
.header("content-type", "application/json")
.body(Body::from(
json!({
"key_id":key_id,
"token":value,
"comment":comment,
"certificate_ids":[if key_id == "certbot-main" { "shared-cert" } else { "deploy-cert" }],
"allow_deploy": key_id == "deploy-hook"
}).to_string(),
))
.unwrap(),
)
.await
.unwrap();
assert_eq!(rotated.status(), StatusCode::OK);
let rotated = json_body(rotated).await;
assert_eq!(rotated["configured"], true);
assert_eq!(rotated["credential"]["key_id"], key_id);
assert_eq!(rotated["credential"]["comment"], comment);
assert_eq!(rotated["credential"]["allow_deploy"], key_id == "deploy-hook");
assert_eq!(rotated["credential"]["certificate_ids"].as_array().unwrap().len(), 1);
assert_eq!(rotated["token"], value);
}
let status = router
.clone()
.oneshot(
request("/api/certificate-api")
.header("cookie", &cookie)
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
let status = json_body(status).await;
assert_eq!(status["configured"], true);
assert_eq!(status["credentials"].as_array().unwrap().len(), 2);
assert!(status.get("token").is_none());
let external = |path: &'static str, key_id: &'static str, value: String| {
request(path)
.method("POST")
.header("authorization", format!("Bearer {value}"))
.header("x-rustproxy-certificate-key", key_id)
.header("content-type", "application/json")
.body(Body::from("{}"))
.unwrap()
};
// Certificate API credentials cannot trigger an all-certificate reload.
let response = router
.clone()
.oneshot(external("/api/certificates/reload", "certbot-main", token.clone()))
.await
.unwrap();
assert_eq!(response.status(), StatusCode::FORBIDDEN);
// The allowlisted ID passes the auth gate and reaches the handler; this
// fixture intentionally has no such configured certificate, so it is 404.
let response = router
.clone()
.oneshot(external("/api/certificates/shared-cert/reload", "certbot-main", token.clone()))
.await
.unwrap();
assert_eq!(response.status(), StatusCode::NOT_FOUND);
// A reload-only token cannot use the deploy endpoint at all.
let response = router
.clone()
.oneshot(
request("/api/certificates/deploy")
.method("POST")
.header("authorization", format!("Bearer {token}"))
.header("x-rustproxy-certificate-key", "certbot-main")
.header("content-type", "application/json")
.body(Body::from("{}"))
.unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::FORBIDDEN);
// A different ID is rejected by the credential grant before the handler.
let response = router
.clone()
.oneshot(external("/api/certificates/deploy-cert/reload", "certbot-main", token.clone()))
.await
.unwrap();
assert_eq!(response.status(), StatusCode::FORBIDDEN);
let response = router
.clone()
.oneshot(external("/api/certificates/deploy-cert/reload", "deploy-hook", token_two.clone()))
.await
.unwrap();
assert_eq!(response.status(), StatusCode::NOT_FOUND);
let response = router
.clone()
.oneshot(
request("/api/certificates/deploy")
.method("POST")
.header("authorization", format!("Bearer {token_two}"))
.header("x-rustproxy-certificate-key", "deploy-hook")
.header("content-type", "application/json")
.body(Body::from(
json!({"certificate_id":"shared-cert","cert_pem":"bad","key_pem":"bad"}).to_string(),
))
.unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::FORBIDDEN);
let missing_key = router
.clone()
.oneshot(
request("/api/certificates/reload")
.method("POST")
.header("authorization", format!("Bearer {token}"))
.header("content-type", "application/json")
.body(Body::from("{}"))
.unwrap(),
)
.await
.unwrap();
assert_eq!(missing_key.status(), StatusCode::UNAUTHORIZED);
let forbidden_scope = router
.clone()
.oneshot(
request("/api/config")
.header("authorization", format!("Bearer {token}"))
.header("x-rustproxy-certificate-key", "certbot-main")
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(forbidden_scope.status(), StatusCode::UNAUTHORIZED);
let revoked = router
.clone()
.oneshot(
request("/api/certificate-api/certbot-main")
.method("DELETE")
.header("cookie", &cookie)
.header("origin", "http://127.0.0.1:9090")
.header("x-rustproxy-request", "1")
.header("x-csrf-token", &csrf)
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(revoked.status(), StatusCode::OK);
let response = router
.clone()
.oneshot(external("/api/certificates/shared-cert/reload", "certbot-main", token.clone()))
.await
.unwrap();
assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
let response = router
.clone()
.oneshot(external("/api/certificates/deploy-cert/reload", "deploy-hook", token_two.clone()))
.await
.unwrap();
assert_eq!(response.status(), StatusCode::NOT_FOUND);
let revoked_all = router
.clone()
.oneshot(
request("/api/certificate-api")
.method("DELETE")
.header("cookie", &cookie)
.header("origin", "http://127.0.0.1:9090")
.header("x-rustproxy-request", "1")
.header("x-csrf-token", &csrf)
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(revoked_all.status(), StatusCode::OK);
let response = router
.oneshot(external("/api/certificates/deploy-cert/reload", "deploy-hook", token_two))
.await
.unwrap();
assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
store.close().await;
}