mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-10-03 08:02:19 +00:00
fix: scope workspace dependencies create to the path workspace (#11385)
* fix: scope workspace dependencies create to the path workspace Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * docs: state the workspace_id must-match contract in the spec and struct Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> --------- 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
5e59cefd1f
commit
3eaf2888c0
@@ -0,0 +1,57 @@
|
||||
use serde_json::json;
|
||||
use sqlx::{Pool, Postgres};
|
||||
|
||||
#[allow(unused_imports)]
|
||||
use windmill_test_utils::*;
|
||||
|
||||
/// An admin of one workspace must not be able to write dependency files into another
|
||||
/// by naming it in the request body.
|
||||
#[sqlx::test(migrations = "../migrations", fixtures("base"))]
|
||||
async fn test_create_rejects_body_workspace_other_than_path(
|
||||
db: Pool<Postgres>,
|
||||
) -> anyhow::Result<()> {
|
||||
initialize_tracing().await;
|
||||
|
||||
// test-user-2 is a plain user of test-workspace and an admin of its own workspace.
|
||||
sqlx::query("INSERT INTO workspace (id, name, owner) VALUES ('own-workspace', 'own-workspace', 'test-user-2')")
|
||||
.execute(&db)
|
||||
.await?;
|
||||
sqlx::query("INSERT INTO usr (workspace_id, email, username, is_admin, role) VALUES ('own-workspace', 'test2@windmill.dev', 'test-user-2', true, 'Admin')")
|
||||
.execute(&db)
|
||||
.await?;
|
||||
|
||||
let (_client, port, _server) = init_client(db.clone()).await;
|
||||
|
||||
let resp = reqwest::Client::new()
|
||||
.post(format!(
|
||||
"http://localhost:{port}/api/w/own-workspace/workspace_dependencies/create"
|
||||
))
|
||||
.header("Authorization", "Bearer SECRET_TOKEN_2")
|
||||
.json(&json!({
|
||||
"workspace_id": "test-workspace",
|
||||
"language": "python3",
|
||||
"content": "requests==2.28.0"
|
||||
}))
|
||||
.send()
|
||||
.await?;
|
||||
let status = resp.status().as_u16();
|
||||
let body = resp.text().await?;
|
||||
assert_eq!(
|
||||
status, 400,
|
||||
"expected the mismatched body to be rejected, got {body}"
|
||||
);
|
||||
assert!(
|
||||
body.contains("does not match"),
|
||||
"expected the workspace mismatch rejection, got {body}"
|
||||
);
|
||||
|
||||
let written: i64 = sqlx::query_scalar("SELECT count(*) FROM workspace_dependencies")
|
||||
.fetch_one(&db)
|
||||
.await?;
|
||||
assert_eq!(
|
||||
written, 0,
|
||||
"no dependency file may be written in either workspace"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -10872,6 +10872,8 @@ paths:
|
||||
text/plain:
|
||||
schema:
|
||||
type: string
|
||||
"400":
|
||||
description: the body's workspace_id does not match the workspace in the path
|
||||
|
||||
/w/{workspace}/workspace_dependencies/archive/{language}:
|
||||
post:
|
||||
@@ -29504,6 +29506,7 @@ components:
|
||||
properties:
|
||||
workspace_id:
|
||||
type: string
|
||||
description: must equal the workspace in the request path
|
||||
language:
|
||||
$ref: "#/components/schemas/ScriptLang"
|
||||
name:
|
||||
|
||||
@@ -34,13 +34,20 @@ async fn create(
|
||||
authed: ApiAuthed,
|
||||
// Extension(user_db): Extension<UserDB>,
|
||||
Extension(db): Extension<DB>,
|
||||
Path(w_id): Path<String>,
|
||||
Json(nwd): Json<NewWorkspaceDependencies>,
|
||||
) -> error::Result<(StatusCode, String)> {
|
||||
tracing::info!(workspace_id = %nwd.workspace_id, name = ?nwd.name, language = ?nwd.language, "create workspace dependencies");
|
||||
tracing::info!(workspace_id = %w_id, name = ?nwd.name, language = ?nwd.language, "create workspace dependencies");
|
||||
require_admin(authed.is_admin, &authed.username)?;
|
||||
// `require_admin` vouches for the path workspace only; the body must not target another.
|
||||
if nwd.workspace_id != w_id {
|
||||
return Err(error::Error::BadRequest(format!(
|
||||
"workspace_id `{}` in the request body does not match the workspace `{}` in the path",
|
||||
nwd.workspace_id, w_id
|
||||
)));
|
||||
}
|
||||
|
||||
let dep_path = WorkspaceDependencies::to_path(&nwd.name, nwd.language)?;
|
||||
let w_id = nwd.workspace_id.clone();
|
||||
let email = authed.email.clone();
|
||||
let username = authed.username.clone();
|
||||
|
||||
|
||||
@@ -11,6 +11,8 @@ use crate::{
|
||||
|
||||
#[derive(sqlx::FromRow, Clone, Serialize, Deserialize, Hash, Debug)]
|
||||
pub struct NewWorkspaceDependencies {
|
||||
/// Trusted as-is by `create`: a caller deserializing this from a request must first check it
|
||||
/// against the workspace the caller is authorized in.
|
||||
pub workspace_id: String,
|
||||
pub language: ScriptLang,
|
||||
pub name: Option<String>,
|
||||
|
||||
Reference in New Issue
Block a user