diff --git a/backend/windmill-api/src/apps.rs b/backend/windmill-api/src/apps.rs index a29883053c18e..96f0f78ab5877 100644 --- a/backend/windmill-api/src/apps.rs +++ b/backend/windmill-api/src/apps.rs @@ -10,6 +10,7 @@ use std::{collections::HashMap, sync::Arc}; use crate::{ db::{ApiAuthed, DB}, + job_helpers_ee::{download_s3_file_internal, DownloadFileQuery}, resources::get_resource_value_interpolated_internal, users::{require_owner_of_path, OptAuthed}, utils::WithStarredInfoQuery, @@ -27,7 +28,7 @@ use crate::{ }; use axum::{ extract::{Extension, Json, Path, Query}, - response::IntoResponse, + response::{IntoResponse, Response}, routing::{delete, get, post}, Router, }; @@ -92,6 +93,7 @@ pub fn unauthed_service() -> Router { Router::new() .route("/execute_component/*path", post(execute_component)) .route("/upload_s3_file/*path", post(upload_s3_file_from_app)) + .route("/download_s3_file/*path", get(download_s3_file_from_app)) .route("/public_app/:secret", get(get_public_app_by_secret)) .route("/public_resource/*path", get(get_public_resource)) } @@ -1678,6 +1680,65 @@ async fn upload_s3_file_from_app( return Ok(Json(UploadFileResponse { file_key })); } +#[cfg(not(feature = "parquet"))] +async fn download_s3_file_from_app() -> Result<()> { + return Err(Error::BadRequest( + "This endpoint requires the parquet feature to be enabled".to_string(), + )); +} + +#[cfg(feature = "parquet")] +async fn download_s3_file_from_app( + OptAuthed(opt_authed): OptAuthed, + Extension(db): Extension, + Path((w_id, path)): Path<(String, StripPath)>, + Query(query): Query, +) -> Result { + let path = path.to_path(); + + let policy_o = sqlx::query_scalar!( + "SELECT policy from app WHERE path = $1 AND workspace_id = $2", + path, + w_id + ) + .fetch_optional(&db) + .await?; + + let policy = policy_o + .map(|p| serde_json::from_value::(p).map_err(to_anyhow)) + .transpose()? + .unwrap_or_else(|| Policy { + execution_mode: ExecutionMode::Viewer, + triggerables: None, + triggerables_v2: None, + on_behalf_of: None, + on_behalf_of_email: None, + s3_inputs: None, + }); + + let (username, permissioned_as, email) = + get_on_behalf_details_from_policy_and_authed(&policy, &opt_authed).await?; + + let on_behalf_authed = + fetch_api_authed_from_permissioned_as(permissioned_as, email, &w_id, &db, Some(username)) + .await?; + + let allowed = sqlx::query_scalar!( + r#"SELECT EXISTS (SELECT 1 FROM completed_job WHERE result @> ('{"s3":"' || $1 || '"}')::jsonb AND workspace_id = $2 AND script_path LIKE $3 || '/%' AND (job_kind = 'appscript' OR job_kind = 'preview'))"#, + query.file_key, + w_id, + path, + ).fetch_one(&db) + .await? + .unwrap_or(false); + + if !allowed { + return Err(Error::BadRequest("File restricted".to_string())); + } + + download_s3_file_internal(on_behalf_authed, &db, None, "", &w_id, query).await +} + fn get_on_behalf_of(policy: &Policy) -> Result<(String, String)> { let permissioned_as = policy .on_behalf_of diff --git a/frontend/src/lib/components/DisplayResult.svelte b/frontend/src/lib/components/DisplayResult.svelte index 5e9c4fef8c5ce..37f57d7a3b649 100644 --- a/frontend/src/lib/components/DisplayResult.svelte +++ b/frontend/src/lib/components/DisplayResult.svelte @@ -47,6 +47,7 @@ export let drawerOpen = false export let nodeId: string | undefined = undefined export let language: string | undefined = undefined + export let appPath: string | undefined = undefined const IMG_MAX_SIZE = 10000000 const TABLE_MAX_SIZE = 5000000 @@ -645,7 +646,7 @@ > {:else if !result?.disable_download} - +