rework default visibility of runs of scripts not belonging to oneself

This commit is contained in:
Ruben Fiszel
2022-12-15 18:55:30 +01:00
parent 59a3ba0a25
commit ec6afe07cc
14 changed files with 224 additions and 156 deletions

View File

@@ -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;
ALTER TABLE completed_job DROP COLUMN visible_to_owner;
DROP POLICY see_own_path ON queue;

View File

@@ -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[]));

View File

@@ -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": [

View File

@@ -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

View File

@@ -29,8 +29,8 @@
{#if item.action}
<button
on:click={(event) => {
event.preventDefault()
if (!item.disabled) {
event.preventDefault()
close()
item.action && item.action(event)
dispatch('click', { item: item?.eventName })
@@ -56,12 +56,14 @@
{/if}
{item.displayName}
</button>
{:else if item.href}
{:else if item.href && !item.disabled}
<a
href={item.href}
on:click={() => {
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 @@
</a>
{:else}
<span
class="block px-4 py-2 text-sm text-gray-700"
class:bg-gray-50={item.disabled}
class="block px-4 py-2 text-sm text-gray-700 cursor-auto"
role="menuitem"
tabindex="-1"
id="user-menu-item-{name}-{i}}"
on:click|preventDefault
>
{#if item.icon}
<Icon
data={item.icon}
scale={0.6}
class="inline mr-2 {item.type == 'delete' ? 'text-red-500' : 'text-gray-700'}"
/>
{/if}
{item.displayName}
</span>
{/if}

View File

@@ -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<string, boolean>
}
| undefined
export let runAction: (scheduledForStr: string | undefined, args: Record<string, any>) => void
export let runAction: (
scheduledForStr: string | undefined,
args: Record<string, any>,
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<string, any> = 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
</script>
<div class="max-w-6xl">
@@ -154,10 +162,23 @@
>
Schedule to run later
</Button>
{#if runnable?.path?.startsWith(`u/${$userStore?.username}`) != true && (runnable?.path?.split('/')?.length ?? 0) > 2}
<div class="flex items-center gap-1">
<Toggle
options={{
right: `make run invisible to ${runnable?.path?.split('/').slice(0, 2).join('/')}`
}}
bind:checked={invisible_to_owner}
/>
<Tooltip
>By default, runs are visible to the owner of the script or flow being triggered</Tooltip
>
</div>
{/if}
<Button
btnClasses="!px-6 !py-1"
disabled={!isValid}
on:click={() => runAction(scheduledForStr, args)}
on:click={() => runAction(scheduledForStr, args, invisible_to_owner)}
>
{scheduledForStr ? 'Schedule run to a later time' : buttonText}
</Button>
@@ -166,7 +187,7 @@
<Button
btnClasses="!px-6 !py-1 w-full"
disabled={!isValid}
on:click={() => runAction(undefined, args)}
on:click={() => runAction(undefined, args, invisible_to_owner)}
>
{buttonText}
</Button>

View File

@@ -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"
/>
</span>
</label>
<div>
<div class="font-bold pb-1 mt-4">Description preview</div>
{#if $flowStore.description}
<div class="prose max-h-48 mt-5 text-xs shadow-inner shadow-blue p-4 overflow-auto">
{$flowStore.description}
</div>
{:else}
<div class="text-sm text-gray-500"> Enter a description to see the preview </div>
{/if}
</div>
</TabContent>
<TabContent value="schedule" class="p-4">
<Alert type="info" title="Primary Schedule">

View File

@@ -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
})}`
}
}

View File

@@ -101,11 +101,16 @@
let runForm: RunForm | undefined
let isValid = true
async function runFlow(scheduledForStr: string | undefined, args: Record<string, any>) {
async function runFlow(
scheduledForStr: string | undefined,
args: Record<string, any>,
invisibleToOwner?: boolean
) {
const scheduledFor = scheduledForStr ? new Date(scheduledForStr).toISOString() : undefined
let run = await JobService.runFlowByPath({
workspace: $workspaceStore!,
path,
invisibleToOwner,
requestBody: args,
scheduledFor
})

View File

@@ -45,11 +45,16 @@
}
}
async function runFlow(scheduledForStr: string | undefined, args: Record<string, any>) {
async function runFlow(
scheduledForStr: string | undefined,
args: Record<string, any>,
invisibleToOwner?: boolean
) {
const scheduledFor = scheduledForStr ? new Date(scheduledForStr).toISOString() : undefined
let run = await JobService.runFlowByPath({
workspace: $workspaceStore!,
path,
invisibleToOwner,
requestBody: args,
scheduledFor
})

View File

@@ -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 @@
<Button
disabled={not_same_workspace}
color="red"
size="xs"
size="md"
startIcon={{ icon: faTimesCircle }}
on:click|once={() => {
if (job?.id) {
@@ -190,7 +191,7 @@
href={runHref}
disabled={isRunning || not_same_workspace}
color="blue"
size="xs"
size="md"
startIcon={{ icon: faRefresh }}>Run again</Button
>
{#if !$userStore?.operator}
@@ -199,7 +200,7 @@
disabled={not_same_workspace}
href={editHref}
color="blue"
size="xs"
size="md"
startIcon={{ icon: faEdit }}>Edit</Button
>
{/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}<Badge color="blue">{job.job_kind}</Badge>
{/if}
{#if !job.visible_to_owner}<Badge color="red"
>only visible to you <Tooltip
>The option to hide this run from the owner of this script or flow was activated</Tooltip
></Badge
>
{/if}
{/if}
</div>
</h1>

View File

@@ -143,14 +143,19 @@
let isValid = true
let runForm: RunForm | undefined
async function runScript(scheduledForStr: string | undefined, args: Record<string, any>) {
async function runScript(
scheduledForStr: string | undefined,
args: Record<string, any>,
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) {

View File

@@ -59,12 +59,17 @@
}
}
async function runScript(scheduledForStr: string | undefined, args: Record<string, any>) {
async function runScript(
scheduledForStr: string | undefined,
args: Record<string, any>,
invisibleToOwner?: boolean
) {
try {
const scheduledFor = scheduledForStr ? new Date(scheduledForStr).toISOString() : undefined
let run = await JobService.runScriptByHash({
workspace: $workspaceStore!,
hash,
invisibleToOwner,
requestBody: args,
scheduledFor
})

View File

@@ -123,14 +123,16 @@
options={{ right: `Auto invite users with the same domain (${domain})` }}
/>
{#if auto_invite}
<Toggle
bind:checked={operatorOnly}
options={{ right: `Auto invited users to join as operators` }}
/>
<Tooltip
>An operator can only execute and view scripts/flows/apps from your workspace, and only those
that he has visibility on</Tooltip
>
<div class="flex items-center gap-1">
<Toggle
bind:checked={operatorOnly}
options={{ right: `Auto invited users to join as operators` }}
/>
<Tooltip
>An operator can only execute and view scripts/flows/apps from your workspace, and only
those that he has visibility on</Tooltip
>
</div>
{/if}
{#if !isDomainAllowed}
<div class="text-gray-600 text-sm mb-4 mt-2">{domain} domain not allowed for auto-invite</div>