diff --git a/backend/sqlx-data.json b/backend/sqlx-data.json index 7c3f3cb1b4..aa3fc4c31d 100644 --- a/backend/sqlx-data.json +++ b/backend/sqlx-data.json @@ -14,6 +14,27 @@ }, "query": "UPDATE usr SET disabled = $1 WHERE username = $2 AND workspace_id = $3" }, + "019258392434b3c8dfabfe53d61ad766626fe4ad67f101c1a58c9c9524531621": { + "describe": { + "columns": [ + { + "name": "id", + "ordinal": 0, + "type_info": "Int8" + } + ], + "nullable": [ + false + ], + "parameters": { + "Left": [ + "Text", + "Text" + ] + } + }, + "query": "SELECT app.id FROM app\n WHERE app.path = $1 AND app.workspace_id = $2" + }, "0355b53b1d45955ca56b2829372ce9c656d7f0ad7b8d0709161047f0d8cdc4f4": { "describe": { "columns": [ @@ -3430,6 +3451,75 @@ }, "query": "SELECT set_config('session.folders_write', $1, true)" }, + "9ae98fbcea508dfc7113621c00b856f98f38dfb701a4660ea1a4058a6d7564f2": { + "describe": { + "columns": [ + { + "name": "id", + "ordinal": 0, + "type_info": "Int8" + }, + { + "name": "path", + "ordinal": 1, + "type_info": "Varchar" + }, + { + "name": "summary", + "ordinal": 2, + "type_info": "Varchar" + }, + { + "name": "versions", + "ordinal": 3, + "type_info": "Int8Array" + }, + { + "name": "policy", + "ordinal": 4, + "type_info": "Jsonb" + }, + { + "name": "extra_perms", + "ordinal": 5, + "type_info": "Jsonb" + }, + { + "name": "value", + "ordinal": 6, + "type_info": "Jsonb" + }, + { + "name": "created_at", + "ordinal": 7, + "type_info": "Timestamptz" + }, + { + "name": "created_by", + "ordinal": 8, + "type_info": "Varchar" + } + ], + "nullable": [ + false, + false, + false, + false, + false, + false, + false, + false, + false + ], + "parameters": { + "Left": [ + "Int8", + "Text" + ] + } + }, + "query": "SELECT app.id, app.path, app.summary, app.versions, app.policy,\n app.extra_perms, app_version.value, \n app_version.created_at, app_version.created_by from app, app_version \n WHERE app.id = $1 AND app.workspace_id = $2 AND app_version.id = app.versions[array_upper(app.versions, 1)]" + }, "9db64c9ff790d8c833c1e831a87803c32103ce9de68cc9c08d6f56cc988d7e37": { "describe": { "columns": [ diff --git a/backend/windmill-api/openapi.yaml b/backend/windmill-api/openapi.yaml index 5966828354..c11de5be83 100644 --- a/backend/windmill-api/openapi.yaml +++ b/backend/windmill-api/openapi.yaml @@ -2528,6 +2528,41 @@ paths: schema: $ref: "#/components/schemas/AppWithLastVersion" + /w/{workspace}/apps/public_app/{path}: + get: + summary: get public app by secret + operationId: getPublicAppBySecret + tags: + - app + parameters: + - $ref: "#/components/parameters/WorkspaceId" + - $ref: "#/components/parameters/Path" + responses: + "200": + description: app details + content: + application/json: + schema: + $ref: "#/components/schemas/AppWithLastVersion" + + + /w/{workspace}/apps/secret_of/{path}: + get: + summary: get public secret of app + operationId: getPublicSecretOfApp + tags: + - app + parameters: + - $ref: "#/components/parameters/WorkspaceId" + - $ref: "#/components/parameters/Path" + responses: + "200": + description: app secret + content: + text/plain: + schema: + type: string + /w/{workspace}/apps/get/v/{id}: get: summary: get app by version diff --git a/backend/windmill-api/src/apps.rs b/backend/windmill-api/src/apps.rs index 8efe00370a..deff7b72c3 100644 --- a/backend/windmill-api/src/apps.rs +++ b/backend/windmill-api/src/apps.rs @@ -11,6 +11,7 @@ use crate::{ db::{UserDB, DB}, jobs::script_path_to_payload, users::{require_owner_of_path, Authed, OptAuthed}, + variables::build_crypt, }; use axum::{ extract::{Extension, Path, Query}, @@ -18,11 +19,13 @@ use axum::{ Json, Router, }; use hyper::StatusCode; +use magic_crypt::MagicCryptTrait; use serde::{Deserialize, Serialize}; use serde_json::{json, Map, Value}; use sha2::{Digest, Sha256}; use sql_builder::{bind::Bind, SqlBuilder}; use sqlx::{types::Uuid, FromRow}; +use std::str; use windmill_audit::{audit_log, ActionKind}; use windmill_common::{ apps::ListAppQuery, @@ -36,6 +39,7 @@ pub fn workspaced_service() -> Router { Router::new() .route("/list", get(list_apps)) .route("/get/p/*path", get(get_app)) + .route("/secret_of/*path", get(get_secret_id)) .route("/get/v/*id", get(get_app_by_id)) .route("/exists/*path", get(exists_app)) .route("/update/*path", post(update_app)) @@ -44,7 +48,9 @@ pub fn workspaced_service() -> Router { } pub fn unauthed_service() -> Router { - Router::new().route("/execute_component/*path", post(execute_component)) + Router::new() + .route("/execute_component/*path", post(execute_component)) + .route("/public_app/:secret", get(get_public_app_by_secret)) } #[derive(FromRow, Deserialize, Serialize)] @@ -223,19 +229,27 @@ async fn get_app_by_id( Ok(Json(app)) } -async fn get_public_app_by_secret_( - authed: Authed, +async fn get_public_app_by_secret( Extension(db): Extension, - Path((w_id, id)): Path<(String, i64)>, + Path((w_id, secret)): Path<(String, String)>, ) -> JsonResult { let mut tx = db.begin().await?; + let mc = build_crypt(&mut tx, &w_id).await?; + + let decrypted = mc + .decrypt_bytes_to_bytes(&(hex::decode(secret)?)) + .map_err(|e| Error::InternalErr(e.to_string()))?; + let bytes = str::from_utf8(&decrypted).map_err(to_anyhow)?; + + let id: i64 = bytes.parse().map_err(to_anyhow)?; + let app_o = sqlx::query_as!( AppWithLastVersion, "SELECT app.id, app.path, app.summary, app.versions, app.policy, app.extra_perms, app_version.value, app_version.created_at, app_version.created_by from app, app_version - WHERE app_version.id = $1 AND app.id = app_version.app_id AND app.workspace_id = $2", + WHERE app.id = $1 AND app.workspace_id = $2 AND app_version.id = app.versions[array_upper(app.versions, 1)]", id, &w_id ) @@ -247,6 +261,34 @@ async fn get_public_app_by_secret_( Ok(Json(app)) } +async fn get_secret_id( + authed: Authed, + Extension(user_db): Extension, + Path((w_id, path)): Path<(String, StripPath)>, +) -> Result { + let path = path.to_path(); + let mut tx = user_db.begin(&authed).await?; + + let id_o = sqlx::query_scalar!( + "SELECT app.id FROM app + WHERE app.path = $1 AND app.workspace_id = $2", + path, + &w_id + ) + .fetch_optional(&mut tx) + .await?; + + let id = not_found_if_none(id_o, "App", path.to_string())?; + + let mc = build_crypt(&mut tx, &w_id).await?; + + let hx = hex::encode(mc.encrypt_str_to_bytes(id.to_string())); + + tx.commit().await?; + + Ok(hx) +} + async fn create_app( authed: Authed, Extension(user_db): Extension, diff --git a/frontend/src/app.html b/frontend/src/app.html index 7d8e472fed..d3e2f16370 100644 --- a/frontend/src/app.html +++ b/frontend/src/app.html @@ -8,7 +8,7 @@ %sveltekit.head% - +
%sveltekit.body%
diff --git a/frontend/src/lib/components/apps/editor/AppEditor.svelte b/frontend/src/lib/components/apps/editor/AppEditor.svelte index 554ebc3b24..3690206c42 100644 --- a/frontend/src/lib/components/apps/editor/AppEditor.svelte +++ b/frontend/src/lib/components/apps/editor/AppEditor.svelte @@ -97,7 +97,7 @@ {#if !$userStore?.operator} {#if initialMode !== 'preview'} - + {/if} {#if previewing} diff --git a/frontend/src/lib/components/apps/editor/AppEditorHeader.svelte b/frontend/src/lib/components/apps/editor/AppEditorHeader.svelte index 8910b03dcc..55dbd38195 100644 --- a/frontend/src/lib/components/apps/editor/AppEditorHeader.svelte +++ b/frontend/src/lib/components/apps/editor/AppEditorHeader.svelte @@ -1,23 +1,28 @@ - - closeDrawer()}> + + closeSaveDrawer()}> -
- + + (publishDrawerOpen = false)}> + {#if appPath == ''} + Save this app once before you can publish it + {:else} + Every runnable will run with the permissions of the publisher of the app. This ensures that + every users gets the same experience. Make sure that the app does not expose actions that + are too sensitive to be exposed publicly. +
+ { + policy.execution_mode = e.detail + ? Policy.execution_mode.ANONYMOUS + : Policy.execution_mode.PUBLISHER + setPublishState() + }} + /> + + {#if policy.execution_mode == 'anonymous' && secretUrl} + {@const url = `${$page.url.hostname}/public/${$workspaceStore}/${secretUrl}`} + {@const href = $page.url.protocol + '//' + url} + + {/if} + {/if} + +
+ + + +
+
+ +
@@ -141,11 +220,11 @@ Full
-
+