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, ) -> 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(()) }