251 lines
9.8 KiB
Rust
251 lines
9.8 KiB
Rust
#[tokio::test]
|
|
async fn docs_require_the_same_session_and_do_not_redirect_schema_fetches() {
|
|
let (router, store, _dir, secret, _) = fixture().await;
|
|
for path in ["/docs", "/docs/"] {
|
|
let response = router
|
|
.clone()
|
|
.oneshot(request(path).body(Body::empty()).unwrap())
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(response.status(), StatusCode::SEE_OTHER);
|
|
assert_eq!(response.headers()["location"], "/?next=docs");
|
|
}
|
|
let cookie = format!("{}={secret}", auth::COOKIE_NAME);
|
|
for path in [
|
|
"/",
|
|
"/docs",
|
|
"/docs/",
|
|
"/docs/licenses.txt",
|
|
"/docs/content.json",
|
|
&format!("/assets/app_{}.js", super::asset_build_id()),
|
|
] {
|
|
let response = router
|
|
.clone()
|
|
.oneshot(
|
|
request(path)
|
|
.header("cookie", &cookie)
|
|
.body(Body::empty())
|
|
.unwrap(),
|
|
)
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(response.status(), StatusCode::OK, "{path}");
|
|
assert_eq!(response.headers()["x-frame-options"], "DENY");
|
|
assert_eq!(response.headers()["cache-control"], "no-store");
|
|
}
|
|
let response = router
|
|
.clone()
|
|
.oneshot(
|
|
request("/docs/openapi.json")
|
|
.header("cookie", &cookie)
|
|
.body(Body::empty())
|
|
.unwrap(),
|
|
)
|
|
.await
|
|
.unwrap();
|
|
let spec = json_body(response).await;
|
|
assert_eq!(spec["openapi"], "3.0.3");
|
|
assert_eq!(spec["info"]["version"], env!("CARGO_PKG_VERSION"));
|
|
assert_eq!(
|
|
spec["components"]["securitySchemes"]["AdminSession"]["in"],
|
|
"cookie"
|
|
);
|
|
assert!(spec["paths"].get("/api/config").is_some());
|
|
assert!(spec["paths"].get("/api/config/history/restore").is_some());
|
|
let response = router
|
|
.clone()
|
|
.oneshot(
|
|
request("/docs/content.json")
|
|
.header("cookie", &cookie)
|
|
.body(Body::empty())
|
|
.unwrap(),
|
|
)
|
|
.await
|
|
.unwrap();
|
|
let docs = json_body(response).await;
|
|
let documents = docs["documents"].as_array().expect("documentation bundle");
|
|
assert!(documents.iter().any(|document| document["path"] == "docs/README.md"));
|
|
assert!(documents.iter().any(|document| document["path"] == "docs/WEB_SERVER.md"));
|
|
assert!(documents.iter().any(|document| document["path"] == "docs/PHP_FPM.md"));
|
|
assert!(documents.iter().any(|document| document["path"] == "docs/SECURITY_AUDIT_0.31.0.md"));
|
|
assert!(documents.iter().any(|document| document["path"] == "docs/TEST_REPORT.md"));
|
|
store.close().await;
|
|
}
|
|
#[tokio::test]
|
|
async fn credentials_in_query_or_duplicate_cookies_cannot_authorize() {
|
|
let (router, store, _dir, secret, _) = fixture().await;
|
|
for cookie in [
|
|
String::new(),
|
|
format!("{}=invalid", auth::COOKIE_NAME),
|
|
format!(
|
|
"{}={secret}; {}={secret}",
|
|
auth::COOKIE_NAME,
|
|
auth::COOKIE_NAME
|
|
),
|
|
] {
|
|
let path = format!("/api/config?token={secret}&session={secret}");
|
|
let response = router
|
|
.clone()
|
|
.oneshot(
|
|
request(&path)
|
|
.header("cookie", cookie)
|
|
.header("authorization", format!("Bearer {secret}"))
|
|
.body(Body::empty())
|
|
.unwrap(),
|
|
)
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
|
|
}
|
|
store.close().await;
|
|
}
|
|
#[tokio::test]
|
|
async fn duplicate_csrf_or_marker_is_rejected_and_session_survives() {
|
|
let (router, store, _dir, secret, csrf) = fixture().await;
|
|
let cookie = format!("{}={secret}", auth::COOKIE_NAME);
|
|
for duplicate in ["x-csrf-token", "x-rustproxy-request"] {
|
|
let response = router
|
|
.clone()
|
|
.oneshot(
|
|
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)
|
|
.header(
|
|
duplicate,
|
|
if duplicate == "x-csrf-token" {
|
|
csrf.as_str()
|
|
} else {
|
|
"1"
|
|
},
|
|
)
|
|
.body(Body::empty())
|
|
.unwrap(),
|
|
)
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(response.status(), StatusCode::FORBIDDEN);
|
|
}
|
|
let response = router
|
|
.oneshot(
|
|
request("/api/auth/session")
|
|
.header("cookie", cookie)
|
|
.body(Body::empty())
|
|
.unwrap(),
|
|
)
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(response.status(), StatusCode::OK);
|
|
store.close().await;
|
|
}
|
|
#[tokio::test]
|
|
async fn malformed_anonymous_json_never_reflects_submitted_values() {
|
|
let (router, store, _dir, _, _) = fixture().await;
|
|
for path in ["/api/auth/login", "/api/auth/setup"] {
|
|
let response = router
|
|
.clone()
|
|
.oneshot(
|
|
request(path)
|
|
.method("POST")
|
|
.header("origin", "http://127.0.0.1:9090")
|
|
.header("x-rustproxy-request", "1")
|
|
.header("content-type", "application/json")
|
|
.body(Body::from(
|
|
r#"{"username":{"untrusted":"DO_NOT_REFLECT_ME"}}"#,
|
|
))
|
|
.unwrap(),
|
|
)
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(response.status(), StatusCode::BAD_REQUEST);
|
|
assert_eq!(
|
|
json_body(response).await,
|
|
json!({"error":"auth.invalid_request"})
|
|
);
|
|
}
|
|
store.close().await;
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn retained_configuration_revision_can_be_restored_as_a_new_revision() {
|
|
let (router, store, _dir, secret, csrf) = fixture().await;
|
|
let cookie = format!("{}={secret}", auth::COOKIE_NAME);
|
|
|
|
let mut first = Config::default();
|
|
first.server_header.value = "first".into();
|
|
let response = router
|
|
.clone()
|
|
.oneshot(
|
|
request("/api/config")
|
|
.method("PUT")
|
|
.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(
|
|
json!({"expected_revision":0,"config":first}).to_string(),
|
|
))
|
|
.unwrap(),
|
|
)
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(response.status(), StatusCode::OK);
|
|
assert_eq!(json_body(response).await["revision"], 1);
|
|
|
|
let mut second = Config::default();
|
|
second.server_header.value = "second".into();
|
|
let response = router
|
|
.clone()
|
|
.oneshot(
|
|
request("/api/config")
|
|
.method("PUT")
|
|
.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(
|
|
json!({"expected_revision":1,"config":second}).to_string(),
|
|
))
|
|
.unwrap(),
|
|
)
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(response.status(), StatusCode::OK);
|
|
assert_eq!(json_body(response).await["revision"], 2);
|
|
|
|
let response = router
|
|
.clone()
|
|
.oneshot(
|
|
request("/api/config/history/restore")
|
|
.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(
|
|
json!({"revision":1,"expected_revision":2}).to_string(),
|
|
))
|
|
.unwrap(),
|
|
)
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(response.status(), StatusCode::OK);
|
|
let restored = json_body(response).await;
|
|
assert_eq!(restored["revision"], 3);
|
|
assert_eq!(restored["config"]["server_header"]["value"], "first");
|
|
|
|
let audit = store.recent_audit().await.unwrap();
|
|
assert_eq!(audit[0].actor, "owner");
|
|
assert_eq!(audit[0].action, "config.restore");
|
|
assert_eq!(audit[0].detail, "source_revision=1 revision=3");
|
|
let detail = store.configuration_revision(3).await.unwrap().unwrap();
|
|
assert_eq!(detail.config.server_header.value, "first");
|
|
assert_eq!(detail.previous_config.unwrap().server_header.value, "second");
|
|
store.close().await;
|
|
}
|