0.31.9
This commit is contained in:
1 parent
180a0e4877
commit
d23fa5e154
39 files changed
+1521
-376
No files matched your search
@@ -1284,6 +1284,156 @@ mod tests {
|
||||
store.close().await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn certificate_api_settings_read_edit_and_regenerate_without_exposing_existing_token() {
|
||||
let (router, store, _dir, secret, csrf) = fixture().await;
|
||||
let cookie = format!("{}={secret}", auth::COOKIE_NAME);
|
||||
let existing_token = "C".repeat(64);
|
||||
let admin = |method: Method, path: &str, body: serde_json::Value| {
|
||||
request(path)
|
||||
.method(method)
|
||||
.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(body.to_string()))
|
||||
.unwrap()
|
||||
};
|
||||
let external = |path: &str, token: &str| {
|
||||
request(path)
|
||||
.method("POST")
|
||||
.header("authorization", format!("Bearer {token}"))
|
||||
.header("x-rustproxy-certificate-key", "certbot-prod")
|
||||
.header("content-type", "application/json")
|
||||
.body(Body::from("{}"))
|
||||
.unwrap()
|
||||
};
|
||||
let created = router.clone().oneshot(admin(
|
||||
Method::PUT,
|
||||
"/api/certificate-api",
|
||||
json!({
|
||||
"key_id": "certbot-prod", "token": existing_token,
|
||||
"comment": "first comment", "certificate_ids": ["first-cert"], "allow_deploy": false
|
||||
}),
|
||||
)).await.unwrap();
|
||||
assert_eq!(created.status(), StatusCode::OK);
|
||||
let created = json_body(created).await;
|
||||
assert_eq!(created["token"], existing_token);
|
||||
let before_hash: String = sqlx::query_scalar(
|
||||
"SELECT token_hash FROM certificate_api_credentials WHERE key_id = 'certbot-prod'",
|
||||
).fetch_one(&store.pool).await.unwrap();
|
||||
assert_eq!(before_hash, auth::secret_hash(&existing_token));
|
||||
|
||||
let detail = router.clone().oneshot(admin(
|
||||
Method::GET, "/api/certificate-api/certbot-prod", json!({}),
|
||||
)).await.unwrap();
|
||||
assert_eq!(detail.status(), StatusCode::OK);
|
||||
let detail = json_body(detail).await;
|
||||
assert_eq!(detail["credential"]["comment"], "first comment");
|
||||
assert_eq!(detail["credential"]["certificate_ids"], json!(["first-cert"]));
|
||||
assert!(detail.get("token").is_none());
|
||||
assert!(detail["credential"].get("token").is_none());
|
||||
assert!(detail["credential"].get("token_hash").is_none());
|
||||
let created_at = detail["credential"]["created_at"].clone();
|
||||
|
||||
// A bearer-only automation client cannot read or modify management records.
|
||||
let denied = router.clone().oneshot(
|
||||
request("/api/certificate-api/certbot-prod")
|
||||
.header("authorization", format!("Bearer {existing_token}"))
|
||||
.header("x-rustproxy-certificate-key", "certbot-prod")
|
||||
.body(Body::empty()).unwrap(),
|
||||
).await.unwrap();
|
||||
assert_eq!(denied.status(), StatusCode::UNAUTHORIZED);
|
||||
let no_csrf = router.clone().oneshot(
|
||||
request("/api/certificate-api/certbot-prod/regenerate")
|
||||
.method("POST").header("cookie", &cookie)
|
||||
.header("origin", "http://127.0.0.1:9090")
|
||||
.header("x-rustproxy-request", "1")
|
||||
.body(Body::empty()).unwrap(),
|
||||
).await.unwrap();
|
||||
assert_eq!(no_csrf.status(), StatusCode::FORBIDDEN);
|
||||
|
||||
let bad_edit = router.clone().oneshot(admin(
|
||||
Method::PATCH, "/api/certificate-api/certbot-prod",
|
||||
json!({"comment":"changed", "certificate_ids":["other-cert"], "allow_deploy":true, "token":"should-not-be-accepted"}),
|
||||
)).await.unwrap();
|
||||
assert!(bad_edit.status().is_client_error());
|
||||
let wildcard = router.clone().oneshot(admin(
|
||||
Method::PATCH, "/api/certificate-api/certbot-prod",
|
||||
json!({"comment":"changed", "certificate_ids":["*"], "allow_deploy":true}),
|
||||
)).await.unwrap();
|
||||
assert_eq!(wildcard.status(), StatusCode::UNPROCESSABLE_ENTITY);
|
||||
|
||||
// An edit changes only metadata and grants, not the current bearer secret.
|
||||
let edited = router.clone().oneshot(admin(
|
||||
Method::PATCH, "/api/certificate-api/certbot-prod",
|
||||
json!({"comment":"updated owner", "certificate_ids":["new-cert", "new-cert"], "allow_deploy":true}),
|
||||
)).await.unwrap();
|
||||
assert_eq!(edited.status(), StatusCode::OK);
|
||||
let edited = json_body(edited).await;
|
||||
assert_eq!(edited["credential"]["comment"], "updated owner");
|
||||
assert_eq!(edited["credential"]["certificate_ids"], json!(["new-cert"]));
|
||||
assert_eq!(edited["credential"]["allow_deploy"], true);
|
||||
assert_eq!(edited["credential"]["created_at"], created_at);
|
||||
assert!(edited.get("token").is_none());
|
||||
let hash_after_edit: String = sqlx::query_scalar(
|
||||
"SELECT token_hash FROM certificate_api_credentials WHERE key_id = 'certbot-prod'",
|
||||
).fetch_one(&store.pool).await.unwrap();
|
||||
assert_eq!(hash_after_edit, before_hash);
|
||||
let allowed = router.clone().oneshot(external(
|
||||
"/api/certificates/new-cert/reload", &existing_token,
|
||||
)).await.unwrap();
|
||||
assert_eq!(allowed.status(), StatusCode::NOT_FOUND);
|
||||
let old_id = router.clone().oneshot(external(
|
||||
"/api/certificates/first-cert/reload", &existing_token,
|
||||
)).await.unwrap();
|
||||
assert_eq!(old_id.status(), StatusCode::FORBIDDEN);
|
||||
|
||||
let regenerated = router.clone().oneshot(admin(
|
||||
Method::POST, "/api/certificate-api/certbot-prod/regenerate", json!({}),
|
||||
)).await.unwrap();
|
||||
assert_eq!(regenerated.status(), StatusCode::OK);
|
||||
let regenerated = json_body(regenerated).await;
|
||||
let new_token = regenerated["token"].as_str().unwrap().to_owned();
|
||||
assert!(new_token.len() >= 32);
|
||||
assert_ne!(new_token, existing_token);
|
||||
assert_eq!(regenerated["credential"]["comment"], "updated owner");
|
||||
assert_eq!(regenerated["credential"]["allow_deploy"], true);
|
||||
assert_eq!(regenerated["credential"]["certificate_ids"], json!(["new-cert"]));
|
||||
assert!(regenerated["credential"]["last_used_at"].is_null());
|
||||
let raw: String = sqlx::query_scalar(
|
||||
"SELECT token_hash FROM certificate_api_credentials WHERE key_id = 'certbot-prod'",
|
||||
).fetch_one(&store.pool).await.unwrap();
|
||||
assert_eq!(raw, auth::secret_hash(&new_token));
|
||||
assert_ne!(raw, new_token);
|
||||
assert_ne!(raw, before_hash);
|
||||
assert_eq!(router.clone().oneshot(external(
|
||||
"/api/certificates/new-cert/reload", &existing_token,
|
||||
)).await.unwrap().status(), StatusCode::UNAUTHORIZED);
|
||||
assert_eq!(router.clone().oneshot(external(
|
||||
"/api/certificates/new-cert/reload", &new_token,
|
||||
)).await.unwrap().status(), StatusCode::NOT_FOUND);
|
||||
|
||||
let after = router.clone().oneshot(admin(
|
||||
Method::GET, "/api/certificate-api/certbot-prod", json!({}),
|
||||
)).await.unwrap();
|
||||
let after = json_body(after).await;
|
||||
assert!(after.get("token").is_none());
|
||||
assert!(after["credential"].get("token").is_none());
|
||||
assert!(after["credential"].get("token_hash").is_none());
|
||||
let missing = router.clone().oneshot(admin(
|
||||
Method::POST, "/api/certificate-api/missing-key/regenerate", json!({}),
|
||||
)).await.unwrap();
|
||||
assert_eq!(missing.status(), StatusCode::NOT_FOUND);
|
||||
let audit: Vec<String> = sqlx::query_scalar(
|
||||
"SELECT action FROM admin_audit WHERE action LIKE 'certificate_api.%' ORDER BY id",
|
||||
).fetch_all(&store.pool).await.unwrap();
|
||||
assert!(audit.iter().any(|entry| entry == "certificate_api.edit"));
|
||||
assert!(audit.iter().any(|entry| entry == "certificate_api.regenerate"));
|
||||
store.close().await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn admin_preferences_are_account_scoped_and_csrf_guarded() {
|
||||
let (router, store, _dir, secret, csrf) = fixture().await;
|
||||
|
||||
Reference in new issue
Block a user