From 1050a7e9944f91237305943efb5c55b296f775f2 Mon Sep 17 00:00:00 2001 From: Diego Imbert Date: Thu, 21 Aug 2025 16:04:25 +0200 Subject: [PATCH] user path restriction works ! --- backend/windmill-api/src/s3_proxy.rs | 46 ++++++++++++++++++----- backend/windmill-common/src/s3_helpers.rs | 31 +++++++++++++++ 2 files changed, 67 insertions(+), 10 deletions(-) diff --git a/backend/windmill-api/src/s3_proxy.rs b/backend/windmill-api/src/s3_proxy.rs index 5c8c311335..585190f942 100644 --- a/backend/windmill-api/src/s3_proxy.rs +++ b/backend/windmill-api/src/s3_proxy.rs @@ -17,15 +17,17 @@ use windmill_common::{ db::UserDB, error::{to_anyhow, Error, Result}, jwt, - s3_helpers::{build_object_store_client, ObjectStoreResource}, + s3_helpers::{ + build_object_store_client, check_lfs_object_path_permissions, ObjectStoreResource, + }, }; use crate::{ auth::AuthCache, db::DB, - job_helpers_ee::upload_file_from_req, + job_helpers_ee::{get_large_file_storage, upload_file_from_req}, job_helpers_oss::{ - delete_s3_file_internal, get_workspace_s3_resource, read_object_streamable, + delete_s3_file_internal, get_workspace_s3_resource_from_lfs, read_object_streamable, DeleteS3FileQuery, }, }; @@ -46,7 +48,7 @@ async fn get_object( Extension(auth_cache): Extension>, req: Request, ) -> Result { - let uri = format!("/api/w/{w_id}/s3_proxy/{storage_str}/{object_key}"); + let uri = format!("/api/w/{}/s3_proxy/{}", w_id, req.uri().to_string()); let token = get_token(req.headers(), req.method().as_str(), &uri).await?; let Some(authed) = auth_cache.get_authed(Some(w_id.clone()), &token).await else { return Err(Error::NotAuthorized("Invalid token".to_string())); @@ -56,8 +58,16 @@ async fn get_object( _ => Some(storage_str.clone()), }; + let lfs = get_large_file_storage(&db, &w_id, storage).await?; + let lfs = match lfs { + Some(lfs) => lfs, + None => { + return Err(Error::InternalErr(format!("Large file storage not found",))); + } + }; + check_lfs_object_path_permissions(&lfs, &object_key, &authed.clone().into())?; let (_, s3_resource) = - get_workspace_s3_resource(&authed, &db, Some(user_db), &token, &w_id, storage).await?; + get_workspace_s3_resource_from_lfs(&authed, &db, Some(user_db), &token, &w_id, lfs).await?; let s3_resource = s3_resource.ok_or_else(|| { Error::InternalErr(format!( "Storage {} not found at the workspace level", @@ -78,7 +88,7 @@ async fn put_object( Extension(auth_cache): Extension>, req: Request, ) -> Result { - let uri = format!("/api/w/{w_id}/s3_proxy/{storage_str}/{object_key}"); + let uri = format!("/api/w/{}/s3_proxy/{}", w_id, req.uri().to_string()); let token = get_token(req.headers(), req.method().as_str(), &uri).await?; let Some(authed) = auth_cache.get_authed(Some(w_id.clone()), &token).await else { return Err(Error::NotAuthorized("Invalid token".to_string())); @@ -88,8 +98,16 @@ async fn put_object( _ => Some(storage_str.clone()), }; + let lfs = get_large_file_storage(&db, &w_id, storage).await?; + let lfs = match lfs { + Some(lfs) => lfs, + None => { + return Err(Error::InternalErr(format!("Large file storage not found",))); + } + }; + check_lfs_object_path_permissions(&lfs, &object_key, &authed.clone().into())?; let (_, s3_resource) = - get_workspace_s3_resource(&authed, &db, Some(user_db), &token, &w_id, storage).await?; + get_workspace_s3_resource_from_lfs(&authed, &db, Some(user_db), &token, &w_id, lfs).await?; let s3_resource = s3_resource.ok_or_else(|| { Error::InternalErr(format!( "Storage {} not found at the workspace level", @@ -131,7 +149,7 @@ async fn post_object( Extension(auth_cache): Extension>, req: Request, ) -> Result { - let uri = format!("/api/w/{w_id}/s3_proxy/{storage_str}/{object_key}"); + let uri = format!("/api/w/{}/s3_proxy/{}", w_id, req.uri().to_string()); let token = get_token(req.headers(), req.method().as_str(), &uri).await?; let Some(authed) = auth_cache.get_authed(Some(w_id.clone()), &token).await else { return Err(Error::NotAuthorized("Invalid token".to_string())); @@ -141,8 +159,16 @@ async fn post_object( _ => Some(storage_str.clone()), }; + let lfs = get_large_file_storage(&db, &w_id, storage).await?; + let lfs = match lfs { + Some(lfs) => lfs, + None => { + return Err(Error::InternalErr(format!("Large file storage not found",))); + } + }; + check_lfs_object_path_permissions(&lfs, &object_key, &authed.clone().into())?; let (_, s3_resource) = - get_workspace_s3_resource(&authed, &db, Some(user_db), &token, &w_id, storage).await?; + get_workspace_s3_resource_from_lfs(&authed, &db, Some(user_db), &token, &w_id, lfs).await?; let s3_resource = s3_resource.ok_or_else(|| { Error::InternalErr(format!( "Storage {} not found at the workspace level", @@ -167,7 +193,7 @@ async fn delete_object( Extension(auth_cache): Extension>, req: Request, ) -> Result<()> { - let uri = format!("/api/w/{w_id}/s3_proxy/{storage_str}/{object_key}"); + let uri = format!("/api/w/{}/s3_proxy/{}", w_id, req.uri().to_string()); let token = get_token(req.headers(), req.method().as_str(), &uri).await?; let Some(authed) = auth_cache.get_authed(Some(w_id.clone()), &token).await else { return Err(Error::NotAuthorized("Invalid token".to_string())); diff --git a/backend/windmill-common/src/s3_helpers.rs b/backend/windmill-common/src/s3_helpers.rs index b013fb298d..557aa5615c 100644 --- a/backend/windmill-common/src/s3_helpers.rs +++ b/backend/windmill-common/src/s3_helpers.rs @@ -1,3 +1,4 @@ +use crate::db::Authed; use crate::error::{self}; #[cfg(feature = "parquet")] use aws_sdk_sts::config::ProvideCredentials; @@ -236,6 +237,16 @@ impl LargeFileStorage { LargeFileStorage::GoogleCloudStorage(gcs_lfs) => &gcs_lfs.gcs_resource_path, } } + pub fn is_restricted_to_user_paths(&self) -> bool { + match self { + LargeFileStorage::S3Storage(lfs) => lfs.restricted_to_user_paths, + LargeFileStorage::S3AwsOidc(lfs) => lfs.restricted_to_user_paths, + LargeFileStorage::AzureBlobStorage(lfs) => lfs.restricted_to_user_paths, + LargeFileStorage::AzureWorkloadIdentity(lfs) => lfs.restricted_to_user_paths, + LargeFileStorage::GoogleCloudStorage(glfs) => glfs.restricted_to_user_paths, + } + .unwrap_or(false) + } } #[derive(Serialize, Deserialize, Debug)] @@ -1104,3 +1115,23 @@ impl ObjectStoreResource { } } } + +pub fn check_lfs_object_path_permissions( + lfs: &LargeFileStorage, + object_path: &str, + authed: &Authed, +) -> error::Result<()> { + if authed.is_admin { + return Ok(()); + } + let username = authed.username.as_str(); + + if lfs.is_restricted_to_user_paths() { + if !object_path.starts_with(&format!("u/{username}/")) { + return Err(error::Error::NotAuthorized(format!( + "Can only access u/{username}/**" + ))); + } + } + Ok(()) +}