mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-08-23 08:00:45 +00:00
fb82748296
* fix: make on_behalf_of control permissions for scripts and flows Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: inherit the recorded on-behalf-of identity when a preserving deploy omits it Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: keep an omitted permissioned_as from re-versioning an unchanged script Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: derive the on-behalf-of principal from the email and reject mismatched pairs Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: stop workspace deploys from carrying a source-workspace principal Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * docs: correct the onBehalfOfPermissionedAs param doc Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * test: pin that workspace deploys never carry a source-workspace principal Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * docs: correct the omitted-principal contract and refresh generated prompts Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: keep external-superadmin principals on email-only redeploys Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: scope the recorded principal to its workspace and prefer real accounts Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: carry the recorded principal correctly through drafts and set-permissioned-as Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: sweep draft identity pairs on email change and offboarding Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: leave group identities alone when sweeping a user's email Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: treat only g/ without an email as a group, and match the offboard preview Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: stop the group guard from skipping rows with no recorded principal Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * docs: state the group guard once instead of restating it Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * refactor: make the permissioned_as the only stored on-behalf-of identity Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * perf: skip resolving the on-behalf-of address for sync clients that discard it Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: address the local review of the identity refactor Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: resolve the on-behalf-of identity coherently across clones, offboarding and no-op deploys * test: pin that a fork keeps only the on-behalf-of identities that resolve in it * fix: decide a principal prefix-first everywhere and canonicalize bare addresses * fix: prefix a slash-containing address so a reader cannot take it for a group * fix: read an address as a username before the group- convention * fix: rewrite the canonical principal when an account's address moves * fix: keep the address form of a principal to accounts without a usr row * fix: reject an identity a job row cannot carry and read it uncached at dispatch * fix: count characters against the job identity width and cap the backfill * refactor: name the script/flow principal on_behalf_of, as apps do * docs: state the caller-must-authorize contract on the identity resolvers * fix: keep writing on_behalf_of_email until every worker reads the principal * fix: err high on the compatibility version and document the last resolver * fix: keep the compatibility address current through identity mutations * fix: carry the compatibility address with the principal on every copy path * chore: re-pin the EE ref to the companion branch merged with EE main * fix: key the dbt retry lookup on the stored principal * fix: keep a mixed-version address recoverable through a fork * fix: read a round-tripped address uncached so a redeploy is not rejected * fix: refuse an email change that would make a principal unenqueueable * chore: update ee-repo-ref to ac3d7d015296f041ae44ab6bc4953485f44d36e4 This commit updates the EE repository reference after PR #704 was merged in windmill-ee-private. Previous ee-repo-ref: 219b0b03905a1a0028054b3a4985724e77d09036 New ee-repo-ref: ac3d7d015296f041ae44ab6bc4953485f44d36e4 Automated by sync-ee-ref workflow. --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com>
138 lines
4.7 KiB
Rust
138 lines
4.7 KiB
Rust
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_audit_endpoints(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/w/test-workspace/audit");
|
|
|
|
// GET /list returns 200 (empty array)
|
|
let resp = authed(client().get(format!("{base}/list"))).send().await?;
|
|
assert_2xx(
|
|
resp.status().as_u16(),
|
|
&resp.text().await?,
|
|
"GET /audit/list",
|
|
);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// A run fired by a labeled token must be findable both by the token that fired it and by the
|
|
/// caller who fired it. `push` builds its own audit author from `(user, permissioned_as)` rather
|
|
/// than from the `ApiAuthed`, so the label only reaches the row through the explicit end-user
|
|
/// argument; and when the runnable declares `on_behalf_of`, the run-as identity takes `username`,
|
|
/// so the caller only stays searchable through the `created_by` parameter.
|
|
///
|
|
/// EE-only: the OSS `audit_log` writes nothing, and a lesser plan redacts the `parameters` this
|
|
/// matches on.
|
|
#[cfg(all(feature = "enterprise", feature = "private"))]
|
|
#[sqlx::test(migrations = "../migrations", fixtures("base"))]
|
|
async fn test_job_run_is_searchable_by_token_and_by_caller(
|
|
db: Pool<Postgres>,
|
|
) -> anyhow::Result<()> {
|
|
use serde_json::json;
|
|
|
|
initialize_tracing().await;
|
|
|
|
// The recorded identity makes a run against this take `u/test-user-2` as its
|
|
// permissioned_as, so the audit `username` slot goes to it rather than to the caller.
|
|
sqlx::query(
|
|
"INSERT INTO script (workspace_id, hash, path, content, language, kind, created_by,
|
|
on_behalf_of, schema, summary, description, lock, extra_perms)
|
|
VALUES ('test-workspace', 900101, 'u/test-user-2/onbehalf', 'export function main() {}',
|
|
'deno', 'script', 'test-user-2', 'u/test-user-2', '{}', '', '', '', '{\"g/all\": true}')",
|
|
)
|
|
.execute(&db)
|
|
.await?;
|
|
|
|
let server = ApiServer::start(db.clone()).await?;
|
|
let port = server.addr.port();
|
|
|
|
let resp = authed(client().post(format!("http://localhost:{port}/api/users/tokens/create")))
|
|
.json(&json!({ "label": "audit-probe" }))
|
|
.send()
|
|
.await?;
|
|
assert_eq!(resp.status(), 201);
|
|
let token = resp.text().await?;
|
|
let bearer = |b: reqwest::RequestBuilder| b.header("Authorization", format!("Bearer {token}"));
|
|
|
|
let resp = bearer(client().post(format!(
|
|
"http://localhost:{port}/api/w/test-workspace/jobs/run/preview"
|
|
)))
|
|
.json(&json!({
|
|
"content": "export function main() { return 1; }",
|
|
"language": "deno",
|
|
"args": {}
|
|
}))
|
|
.send()
|
|
.await?;
|
|
assert_2xx(
|
|
resp.status().as_u16(),
|
|
&resp.text().await?,
|
|
"POST /jobs/run/preview",
|
|
);
|
|
|
|
let resp = bearer(client().post(format!(
|
|
"http://localhost:{port}/api/w/test-workspace/jobs/run/p/u/test-user-2/onbehalf"
|
|
)))
|
|
.json(&json!({}))
|
|
.send()
|
|
.await?;
|
|
assert_2xx(
|
|
resp.status().as_u16(),
|
|
&resp.text().await?,
|
|
"POST /jobs/run/p/u/test-user-2/onbehalf",
|
|
);
|
|
|
|
let search = |q: &str| {
|
|
let url = format!("http://localhost:{port}/api/w/test-workspace/audit/list?username={q}");
|
|
async move {
|
|
let resp = authed(client().get(url)).send().await.unwrap();
|
|
assert_eq!(resp.status(), 200);
|
|
resp.json::<Vec<serde_json::Value>>().await.unwrap()
|
|
}
|
|
};
|
|
|
|
let by_token = search("label-audit-probe").await;
|
|
assert!(
|
|
by_token
|
|
.iter()
|
|
.any(|l| l["operation"] == "jobs.run.preview"),
|
|
"the run must be searchable by token label, got {by_token:?}"
|
|
);
|
|
assert!(
|
|
by_token
|
|
.iter()
|
|
.any(|l| l["operation"] == "jobs.run.script" && l["username"] == "test-user-2"),
|
|
"the on-behalf run must be searchable by token label, got {by_token:?}"
|
|
);
|
|
|
|
// `username` is `test-user-2` on that row, so this can only match through `created_by`.
|
|
let by_caller = search("test-user").await;
|
|
assert!(
|
|
by_caller
|
|
.iter()
|
|
.any(|l| l["operation"] == "jobs.run.script" && l["username"] == "test-user-2"),
|
|
"the on-behalf run must stay searchable by the caller who fired it, got {by_caller:?}"
|
|
);
|
|
|
|
Ok(())
|
|
}
|