mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-07 00:01:49 +00:00
* feat(forks): let a fork's creator manage developers on it without being an admin Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(forks): bind the fork-creator grant to the member's parent username and lock the delete Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
188 lines
5.4 KiB
Rust
188 lines
5.4 KiB
Rust
use serde_json::json;
|
|
use sqlx::{Pool, Postgres};
|
|
|
|
use windmill_test_utils::*;
|
|
|
|
fn client() -> reqwest::Client {
|
|
reqwest::Client::new()
|
|
}
|
|
|
|
/// `test2` created the fork `wm-fork-test` but is only a developer in it.
|
|
const FORK_OWNER_TOKEN: &str = "SECRET_TOKEN_2";
|
|
/// `test3` is a developer of the parent and of the fork, but created neither.
|
|
const FORK_MEMBER_TOKEN: &str = "SECRET_TOKEN_3";
|
|
|
|
fn as_user(builder: reqwest::RequestBuilder, token: &str) -> reqwest::RequestBuilder {
|
|
builder.header("Authorization", format!("Bearer {token}"))
|
|
}
|
|
|
|
async fn add_user(
|
|
port: u16,
|
|
w_id: &str,
|
|
token: &str,
|
|
body: serde_json::Value,
|
|
) -> reqwest::Response {
|
|
as_user(
|
|
client().post(format!(
|
|
"http://localhost:{port}/api/w/{w_id}/workspaces/add_user"
|
|
)),
|
|
token,
|
|
)
|
|
.json(&body)
|
|
.send()
|
|
.await
|
|
.unwrap()
|
|
}
|
|
|
|
fn developer(email: &str) -> serde_json::Value {
|
|
json!({ "email": email, "is_admin": false, "operator": false })
|
|
}
|
|
|
|
/// The creator of a fork may manage developers on it without being an admin of it, and may do
|
|
/// nothing beyond that. A fork clones its parent wholesale (secrets included), so each of these
|
|
/// bounds is what keeps the grant from becoming a way for any developer to widen access to the
|
|
/// parent's data or to mint an admin.
|
|
#[sqlx::test(migrations = "../migrations", fixtures("base", "fork_member_grant"))]
|
|
async fn test_fork_creator_can_only_manage_developers_on_their_fork(
|
|
db: Pool<Postgres>,
|
|
) -> anyhow::Result<()> {
|
|
initialize_tracing().await;
|
|
let server = ApiServer::start(db.clone()).await?;
|
|
let port = server.addr.port();
|
|
|
|
// Seating an eligible member on someone else's username is what would hand them that user's
|
|
// cloned private assets. `test-user-4` is the target that matters: a fork clones the parent's
|
|
// `u/test-user-4/` scripts, variables and secrets but not their membership, so the username is
|
|
// free of the unique constraint on `usr` and the squat would otherwise land.
|
|
let resp = add_user(
|
|
port,
|
|
"wm-fork-test",
|
|
FORK_OWNER_TOKEN,
|
|
json!({ "email": "test3@windmill.dev", "username": "test-user-4", "is_admin": false, "operator": false }),
|
|
)
|
|
.await;
|
|
assert_eq!(
|
|
resp.status(),
|
|
403,
|
|
"fork creator cannot choose the username a member joins under"
|
|
);
|
|
|
|
// The creator adds a developer of the parent as a developer of their fork.
|
|
let resp = add_user(
|
|
port,
|
|
"wm-fork-test",
|
|
FORK_OWNER_TOKEN,
|
|
developer("test3@windmill.dev"),
|
|
)
|
|
.await;
|
|
assert_eq!(resp.status(), 201, "fork creator can add a developer");
|
|
|
|
// They join under the username they hold in the parent, which is the `u/` namespace the fork
|
|
// cloned for them.
|
|
let username: String = sqlx::query_scalar(
|
|
"SELECT username FROM usr WHERE workspace_id = 'wm-fork-test' AND email = 'test3@windmill.dev'",
|
|
)
|
|
.fetch_one(&db)
|
|
.await?;
|
|
assert_eq!(
|
|
username, "test-user-3",
|
|
"added member keeps their parent username"
|
|
);
|
|
|
|
// ... but never as an admin.
|
|
let resp = add_user(
|
|
port,
|
|
"wm-fork-test",
|
|
FORK_OWNER_TOKEN,
|
|
json!({ "email": "test4@windmill.dev", "is_admin": true, "operator": false }),
|
|
)
|
|
.await;
|
|
assert_eq!(resp.status(), 403, "fork creator cannot add an admin");
|
|
|
|
// ... nor anyone who is only an operator of the parent, which would widen their access.
|
|
let resp = add_user(
|
|
port,
|
|
"wm-fork-test",
|
|
FORK_OWNER_TOKEN,
|
|
developer("test4@windmill.dev"),
|
|
)
|
|
.await;
|
|
assert_eq!(
|
|
resp.status(),
|
|
403,
|
|
"fork creator cannot add an operator of the parent"
|
|
);
|
|
|
|
// ... nor anyone from outside the parent workspace.
|
|
let resp = add_user(
|
|
port,
|
|
"wm-fork-test",
|
|
FORK_OWNER_TOKEN,
|
|
developer("outsider@windmill.dev"),
|
|
)
|
|
.await;
|
|
assert_eq!(
|
|
resp.status(),
|
|
403,
|
|
"fork creator cannot add a non-member of the parent"
|
|
);
|
|
|
|
// The grant covers the fork alone, not the workspace it was forked from.
|
|
let resp = add_user(
|
|
port,
|
|
"test-workspace",
|
|
FORK_OWNER_TOKEN,
|
|
developer("test4@windmill.dev"),
|
|
)
|
|
.await;
|
|
assert_eq!(
|
|
resp.status(),
|
|
403,
|
|
"fork creator gains nothing on the parent workspace"
|
|
);
|
|
|
|
// ... and belongs to the creator, not to every member of the fork.
|
|
let resp = add_user(
|
|
port,
|
|
"wm-fork-test",
|
|
FORK_MEMBER_TOKEN,
|
|
developer("test4@windmill.dev"),
|
|
)
|
|
.await;
|
|
assert_eq!(
|
|
resp.status(),
|
|
403,
|
|
"a fork member who did not create it gains nothing"
|
|
);
|
|
|
|
// Removing is the counterpart of adding: allowed for the developer they just added...
|
|
let resp = as_user(
|
|
client().delete(format!(
|
|
"http://localhost:{port}/api/w/wm-fork-test/users/delete/test-user-3"
|
|
)),
|
|
FORK_OWNER_TOKEN,
|
|
)
|
|
.send()
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(resp.status(), 200, "fork creator can remove a developer");
|
|
|
|
// ... but not for an admin of the fork.
|
|
let resp = as_user(
|
|
client().delete(format!(
|
|
"http://localhost:{port}/api/w/wm-fork-test/users/delete/test-user"
|
|
)),
|
|
FORK_OWNER_TOKEN,
|
|
)
|
|
.send()
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(
|
|
resp.status(),
|
|
403,
|
|
"fork creator cannot remove an admin of the fork"
|
|
);
|
|
|
|
Ok(())
|
|
}
|