From ec6afe07cc4c6fb48e68cab48e9ec66717a86ab5 Mon Sep 17 00:00:00 2001 From: Ruben Fiszel Date: Thu, 15 Dec 2022 18:55:30 +0100 Subject: [PATCH] rework default visibility of runs of scripts not belonging to oneself --- ...20221215101605_share_job_to_owner.down.sql | 4 +- .../20221215101605_share_job_to_owner.up.sql | 12 + backend/sqlx-data.json | 220 +++++++++--------- backend/windmill-api/openapi.yaml | 3 + frontend/src/lib/components/Dropdown.svelte | 19 +- frontend/src/lib/components/RunForm.svelte | 29 ++- .../flows/content/FlowSettings.svelte | 13 +- frontend/src/lib/utils.ts | 13 +- .../src/routes/flows/get/[...path].svelte | 7 +- .../src/routes/flows/run/[...path].svelte | 7 +- frontend/src/routes/run/[...run].svelte | 19 +- .../src/routes/scripts/get/[...hash].svelte | 9 +- .../src/routes/scripts/run/[...hash].svelte | 7 +- .../routes/user/create_workspace@user.svelte | 18 +- 14 files changed, 224 insertions(+), 156 deletions(-) diff --git a/backend/migrations/20221215101605_share_job_to_owner.down.sql b/backend/migrations/20221215101605_share_job_to_owner.down.sql index a23ec345aa..0bf4bab40c 100644 --- a/backend/migrations/20221215101605_share_job_to_owner.down.sql +++ b/backend/migrations/20221215101605_share_job_to_owner.down.sql @@ -1,3 +1,5 @@ -- Add down migration script here ALTER TABLE queue DROP COLUMN visible_to_owner; -ALTER TABLE completed_job DROP COLUMN visible_to_owner; \ No newline at end of file +ALTER TABLE completed_job DROP COLUMN visible_to_owner; + +DROP POLICY see_own_path ON queue; \ No newline at end of file diff --git a/backend/migrations/20221215101605_share_job_to_owner.up.sql b/backend/migrations/20221215101605_share_job_to_owner.up.sql index a06405122b..53206dc613 100644 --- a/backend/migrations/20221215101605_share_job_to_owner.up.sql +++ b/backend/migrations/20221215101605_share_job_to_owner.up.sql @@ -1,3 +1,15 @@ -- Add up migration script here ALTER TABLE queue ADD COLUMN visible_to_owner BOOLEAN DEFAULT true; ALTER TABLE completed_job ADD COLUMN visible_to_owner BOOLEAN DEFAULT true; + +CREATE POLICY see_own_path ON queue FOR ALL +USING (queue.visible_to_owner IS true AND SPLIT_PART(queue.script_path, '/', 1) = 'u' AND SPLIT_PART(queue.script_path, '/', 2) = current_setting('session.user')); + +CREATE POLICY see_member_path ON queue FOR ALL +USING (queue.visible_to_owner IS true AND SPLIT_PART(queue.script_path, '/', 1) = 'g' AND SPLIT_PART(queue.script_path, '/', 2) = any(regexp_split_to_array(current_setting('session.groups'), ',')::text[])); + +CREATE POLICY see_own_path ON completed_job FOR ALL +USING (completed_job.visible_to_owner IS true AND SPLIT_PART(completed_job.script_path, '/', 1) = 'u' AND SPLIT_PART(completed_job.script_path, '/', 2) = current_setting('session.user')); + +CREATE POLICY see_member_path ON completed_job FOR ALL +USING (completed_job.visible_to_owner IS true AND SPLIT_PART(completed_job.script_path, '/', 1) = 'g' AND SPLIT_PART(completed_job.script_path, '/', 2) = any(regexp_split_to_array(current_setting('session.groups'), ',')::text[])); diff --git a/backend/sqlx-data.json b/backend/sqlx-data.json index 0ebb8f3c0d..a94d170e3b 100644 --- a/backend/sqlx-data.json +++ b/backend/sqlx-data.json @@ -500,47 +500,7 @@ }, "query": "UPDATE queue SET logs = $1 WHERE id = $2" }, - "1f040850c2a82bc09789226b167c43fd4935cfbb4951760a4d527665b70a5ac7": { - "describe": { - "columns": [ - { - "name": "result", - "ordinal": 0, - "type_info": "Jsonb" - }, - { - "name": "id", - "ordinal": 1, - "type_info": "Uuid" - } - ], - "nullable": [ - true, - false - ], - "parameters": { - "Left": [ - "UuidArray", - "Text" - ] - } - }, - "query": "\n SELECT result, id\n FROM completed_job\n WHERE id = ANY($1)\n AND workspace_id = $2\n " - }, - "1f93b533fa6fee0db4340445da3fac8e6773bc1db1f88cd60fd3c1e8c9781eb0": { - "describe": { - "columns": [], - "nullable": [], - "parameters": { - "Left": [ - "Int4", - "Uuid" - ] - } - }, - "query": "UPDATE queue SET suspend = $1 WHERE id = $2" - }, - "207b9cca0240dd6eea3f3b95c093ead9d7bb48fae2484e1a4086636396c0dcb7": { + "1e35c39bc786d638252e5483ca4efae9a041f7e845341f8bfd715ddd9e899499": { "describe": { "columns": [ { @@ -602,11 +562,52 @@ }, "Bool", "Text", - "Varchar" + "Varchar", + "Bool" ] } }, - "query": "INSERT INTO queue\n (workspace_id, id, running, parent_job, created_by, permissioned_as, scheduled_for, \n script_hash, script_path, raw_code, raw_lock, args, job_kind, schedule_path, raw_flow, flow_status, is_flow_step, language, started_at, same_worker, pre_run_error, email)\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, CASE WHEN $3 THEN now() END, $19, $20, $21) RETURNING id" + "query": "INSERT INTO queue\n (workspace_id, id, running, parent_job, created_by, permissioned_as, scheduled_for, \n script_hash, script_path, raw_code, raw_lock, args, job_kind, schedule_path, raw_flow, flow_status, is_flow_step, language, started_at, same_worker, pre_run_error, email, visible_to_owner)\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, CASE WHEN $3 THEN now() END, $19, $20, $21, $22) RETURNING id" + }, + "1f040850c2a82bc09789226b167c43fd4935cfbb4951760a4d527665b70a5ac7": { + "describe": { + "columns": [ + { + "name": "result", + "ordinal": 0, + "type_info": "Jsonb" + }, + { + "name": "id", + "ordinal": 1, + "type_info": "Uuid" + } + ], + "nullable": [ + true, + false + ], + "parameters": { + "Left": [ + "UuidArray", + "Text" + ] + } + }, + "query": "\n SELECT result, id\n FROM completed_job\n WHERE id = ANY($1)\n AND workspace_id = $2\n " + }, + "1f93b533fa6fee0db4340445da3fac8e6773bc1db1f88cd60fd3c1e8c9781eb0": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Left": [ + "Int4", + "Uuid" + ] + } + }, + "query": "UPDATE queue SET suspend = $1 WHERE id = $2" }, "20e8a8343adc4a3a28a1c4908a13047cddafc4480cf028d3455fd17347cace73": { "describe": { @@ -1629,6 +1630,73 @@ }, "query": "SELECT * from resource_type WHERE name = $1 AND (workspace_id = $2 OR workspace_id = 'starter')" }, + "59fc51efa01e823ec63f55a8081f282db0d7a40716c04f4043925eb84fe4429b": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Left": [ + "Varchar", + "Uuid", + "Uuid", + "Varchar", + "Timestamptz", + "Timestamptz", + "Bool", + "Int8", + "Varchar", + "Jsonb", + "Jsonb", + "Text", + "Text", + "Text", + "Bool", + "Varchar", + "Text", + { + "Custom": { + "kind": { + "Enum": [ + "script", + "preview", + "flow", + "dependencies", + "flowpreview", + "script_hub", + "identity", + "flowdependencies" + ] + }, + "name": "job_kind" + } + }, + "Varchar", + "Varchar", + "Jsonb", + "Jsonb", + "Bool", + "Bool", + { + "Custom": { + "kind": { + "Enum": [ + "python3", + "deno", + "go", + "bash" + ] + }, + "name": "script_lang" + } + }, + "Numeric", + "Varchar", + "Bool" + ] + } + }, + "query": "INSERT INTO completed_job AS cj\n ( workspace_id\n , id\n , parent_job\n , created_by\n , created_at\n , started_at\n , duration_ms\n , success\n , script_hash\n , script_path\n , args\n , result\n , logs\n , raw_code\n , raw_lock\n , canceled\n , canceled_by\n , canceled_reason\n , job_kind\n , schedule_path\n , permissioned_as\n , flow_status\n , raw_flow\n , is_flow_step\n , is_skipped\n , language\n , email\n , visible_to_owner\n )\n VALUES ($1, $2, $3, $4, $5, $6, COALESCE($26, EXTRACT(milliseconds FROM (now() - $6))), $7, $8, $9,$10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25, $27, $28)\n ON CONFLICT (id) DO UPDATE SET success = $7, result = $11, logs = concat(cj.logs, $12)" + }, "5b9b58612ca0f703a5d154a76fab82ac2329aef965fa937bfab2810b6e1336a4": { "describe": { "columns": [], @@ -2077,72 +2145,6 @@ }, "query": "SELECT email, login_type::text, verified, super_admin, name, company from password LIMIT $1 OFFSET $2" }, - "77c3915246e37ae158d0c600e2402b93ce4fb1179d034daf231e00797e214207": { - "describe": { - "columns": [], - "nullable": [], - "parameters": { - "Left": [ - "Varchar", - "Uuid", - "Uuid", - "Varchar", - "Timestamptz", - "Timestamptz", - "Bool", - "Int8", - "Varchar", - "Jsonb", - "Jsonb", - "Text", - "Text", - "Text", - "Bool", - "Varchar", - "Text", - { - "Custom": { - "kind": { - "Enum": [ - "script", - "preview", - "flow", - "dependencies", - "flowpreview", - "script_hub", - "identity", - "flowdependencies" - ] - }, - "name": "job_kind" - } - }, - "Varchar", - "Varchar", - "Jsonb", - "Jsonb", - "Bool", - "Bool", - { - "Custom": { - "kind": { - "Enum": [ - "python3", - "deno", - "go", - "bash" - ] - }, - "name": "script_lang" - } - }, - "Numeric", - "Varchar" - ] - } - }, - "query": "INSERT INTO completed_job AS cj\n ( workspace_id\n , id\n , parent_job\n , created_by\n , created_at\n , started_at\n , duration_ms\n , success\n , script_hash\n , script_path\n , args\n , result\n , logs\n , raw_code\n , raw_lock\n , canceled\n , canceled_by\n , canceled_reason\n , job_kind\n , schedule_path\n , permissioned_as\n , flow_status\n , raw_flow\n , is_flow_step\n , is_skipped\n , language\n , email )\n VALUES ($1, $2, $3, $4, $5, $6, COALESCE($26, EXTRACT(milliseconds FROM (now() - $6))), $7, $8, $9,$10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25, $27)\n ON CONFLICT (id) DO UPDATE SET success = $7, result = $11, logs = concat(cj.logs, $12)" - }, "7a511ce8dbbf761423b527672ab02156aba5594623dc269992b34398673ca387": { "describe": { "columns": [ diff --git a/backend/windmill-api/openapi.yaml b/backend/windmill-api/openapi.yaml index 660fdcecd2..1601e01c1f 100644 --- a/backend/windmill-api/openapi.yaml +++ b/backend/windmill-api/openapi.yaml @@ -4154,6 +4154,8 @@ components: enum: [python3, deno, go, bash] email: type: string + visible_to_owner: + type: boolean required: - id - running @@ -4162,6 +4164,7 @@ components: - permissioned_as - is_flow_step - email + - visible_to_owner CompletedJob: type: object diff --git a/frontend/src/lib/components/Dropdown.svelte b/frontend/src/lib/components/Dropdown.svelte index 60e3d95d7a..f19f9a87ff 100644 --- a/frontend/src/lib/components/Dropdown.svelte +++ b/frontend/src/lib/components/Dropdown.svelte @@ -29,8 +29,8 @@ {#if item.action} - {:else if item.href} + {:else if item.href && !item.disabled} { + on:click={(e) => { if (!item.disabled) { close() + } else { + e.preventDefault() } }} class="block w-full px-4 py-2 text-sm text-gray-700 hover:drop-shadow-sm hover:bg-gray-50 hover:bg-opacity-30" @@ -81,11 +83,20 @@ {:else} + {#if item.icon} + + {/if} {item.displayName} {/if} diff --git a/frontend/src/lib/components/RunForm.svelte b/frontend/src/lib/components/RunForm.svelte index 6cfa6566b8..da6774f337 100644 --- a/frontend/src/lib/components/RunForm.svelte +++ b/frontend/src/lib/components/RunForm.svelte @@ -15,6 +15,9 @@ import type { Schema } from '$lib/common' import { Badge, Button } from './common' import SharedBadge from './SharedBadge.svelte' + import Toggle from './Toggle.svelte' + import { userStore } from '$lib/stores' + import Tooltip from './Tooltip.svelte' export let runnable: | { @@ -31,7 +34,11 @@ extra_perms?: Record } | undefined - export let runAction: (scheduledForStr: string | undefined, args: Record) => void + export let runAction: ( + scheduledForStr: string | undefined, + args: Record, + invisible_to_owner?: boolean + ) => void export let buttonText = 'Run' export let schedulable = true export let detailed = true @@ -41,7 +48,7 @@ export let args: Record = decodeArgs($page.url.searchParams.get('args') ?? undefined) export function run() { - runAction(scheduledForStr, args) + runAction(scheduledForStr, args, invisible_to_owner) } export let isValid = true @@ -49,6 +56,7 @@ // Run later let viewOptions = false let scheduledForStr: string | undefined + let invisible_to_owner: false
@@ -154,10 +162,23 @@ > Schedule to run later + {#if runnable?.path?.startsWith(`u/${$userStore?.username}`) != true && (runnable?.path?.split('/')?.length ?? 0) > 2} +
+ + By default, runs are visible to the owner of the script or flow being triggered +
+ {/if} @@ -166,7 +187,7 @@ diff --git a/frontend/src/lib/components/flows/content/FlowSettings.svelte b/frontend/src/lib/components/flows/content/FlowSettings.svelte index 39e503750c..a0e508457c 100644 --- a/frontend/src/lib/components/flows/content/FlowSettings.svelte +++ b/frontend/src/lib/components/flows/content/FlowSettings.svelte @@ -55,22 +55,11 @@ class="text-sm" id="inp" bind:value={$flowStore.description} - placeholder="A description to help users understand what this flow does and how to use it. Markdown accepted." + placeholder="A description to help users understand what this flow does and how to use it." rows="3" /> - -
-
Description preview
- {#if $flowStore.description} -
- {$flowStore.description} -
- {:else} -
Enter a description to see the preview
- {/if} -
diff --git a/frontend/src/lib/utils.ts b/frontend/src/lib/utils.ts index 1d992f381b..a359e552fa 100644 --- a/frontend/src/lib/utils.ts +++ b/frontend/src/lib/utils.ts @@ -60,13 +60,12 @@ export function displayDate(dateString: string | undefined, displaySecond = fals if (date.toString() === 'Invalid Date') { return '' } else { - return `${date.getFullYear()}/${ - date.getMonth() + 1 - }/${date.getDate()} at ${date.toLocaleTimeString([], { - hour: '2-digit', - minute: '2-digit', - second: displaySecond ? '2-digit' : undefined - })}` + return `${date.getFullYear()}/${date.getMonth() + 1 + }/${date.getDate()} at ${date.toLocaleTimeString([], { + hour: '2-digit', + minute: '2-digit', + second: displaySecond ? '2-digit' : undefined + })}` } } diff --git a/frontend/src/routes/flows/get/[...path].svelte b/frontend/src/routes/flows/get/[...path].svelte index 5ab391cbed..33430749eb 100644 --- a/frontend/src/routes/flows/get/[...path].svelte +++ b/frontend/src/routes/flows/get/[...path].svelte @@ -101,11 +101,16 @@ let runForm: RunForm | undefined let isValid = true - async function runFlow(scheduledForStr: string | undefined, args: Record) { + async function runFlow( + scheduledForStr: string | undefined, + args: Record, + invisibleToOwner?: boolean + ) { const scheduledFor = scheduledForStr ? new Date(scheduledForStr).toISOString() : undefined let run = await JobService.runFlowByPath({ workspace: $workspaceStore!, path, + invisibleToOwner, requestBody: args, scheduledFor }) diff --git a/frontend/src/routes/flows/run/[...path].svelte b/frontend/src/routes/flows/run/[...path].svelte index f59aad2511..f6aa2b7022 100644 --- a/frontend/src/routes/flows/run/[...path].svelte +++ b/frontend/src/routes/flows/run/[...path].svelte @@ -45,11 +45,16 @@ } } - async function runFlow(scheduledForStr: string | undefined, args: Record) { + async function runFlow( + scheduledForStr: string | undefined, + args: Record, + invisibleToOwner?: boolean + ) { const scheduledFor = scheduledForStr ? new Date(scheduledForStr).toISOString() : undefined let run = await JobService.runFlowByPath({ workspace: $workspaceStore!, path, + invisibleToOwner, requestBody: args, scheduledFor }) diff --git a/frontend/src/routes/run/[...run].svelte b/frontend/src/routes/run/[...run].svelte index 3b14165f6e..1ea9112f1e 100644 --- a/frontend/src/routes/run/[...run].svelte +++ b/frontend/src/routes/run/[...run].svelte @@ -38,6 +38,7 @@ import FlowProgressBar from '$lib/components/flows/FlowProgressBar.svelte' import Tabs from '$lib/components/common/tabs/Tabs.svelte' import Badge from '$lib/components/common/badge/Badge.svelte' + import Tooltip from '$lib/components/Tooltip.svelte' $: workspace_id = $page.url.searchParams.get('workspace') ?? $workspaceStore $: not_same_workspace = workspace_id !== $workspaceStore @@ -134,7 +135,7 @@ disabled={not_same_workspace} variant="border" color="red" - size="xs" + size="md" startIcon={{ icon: faTrash }} on:click={() => job?.id && deleteCompletedJob(job.id)} > @@ -145,7 +146,7 @@ href={runsHref} variant="border" color="blue" - size="xs" + size="md" startIcon={{ icon: faList }} > View runs @@ -170,7 +171,7 @@ {#if !$userStore?.operator} @@ -199,7 +200,7 @@ disabled={not_same_workspace} href={editHref} color="blue" - size="xs" + size="md" startIcon={{ icon: faEdit }}>Edit {/if} @@ -208,7 +209,7 @@ disabled={not_same_workspace} href={viewHref} color="blue" - size="xs" + size="md" startIcon={{ icon: faScroll }} > View {job?.job_kind} @@ -273,6 +274,12 @@ {/if} {#if job && 'job_kind' in job}{job.job_kind} {/if} + {#if !job.visible_to_owner}only visible to you The option to hide this run from the owner of this script or flow was activated + {/if} {/if}
diff --git a/frontend/src/routes/scripts/get/[...hash].svelte b/frontend/src/routes/scripts/get/[...hash].svelte index 3ad57775a3..8f3b8fb6f7 100644 --- a/frontend/src/routes/scripts/get/[...hash].svelte +++ b/frontend/src/routes/scripts/get/[...hash].svelte @@ -143,14 +143,19 @@ let isValid = true let runForm: RunForm | undefined - async function runScript(scheduledForStr: string | undefined, args: Record) { + async function runScript( + scheduledForStr: string | undefined, + args: Record, + invisibleToOwner?: boolean + ) { try { const scheduledFor = scheduledForStr ? new Date(scheduledForStr).toISOString() : undefined let run = await JobService.runScriptByHash({ workspace: $workspaceStore!, hash: script?.hash ?? '', requestBody: args, - scheduledFor + scheduledFor, + invisibleToOwner }) await goto('/run/' + run + '?workspace=' + $workspaceStore) } catch (err) { diff --git a/frontend/src/routes/scripts/run/[...hash].svelte b/frontend/src/routes/scripts/run/[...hash].svelte index ad5cb12f1f..db960bb131 100644 --- a/frontend/src/routes/scripts/run/[...hash].svelte +++ b/frontend/src/routes/scripts/run/[...hash].svelte @@ -59,12 +59,17 @@ } } - async function runScript(scheduledForStr: string | undefined, args: Record) { + async function runScript( + scheduledForStr: string | undefined, + args: Record, + invisibleToOwner?: boolean + ) { try { const scheduledFor = scheduledForStr ? new Date(scheduledForStr).toISOString() : undefined let run = await JobService.runScriptByHash({ workspace: $workspaceStore!, hash, + invisibleToOwner, requestBody: args, scheduledFor }) diff --git a/frontend/src/routes/user/create_workspace@user.svelte b/frontend/src/routes/user/create_workspace@user.svelte index b241470833..e0188c4559 100644 --- a/frontend/src/routes/user/create_workspace@user.svelte +++ b/frontend/src/routes/user/create_workspace@user.svelte @@ -123,14 +123,16 @@ options={{ right: `Auto invite users with the same domain (${domain})` }} /> {#if auto_invite} - - An operator can only execute and view scripts/flows/apps from your workspace, and only those - that he has visibility on +
+ + An operator can only execute and view scripts/flows/apps from your workspace, and only + those that he has visibility on +
{/if} {#if !isDomainAllowed}
{domain} domain not allowed for auto-invite