mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-21 00:02:30 +00:00
fix: judge trigger permissions by the user acting in the session workspace
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
fffd8b5217
commit
f5d4fdab7d
@@ -2,6 +2,7 @@ import { getContext, setContext } from 'svelte'
|
||||
import { fromStore, toStore, type Readable } from 'svelte/store'
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import { withWorkspaceParam } from './sessions/sessionMode.svelte'
|
||||
import { useActingUser } from '$lib/actingUser.svelte'
|
||||
|
||||
// The workspace a subtree acts on. An AI session edits its (possibly forked) workspace while
|
||||
// `workspaceStore` stays on the navigation workspace, and a component that reads the navigation
|
||||
@@ -29,6 +30,16 @@ export function useOperatingWorkspace(): Readable<string | undefined> {
|
||||
return resolve ? toStore(() => resolve() ?? navigation.current) : workspaceStore
|
||||
}
|
||||
|
||||
/** The user acting in this subtree's operating workspace: `$userStore` describes the navigation
|
||||
* workspace, so a permission check reading it inside a fork's editor answers about the parent —
|
||||
* enabling or disabling controls by the wrong roles. Unresolved reads as unknown, which
|
||||
* `canWrite`/`isOwner` refuse; ask `resolved` before rendering that as a denial. Reads context
|
||||
* and registers an effect: call during component initialisation. */
|
||||
export function useOperatingUser(): ReturnType<typeof useActingUser> {
|
||||
const operating = fromStore(useOperatingWorkspace())
|
||||
return useActingUser(() => operating.current)
|
||||
}
|
||||
|
||||
/** Stamps this component's operating workspace onto an in-app link, which otherwise opens in
|
||||
* the navigation workspace. Reads context: call during component initialisation. */
|
||||
export function useOperatingWorkspaceHref(): (href: string) => string {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { existsSync, readFileSync } from 'node:fs'
|
||||
import { existsSync, readdirSync, readFileSync } from 'node:fs'
|
||||
import { dirname, join, relative, resolve } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
@@ -66,6 +66,25 @@ function readsNavigationStore(file: string): boolean {
|
||||
return /\$workspaceStore\b|\bget\(workspaceStore\)/.test(source)
|
||||
}
|
||||
|
||||
describe('trigger editors', () => {
|
||||
it('judge permissions by the acting user, not the navigation one', () => {
|
||||
// `$userStore` describes the navigation workspace: a session tab editing a fork would
|
||||
// enable or disable its controls by the parent's roles. `useOperatingUser()` answers for
|
||||
// the workspace the editor acts on.
|
||||
const editors = readdirSync(join(components, 'triggers'), {
|
||||
recursive: true,
|
||||
encoding: 'utf-8'
|
||||
})
|
||||
.filter((f) => f.endsWith('EditorInner.svelte'))
|
||||
.map((f) => join('triggers', f))
|
||||
expect(editors.length).toBeGreaterThan(10)
|
||||
const offenders = editors.filter((f) =>
|
||||
/\$userStore\b/.test(readFileSync(join(components, f), 'utf-8'))
|
||||
)
|
||||
expect(offenders).toEqual([])
|
||||
})
|
||||
})
|
||||
|
||||
describe('components under a session editor', () => {
|
||||
it('read the operating workspace, not the navigation store', () => {
|
||||
const reachable = reachableComponents()
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
import DrawerContent from '$lib/components/common/drawer/DrawerContent.svelte'
|
||||
import Path from '$lib/components/Path.svelte'
|
||||
import TriggerRunnablePicker from '$lib/components/triggers/TriggerRunnablePicker.svelte'
|
||||
import { usedTriggerKinds, userStore } from '$lib/stores'
|
||||
import { usedTriggerKinds } from '$lib/stores'
|
||||
import { canWrite, capitalize, emptyString, sendUserToast } from '$lib/utils'
|
||||
import { withForkConflictRetry } from '$lib/utils/forkConflict'
|
||||
import Section from '$lib/components/Section.svelte'
|
||||
@@ -42,7 +42,10 @@
|
||||
import { deepEqual } from 'fast-equals'
|
||||
import { useTriggerDraftSync } from '../useTriggerDraftSync.svelte'
|
||||
import LocalDraftBanner from '$lib/components/LocalDraftBanner.svelte'
|
||||
import { useOperatingWorkspace } from '$lib/components/operatingWorkspace.svelte'
|
||||
import {
|
||||
useOperatingUser,
|
||||
useOperatingWorkspace
|
||||
} from '$lib/components/operatingWorkspace.svelte'
|
||||
|
||||
interface Props {
|
||||
useDrawer?: boolean
|
||||
@@ -86,6 +89,8 @@
|
||||
cloudDisabled = false
|
||||
}: Props = $props()
|
||||
const operatingWorkspace = useOperatingWorkspace()
|
||||
const operatingUser = useOperatingUser()
|
||||
const actingUser = $derived(operatingUser.current)
|
||||
const wsId = $derived($operatingWorkspace)
|
||||
|
||||
let amqp_resource_path: string = $state('')
|
||||
@@ -101,7 +106,13 @@
|
||||
let pathError = $state('')
|
||||
let mode = $state<TriggerMode>('enabled')
|
||||
let dirtyPath = $state(false)
|
||||
let can_write = $state(true)
|
||||
let permsPath = $state<string | undefined>(undefined)
|
||||
let permsForWrite = $state<Record<string, boolean> | undefined>(undefined)
|
||||
// The acting user in the operating workspace arrives asynchronously, and an unknown user
|
||||
// refuses — so the editor stays read-only until the lookup lands, which is the safe answer.
|
||||
const can_write = $derived(
|
||||
permsPath === undefined ? true : canWrite(permsPath, permsForWrite ?? {}, actingUser)
|
||||
)
|
||||
let drawerLoading = $state(true)
|
||||
let showLoading = $state(false)
|
||||
let queue_name: string = $state('')
|
||||
@@ -166,6 +177,8 @@
|
||||
fixedScriptPath_?: string
|
||||
) {
|
||||
if (handOffPageDrawer(TRIGGER_PAGES.amqp.path, ePath)) return
|
||||
// A `whoami` that failed earlier would otherwise pin this workspace to "unknown user".
|
||||
operatingUser.forgetFailures()
|
||||
let loadingTimeout = setTimeout(() => {
|
||||
showLoading = true
|
||||
}, 100) // Do not show loading spinner for the first 100ms
|
||||
@@ -250,7 +263,8 @@
|
||||
is_flow = cfg?.is_flow
|
||||
path = cfg?.path
|
||||
mode = cfg?.mode ?? 'enabled'
|
||||
can_write = canWrite(cfg?.path, cfg?.extra_perms, $userStore)
|
||||
permsPath = cfg?.path
|
||||
permsForWrite = cfg?.extra_perms
|
||||
error_handler_path = cfg?.error_handler_path
|
||||
error_handler_args = cfg?.error_handler_args ?? {}
|
||||
retry = cfg?.retry
|
||||
@@ -541,7 +555,7 @@
|
||||
bind:scriptPath={script_path}
|
||||
{initialScriptPath}
|
||||
canWrite={can_write}
|
||||
isOperator={!!$userStore?.operator}
|
||||
isOperator={!!actingUser?.operator}
|
||||
/>
|
||||
</Section>
|
||||
{/if}
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
import Drawer from '$lib/components/common/drawer/Drawer.svelte'
|
||||
import DrawerContent from '$lib/components/common/drawer/DrawerContent.svelte'
|
||||
import Path from '$lib/components/Path.svelte'
|
||||
import { usedTriggerKinds, userStore } from '$lib/stores'
|
||||
import { usedTriggerKinds } from '$lib/stores'
|
||||
import { canWrite, capitalize, emptyString, sendUserToast } from '$lib/utils'
|
||||
import { withForkConflictRetry } from '$lib/utils/forkConflict'
|
||||
import { Loader2 } from 'lucide-svelte'
|
||||
@@ -41,6 +41,7 @@
|
||||
import TriggerRetriesAndErrorHandler from '../TriggerRetriesAndErrorHandler.svelte'
|
||||
import TriggerAdvancedBadges from '../TriggerAdvancedBadges.svelte'
|
||||
import {
|
||||
useOperatingUser,
|
||||
useOperatingWorkspace,
|
||||
useOperatingWorkspaceHref
|
||||
} from '$lib/components/operatingWorkspace.svelte'
|
||||
@@ -56,7 +57,15 @@
|
||||
let pathError = $state('')
|
||||
let mode = $state<TriggerMode>('enabled')
|
||||
let dirtyPath = $state(false)
|
||||
let can_write = $state(true)
|
||||
const operatingUser = useOperatingUser()
|
||||
const actingUser = $derived(operatingUser.current)
|
||||
let permsPath = $state<string | undefined>(undefined)
|
||||
let permsForWrite = $state<Record<string, boolean> | undefined>(undefined)
|
||||
// The acting user in the operating workspace arrives asynchronously, and an unknown user
|
||||
// refuses — so the editor stays read-only until the lookup lands, which is the safe answer.
|
||||
const can_write = $derived(
|
||||
permsPath === undefined ? true : canWrite(permsPath, permsForWrite ?? {}, actingUser)
|
||||
)
|
||||
let drawerLoading = $state(true)
|
||||
|
||||
let azure_resource_path: string = $state('')
|
||||
@@ -148,6 +157,8 @@
|
||||
defaultValues?: Record<string, any>
|
||||
) {
|
||||
if (handOffPageDrawer(TRIGGER_PAGES.azure.path, ePath)) return
|
||||
// A `whoami` that failed earlier would otherwise pin this workspace to "unknown user".
|
||||
operatingUser.forgetFailures()
|
||||
drawerLoading = true
|
||||
try {
|
||||
drawer?.openDrawer()
|
||||
@@ -251,7 +262,8 @@
|
||||
event_type_filters = cfg?.event_type_filters
|
||||
path = cfg?.path
|
||||
mode = cfg?.mode ?? 'enabled'
|
||||
can_write = canWrite(cfg?.path, cfg?.extra_perms, $userStore)
|
||||
permsPath = cfg?.path
|
||||
permsForWrite = cfg?.extra_perms
|
||||
error_handler_path = cfg?.error_handler_path
|
||||
error_handler_args = cfg?.error_handler_args ?? {}
|
||||
retry = cfg?.retry
|
||||
@@ -506,7 +518,7 @@
|
||||
bind:itemKind
|
||||
bind:scriptPath={script_path}
|
||||
allowRefresh={can_write}
|
||||
allowEdit={!$userStore?.operator}
|
||||
allowEdit={!actingUser?.operator}
|
||||
clearable
|
||||
/>
|
||||
{#if emptyString(script_path)}
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
type Retry,
|
||||
type TriggerMode
|
||||
} from '$lib/gen'
|
||||
import { usedTriggerKinds, userStore } from '$lib/stores'
|
||||
import { usedTriggerKinds } from '$lib/stores'
|
||||
import { canWrite, capitalize, emptyString, sendUserToast } from '$lib/utils'
|
||||
import Section from '$lib/components/Section.svelte'
|
||||
import { Loader2 } from 'lucide-svelte'
|
||||
@@ -39,6 +39,7 @@
|
||||
import TriggerSuspendedJobsAlert from '../TriggerSuspendedJobsAlert.svelte'
|
||||
import TriggerSuspendedJobsModal from '../TriggerSuspendedJobsModal.svelte'
|
||||
import {
|
||||
useOperatingUser,
|
||||
useOperatingWorkspace,
|
||||
useOperatingWorkspaceHref
|
||||
} from '$lib/components/operatingWorkspace.svelte'
|
||||
@@ -62,6 +63,8 @@
|
||||
customSaveBehavior = undefined
|
||||
} = $props()
|
||||
const operatingWorkspace = useOperatingWorkspace()
|
||||
const operatingUser = useOperatingUser()
|
||||
const actingUser = $derived(operatingUser.current)
|
||||
const operatingHref = useOperatingWorkspaceHref()
|
||||
const wsId = $derived($operatingWorkspace)
|
||||
|
||||
@@ -82,7 +85,13 @@
|
||||
let workspaced_local_part = $state(false)
|
||||
let drawerLoading = $state(true)
|
||||
let showLoader = $state(false)
|
||||
let can_write = $state(true)
|
||||
let permsPath = $state<string | undefined>(undefined)
|
||||
let permsForWrite = $state<Record<string, boolean> | undefined>(undefined)
|
||||
// The acting user in the operating workspace arrives asynchronously, and an unknown user
|
||||
// refuses — so the editor stays read-only until the lookup lands, which is the safe answer.
|
||||
const can_write = $derived(
|
||||
permsPath === undefined ? true : canWrite(permsPath, permsForWrite ?? {}, actingUser)
|
||||
)
|
||||
let extraPerms = $state<Record<string, boolean> | undefined>(undefined)
|
||||
let error_handler_path: string | undefined = $state()
|
||||
let error_handler_args: Record<string, any> = $state({})
|
||||
@@ -102,7 +111,7 @@
|
||||
let originalConfig = $state<NewEmailTrigger | undefined>(undefined)
|
||||
|
||||
let hasChanged = $derived(!deepEqual(getEmailTriggerConfig(), originalConfig ?? {}))
|
||||
const isAdmin = $derived($userStore?.is_admin || $userStore?.is_super_admin)
|
||||
const isAdmin = $derived(actingUser?.is_admin || actingUser?.is_super_admin)
|
||||
const emailConfig = $derived.by(getEmailTriggerConfig)
|
||||
|
||||
const draftSync = useTriggerDraftSync({
|
||||
@@ -135,6 +144,8 @@
|
||||
fixedScriptPath_?: string
|
||||
) {
|
||||
if (handOffPageDrawer(TRIGGER_PAGES.email.path, ePath)) return
|
||||
// A `whoami` that failed earlier would otherwise pin this workspace to "unknown user".
|
||||
operatingUser.forgetFailures()
|
||||
drawerLoading = true
|
||||
let loader = setTimeout(() => {
|
||||
showLoader = true
|
||||
@@ -223,7 +234,8 @@
|
||||
local_part = cfg?.local_part ?? ''
|
||||
workspaced_local_part = cfg?.workspaced_local_part ?? false
|
||||
extraPerms = cfg?.extra_perms ?? undefined
|
||||
can_write = canWrite(path, cfg?.extra_perms ?? {}, $userStore)
|
||||
permsPath = path
|
||||
permsForWrite = cfg?.extra_perms ?? {}
|
||||
error_handler_path = cfg?.error_handler_path
|
||||
error_handler_args = cfg?.error_handler_args ?? {}
|
||||
retry = cfg?.retry
|
||||
@@ -275,7 +287,7 @@
|
||||
saveCfg,
|
||||
edit,
|
||||
wsId!,
|
||||
!!$userStore?.is_admin || !!$userStore?.is_super_admin,
|
||||
!!actingUser?.is_admin || !!actingUser?.is_super_admin,
|
||||
usedTriggerKinds
|
||||
)
|
||||
if (isSaved) {
|
||||
@@ -415,7 +427,7 @@
|
||||
bind:scriptPath={script_path}
|
||||
{initialScriptPath}
|
||||
canWrite={can_write}
|
||||
isOperator={!!$userStore?.operator}
|
||||
isOperator={!!actingUser?.operator}
|
||||
promptClass="text-xs mt-3 mb-1 text-primary"
|
||||
>
|
||||
{#snippet createButton()}
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
import Drawer from '$lib/components/common/drawer/Drawer.svelte'
|
||||
import DrawerContent from '$lib/components/common/drawer/DrawerContent.svelte'
|
||||
import Path from '$lib/components/Path.svelte'
|
||||
import { usedTriggerKinds, userStore } from '$lib/stores'
|
||||
import { usedTriggerKinds } from '$lib/stores'
|
||||
import { canWrite, capitalize, emptyString, sendUserToast } from '$lib/utils'
|
||||
import { withForkConflictRetry } from '$lib/utils/forkConflict'
|
||||
import { Loader2 } from 'lucide-svelte'
|
||||
@@ -44,6 +44,7 @@
|
||||
import Subsection from '$lib/components/Subsection.svelte'
|
||||
import Toggle from '$lib/components/Toggle.svelte'
|
||||
import {
|
||||
useOperatingUser,
|
||||
useOperatingWorkspace,
|
||||
useOperatingWorkspaceHref
|
||||
} from '$lib/components/operatingWorkspace.svelte'
|
||||
@@ -60,7 +61,15 @@
|
||||
let pathError = $state('')
|
||||
let mode = $state<TriggerMode>('enabled')
|
||||
let dirtyPath = $state(false)
|
||||
let can_write = $state(true)
|
||||
const operatingUser = useOperatingUser()
|
||||
const actingUser = $derived(operatingUser.current)
|
||||
let permsPath = $state<string | undefined>(undefined)
|
||||
let permsForWrite = $state<Record<string, boolean> | undefined>(undefined)
|
||||
// The acting user in the operating workspace arrives asynchronously, and an unknown user
|
||||
// refuses — so the editor stays read-only until the lookup lands, which is the safe answer.
|
||||
const can_write = $derived(
|
||||
permsPath === undefined ? true : canWrite(permsPath, permsForWrite ?? {}, actingUser)
|
||||
)
|
||||
let drawerLoading = $state(true)
|
||||
let topic_id: string = $state('')
|
||||
let gcp_resource_path: string | undefined = $state('')
|
||||
@@ -159,6 +168,8 @@
|
||||
fixedScriptPath_?: string
|
||||
) {
|
||||
if (handOffPageDrawer(TRIGGER_PAGES.gcp.path, ePath)) return
|
||||
// A `whoami` that failed earlier would otherwise pin this workspace to "unknown user".
|
||||
operatingUser.forgetFailures()
|
||||
drawerLoading = true
|
||||
try {
|
||||
drawer?.openDrawer()
|
||||
@@ -281,7 +292,8 @@
|
||||
path = cfg?.path
|
||||
mode = cfg?.mode ?? 'enabled'
|
||||
topic_id = cfg?.topic_id ?? ''
|
||||
can_write = canWrite(cfg?.path, cfg?.extra_perms, $userStore)
|
||||
permsPath = cfg?.path
|
||||
permsForWrite = cfg?.extra_perms
|
||||
error_handler_path = cfg?.error_handler_path
|
||||
error_handler_args = cfg?.error_handler_args ?? {}
|
||||
retry = cfg?.retry
|
||||
@@ -554,7 +566,7 @@
|
||||
bind:scriptPath={script_path}
|
||||
{initialScriptPath}
|
||||
canWrite={can_write}
|
||||
isOperator={!!$userStore?.operator}
|
||||
isOperator={!!actingUser?.operator}
|
||||
promptText="Pick a script or flow to be triggered "
|
||||
>
|
||||
{#snippet createButton()}
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
type Retry,
|
||||
type TriggerMode
|
||||
} from '$lib/gen'
|
||||
import { usedTriggerKinds, userStore } from '$lib/stores'
|
||||
import { usedTriggerKinds } from '$lib/stores'
|
||||
import {
|
||||
canWrite,
|
||||
capitalize,
|
||||
@@ -78,6 +78,7 @@
|
||||
import UserSettings from '$lib/components/UserSettings.svelte'
|
||||
import Tooltip from '$lib/components/Tooltip.svelte'
|
||||
import {
|
||||
useOperatingUser,
|
||||
useOperatingWorkspace,
|
||||
useOperatingWorkspaceHref
|
||||
} from '$lib/components/operatingWorkspace.svelte'
|
||||
@@ -101,6 +102,8 @@
|
||||
customSaveBehavior = undefined
|
||||
} = $props()
|
||||
const operatingWorkspace = useOperatingWorkspace()
|
||||
const operatingUser = useOperatingUser()
|
||||
const actingUser = $derived(operatingUser.current)
|
||||
const operatingHref = useOperatingWorkspaceHref()
|
||||
const wsId = $derived($operatingWorkspace)
|
||||
|
||||
@@ -156,7 +159,13 @@
|
||||
let authentication_resource_path = $state('')
|
||||
let variable_path = $state('')
|
||||
let signature_options_type = $state<'custom_script' | 'custom_signature'>('custom_signature')
|
||||
let can_write = $state(true)
|
||||
let permsPath = $state<string | undefined>(undefined)
|
||||
let permsForWrite = $state<Record<string, boolean> | undefined>(undefined)
|
||||
// The acting user in the operating workspace arrives asynchronously, and an unknown user
|
||||
// refuses — so the editor stays read-only until the lookup lands, which is the safe answer.
|
||||
const can_write = $derived(
|
||||
permsPath === undefined ? true : canWrite(permsPath, permsForWrite ?? {}, actingUser)
|
||||
)
|
||||
let extraPerms = $state<Record<string, boolean> | undefined>(undefined)
|
||||
let summary: string | undefined = $state()
|
||||
let routeDescription: string | undefined = $state()
|
||||
@@ -266,6 +275,8 @@
|
||||
defaultConfig?: Partial<NewHttpTrigger>
|
||||
) {
|
||||
if (handOffPageDrawer(TRIGGER_PAGES.http.path, ePath)) return
|
||||
// A `whoami` that failed earlier would otherwise pin this workspace to "unknown user".
|
||||
operatingUser.forgetFailures()
|
||||
drawerLoading = true
|
||||
let loader = setTimeout(() => {
|
||||
showLoader = true
|
||||
@@ -387,7 +398,8 @@
|
||||
is_static_website = cfg?.is_static_website ?? false
|
||||
}
|
||||
extraPerms = cfg?.extra_perms ?? undefined
|
||||
can_write = canWrite(path, cfg?.extra_perms ?? {}, $userStore)
|
||||
permsPath = path
|
||||
permsForWrite = cfg?.extra_perms ?? {}
|
||||
error_handler_path = cfg?.error_handler_path
|
||||
error_handler_args = cfg?.error_handler_args ?? {}
|
||||
retry = cfg?.retry
|
||||
@@ -433,7 +445,7 @@
|
||||
saveCfg,
|
||||
edit,
|
||||
wsId!,
|
||||
!!$userStore?.is_admin || !!$userStore?.is_super_admin,
|
||||
!!actingUser?.is_admin || !!actingUser?.is_super_admin,
|
||||
usedTriggerKinds
|
||||
)
|
||||
if (isSaved) {
|
||||
@@ -549,7 +561,7 @@
|
||||
<UserSettings
|
||||
bind:this={userSettings}
|
||||
newTokenWorkspace={wsId}
|
||||
newTokenLabel={`http-${$userStore?.username ?? 'superadmin'}-${generateRandomString(4)}`}
|
||||
newTokenLabel={`http-${actingUser?.username ?? 'superadmin'}-${generateRandomString(4)}`}
|
||||
{scopes}
|
||||
/>
|
||||
{/if}
|
||||
@@ -760,7 +772,7 @@
|
||||
bind:itemKind
|
||||
bind:scriptPath={script_path}
|
||||
allowRefresh={can_write}
|
||||
allowEdit={!$userStore?.operator}
|
||||
allowEdit={!actingUser?.operator}
|
||||
clearable
|
||||
/>
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
import Path from '$lib/components/Path.svelte'
|
||||
import TriggerRunnablePicker from '$lib/components/triggers/TriggerRunnablePicker.svelte'
|
||||
import { KafkaTriggerService, type ErrorHandler, type Retry, type TriggerMode } from '$lib/gen'
|
||||
import { usedTriggerKinds, userStore } from '$lib/stores'
|
||||
import { usedTriggerKinds } from '$lib/stores'
|
||||
import { canWrite, capitalize, emptyString, sendUserToast } from '$lib/utils'
|
||||
import { withForkConflictRetry } from '$lib/utils/forkConflict'
|
||||
import Section from '$lib/components/Section.svelte'
|
||||
@@ -38,6 +38,7 @@
|
||||
import Select from '$lib/components/select/Select.svelte'
|
||||
import Toggle from '$lib/components/Toggle.svelte'
|
||||
import {
|
||||
useOperatingUser,
|
||||
useOperatingWorkspace,
|
||||
useOperatingWorkspaceHref
|
||||
} from '$lib/components/operatingWorkspace.svelte'
|
||||
@@ -84,6 +85,8 @@
|
||||
onReset = undefined
|
||||
}: Props = $props()
|
||||
const operatingWorkspace = useOperatingWorkspace()
|
||||
const operatingUser = useOperatingUser()
|
||||
const actingUser = $derived(operatingUser.current)
|
||||
const operatingHref = useOperatingWorkspaceHref()
|
||||
const wsId = $derived($operatingWorkspace)
|
||||
|
||||
@@ -99,7 +102,13 @@
|
||||
let pathError = $state('')
|
||||
let mode = $state<TriggerMode>('enabled')
|
||||
let dirtyPath = $state(false)
|
||||
let can_write = $state(true)
|
||||
let permsPath = $state<string | undefined>(undefined)
|
||||
let permsForWrite = $state<Record<string, boolean> | undefined>(undefined)
|
||||
// The acting user in the operating workspace arrives asynchronously, and an unknown user
|
||||
// refuses — so the editor stays read-only until the lookup lands, which is the safe answer.
|
||||
const can_write = $derived(
|
||||
permsPath === undefined ? true : canWrite(permsPath, permsForWrite ?? {}, actingUser)
|
||||
)
|
||||
let drawerLoading = $state(true)
|
||||
let showLoading = $state(false)
|
||||
let initialConfig: Record<string, any> | undefined = undefined
|
||||
@@ -170,6 +179,8 @@
|
||||
fixedScriptPath_?: string
|
||||
) {
|
||||
if (handOffPageDrawer(TRIGGER_PAGES.kafka.path, ePath)) return
|
||||
// A `whoami` that failed earlier would otherwise pin this workspace to "unknown user".
|
||||
operatingUser.forgetFailures()
|
||||
let loadingTimeout = setTimeout(() => {
|
||||
showLoading = true
|
||||
}, 100) // Do not show loading spinner for the first 100ms
|
||||
@@ -263,7 +274,8 @@
|
||||
autoCommit = cfg?.auto_commit ?? true
|
||||
mode = cfg?.mode ?? 'enabled'
|
||||
extra_perms = cfg?.extra_perms
|
||||
can_write = canWrite(path, cfg?.extra_perms, $userStore)
|
||||
permsPath = path
|
||||
permsForWrite = cfg?.extra_perms
|
||||
error_handler_path = cfg?.error_handler_path
|
||||
error_handler_args = cfg?.error_handler_args ?? {}
|
||||
retry = cfg?.retry
|
||||
@@ -565,7 +577,7 @@
|
||||
bind:scriptPath={script_path}
|
||||
{initialScriptPath}
|
||||
canWrite={can_write}
|
||||
isOperator={!!$userStore?.operator}
|
||||
isOperator={!!actingUser?.operator}
|
||||
>
|
||||
{#snippet createButton()}
|
||||
{#if emptyString(script_path)}
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
import DrawerContent from '$lib/components/common/drawer/DrawerContent.svelte'
|
||||
import Path from '$lib/components/Path.svelte'
|
||||
import TriggerRunnablePicker from '$lib/components/triggers/TriggerRunnablePicker.svelte'
|
||||
import { usedTriggerKinds, userStore } from '$lib/stores'
|
||||
import { usedTriggerKinds } from '$lib/stores'
|
||||
import { canWrite, capitalize, emptyString, sendUserToast } from '$lib/utils'
|
||||
import { withForkConflictRetry } from '$lib/utils/forkConflict'
|
||||
import Section from '$lib/components/Section.svelte'
|
||||
@@ -47,6 +47,7 @@
|
||||
import { useTriggerDraftSync } from '../useTriggerDraftSync.svelte'
|
||||
import LocalDraftBanner from '$lib/components/LocalDraftBanner.svelte'
|
||||
import {
|
||||
useOperatingUser,
|
||||
useOperatingWorkspace,
|
||||
useOperatingWorkspaceHref
|
||||
} from '$lib/components/operatingWorkspace.svelte'
|
||||
@@ -93,6 +94,8 @@
|
||||
cloudDisabled = false
|
||||
}: Props = $props()
|
||||
const operatingWorkspace = useOperatingWorkspace()
|
||||
const operatingUser = useOperatingUser()
|
||||
const actingUser = $derived(operatingUser.current)
|
||||
const operatingHref = useOperatingWorkspaceHref()
|
||||
const wsId = $derived($operatingWorkspace)
|
||||
|
||||
@@ -109,7 +112,13 @@
|
||||
let pathError = $state('')
|
||||
let mode = $state<TriggerMode>('enabled')
|
||||
let dirtyPath = $state(false)
|
||||
let can_write = $state(true)
|
||||
let permsPath = $state<string | undefined>(undefined)
|
||||
let permsForWrite = $state<Record<string, boolean> | undefined>(undefined)
|
||||
// The acting user in the operating workspace arrives asynchronously, and an unknown user
|
||||
// refuses — so the editor stays read-only until the lookup lands, which is the safe answer.
|
||||
const can_write = $derived(
|
||||
permsPath === undefined ? true : canWrite(permsPath, permsForWrite ?? {}, actingUser)
|
||||
)
|
||||
let drawerLoading = $state(true)
|
||||
let showLoading = $state(false)
|
||||
let subscribe_topics: MqttSubscribeTopic[] = $state([])
|
||||
@@ -165,6 +174,8 @@
|
||||
fixedScriptPath_?: string
|
||||
) {
|
||||
if (handOffPageDrawer(TRIGGER_PAGES.mqtt.path, ePath)) return
|
||||
// A `whoami` that failed earlier would otherwise pin this workspace to "unknown user".
|
||||
operatingUser.forgetFailures()
|
||||
let loadingTimeout = setTimeout(() => {
|
||||
showLoading = true
|
||||
}, 100) // Do not show loading spinner for the first 100ms
|
||||
@@ -258,7 +269,8 @@
|
||||
v3_config = cfg?.v3_config ?? DEFAULT_V3_CONFIG
|
||||
v5_config = cfg?.v5_config ?? DEFAULT_V5_CONFIG
|
||||
client_id = cfg?.client_id ?? ''
|
||||
can_write = canWrite(cfg?.path, cfg?.extra_perms, $userStore)
|
||||
permsPath = cfg?.path
|
||||
permsForWrite = cfg?.extra_perms
|
||||
error_handler_path = cfg?.error_handler_path
|
||||
error_handler_args = cfg?.error_handler_args ?? {}
|
||||
retry = cfg?.retry
|
||||
@@ -539,7 +551,7 @@
|
||||
bind:scriptPath={script_path}
|
||||
{initialScriptPath}
|
||||
canWrite={can_write}
|
||||
isOperator={!!$userStore?.operator}
|
||||
isOperator={!!actingUser?.operator}
|
||||
>
|
||||
{#snippet createButton()}
|
||||
{#if emptyString(script_path)}
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
import Path from '$lib/components/Path.svelte'
|
||||
import TriggerRunnablePicker from '$lib/components/triggers/TriggerRunnablePicker.svelte'
|
||||
import { NatsTriggerService, type ErrorHandler, type Retry, type TriggerMode } from '$lib/gen'
|
||||
import { usedTriggerKinds, userStore } from '$lib/stores'
|
||||
import { usedTriggerKinds } from '$lib/stores'
|
||||
import { canWrite, capitalize, emptyString, sendUserToast } from '$lib/utils'
|
||||
import { withForkConflictRetry } from '$lib/utils/forkConflict'
|
||||
import Section from '$lib/components/Section.svelte'
|
||||
@@ -33,6 +33,7 @@
|
||||
import TriggerSuspendedJobsAlert from '../TriggerSuspendedJobsAlert.svelte'
|
||||
import TriggerSuspendedJobsModal from '../TriggerSuspendedJobsModal.svelte'
|
||||
import {
|
||||
useOperatingUser,
|
||||
useOperatingWorkspace,
|
||||
useOperatingWorkspaceHref
|
||||
} from '$lib/components/operatingWorkspace.svelte'
|
||||
@@ -80,6 +81,8 @@
|
||||
onReset = undefined
|
||||
}: Props = $props()
|
||||
const operatingWorkspace = useOperatingWorkspace()
|
||||
const operatingUser = useOperatingUser()
|
||||
const actingUser = $derived(operatingUser.current)
|
||||
const operatingHref = useOperatingWorkspaceHref()
|
||||
const wsId = $derived($operatingWorkspace)
|
||||
|
||||
@@ -94,7 +97,13 @@
|
||||
let path: string = $state('')
|
||||
let pathError = $state('')
|
||||
let dirtyPath = $state(false)
|
||||
let can_write = $state(true)
|
||||
let permsPath = $state<string | undefined>(undefined)
|
||||
let permsForWrite = $state<Record<string, boolean> | undefined>(undefined)
|
||||
// The acting user in the operating workspace arrives asynchronously, and an unknown user
|
||||
// refuses — so the editor stays read-only until the lookup lands, which is the safe answer.
|
||||
const can_write = $derived(
|
||||
permsPath === undefined ? true : canWrite(permsPath, permsForWrite ?? {}, actingUser)
|
||||
)
|
||||
let drawerLoading = $state(true)
|
||||
let showLoading = $state(false)
|
||||
let defaultValues: Record<string, any> | undefined = $state(undefined)
|
||||
@@ -153,6 +162,8 @@
|
||||
fixedScriptPath_?: string
|
||||
) {
|
||||
if (handOffPageDrawer(TRIGGER_PAGES.nats.path, ePath)) return
|
||||
// A `whoami` that failed earlier would otherwise pin this workspace to "unknown user".
|
||||
operatingUser.forgetFailures()
|
||||
let loadingTimeout = setTimeout(() => {
|
||||
showLoading = true
|
||||
}, 100) // Do not show loading spinner for the first 100ms
|
||||
@@ -252,7 +263,8 @@
|
||||
consumer_name: useJetstream ? cfg?.consumer_name || '' : undefined
|
||||
}
|
||||
mode = cfg?.mode ?? 'enabled'
|
||||
can_write = canWrite(cfg?.path, cfg?.extra_perms, $userStore)
|
||||
permsPath = cfg?.path
|
||||
permsForWrite = cfg?.extra_perms
|
||||
error_handler_path = cfg?.error_handler_path
|
||||
error_handler_args = cfg?.error_handler_args ?? {}
|
||||
retry = cfg?.retry
|
||||
@@ -534,7 +546,7 @@
|
||||
bind:scriptPath={script_path}
|
||||
{initialScriptPath}
|
||||
canWrite={can_write}
|
||||
isOperator={!!$userStore?.operator}
|
||||
isOperator={!!actingUser?.operator}
|
||||
>
|
||||
{#snippet createButton()}
|
||||
{#if emptyString(script_path)}
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
type Retry,
|
||||
type TriggerMode
|
||||
} from '$lib/gen'
|
||||
import { usedTriggerKinds, userStore } from '$lib/stores'
|
||||
import { usedTriggerKinds } from '$lib/stores'
|
||||
import { canWrite, emptyString, emptyStringTrimmed, sendUserToast } from '$lib/utils'
|
||||
import { withForkConflictRetry } from '$lib/utils/forkConflict'
|
||||
import Section from '$lib/components/Section.svelte'
|
||||
@@ -55,6 +55,7 @@
|
||||
import LocalDraftBanner from '$lib/components/LocalDraftBanner.svelte'
|
||||
import { capitalize } from '$lib/utils'
|
||||
import {
|
||||
useOperatingUser,
|
||||
useOperatingWorkspace,
|
||||
useOperatingWorkspaceHref
|
||||
} from '$lib/components/operatingWorkspace.svelte'
|
||||
@@ -101,6 +102,8 @@
|
||||
onReset = undefined
|
||||
}: Props = $props()
|
||||
const operatingWorkspace = useOperatingWorkspace()
|
||||
const operatingUser = useOperatingUser()
|
||||
const actingUser = $derived(operatingUser.current)
|
||||
const operatingHref = useOperatingWorkspaceHref()
|
||||
const wsId = $derived($operatingWorkspace)
|
||||
|
||||
@@ -115,7 +118,13 @@
|
||||
let path: string = $state('')
|
||||
let pathError = $state('')
|
||||
let dirtyPath: boolean = $state(false)
|
||||
let can_write: boolean = $state(true)
|
||||
let permsPath = $state<string | undefined>(undefined)
|
||||
let permsForWrite = $state<Record<string, boolean> | undefined>(undefined)
|
||||
// The acting user in the operating workspace arrives asynchronously, and an unknown user
|
||||
// refuses — so the editor stays read-only until the lookup lands, which is the safe answer.
|
||||
const can_write = $derived(
|
||||
permsPath === undefined ? true : canWrite(permsPath, permsForWrite ?? {}, actingUser)
|
||||
)
|
||||
let drawerLoading: boolean = $state(true)
|
||||
let showLoading: boolean = $state(false)
|
||||
let postgres_resource_path: string = $state('')
|
||||
@@ -254,6 +263,8 @@
|
||||
fixedScriptPath_?: string
|
||||
) {
|
||||
if (handOffPageDrawer(TRIGGER_PAGES.postgres.path, ePath)) return
|
||||
// A `whoami` that failed earlier would otherwise pin this workspace to "unknown user".
|
||||
operatingUser.forgetFailures()
|
||||
let loadingTimeout = setTimeout(() => {
|
||||
showLoading = true
|
||||
}, 100) // Do not show loading spinner for the first 100ms
|
||||
@@ -382,7 +393,8 @@
|
||||
postgres_resource_path = cfg?.postgres_resource_path
|
||||
publication_name = cfg?.publication_name
|
||||
replication_slot_name = cfg?.replication_slot_name
|
||||
can_write = canWrite(path, cfg?.extra_perms, $userStore)
|
||||
permsPath = path
|
||||
permsForWrite = cfg?.extra_perms
|
||||
transaction_to_track = [...cfg?.publication?.transaction_to_track]
|
||||
relations = cfg?.publication?.table_to_track ?? []
|
||||
error_handler_path = cfg?.error_handler_path
|
||||
@@ -718,7 +730,7 @@
|
||||
bind:scriptPath={script_path}
|
||||
{initialScriptPath}
|
||||
canWrite={can_write}
|
||||
isOperator={!!$userStore?.operator}
|
||||
isOperator={!!actingUser?.operator}
|
||||
promptText="Pick a script or flow to be triggered "
|
||||
promptClass="text-xs text-primary"
|
||||
>
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
import Drawer from '$lib/components/common/drawer/Drawer.svelte'
|
||||
import DrawerContent from '$lib/components/common/drawer/DrawerContent.svelte'
|
||||
import Path from '$lib/components/Path.svelte'
|
||||
import { usedTriggerKinds, userStore } from '$lib/stores'
|
||||
import { usedTriggerKinds } from '$lib/stores'
|
||||
import { canWrite, capitalize, emptyString, sendUserToast } from '$lib/utils'
|
||||
import { withForkConflictRetry } from '$lib/utils/forkConflict'
|
||||
import { Loader2 } from 'lucide-svelte'
|
||||
@@ -39,6 +39,7 @@
|
||||
import { useTriggerDraftSync } from '../useTriggerDraftSync.svelte'
|
||||
import LocalDraftBanner from '$lib/components/LocalDraftBanner.svelte'
|
||||
import {
|
||||
useOperatingUser,
|
||||
useOperatingWorkspace,
|
||||
useOperatingWorkspaceHref
|
||||
} from '$lib/components/operatingWorkspace.svelte'
|
||||
@@ -85,6 +86,8 @@
|
||||
onReset = undefined
|
||||
}: Props = $props()
|
||||
const operatingWorkspace = useOperatingWorkspace()
|
||||
const operatingUser = useOperatingUser()
|
||||
const actingUser = $derived(operatingUser.current)
|
||||
const operatingHref = useOperatingWorkspaceHref()
|
||||
const wsId = $derived($operatingWorkspace)
|
||||
|
||||
@@ -100,7 +103,13 @@
|
||||
let pathError = $state('')
|
||||
let mode = $state<TriggerMode>('enabled')
|
||||
let dirtyPath = $state(false)
|
||||
let can_write = $state(true)
|
||||
let permsPath = $state<string | undefined>(undefined)
|
||||
let permsForWrite = $state<Record<string, boolean> | undefined>(undefined)
|
||||
// The acting user in the operating workspace arrives asynchronously, and an unknown user
|
||||
// refuses — so the editor stays read-only until the lookup lands, which is the safe answer.
|
||||
const can_write = $derived(
|
||||
permsPath === undefined ? true : canWrite(permsPath, permsForWrite ?? {}, actingUser)
|
||||
)
|
||||
let drawerLoading = $state(true)
|
||||
let showLoading = $state(false)
|
||||
let aws_resource_path: string = $state('')
|
||||
@@ -148,6 +157,8 @@
|
||||
fixedScriptPath_?: string
|
||||
) {
|
||||
if (handOffPageDrawer(TRIGGER_PAGES.sqs.path, ePath)) return
|
||||
// A `whoami` that failed earlier would otherwise pin this workspace to "unknown user".
|
||||
operatingUser.forgetFailures()
|
||||
let loadingTimeout = setTimeout(() => {
|
||||
showLoading = true
|
||||
}, 100) // Do not show loading spinner for the first 100ms
|
||||
@@ -238,7 +249,8 @@
|
||||
path = cfg?.path
|
||||
mode = cfg?.mode ?? 'enabled'
|
||||
aws_auth_resource_type = cfg?.aws_auth_resource_type
|
||||
can_write = canWrite(cfg?.path, cfg?.extra_perms, $userStore)
|
||||
permsPath = cfg?.path
|
||||
permsForWrite = cfg?.extra_perms
|
||||
error_handler_path = cfg?.error_handler_path
|
||||
error_handler_args = cfg?.error_handler_args ?? {}
|
||||
retry = cfg?.retry
|
||||
@@ -518,7 +530,7 @@
|
||||
bind:scriptPath={script_path}
|
||||
{initialScriptPath}
|
||||
canWrite={can_write}
|
||||
isOperator={!!$userStore?.operator}
|
||||
isOperator={!!actingUser?.operator}
|
||||
promptText="Pick a script or flow to be triggered "
|
||||
>
|
||||
{#snippet createButton()}
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
type ErrorHandler,
|
||||
type TriggerMode
|
||||
} from '$lib/gen'
|
||||
import { usedTriggerKinds, userStore } from '$lib/stores'
|
||||
import { usedTriggerKinds } from '$lib/stores'
|
||||
import { canWrite, emptySchema, emptyString, sendUserToast } from '$lib/utils'
|
||||
import { withForkConflictRetry } from '$lib/utils/forkConflict'
|
||||
import Section from '$lib/components/Section.svelte'
|
||||
@@ -55,6 +55,7 @@
|
||||
import TriggerSuspendedJobsModal from '../TriggerSuspendedJobsModal.svelte'
|
||||
import { capitalize } from '$lib/utils'
|
||||
import {
|
||||
useOperatingUser,
|
||||
useOperatingWorkspace,
|
||||
useOperatingWorkspaceHref
|
||||
} from '$lib/components/operatingWorkspace.svelte'
|
||||
@@ -102,6 +103,8 @@
|
||||
cloudDisabled = false
|
||||
}: Props = $props()
|
||||
const operatingWorkspace = useOperatingWorkspace()
|
||||
const operatingUser = useOperatingUser()
|
||||
const actingUser = $derived(operatingUser.current)
|
||||
const operatingHref = useOperatingWorkspaceHref()
|
||||
const wsId = $derived($operatingWorkspace)
|
||||
|
||||
@@ -128,7 +131,13 @@
|
||||
let heartbeat_message = $state('')
|
||||
let heartbeat_state_field = $state('')
|
||||
let dirtyPath = $state(false)
|
||||
let can_write = $state(true)
|
||||
let permsPath = $state<string | undefined>(undefined)
|
||||
let permsForWrite = $state<Record<string, boolean> | undefined>(undefined)
|
||||
// The acting user in the operating workspace arrives asynchronously, and an unknown user
|
||||
// refuses — so the editor stays read-only until the lookup lands, which is the safe answer.
|
||||
const can_write = $derived(
|
||||
permsPath === undefined ? true : canWrite(permsPath, permsForWrite ?? {}, actingUser)
|
||||
)
|
||||
let drawerLoading = $state(true)
|
||||
let showLoading = $state(false)
|
||||
let initialConfig: Record<string, any> | undefined = undefined
|
||||
@@ -194,6 +203,8 @@
|
||||
defaultConfig?: Record<string, any>
|
||||
) {
|
||||
if (handOffPageDrawer(TRIGGER_PAGES.websocket.path, ePath)) return
|
||||
// A `whoami` that failed earlier would otherwise pin this workspace to "unknown user".
|
||||
operatingUser.forgetFailures()
|
||||
let loadingTimeout = setTimeout(() => {
|
||||
showLoading = true
|
||||
}, 100) // Do not show loading spinner for the first 100ms
|
||||
@@ -289,7 +300,8 @@
|
||||
heartbeat_interval_secs = hb?.interval_secs ?? 41
|
||||
heartbeat_message = hb?.message ?? ''
|
||||
heartbeat_state_field = hb?.state_field ?? ''
|
||||
can_write = canWrite(path, cfg?.extra_perms, $userStore)
|
||||
permsPath = path
|
||||
permsForWrite = cfg?.extra_perms
|
||||
error_handler_path = cfg?.error_handler_path
|
||||
error_handler_args = cfg?.error_handler_args ?? {}
|
||||
retry = cfg?.retry
|
||||
@@ -614,7 +626,7 @@
|
||||
bind:itemKind
|
||||
bind:scriptPath={script_path}
|
||||
allowRefresh={can_write}
|
||||
allowEdit={!$userStore?.operator}
|
||||
allowEdit={!actingUser?.operator}
|
||||
clearable
|
||||
/>
|
||||
{#if emptyString(script_path)}
|
||||
|
||||
Reference in New Issue
Block a user