fix: let the hub_sync job read the uid and hub_base_url settings (#11106)

Claude-Session: https://claude.ai/code/session_01Q6triDksvGJ4YK2gA1acEc

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ruben Fiszel
2026-09-13 17:10:33 +00:00
committed by GitHub
co-authored by Claude Opus 5
parent 62d4632fad
commit 45102c8265
3 changed files with 40 additions and 16 deletions
+25 -9
View File
@@ -318,19 +318,35 @@ async fn test_wm_token_is_confined_to_its_workspace(db: Pool<Postgres>) -> 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.
// ...and the `settings/global` keys on the allowlist, which the CLI reads from a job: on a
// git-sync push, and in `u/admin/hub_sync`. `ws_base_url` is the control: the handler
// leaves it as ungated as these, so only the allowlist stops it.
for key in ["automate_username_creation", "uid", "hub_base_url"] {
let resp = authed(
client().get(format!("{api}/settings/global/{key}")),
&user_wm,
)
.send()
.await?;
assert_eq!(
resp.status(),
200,
"WM_TOKEN must still read {key}: {}",
resp.text().await?
);
}
// The same hub pull reads `hub_api_secret` for a private Hub, but a secret stays out of
// a job's reach even when the token borrows a superadmin.
let resp = authed(
client().get(format!("{api}/settings/global/automate_username_creation")),
&user_wm,
client().get(format!("{api}/settings/global/hub_api_secret")),
&sa_wm,
)
.send()
.await?;
assert_eq!(
resp.status(),
200,
"WM_TOKEN must still read automate_username_creation: {}",
let status = resp.status().as_u16();
assert!(
status == 401 || status == 403,
"superadmin WM_TOKEN must not read hub_api_secret ({status}): {}",
resp.text().await?
);
let resp = authed(
+11 -6
View File
@@ -1073,12 +1073,15 @@ 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.
/// Three instance settings are on the list. `get_global_setting` exempts a handful of keys
/// from its own super-admin gate, these among them, so each is already readable by every
/// authenticated user; each is here because the CLI reads it from a job:
/// `automate_username_creation` before creating a user during a git-sync push, `uid` and
/// `hub_base_url` when `u/admin/hub_sync` pulls resource types from the Hub. The other
/// ungated keys have no such caller, so they stay confined — being ungated earns a key
/// nothing on its own. Listing a gated key earns it nothing either: `require_super_admin`
/// refuses every job token, so `hub_api_secret`, which that pull reads for a private Hub,
/// stays out of a job's reach whatever this list says.
///
/// Deliberately absent, as each crosses that line: `users/list_invites` (returns the
/// workspace ids the identity was invited to), `users/tokens/list` (credential metadata
@@ -1096,6 +1099,8 @@ fn is_global_read_open_to_job_token(route_path: &str) -> bool {
| "/api/users/tutorial_progress"
| "/api/workspaces/allowed_domain_auto_invite"
| "/api/settings/global/automate_username_creation"
| "/api/settings/global/uid"
| "/api/settings/global/hub_base_url"
| "/api/docs/search"
| "/api/docs/page"
| "/api/integrations/hub/list"
+4 -1
View File
@@ -61,7 +61,7 @@ use windmill_common::{
GITHUB_APP_WEBHOOK_BASE_URL_SETTING, HTTP_ROUTE_WORKSPACED_ROUTE_SETTING,
HUB_ACCESSIBLE_URL_SETTING, HUB_BASE_URL_SETTING, INSTANCE_BANNER_SETTING,
MAX_RETENTION_OVERRIDE_WORKSPACES, RETENTION_PERIOD_SECS_OVERRIDES_SETTING,
RUFF_CONFIG_SETTING, WORKSPACE_FAIRNESS_DURATION_SECS_SETTING,
RUFF_CONFIG_SETTING, UNIQUE_ID_SETTING, WORKSPACE_FAIRNESS_DURATION_SECS_SETTING,
WORKSPACE_FAIRNESS_ENABLED_SETTING, WORKSPACE_FAIRNESS_MAX_PERCENT_SETTING,
WORKSPACE_FAIRNESS_MIN_TOTAL_SETTING, WS_BASE_URL_SETTING,
},
@@ -1321,6 +1321,9 @@ pub async fn get_global_setting(
&& key != AUTOMATE_USERNAME_CREATION_SETTING
&& key != DEFAULT_TAGS_WORKSPACES_SETTING
&& key != HUB_BASE_URL_SETTING
// `wmill hub pull` reads it from a job, and no job token clears the gate. It binds an
// offline license only together with `license_key`, which stays gated.
&& key != UNIQUE_ID_SETTING
&& key != HUB_ACCESSIBLE_URL_SETTING
&& key != DISABLE_HUB_SETTING
&& key != EMAIL_DOMAIN_SETTING