From c2279db8a96ac76382eafe254627dafd24d173fd Mon Sep 17 00:00:00 2001 From: hugocasa Date: Thu, 27 Aug 2026 11:37:37 +0200 Subject: [PATCH] fix: allow job tokens to read the automate_username_creation setting (#10869) * fix: let a job token read the automate_username_creation setting Co-Authored-By: Claude Opus 5 * test: use an ungated global setting as the confinement control Co-Authored-By: Claude Opus 5 --------- Co-authored-by: Claude Opus 5 --- backend/tests/wm_token_confinement.rs | 27 +++++++++++++++++++++++++ backend/windmill-api-auth/src/scopes.rs | 8 ++++++++ 2 files changed, 35 insertions(+) diff --git a/backend/tests/wm_token_confinement.rs b/backend/tests/wm_token_confinement.rs index 5eb885fba9..2d3f1373fe 100644 --- a/backend/tests/wm_token_confinement.rs +++ b/backend/tests/wm_token_confinement.rs @@ -318,6 +318,33 @@ async fn test_wm_token_is_confined_to_its_workspace(db: Pool) -> anyho resp.text().await? ); } + // ...and the one `settings/global` key on the allowlist, which the CLI reads before + // creating a user on a git-sync push. `ws_base_url` is the control: the handler leaves + // it as ungated as `automate_username_creation`, so only the allowlist stops it. + let resp = authed( + client().get(format!("{api}/settings/global/automate_username_creation")), + &user_wm, + ) + .send() + .await?; + assert_eq!( + resp.status(), + 200, + "WM_TOKEN must still read automate_username_creation: {}", + resp.text().await? + ); + let resp = authed( + client().get(format!("{api}/settings/global/ws_base_url")), + &user_wm, + ) + .send() + .await?; + assert_eq!( + resp.status(), + 403, + "WM_TOKEN must not read any other global setting: {}", + resp.text().await? + ); let resp = authed(client().post(format!("{api}/schedules/preview")), &user_wm) .json(&json!({ "schedule": "0 0 12 * * *", "timezone": "UTC" })) .send() diff --git a/backend/windmill-api-auth/src/scopes.rs b/backend/windmill-api-auth/src/scopes.rs index 7109a04425..d99d955de9 100644 --- a/backend/windmill-api-auth/src/scopes.rs +++ b/backend/windmill-api-auth/src/scopes.rs @@ -996,6 +996,13 @@ fn scope_grants_access( /// the caller's own row; `email` and `allowed_domain_auto_invite` are derived from the /// token itself and touch no table. /// +/// `settings/global/automate_username_creation` is the one instance setting on the list. +/// `get_global_setting` exempts a handful of keys from its own super-admin gate, that one +/// among them, so the boolean is already readable by every authenticated user; it is here +/// because the CLI reads it before creating a user during a git-sync push, which runs as a +/// job. The other ungated keys have no such caller, so they stay confined — being ungated +/// earns a key nothing on its own. +/// /// Deliberately absent, as each crosses that line: `users/list_invites` (returns the /// workspace ids the identity was invited to), `users/tokens/list` (credential metadata /// of the borrowed identity), `users/exists/{email}` (an oracle over arbitrary @@ -1011,6 +1018,7 @@ fn is_global_read_open_to_job_token(route_path: &str) -> bool { | "/api/users/usage" | "/api/users/tutorial_progress" | "/api/workspaces/allowed_domain_auto_invite" + | "/api/settings/global/automate_username_creation" | "/api/docs/search" | "/api/docs/page" | "/api/integrations/hub/list"