diff --git a/backend/windmill-api/src/apps.rs b/backend/windmill-api/src/apps.rs index fad23ac8c4..97409f5b81 100644 --- a/backend/windmill-api/src/apps.rs +++ b/backend/windmill-api/src/apps.rs @@ -1383,7 +1383,30 @@ async fn build_args( let arg_str = v.get(); - if !arg_str.contains("\"$var:") && !arg_str.contains("\"$res:") { + if arg_str.starts_with("\"$ctx:") { + let prop = arg_str.trim_start_matches("\"$ctx:").trim_end_matches("\""); + let value = match prop { + "username" => authed.as_ref().map(|a| { + serde_json::to_value(a.username_override.as_ref().unwrap_or(&a.username)) + }), + "email" => authed.as_ref().map(|a| serde_json::to_value(&a.email)), + "workspace" => Some(serde_json::to_value(&w_id)), + "groups" => authed.as_ref().map(|a| serde_json::to_value(&a.groups)), + "author" => Some(serde_json::to_value(&policy.on_behalf_of_email)), + _ => { + return Err(Error::BadRequest(format!( + "context variable {} not allowed", + prop + ))) + } + }; + safe_args.insert( + k.to_string(), + to_raw_value(&value.unwrap_or(Ok(serde_json::Value::Null)).map_err(|e| { + Error::InternalErr(format!("failed to serialize ctx variable for {}: {}", k, e)) + })?), + ); + } else if !arg_str.contains("\"$var:") && !arg_str.contains("\"$res:") { safe_args.insert(k.to_string(), v); } else { safe_args.insert( diff --git a/frontend/src/lib/components/apps/components/helpers/RunnableComponent.svelte b/frontend/src/lib/components/apps/components/helpers/RunnableComponent.svelte index bea9c6dadc..e38b469c84 100644 --- a/frontend/src/lib/components/apps/components/helpers/RunnableComponent.svelte +++ b/frontend/src/lib/components/apps/components/helpers/RunnableComponent.svelte @@ -25,6 +25,7 @@ import { userStore } from '$lib/stores' import { get } from 'svelte/store' import RefreshButton from '$lib/components/apps/components/helpers/RefreshButton.svelte' + import { ctxRegex } from '../../utils' // Component props export let id: string @@ -341,7 +342,12 @@ allowUserResources.push(k) } } else if (field?.type == 'eval' || (field?.type == 'evalv2' && inputValues[k])) { - nonStaticRunnableInputs[k] = await inputValues[k]?.computeExpr() + const ctxMatch = field.expr.match(ctxRegex) + if (ctxMatch) { + nonStaticRunnableInputs[k] = '$ctx:' + ctxMatch[1] + } else { + nonStaticRunnableInputs[k] = await inputValues[k]?.computeExpr() + } if (isEditor && field?.type == 'evalv2' && field.allowUserResources) { allowUserResources.push(k) } diff --git a/frontend/src/lib/components/apps/editor/settingsPanel/ComponentPanel.svelte b/frontend/src/lib/components/apps/editor/settingsPanel/ComponentPanel.svelte index 14cf1af3c3..024b76e89f 100644 --- a/frontend/src/lib/components/apps/editor/settingsPanel/ComponentPanel.svelte +++ b/frontend/src/lib/components/apps/editor/settingsPanel/ComponentPanel.svelte @@ -363,6 +363,7 @@ component.type === 'aggridinfinitecomponentee' ? ['offset', 'limit', 'orderBy', 'isDesc', 'search'] : []} + securedContext /> {/if} diff --git a/frontend/src/lib/components/apps/editor/settingsPanel/InputsSpecEditor.svelte b/frontend/src/lib/components/apps/editor/settingsPanel/InputsSpecEditor.svelte index 246a25d48b..14ecb4855c 100644 --- a/frontend/src/lib/components/apps/editor/settingsPanel/InputsSpecEditor.svelte +++ b/frontend/src/lib/components/apps/editor/settingsPanel/InputsSpecEditor.svelte @@ -42,6 +42,7 @@ export let showOnDemandOnlyToggle = true export let documentationLink: string | undefined = undefined export let markdownTooltip: string | undefined = undefined + export let securedContext = false const { connectingInput, app } = getContext('AppViewerContext') @@ -185,6 +186,7 @@ {fixedOverflowWidgets} {recomputeOnInputChanged} {showOnDemandOnlyToggle} + {securedContext} /> {:else if componentInput?.type === 'upload'} diff --git a/frontend/src/lib/components/apps/editor/settingsPanel/InputsSpecsEditor.svelte b/frontend/src/lib/components/apps/editor/settingsPanel/InputsSpecsEditor.svelte index 4dad7ce22e..9b76c3c759 100644 --- a/frontend/src/lib/components/apps/editor/settingsPanel/InputsSpecsEditor.svelte +++ b/frontend/src/lib/components/apps/editor/settingsPanel/InputsSpecsEditor.svelte @@ -18,6 +18,7 @@ export let acceptSelf: boolean = false export let recomputeOnInputChanged = true export let showOnDemandOnlyToggle = false + export let securedContext = false export let overridenByComponent: string[] = [] $: finalInputSpecsConfiguration = inputSpecsConfiguration ?? inputSpecs @@ -79,6 +80,7 @@ {displayType} {recomputeOnInputChanged} {showOnDemandOnlyToggle} + {securedContext} /> {#if deletable}
diff --git a/frontend/src/lib/components/apps/editor/settingsPanel/inputEditor/EvalV2InputEditor.svelte b/frontend/src/lib/components/apps/editor/settingsPanel/inputEditor/EvalV2InputEditor.svelte index 2bbb71a2e1..5296fbc21c 100644 --- a/frontend/src/lib/components/apps/editor/settingsPanel/inputEditor/EvalV2InputEditor.svelte +++ b/frontend/src/lib/components/apps/editor/settingsPanel/inputEditor/EvalV2InputEditor.svelte @@ -4,13 +4,14 @@ import { getContext } from 'svelte' import type { AppEditorContext, AppViewerContext } from '$lib/components/apps/types' import SimpleEditor from '$lib/components/SimpleEditor.svelte' - import { buildExtraLib } from '$lib/components/apps/utils' + import { buildExtraLib, ctxRegex } from '$lib/components/apps/utils' import { inferDeps } from '../../appUtilsInfer' - import { Maximize2, X } from 'lucide-svelte' + import { Maximize2, Shield, X } from 'lucide-svelte' import { Drawer } from '$lib/components/common' import { Pane, Splitpanes } from 'svelte-splitpanes' import Toggle from '$lib/components/Toggle.svelte' import { zIndexes } from '$lib/zIndexes' + import Popover from '$lib/components/Popover.svelte' export let componentInput: EvalV2AppInput | undefined export let id: string @@ -19,6 +20,7 @@ export let acceptSelf: boolean = false export let recomputeOnInputChanged = true export let showOnDemandOnlyToggle = false + export let securedContext = false const { onchange, worldStore, state, app } = getContext('AppViewerContext') const { evalPreview } = getContext('AppEditorContext') @@ -119,6 +121,17 @@ inferDepsFromCode(e.detail.code) }} /> + {#if securedContext && componentInput?.expr?.match(ctxRegex)} +
+ + + + This context variable is securely provided by the backend and cannot be altered by + users + + +
+ {/if} -
- {#key args} - {#key requestType} - {#key webhookType} - {#key tokenType} - {#key token} -
- -
+ {#if emailDomain} +
+ {#key args} + {#key requestType} + {#key webhookType} + {#key tokenType} + {#key token} +
+ +
+ {/key} {/key} {/key} {/key} {/key} - {/key} - - To trigger the job by email, send an email to the address above. The job will receive - two arguments: `raw_email` containing the raw email as string, and `parsed_email` - containing the parsed email as an object. - -
+ + To trigger the job by email, send an email to the address above. The job will + receive two arguments: `raw_email` containing the raw email as string, and + `parsed_email` containing the parsed email as an object. + +
+ {:else} +
+ + Ask an instance superadmin to setup the instance for email triggering (docs) and to set the email domain in the instance settings. + +
+ {/if}
{/key} diff --git a/frontend/src/lib/components/instanceSettings.ts b/frontend/src/lib/components/instanceSettings.ts index 5f1d4f765f..b5678b6b1f 100644 --- a/frontend/src/lib/components/instanceSettings.ts +++ b/frontend/src/lib/components/instanceSettings.ts @@ -46,8 +46,7 @@ export const settings: Record = { }, { label: 'Email domain', - description: - 'Domain to display in webhooks for email triggers, default is the webpage domain prefixed by "mail."', + description: 'Domain to display in webhooks for email triggers (should match the MX record)', key: 'email_domain', fieldType: 'text', storage: 'setting',