Files
windmill/backend/windmill-api-integration-tests/tests/settings.rs
T
99e96d3f78 fix: let instances withhold signing secrets from the settings API (#11483)
* fix: never return the instance signing secrets from the settings API

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix: tell wmill instance get-config users that jwt_secret is not exported

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* chore: point ee-repo-ref at the oidc signing key fix

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix: block rsa_keys from agent workers and keep get-config stdout pure yaml

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix: keep server secrets in exports by default behind EXPORT_SERVER_SECRETS

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* chore: update ee-repo-ref to 43b2ce8866a393b2666b17647ddb1466afc70bb1

This commit updates the EE repository reference after PR #842 was merged in windmill-ee-private.

Previous ee-repo-ref: 3a0d0c3f45eeb9f5fe8d50ce3798239aa9101a22

New ee-repo-ref: 43b2ce8866a393b2666b17647ddb1466afc70bb1

Automated by sync-ee-ref workflow.

* fix: withhold server secrets on any non-true EXPORT_SERVER_SECRETS value

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com>
2026-10-02 19:25:04 +02:00

281 lines
8.6 KiB
Rust

use serde_json::json;
use sqlx::{Pool, Postgres};
use windmill_test_utils::*;
fn client() -> reqwest::Client {
reqwest::Client::new()
}
fn authed(builder: reqwest::RequestBuilder) -> reqwest::RequestBuilder {
builder.header("Authorization", "Bearer SECRET_TOKEN")
}
fn assert_2xx(status: u16, body: &str, endpoint: &str) {
assert!(
(200..300).contains(&status),
"{endpoint} returned {status}: {body}",
);
}
#[sqlx::test(migrations = "../migrations", fixtures("base"))]
async fn test_settings_2xx(db: Pool<Postgres>) -> anyhow::Result<()> {
initialize_tracing().await;
let server = ApiServer::start(db.clone()).await?;
let port = server.addr.port();
let base = format!("http://localhost:{port}/api/settings");
let resp = authed(client().get(format!("{base}/envs"))).send().await?;
assert_2xx(resp.status().as_u16(), &resp.text().await?, "GET /envs");
let resp = authed(client().get(format!("{base}/global/hub_base_url")))
.send()
.await?;
assert_2xx(
resp.status().as_u16(),
&resp.text().await?,
"GET /global/hub_base_url",
);
let resp = authed(client().post(format!("{base}/global/test_key")))
.json(&json!({"value": "test"}))
.send()
.await?;
assert_2xx(
resp.status().as_u16(),
&resp.text().await?,
"POST /global/test_key",
);
let resp = authed(client().get(format!("{base}/instance_config")))
.send()
.await?;
assert_2xx(
resp.status().as_u16(),
&resp.text().await?,
"GET /instance_config",
);
let resp = authed(client().get(format!("{base}/instance_config/yaml")))
.send()
.await?;
assert_2xx(
resp.status().as_u16(),
&resp.text().await?,
"GET /instance_config/yaml",
);
let resp = authed(client().get(format!("{base}/latest_key_renewal_attempt")))
.send()
.await?;
assert_2xx(
resp.status().as_u16(),
&resp.text().await?,
"GET /latest_key_renewal_attempt",
);
let resp = authed(client().post(format!("{base}/sync_cached_resource_types")))
.send()
.await?;
assert_2xx(
resp.status().as_u16(),
&resp.text().await?,
"POST /sync_cached_resource_types",
);
// --- Reachability only (need external services) ---
let resp = authed(
client()
.post(format!("{base}/test_smtp"))
.json(&json!({"to": "test@test.com", "subject": "test", "content": "test"})),
)
.send()
.await?;
let status = resp.status().as_u16();
let body = resp.text().await?;
assert!(
status != 404 || !body.is_empty(),
"Router-level 404 for POST /test_smtp"
);
let resp = authed(
client()
.post(format!("{base}/test_license_key"))
.json(&json!({"license_key": "fake"})),
)
.send()
.await?;
let status = resp.status().as_u16();
let body = resp.text().await?;
assert!(
status != 404 || !body.is_empty(),
"Router-level 404 for POST /test_license_key"
);
Ok(())
}
#[sqlx::test(migrations = "../migrations", fixtures("base"))]
async fn test_alert_job_queue_waiting_in_global_settings(db: Pool<Postgres>) -> anyhow::Result<()> {
initialize_tracing().await;
let server = ApiServer::start(db.clone()).await?;
let port = server.addr.port();
let settings_base = format!("http://localhost:{port}/api/settings");
let configs_base = format!("http://localhost:{port}/api/configs");
let alert_payload = json!({
"alerts": [
{
"name": "Test Alert",
"tags_to_monitor": ["default", "gpu"],
"jobs_num_threshold": 5,
"alert_cooldown_seconds": 300,
"alert_time_threshold_seconds": 60
}
]
});
// Set alert_job_queue_waiting in global_settings
let resp = authed(client().post(format!("{settings_base}/global/alert_job_queue_waiting")))
.json(&json!({"value": alert_payload}))
.send()
.await?;
assert_2xx(
resp.status().as_u16(),
&resp.text().await?,
"POST /settings/global/alert_job_queue_waiting",
);
// Read it back
let resp = authed(client().get(format!("{settings_base}/global/alert_job_queue_waiting")))
.send()
.await?;
let status = resp.status().as_u16();
let body = resp.text().await?;
assert_2xx(
status,
&body,
"GET /settings/global/alert_job_queue_waiting",
);
let value: serde_json::Value = serde_json::from_str(&body)?;
assert_eq!(value["alerts"][0]["name"], "Test Alert");
assert_eq!(value["alerts"][0]["jobs_num_threshold"], 5);
// Verify alert_job_queue_waiting does NOT appear in list_worker_groups
let resp = authed(client().get(format!("{configs_base}/list_worker_groups")))
.send()
.await?;
let body = resp.text().await?;
let groups: Vec<serde_json::Value> = serde_json::from_str(&body)?;
let alert_in_groups = groups.iter().find(|g| {
let name = g["name"].as_str().unwrap_or("");
name.contains("alert")
});
assert!(
alert_in_groups.is_none(),
"alert_job_queue_waiting should not appear in worker groups"
);
// Delete by setting to null
let resp = authed(client().post(format!("{settings_base}/global/alert_job_queue_waiting")))
.json(&json!({"value": null}))
.send()
.await?;
assert_2xx(
resp.status().as_u16(),
&resp.text().await?,
"POST /settings/global/alert_job_queue_waiting (null)",
);
// Verify it reads back as null
let resp = authed(client().get(format!("{settings_base}/global/alert_job_queue_waiting")))
.send()
.await?;
let body = resp.text().await?;
let value: serde_json::Value = serde_json::from_str(&body)?;
assert!(
value.is_null(),
"alert_job_queue_waiting should be null after deletion"
);
Ok(())
}
async fn read_secret_surfaces(
base: &str,
names: &[&str],
) -> anyhow::Result<Vec<(String, u16, String)>> {
let mut paths = vec![
"instance_config".to_string(),
"instance_config/yaml".to_string(),
];
#[cfg(feature = "enterprise")]
paths.push("list_global".to_string());
paths.extend(names.iter().map(|n| format!("global/{n}")));
let mut out = vec![];
for path in paths {
let resp = authed(client().get(format!("{base}/{path}")))
.send()
.await?;
let status = resp.status().as_u16();
out.push((path, status, resp.text().await?));
}
Ok(out)
}
#[sqlx::test(migrations = "../migrations", fixtures("base"))]
async fn test_server_secrets_follow_export_flag(db: Pool<Postgres>) -> anyhow::Result<()> {
initialize_tracing().await;
let secrets = [
("jwt_secret", json!("planted-jwt-secret")),
("rsa_keys", json!({"private_key": "planted-rsa-key"})),
(
"custom_instance_replication_pwd",
json!("planted-replication-pwd"),
),
(
"external_instance_pg_state",
json!({"admin_pwd": "planted-pg-state"}),
),
];
for (name, value) in &secrets {
sqlx::query(
"INSERT INTO global_settings (name, value) VALUES ($1, $2)
ON CONFLICT (name) DO UPDATE SET value = EXCLUDED.value",
)
.bind(name)
.bind(value)
.execute(&db)
.await?;
}
let names: Vec<&str> = secrets.iter().map(|(n, _)| *n).collect();
let server = ApiServer::start(db.clone()).await?;
let base = format!("http://localhost:{}/api/settings", server.addr.port());
// Default: a full export, so `get-config` and `pull` can migrate an instance.
std::env::remove_var("EXPORT_SERVER_SECRETS");
for (path, status, body) in read_secret_surfaces(&base, &names).await? {
assert_2xx(status, &body, &path);
assert!(
body.contains("planted-"),
"{path} dropped a server secret: {body}"
);
}
std::env::set_var("EXPORT_SERVER_SECRETS", "0");
let withheld = read_secret_surfaces(&base, &names).await;
std::env::remove_var("EXPORT_SERVER_SECRETS");
for (path, status, body) in withheld? {
if path.starts_with("global/") {
assert_eq!(status, 400, "GET {path} returned {status}");
} else {
assert_2xx(status, &body, &path);
}
assert!(
!body.contains("planted-"),
"{path} returned a server secret: {body}"
);
}
Ok(())
}