Files
windmill/backend/tests/folder_default_permissioned_as.rs
fb82748296 fix: make on_behalf_of control permissions for scripts and flows (#10438)
* 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>
2026-08-01 20:37:21 +02:00

775 lines
23 KiB
Rust

//! Integration tests for folder `default_permissioned_as` rules.
//!
//! These tests exhaustively verify the feature across every surface where a
//! `permissioned_as` / `on_behalf_of_email` default is applied at create-time:
//!
//! - folder CRUD (persistence, validation)
//! - audit log emission
//! - schedules, flows, scripts, apps — all entry points
//! - admin / wm_deployers / regular-user behavior
//! - create-only semantics (updates never rewrite)
//! - explicit preserve beats folder default
//! - first-match-wins ordering for overlapping rules
//! - stale rule rejection
//! - paths outside folders are untouched
//!
//! The tests share one workspace and one folder because ApiServer::start has
//! significant setup cost; keeping everything in one test minimizes total run
//! time while still exercising every branch.
use serde_json::json;
use sqlx::{Pool, Postgres};
use windmill_test_utils::*;
fn client() -> reqwest::Client {
reqwest::Client::new()
}
fn authed(builder: reqwest::RequestBuilder, token: &str) -> reqwest::RequestBuilder {
builder.header("Authorization", format!("Bearer {}", token))
}
fn new_script(path: &str) -> serde_json::Value {
json!({
"path": path,
"summary": "Test script",
"description": "",
"content": "export async function main() { return 42; }",
"language": "deno",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {},
"required": []
}
})
}
fn new_flow(path: &str) -> serde_json::Value {
json!({
"path": path,
"summary": "Test flow",
"description": "",
"value": { "modules": [] },
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {},
"required": []
}
})
}
fn new_app(path: &str) -> serde_json::Value {
json!({
"path": path,
"summary": "Test app",
"value": {
"type": "rawapp",
"inline_script": null
},
"policy": {
"execution_mode": "anonymous",
"triggerables": {}
}
})
}
fn new_schedule(path: &str, script_path: &str) -> serde_json::Value {
json!({
"path": path,
"schedule": "0 0 */6 * * *",
"timezone": "UTC",
"script_path": script_path,
"is_flow": false,
"enabled": false,
})
}
/// Exhaustive create-time folder default_permissioned_as coverage.
#[sqlx::test(fixtures("folder_default_permissioned_as"))]
async fn test_folder_default_permissioned_as(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");
// ========================================================================
// 0. Setup — create the folder with no rules (admin becomes owner)
// and grant the deployer + regular users writer access so they can
// deploy items inside it.
// ========================================================================
let resp = authed(
client().post(format!("{base}/folders/create")),
"SECRET_TOKEN",
)
.json(&json!({
"name": "prodfolder",
"extra_perms": {
"u/deployer-user": true,
"u/test-user-2": true
}
}))
.send()
.await?;
assert_eq!(
resp.status(),
200,
"Admin should create folder: {}",
resp.text().await?
);
// A helper script that schedules reference
let resp = authed(
client().post(format!("{base}/scripts/create")),
"SECRET_TOKEN",
)
.json(&new_script("f/prodfolder/helper"))
.send()
.await?;
assert_eq!(
resp.status(),
201,
"Admin should create helper script: {}",
resp.text().await?
);
// ========================================================================
// 1. Rule validation — bad inputs are rejected with 400
// ========================================================================
// Non-array
let resp = authed(
client().post(format!("{base}/folders/update/prodfolder")),
"SECRET_TOKEN",
)
.json(&json!({ "default_permissioned_as": { "not": "an array" } }))
.send()
.await?;
assert_eq!(resp.status(), 400, "non-array should 400");
// Invalid glob
let resp = authed(
client().post(format!("{base}/folders/update/prodfolder")),
"SECRET_TOKEN",
)
.json(&json!({
"default_permissioned_as": [{ "path_glob": "[unclosed", "permissioned_as": "u/original-user" }]
}))
.send()
.await?;
assert_eq!(resp.status(), 400, "invalid glob should 400");
let body = resp.text().await?;
assert!(
body.contains("path_glob is not a valid glob"),
"error should mention glob: {body}"
);
// Invalid permissioned_as format
let resp = authed(
client().post(format!("{base}/folders/update/prodfolder")),
"SECRET_TOKEN",
)
.json(&json!({
"default_permissioned_as": [{ "path_glob": "**", "permissioned_as": "bogus" }]
}))
.send()
.await?;
assert_eq!(resp.status(), 400, "invalid permissioned_as should 400");
// Missing required field
let resp = authed(
client().post(format!("{base}/folders/update/prodfolder")),
"SECRET_TOKEN",
)
.json(&json!({
"default_permissioned_as": [{ "path_glob": "**" }]
}))
.send()
.await?;
assert_eq!(resp.status(), 400, "missing permissioned_as should 400");
// ========================================================================
// 2. Rule persistence — valid rules round-trip through GET folder
// ========================================================================
let resp = authed(
client().post(format!("{base}/folders/update/prodfolder")),
"SECRET_TOKEN",
)
.json(&json!({
"default_permissioned_as": [
{ "path_glob": "jobs/critical/**", "permissioned_as": "u/original-user" },
{ "path_glob": "jobs/**", "permissioned_as": "g/wm_deployers" },
{ "path_glob": "reports/*", "permissioned_as": "original@windmill.dev" }
]
}))
.send()
.await?;
assert_eq!(
resp.status(),
200,
"Valid rules should update: {}",
resp.text().await?
);
let resp = authed(
client().get(format!("{base}/folders/get/prodfolder")),
"SECRET_TOKEN",
)
.send()
.await?;
assert_eq!(resp.status(), 200);
let folder: serde_json::Value = resp.json().await?;
let rules = folder
.get("default_permissioned_as")
.and_then(|v| v.as_array())
.expect("rules should be an array");
assert_eq!(rules.len(), 3, "should have 3 rules");
assert_eq!(rules[0]["path_glob"], "jobs/critical/**");
assert_eq!(rules[0]["permissioned_as"], "u/original-user");
assert_eq!(rules[1]["permissioned_as"], "g/wm_deployers");
assert_eq!(rules[2]["permissioned_as"], "original@windmill.dev");
// ========================================================================
// 4. Schedules — the core matrix
// ========================================================================
// 4a. Admin, matching path — folder default wins
let resp = authed(
client().post(format!("{base}/schedules/create")),
"SECRET_TOKEN",
)
.json(&new_schedule(
"f/prodfolder/jobs/sched_admin_match",
"f/prodfolder/helper",
))
.send()
.await?;
assert_eq!(
resp.status(),
200,
"Admin should create schedule: {}",
resp.text().await?
);
let sched = sqlx::query!(
"SELECT permissioned_as, email FROM schedule WHERE path = $1 AND workspace_id = $2",
"f/prodfolder/jobs/sched_admin_match",
"test-workspace"
)
.fetch_one(&db)
.await?;
assert_eq!(
sched.permissioned_as, "g/wm_deployers",
"matching rule should apply (jobs/** wins over critical since path is not critical)"
);
// 4b. Admin, path matching the MORE SPECIFIC rule which is listed first
let resp = authed(
client().post(format!("{base}/schedules/create")),
"SECRET_TOKEN",
)
.json(&new_schedule(
"f/prodfolder/jobs/critical/prod_run",
"f/prodfolder/helper",
))
.send()
.await?;
assert_eq!(
resp.status(),
200,
"Admin should create critical schedule: {}",
resp.text().await?
);
let sched = sqlx::query!(
"SELECT permissioned_as FROM schedule WHERE path = $1 AND workspace_id = $2",
"f/prodfolder/jobs/critical/prod_run",
"test-workspace"
)
.fetch_one(&db)
.await?;
assert_eq!(
sched.permissioned_as, "u/original-user",
"first matching rule wins (critical/** listed first)"
);
// 4c. Admin, non-matching path — acting user identity
let resp = authed(
client().post(format!("{base}/schedules/create")),
"SECRET_TOKEN",
)
.json(&new_schedule(
"f/prodfolder/dev/sched_no_match",
"f/prodfolder/helper",
))
.send()
.await?;
assert_eq!(
resp.status(),
200,
"Admin should create non-matching schedule: {}",
resp.text().await?
);
let sched = sqlx::query!(
"SELECT permissioned_as, email FROM schedule WHERE path = $1 AND workspace_id = $2",
"f/prodfolder/dev/sched_no_match",
"test-workspace"
)
.fetch_one(&db)
.await?;
assert_eq!(
sched.permissioned_as, "u/test-user",
"non-matching path falls back to acting user"
);
assert_eq!(sched.email, "test@windmill.dev");
// 4d. Deployer (non-admin in wm_deployers), matching path — default applies
let resp = authed(
client().post(format!("{base}/schedules/create")),
"DEPLOYER_TOKEN",
)
.json(&new_schedule(
"f/prodfolder/jobs/sched_deployer",
"f/prodfolder/helper",
))
.send()
.await?;
assert_eq!(
resp.status(),
200,
"Deployer should create schedule: {}",
resp.text().await?
);
let sched = sqlx::query!(
"SELECT permissioned_as, edited_by FROM schedule WHERE path = $1 AND workspace_id = $2",
"f/prodfolder/jobs/sched_deployer",
"test-workspace"
)
.fetch_one(&db)
.await?;
assert_eq!(
sched.permissioned_as, "g/wm_deployers",
"deployer gets folder default"
);
assert_eq!(
sched.edited_by, "deployer-user",
"edited_by is still the acting user"
);
// 4e. Non-admin/non-deployer, matching path — default is NOT applied
let resp = authed(
client().post(format!("{base}/schedules/create")),
"SECRET_TOKEN_2",
)
.json(&new_schedule(
"f/prodfolder/jobs/sched_regular",
"f/prodfolder/helper",
))
.send()
.await?;
assert_eq!(
resp.status(),
200,
"Regular user should create schedule: {}",
resp.text().await?
);
let sched = sqlx::query!(
"SELECT permissioned_as FROM schedule WHERE path = $1 AND workspace_id = $2",
"f/prodfolder/jobs/sched_regular",
"test-workspace"
)
.fetch_one(&db)
.await?;
assert_eq!(
sched.permissioned_as, "u/test-user-2",
"regular user never gets folder default"
);
// 4f. Explicit preserve beats folder default
let resp = authed(
client().post(format!("{base}/schedules/create")),
"SECRET_TOKEN",
)
.json(&json!({
"path": "f/prodfolder/jobs/sched_preserve",
"schedule": "0 0 */6 * * *",
"timezone": "UTC",
"script_path": "f/prodfolder/helper",
"is_flow": false,
"enabled": false,
"permissioned_as": "u/original-user",
"preserve_permissioned_as": true
}))
.send()
.await?;
assert_eq!(
resp.status(),
200,
"Admin should create preserved schedule: {}",
resp.text().await?
);
let sched = sqlx::query!(
"SELECT permissioned_as FROM schedule WHERE path = $1 AND workspace_id = $2",
"f/prodfolder/jobs/sched_preserve",
"test-workspace"
)
.fetch_one(&db)
.await?;
assert_eq!(
sched.permissioned_as, "u/original-user",
"explicit preserve beats folder default (even though folder default would be g/wm_deployers)"
);
// 4g. Update does NOT rewrite permissioned_as to folder default
// Edit the non-matching schedule created in 4c, with no permissioned_as in the payload.
let resp = authed(
client().post(format!(
"{base}/schedules/update/f/prodfolder/dev/sched_no_match"
)),
"SECRET_TOKEN",
)
.json(&json!({
"schedule": "0 0 */12 * * *",
"timezone": "UTC",
"script_path": "f/prodfolder/helper",
"is_flow": false,
}))
.send()
.await?;
assert_eq!(
resp.status(),
200,
"Admin should update schedule: {}",
resp.text().await?
);
let sched = sqlx::query!(
"SELECT permissioned_as FROM schedule WHERE path = $1 AND workspace_id = $2",
"f/prodfolder/dev/sched_no_match",
"test-workspace"
)
.fetch_one(&db)
.await?;
assert_eq!(
sched.permissioned_as, "u/test-user",
"update still shows acting user — the existing schedule wasn't rewritten by a folder rule change (this would be defaults-on-update, which we don't do)"
);
// ========================================================================
// 5. Flows — on_behalf_of_email variant
// ========================================================================
// 5a. Admin, matching path
let resp = authed(
client().post(format!("{base}/flows/create")),
"SECRET_TOKEN",
)
.json(&new_flow("f/prodfolder/jobs/flow_admin_match"))
.send()
.await?;
assert_eq!(
resp.status(),
201,
"Admin should create flow: {}",
resp.text().await?
);
let flow = sqlx::query!(
"SELECT on_behalf_of FROM flow WHERE path = $1 AND workspace_id = $2",
"f/prodfolder/jobs/flow_admin_match",
"test-workspace"
)
.fetch_one(&db)
.await?;
assert_eq!(
flow.on_behalf_of.as_deref(),
Some("g/wm_deployers"),
"flow should resolve folder default to group email"
);
// The group email is synthetic and resolves back to nobody, so the group can
// only survive as the stored permissioned_as.
assert_eq!(
flow.on_behalf_of.as_deref(),
Some("g/wm_deployers"),
"flow should run as the group the folder rule names"
);
// 5b. Admin, reports/* rule (email directly as permissioned_as)
let resp = authed(
client().post(format!("{base}/flows/create")),
"SECRET_TOKEN",
)
.json(&new_flow("f/prodfolder/reports/weekly"))
.send()
.await?;
assert_eq!(
resp.status(),
201,
"Admin should create reports flow: {}",
resp.text().await?
);
let flow = sqlx::query!(
"SELECT on_behalf_of FROM flow WHERE path = $1 AND workspace_id = $2",
"f/prodfolder/reports/weekly",
"test-workspace"
)
.fetch_one(&db)
.await?;
assert_eq!(
flow.on_behalf_of.as_deref(),
Some("u/original-user"),
"a rule naming an address stores the principal it names, which is what carries that user's groups and folders"
);
// 5c. Non-matching flow path — no default
let resp = authed(
client().post(format!("{base}/flows/create")),
"SECRET_TOKEN",
)
.json(&new_flow("f/prodfolder/dev/flow_no_match"))
.send()
.await?;
assert_eq!(resp.status(), 201);
let flow = sqlx::query!(
"SELECT on_behalf_of FROM flow WHERE path = $1 AND workspace_id = $2",
"f/prodfolder/dev/flow_no_match",
"test-workspace"
)
.fetch_one(&db)
.await?;
assert_eq!(
flow.on_behalf_of, None,
"no folder default ⇒ no on_behalf_of_email written"
);
// 5d. Path outside any folder (user folder) — default never applies
let resp = authed(
client().post(format!("{base}/flows/create")),
"SECRET_TOKEN",
)
.json(&new_flow("u/test-user/outside_flow"))
.send()
.await?;
assert_eq!(resp.status(), 201);
let flow = sqlx::query!(
"SELECT on_behalf_of FROM flow WHERE path = $1 AND workspace_id = $2",
"u/test-user/outside_flow",
"test-workspace"
)
.fetch_one(&db)
.await?;
assert_eq!(
flow.on_behalf_of, None,
"paths outside folders are never touched"
);
// ========================================================================
// 6. Scripts — first-time create applies default; subsequent updates don't
// ========================================================================
// 6a. Admin creates a new script at matching path
let resp = authed(
client().post(format!("{base}/scripts/create")),
"SECRET_TOKEN",
)
.json(&new_script("f/prodfolder/jobs/new_script"))
.send()
.await?;
assert_eq!(
resp.status(),
201,
"Admin should create script: {}",
resp.text().await?
);
let script = sqlx::query!(
"SELECT on_behalf_of FROM script WHERE path = $1 AND workspace_id = $2 ORDER BY created_at DESC LIMIT 1",
"f/prodfolder/jobs/new_script",
"test-workspace"
)
.fetch_one(&db)
.await?;
assert_eq!(
script.on_behalf_of.as_deref(),
Some("g/wm_deployers"),
"new script at matching path gets folder default"
);
// Note: Windmill scripts require the previous non-archived version to be
// explicitly archived before a new hash can be created at the same path.
// We rely on the existence check inside `create_script_internal`
// (`path_already_exists` → skip folder default) which is unit-level logic
// exercised on every subsequent deploy at an existing path. The flow 5c
// case above covers create-only semantics end-to-end through the API.
// ========================================================================
// 7. Apps — policy.on_behalf_of variant
// ========================================================================
// 7a. Admin, matching path
let resp = authed(client().post(format!("{base}/apps/create")), "SECRET_TOKEN")
.json(&new_app("f/prodfolder/jobs/app_match"))
.send()
.await?;
assert_eq!(
resp.status(),
201,
"Admin should create app: {}",
resp.text().await?
);
let app = sqlx::query!(
"SELECT policy FROM app WHERE path = $1 AND workspace_id = $2",
"f/prodfolder/jobs/app_match",
"test-workspace"
)
.fetch_one(&db)
.await?;
let policy: serde_json::Value = app.policy;
assert_eq!(
policy["on_behalf_of"], "g/wm_deployers",
"app policy.on_behalf_of gets folder default"
);
assert_eq!(
policy["on_behalf_of_email"], "group-wm_deployers@windmill.dev",
"app policy.on_behalf_of_email gets folder default email"
);
// 7b. Admin, non-matching path — acting user
let resp = authed(client().post(format!("{base}/apps/create")), "SECRET_TOKEN")
.json(&new_app("f/prodfolder/dev/app_no_match"))
.send()
.await?;
assert_eq!(resp.status(), 201);
let app = sqlx::query!(
"SELECT policy FROM app WHERE path = $1 AND workspace_id = $2",
"f/prodfolder/dev/app_no_match",
"test-workspace"
)
.fetch_one(&db)
.await?;
let policy: serde_json::Value = app.policy;
assert_eq!(policy["on_behalf_of"], "u/test-user");
// 7c. Regular user, matching path — acting user (not folder default)
let resp = authed(
client().post(format!("{base}/apps/create")),
"SECRET_TOKEN_2",
)
.json(&new_app("f/prodfolder/jobs/app_regular"))
.send()
.await?;
assert_eq!(
resp.status(),
201,
"Regular user should create app: {}",
resp.text().await?
);
let app = sqlx::query!(
"SELECT policy FROM app WHERE path = $1 AND workspace_id = $2",
"f/prodfolder/jobs/app_regular",
"test-workspace"
)
.fetch_one(&db)
.await?;
let policy: serde_json::Value = app.policy;
assert_eq!(
policy["on_behalf_of"], "u/test-user-2",
"regular user never gets folder default in app policy"
);
// ========================================================================
// 8. Stale rule — rule resolves to a user that does not exist
// ========================================================================
let resp = authed(
client().post(format!("{base}/folders/update/prodfolder")),
"SECRET_TOKEN",
)
.json(&json!({
"default_permissioned_as": [
{ "path_glob": "stale/**", "permissioned_as": "u/ghost" }
]
}))
.send()
.await?;
assert_eq!(
resp.status(),
200,
"Admin should update folder rules: {}",
resp.text().await?
);
let resp = authed(
client().post(format!("{base}/schedules/create")),
"SECRET_TOKEN",
)
.json(&new_schedule(
"f/prodfolder/stale/should_fail",
"f/prodfolder/helper",
))
.send()
.await?;
assert_eq!(
resp.status(),
400,
"stale rule should return 400 when a new schedule matches it"
);
let body = resp.text().await?;
assert!(
body.contains("u/ghost") && body.contains("does not exist"),
"error should identify the stale user: {body}"
);
// Non-matching paths in the same folder still work fine.
let resp = authed(
client().post(format!("{base}/schedules/create")),
"SECRET_TOKEN",
)
.json(&new_schedule("f/prodfolder/safe/ok", "f/prodfolder/helper"))
.send()
.await?;
assert_eq!(
resp.status(),
200,
"non-matching path should not hit the stale rule: {}",
resp.text().await?
);
// ========================================================================
// 9. Clearing rules by passing an empty array
// ========================================================================
let resp = authed(
client().post(format!("{base}/folders/update/prodfolder")),
"SECRET_TOKEN",
)
.json(&json!({ "default_permissioned_as": [] }))
.send()
.await?;
assert_eq!(resp.status(), 200);
let resp = authed(
client().post(format!("{base}/schedules/create")),
"SECRET_TOKEN",
)
.json(&new_schedule(
"f/prodfolder/jobs/cleared",
"f/prodfolder/helper",
))
.send()
.await?;
assert_eq!(resp.status(), 200);
let sched = sqlx::query!(
"SELECT permissioned_as FROM schedule WHERE path = $1 AND workspace_id = $2",
"f/prodfolder/jobs/cleared",
"test-workspace"
)
.fetch_one(&db)
.await?;
assert_eq!(
sched.permissioned_as, "u/test-user",
"after rules cleared, new schedule uses acting user again"
);
Ok(())
}