#[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; }