mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-10-18 08:03:11 +00:00
fix: enforce token path scope on hash-based script read routes
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
318f89960e
commit
2e08490f5b
+10
-4
@@ -1,20 +1,25 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT s.lock, s.lock_error_logs, dm.job_id\n FROM script s\n LEFT JOIN deployment_metadata dm ON s.hash = dm.script_hash AND s.workspace_id = dm.workspace_id\n WHERE s.hash = $1 AND s.workspace_id = $2",
|
||||
"query": "SELECT s.path, s.lock, s.lock_error_logs, dm.job_id\n FROM script s\n LEFT JOIN deployment_metadata dm ON s.hash = dm.script_hash AND s.workspace_id = dm.workspace_id\n WHERE s.hash = $1 AND s.workspace_id = $2",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "path",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "lock",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"ordinal": 2,
|
||||
"name": "lock_error_logs",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"ordinal": 3,
|
||||
"name": "job_id",
|
||||
"type_info": "Uuid"
|
||||
}
|
||||
@@ -26,10 +31,11 @@
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "513ed713afdbafb587026d1536c47a9bbaa6967e36777454746b8817d68219a5"
|
||||
"hash": "3a85abf71e6b3008f92528bd43b9bb8113b163a635b5480911b7aab625d892b9"
|
||||
}
|
||||
@@ -1014,3 +1014,68 @@ async fn test_list_search_scope_filtering(db: Pool<Postgres>) -> anyhow::Result<
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// The hash-addressed read routes resolve the path only after the route-level scope check, so
|
||||
/// each must check the token's path scope itself.
|
||||
#[sqlx::test(migrations = "../migrations", fixtures("base"))]
|
||||
async fn test_hash_read_routes_enforce_path_scope(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/scripts");
|
||||
|
||||
let mut hashes = std::collections::HashMap::new();
|
||||
for folder in ["allowed", "private"] {
|
||||
let resp = authed(client().post(format!(
|
||||
"http://localhost:{port}/api/w/test-workspace/folders/create"
|
||||
)))
|
||||
.json(&json!({ "name": folder }))
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(resp.status(), 200, "create folder: {}", resp.text().await?);
|
||||
|
||||
let path = format!("f/{folder}/s");
|
||||
let resp = authed(client().post(format!("{base}/create")))
|
||||
.json(&new_script(
|
||||
&path,
|
||||
"summary",
|
||||
"export async function main() { return 1; }",
|
||||
))
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(resp.status(), 201, "create {path}: {}", resp.text().await?);
|
||||
hashes.insert(folder, resp.text().await?);
|
||||
}
|
||||
|
||||
sqlx::query(
|
||||
"INSERT INTO token (token_hash, token_prefix, token, email, label, super_admin, scopes) VALUES
|
||||
(encode(sha256('SCOPED_TOKEN'::bytea), 'hex'), 'SCOPED_TOK', 'SCOPED_TOKEN', 'test@windmill.dev', 'scoped', true, ARRAY['scripts:read:f/allowed/*'])",
|
||||
)
|
||||
.execute(&db)
|
||||
.await?;
|
||||
|
||||
for (token, folder, allowed) in [
|
||||
("SCOPED_TOKEN", "allowed", true),
|
||||
("SCOPED_TOKEN", "private", false),
|
||||
("SECRET_TOKEN", "private", true),
|
||||
] {
|
||||
let hash = &hashes[folder];
|
||||
for url in [
|
||||
format!("{base}/raw/h/{hash}.ts"),
|
||||
format!("{base}/deployment_status/h/{hash}"),
|
||||
] {
|
||||
let status = client()
|
||||
.get(&url)
|
||||
.header("Authorization", format!("Bearer {token}"))
|
||||
.send()
|
||||
.await
|
||||
.unwrap()
|
||||
.status();
|
||||
assert_eq!(status.is_success(), allowed, "{token} GET {url}: {status}");
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -3739,6 +3739,7 @@ async fn get_script_by_hash(
|
||||
}
|
||||
|
||||
async fn raw_script_by_hash(
|
||||
authed: ApiAuthed,
|
||||
Extension(db): Extension<DB>,
|
||||
Path((w_id, hash_str)): Path<(String, String)>,
|
||||
) -> Result<String> {
|
||||
@@ -3749,6 +3750,8 @@ async fn raw_script_by_hash(
|
||||
let r = get_script_by_hash_internal(&mut tx, &w_id, &hash, None).await?;
|
||||
tx.commit().await?;
|
||||
|
||||
check_scopes(&authed, || format!("scripts:read:{}", &r.script.path))?;
|
||||
|
||||
Ok(r.script.content)
|
||||
}
|
||||
|
||||
@@ -3759,12 +3762,13 @@ struct DeploymentStatus {
|
||||
job_id: Option<sqlx::types::Uuid>,
|
||||
}
|
||||
async fn get_deployment_status(
|
||||
authed: ApiAuthed,
|
||||
Extension(db): Extension<DB>,
|
||||
Path((w_id, hash)): Path<(String, ScriptHash)>,
|
||||
) -> JsonResult<DeploymentStatus> {
|
||||
let mut tx = db.begin().await?;
|
||||
let status_o = sqlx::query!(
|
||||
"SELECT s.lock, s.lock_error_logs, dm.job_id
|
||||
"SELECT s.path, s.lock, s.lock_error_logs, dm.job_id
|
||||
FROM script s
|
||||
LEFT JOIN deployment_metadata dm ON s.hash = dm.script_hash AND s.workspace_id = dm.workspace_id
|
||||
WHERE s.hash = $1 AND s.workspace_id = $2",
|
||||
@@ -3775,6 +3779,7 @@ async fn get_deployment_status(
|
||||
.await?;
|
||||
|
||||
let status = not_found_if_none(status_o, "DeploymentStatus", hash.to_string())?;
|
||||
check_scopes(&authed, || format!("scripts:read:{}", &status.path))?;
|
||||
|
||||
let deployment_status = DeploymentStatus {
|
||||
lock: status.lock,
|
||||
|
||||
Reference in New Issue
Block a user