mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-10-05 00:02:24 +00:00
feat: restricted job tokens per script and flow (#11484)
* feat: restricted job tokens (job_token_scopes on scripts and flows) Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix: admit flow-run reads, skip dedicated workers, gate on worker version Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix: keep restricted jobs off flow runners, preserve scopes on rename and promotion Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix: keep restricted jobs off every dedicated handoff, confine progress flow id Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix: exclude restricted runnables from dedicated worker startup Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix: gate restrictions on the release after 1.821.0 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix: store per-job scopes on job_perms instead of v2_job, pin inline runs to the checked version Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * feat: step-level job_token_scopes for flow steps and agent tools Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014FHGE3ynoAu6yrgeg4kLwL * fix: fail closed on perms read errors, refuse restricted queue imports, gate step scopes in previews Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014FHGE3ynoAu6yrgeg4kLwL * fix: carry a job's scopes on its completion so a re-run keeps the caller's cap Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014FHGE3ynoAu6yrgeg4kLwL * fix: carry a zombie job's scopes into its completion Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014FHGE3ynoAu6yrgeg4kLwL * fix: leave a zombie for the next sweep when its scopes cannot be read Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014FHGE3ynoAu6yrgeg4kLwL * docs: correct the QueuedJobV2 completion comment Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014FHGE3ynoAu6yrgeg4kLwL * fix: validate step scopes in batch flows, fail closed on unvalidated step scopes Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014FHGE3ynoAu6yrgeg4kLwL * fix: refuse flows with step or tool restrictions at push while an older worker is live Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014FHGE3ynoAu6yrgeg4kLwL * fix: apply the step-scope worker gate to flow restarts Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014FHGE3ynoAu6yrgeg4kLwL * fix: list the job token toggle with the other step and flow settings Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014FHGE3ynoAu6yrgeg4kLwL * chore: pin the EE companion merged with EE main Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014FHGE3ynoAu6yrgeg4kLwL * perf: skip scope lookups for unrestricted jobs; list job token setting last Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014FHGE3ynoAu6yrgeg4kLwL * style: rustfmt scopes tests Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014FHGE3ynoAu6yrgeg4kLwL * fix: confine restricted job tokens to their own run lineage; drop remaining extra lookups Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014FHGE3ynoAu6yrgeg4kLwL * chore: update ee-repo-ref to 259ad3bfeef5285ba80eedc86309b11dca001220 This commit updates the EE repository reference after PR #843 was merged in windmill-ee-private. Previous ee-repo-ref: 2b77c0225dca441235daf7bf0a06ba968df0c927 New ee-repo-ref: 259ad3bfeef5285ba80eedc86309b11dca001220 Automated by sync-ee-ref workflow. --------- 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>
This commit is contained in:
co-authored by
Claude Opus 5.5
windmill-internal-app[bot]
parent
e952298f84
commit
e7fc1b2e2e
@@ -899,6 +899,7 @@ async fn is_noop_deploy_against_parent(
|
||||
ns: &NewScript,
|
||||
parent: &Script<ScriptRunnableSettingsHandle>,
|
||||
resolved_on_behalf_of: Option<&str>,
|
||||
resolved_job_token_scopes: Option<&[String]>,
|
||||
db: &DB,
|
||||
) -> Result<bool> {
|
||||
if parent.archived || parent.deleted {
|
||||
@@ -952,6 +953,8 @@ async fn is_noop_deploy_against_parent(
|
||||
// caller-intent flag (auto-resolve parent), not script state
|
||||
auto_parent: _,
|
||||
labels,
|
||||
// resolved against the deployed value into `resolved_job_token_scopes`, compared below
|
||||
job_token_scopes: _,
|
||||
// caller-intent flag (preserve user drafts on CLI/git-sync deploys);
|
||||
// transient, never persisted, does not change what the script *is*
|
||||
skip_draft_deletion: _,
|
||||
@@ -1025,6 +1028,9 @@ async fn is_noop_deploy_against_parent(
|
||||
if resolved_on_behalf_of != parent.on_behalf_of.as_deref() {
|
||||
return Ok(false);
|
||||
}
|
||||
if resolved_job_token_scopes != parent.job_token_scopes.as_deref() {
|
||||
return Ok(false);
|
||||
}
|
||||
// Both of a dbt script's derived fields are compared as they WOULD BE STORED,
|
||||
// not as they arrived: the schema comes from the descriptor and the clients
|
||||
// cannot derive it (`windmill-parser-wasm` has no dbt arm), so they send the
|
||||
@@ -1492,6 +1498,37 @@ async fn create_script_internal<'c>(
|
||||
parent_adopted_from_retired_path = ns.parent_hash.is_some();
|
||||
}
|
||||
|
||||
// Absent keeps the previous version's value, read once the parent is settled (a rename
|
||||
// adopts its source head above), so a client unaware of the setting cannot drop a
|
||||
// restriction by redeploying or renaming.
|
||||
let resolved_job_token_scopes: Option<Vec<String>> = match (&ns.job_token_scopes, &ns.parent_hash) {
|
||||
(Some(Some(scopes)), _) => {
|
||||
windmill_common::min_version::MIN_VERSION_SUPPORTS_JOB_TOKEN_SCOPES
|
||||
.assert()
|
||||
.await?;
|
||||
Some(windmill_common::scopes::validate_job_token_scopes(scopes)?)
|
||||
}
|
||||
(Some(None), _) => None,
|
||||
(None, Some(parent_hash)) => sqlx::query_scalar!(
|
||||
"SELECT job_token_scopes FROM script WHERE hash = $1 AND workspace_id = $2",
|
||||
parent_hash.0,
|
||||
&w_id
|
||||
)
|
||||
.fetch_optional(&db)
|
||||
.await?
|
||||
.flatten(),
|
||||
(None, None) => sqlx::query_scalar!(
|
||||
"SELECT job_token_scopes FROM script WHERE path = $1 AND workspace_id = $2 \
|
||||
AND deleted = false ORDER BY created_at DESC LIMIT 1",
|
||||
&ns.path,
|
||||
&w_id
|
||||
)
|
||||
.fetch_optional(&db)
|
||||
.await?
|
||||
.flatten(),
|
||||
};
|
||||
ns.job_token_scopes = Some(resolved_job_token_scopes.clone());
|
||||
|
||||
// Before hashing, so the hash and the no-op check see the schema that gets stored.
|
||||
// `{}` counts as absent: an agent filling every tool argument sends it for "none".
|
||||
let schema_absent = ns.schema.as_ref().is_none_or(|s| {
|
||||
@@ -1620,8 +1657,14 @@ async fn create_script_internal<'c>(
|
||||
// CLI pushes must not produce phantom commits on the downstream
|
||||
// git repository.
|
||||
if skip_if_noop
|
||||
&& is_noop_deploy_against_parent(&ns, &ps, resolved_on_behalf_of.as_deref(), &db)
|
||||
.await?
|
||||
&& is_noop_deploy_against_parent(
|
||||
&ns,
|
||||
&ps,
|
||||
resolved_on_behalf_of.as_deref(),
|
||||
resolved_job_token_scopes.as_deref(),
|
||||
&db,
|
||||
)
|
||||
.await?
|
||||
{
|
||||
tracing::info!(
|
||||
workspace_id = %w_id,
|
||||
@@ -2197,8 +2240,8 @@ async fn create_script_internal<'c>(
|
||||
content, created_by, schema, is_template, extra_perms, lock, language, kind, tag, \
|
||||
envs, concurrent_limit, concurrency_time_window_s, cache_ttl, \
|
||||
dedicated_worker, ws_error_handler_muted, priority, restart_unless_cancelled, \
|
||||
delete_after_use, delete_after_secs, timeout, concurrency_key, visible_to_runner_only, auto_kind, codebase, has_preprocessor, schema_validation, assets, debounce_key, debounce_delay_s, cache_ignore_s3_path, runnable_settings_handle, modules, labels, on_behalf_of, on_behalf_of_email) \
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9::text::json, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25, $26, $27, $28, $29, $30, $31, $32, $33, $34, $35, $36, $37, $38, $39, $40, $41)",
|
||||
delete_after_use, delete_after_secs, timeout, concurrency_key, visible_to_runner_only, auto_kind, codebase, has_preprocessor, schema_validation, assets, debounce_key, debounce_delay_s, cache_ignore_s3_path, runnable_settings_handle, modules, labels, on_behalf_of, on_behalf_of_email, job_token_scopes) \
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9::text::json, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25, $26, $27, $28, $29, $30, $31, $32, $33, $34, $35, $36, $37, $38, $39, $40, $41, $42)",
|
||||
&w_id,
|
||||
&hash.0,
|
||||
ns.path,
|
||||
@@ -2242,9 +2285,14 @@ async fn create_script_internal<'c>(
|
||||
ns.labels.as_deref() as Option<&[String]>,
|
||||
resolved_on_behalf_of,
|
||||
legacy_on_behalf_of_email,
|
||||
resolved_job_token_scopes.as_deref() as Option<&[String]>,
|
||||
)
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
windmill_common::scopes::log_job_token_scopes_deploy(
|
||||
"script",
|
||||
resolved_job_token_scopes.as_deref(),
|
||||
);
|
||||
|
||||
// A lock that is not left to a dependency job queues none, so this is the only place its hash
|
||||
// can be recorded. `try_skip_relock` treats a missing hash for an imported script as changed,
|
||||
@@ -2954,6 +3002,7 @@ async fn create_script_internal<'c>(
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -3077,6 +3126,7 @@ async fn create_script_internal<'c>(
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
tracing::info!("pushed auto-build binary job {job_id} for {script_path}");
|
||||
|
||||
Reference in New Issue
Block a user