mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-10-30 16:17:25 +00:00
fix: check session admin rights in the session's workspace (#12071)
This commit is contained in:
@@ -10,7 +10,6 @@
|
||||
import PermissionedAsLine from '$lib/components/triggers/PermissionedAsLine.svelte'
|
||||
import AgentSlackSettings from './AgentSlackSettings.svelte'
|
||||
import { WorkspaceService } from '$lib/gen'
|
||||
import { userStore } from '$lib/stores'
|
||||
import { useActingUser } from '$lib/actingUser.svelte'
|
||||
import type { AgentDraftHandle, AgentRunAs } from '../agentDraft.svelte'
|
||||
|
||||
@@ -36,8 +35,8 @@
|
||||
let nextRunAs = $derived(
|
||||
pickedRunAs?.preserve && pickedRunAs.permissionedAs
|
||||
? pickedRunAs.permissionedAs
|
||||
: $userStore?.username
|
||||
? `u/${$userStore.username}`
|
||||
: acting.current?.username
|
||||
? `u/${acting.current.username}`
|
||||
: undefined
|
||||
)
|
||||
// Only a workspace that deploys somewhere has anything to keep an agent out of.
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
import TextInput from '$lib/components/text_input/TextInput.svelte'
|
||||
import Toggle from '$lib/components/Toggle.svelte'
|
||||
import { SlackAgentsService, type SlackAgentChannel } from '$lib/gen'
|
||||
import { userStore } from '$lib/stores'
|
||||
import { useActingUser } from '$lib/actingUser.svelte'
|
||||
import { sendUserToast } from '$lib/toast'
|
||||
import type { AgentDraftHandle } from '../agentDraft.svelte'
|
||||
|
||||
@@ -26,16 +26,18 @@
|
||||
|
||||
let { draft, path, workspace, nextRunAs }: Props = $props()
|
||||
|
||||
const acting = useActingUser(() => workspace)
|
||||
|
||||
let readOnly = $derived(!draft.canWrite)
|
||||
let obo = $derived(!!draft.state?.args.run_on_behalf_of)
|
||||
let runsAs = $derived(
|
||||
readOnly
|
||||
? (draft.onBehalfOf ?? 'its run-as user')
|
||||
: !nextRunAs || nextRunAs === `u/${$userStore?.username}`
|
||||
: !nextRunAs || nextRunAs === `u/${acting.current?.username}`
|
||||
? 'you'
|
||||
: nextRunAs
|
||||
)
|
||||
let isAdmin = $derived(!!$userStore?.is_admin)
|
||||
let isAdmin = $derived(!!acting.current?.is_admin)
|
||||
|
||||
const slack = resource(
|
||||
() => ({ ws: workspace, path, deployed: !draft.noDeployed }),
|
||||
|
||||
@@ -9,8 +9,10 @@
|
||||
import { createAsyncConfirmationModal } from '$lib/components/common/confirmationModal/asyncConfirmationModal.svelte'
|
||||
import { sendUserToast } from '$lib/toast'
|
||||
import { Plus, RefreshCw, Unplug } from 'lucide-svelte'
|
||||
import { userStore } from '$lib/stores'
|
||||
import { useOperatingWorkspace } from '$lib/components/operatingWorkspace.svelte'
|
||||
import {
|
||||
useOperatingUser,
|
||||
useOperatingWorkspace
|
||||
} from '$lib/components/operatingWorkspace.svelte'
|
||||
import { connectNativeAccount, getServiceConfig } from './utils'
|
||||
|
||||
interface Props {
|
||||
@@ -45,6 +47,7 @@
|
||||
}: Props = $props()
|
||||
|
||||
const operatingWorkspace = useOperatingWorkspace()
|
||||
const operatingUser = useOperatingUser()
|
||||
const displayName = $derived(getServiceConfig(service)?.serviceDisplayName ?? service)
|
||||
const confirmationModal = createAsyncConfirmationModal()
|
||||
|
||||
@@ -95,7 +98,7 @@
|
||||
}
|
||||
|
||||
async function reconnect(c: NativeTriggerConnection) {
|
||||
if (c.owner && c.owner !== $userStore?.username) {
|
||||
if (c.owner && c.owner !== operatingUser.current?.username) {
|
||||
const confirmed = await confirmationModal.ask({
|
||||
title: `Reconnect ${c.path} with your account?`,
|
||||
confirmationText: 'Reconnect',
|
||||
@@ -130,13 +133,16 @@
|
||||
|
||||
// Members cannot write the workspace connection; the server refuses anyone else it does not own.
|
||||
function canDisconnect(c: NativeTriggerConnection): boolean {
|
||||
if ($userStore?.is_admin) return true
|
||||
return !c.workspace && !!c.owner && c.owner === $userStore?.username
|
||||
if (operatingUser.current?.is_admin) return true
|
||||
return !c.workspace && !!c.owner && c.owner === operatingUser.current?.username
|
||||
}
|
||||
|
||||
// The server lets only the connection's owner or an admin replace it.
|
||||
function canReconnect(c: NativeTriggerConnection): boolean {
|
||||
return !!$userStore?.is_admin || (!!c.owner && c.owner === $userStore?.username)
|
||||
return (
|
||||
!!operatingUser.current?.is_admin ||
|
||||
(!!c.owner && c.owner === operatingUser.current?.username)
|
||||
)
|
||||
}
|
||||
|
||||
const items = $derived.by(() => {
|
||||
|
||||
@@ -10,10 +10,9 @@
|
||||
* files, then createAppRaw/updateAppRaw. The source→AppDraftValue projection is
|
||||
* shared via `rawAppDraftValue` so the two deploy paths can't drift.
|
||||
*/
|
||||
import { get } from 'svelte/store'
|
||||
import { AppService } from '$lib/gen'
|
||||
import type { Policy } from '$lib/gen'
|
||||
import { userStore } from '$lib/stores'
|
||||
import { isAdminIn } from '$lib/user'
|
||||
import { bundleRawAppDraft } from '$lib/components/copilot/chat/global/rawAppBundlerBridge'
|
||||
import type { AppDraftValue } from '$lib/components/copilot/chat/global/workspaceItems'
|
||||
import { updateRawAppPolicy } from '$lib/components/raw_apps/rawAppPolicy'
|
||||
@@ -63,7 +62,7 @@ export async function deployRawAppValue({
|
||||
|
||||
const bundle = await bundleRawAppDraft({ workspace, files })
|
||||
|
||||
const isAdmin = !!(get(userStore)?.is_admin || get(userStore)?.is_super_admin)
|
||||
const isAdmin = await isAdminIn(workspace)
|
||||
await AppService.updateAppRaw({
|
||||
workspace,
|
||||
path,
|
||||
@@ -131,7 +130,7 @@ export async function deployRawAppDraft(
|
||||
// non-admin deploying a draft for an app that has a custom route would hit
|
||||
// RequireAdmin (the deployed custom_path is sent via the appSourceToDraftValue
|
||||
// fallback even when unchanged).
|
||||
const isAdmin = !!(get(userStore)?.is_admin || get(userStore)?.is_super_admin)
|
||||
const isAdmin = await isAdminIn(workspace)
|
||||
await AppService.updateAppRaw({
|
||||
workspace,
|
||||
path,
|
||||
|
||||
@@ -39,6 +39,12 @@ export function getWorkspaceRole(workspace: string): Promise<RoleLookup> {
|
||||
return lookup
|
||||
}
|
||||
|
||||
/** Admin of `workspace`, which may not be the navigated one; an unknown user is not. */
|
||||
export async function isAdminIn(workspace: string): Promise<boolean> {
|
||||
const role = await getWorkspaceRole(workspace)
|
||||
return role.kind === 'resolved' && !!(role.user.is_admin || role.user.is_super_admin)
|
||||
}
|
||||
|
||||
/** Roles are per-identity, so anything that changes who is logged in must drop this. */
|
||||
export function clearWorkspaceRoleCache(): void {
|
||||
workspaceRoleCache.clear()
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
* draft, so discarding deletes the whole item (mirrors `common/table/*Row.svelte`);
|
||||
* a draft on an already-deployed item just deletes the draft row.
|
||||
*/
|
||||
import { get, writable } from 'svelte/store'
|
||||
import { writable } from 'svelte/store'
|
||||
import {
|
||||
DraftService,
|
||||
ScriptService,
|
||||
@@ -43,7 +43,7 @@ import { classicAppDraftParts } from '$lib/appDiffSides'
|
||||
import { invalidateWorkspaceDrafts } from '$lib/workspaceDrafts.svelte'
|
||||
import { invalidateWorkspaceComparison } from '$lib/workspaceComparison'
|
||||
import { setLocalDraftHint } from '$lib/localDraftHints.svelte'
|
||||
import { userStore } from '$lib/stores'
|
||||
import { isAdminIn } from '$lib/user'
|
||||
import { deployTriggers, type Trigger } from '$lib/components/triggers/utils'
|
||||
import { saveScheduleFromCfg } from '$lib/components/flows/scheduleUtils'
|
||||
import { dropStaleStepLocks } from '$lib/components/flows/staleStepLocks'
|
||||
@@ -449,7 +449,7 @@ async function deployDraftTriggers(
|
||||
): Promise<void> {
|
||||
const triggers = (draftTriggers ?? []).filter((t) => t?.draftConfig)
|
||||
if (triggers.length === 0) return
|
||||
const isAdmin = !!(get(userStore)?.is_admin || get(userStore)?.is_super_admin)
|
||||
const isAdmin = await isAdminIn(workspace)
|
||||
await deployTriggers(triggers, workspace, isAdmin, writable<string[]>([]), path, isNew)
|
||||
}
|
||||
|
||||
@@ -586,7 +586,7 @@ export async function deployDraft(
|
||||
// (or a default). custom_path requires admin on update; non-admins send
|
||||
// undefined so the backend preserves the existing route. The draft has no
|
||||
// custom_path, so admins fall back to the deployed route (`''` when none).
|
||||
const isAdmin = !!(get(userStore)?.is_admin || get(userStore)?.is_super_admin)
|
||||
const isAdmin = await isAdminIn(workspace)
|
||||
const policy = r.policy ?? { execution_mode: 'publisher', sandbox: true }
|
||||
const requestBody = {
|
||||
value: appValue,
|
||||
@@ -720,7 +720,7 @@ export async function deployDraft(
|
||||
} else if (kind in TRIGGER_SAVERS) {
|
||||
const getter = OVERLAY_GETTERS[kind]!
|
||||
const { draft: d } = splitOverlay(await getter(workspace, path))
|
||||
const isAdmin = !!(get(userStore)?.is_admin || get(userStore)?.is_super_admin)
|
||||
const isAdmin = await isAdminIn(workspace)
|
||||
const ok = await TRIGGER_SAVERS[kind]!(
|
||||
path,
|
||||
{ ...d, path: d.path ?? path },
|
||||
|
||||
Reference in New Issue
Block a user