mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-08-24 08:01:38 +00:00
fix(forks): show workspace settings link in sidebar for fork creators (#10216)
* fix(forks): show workspace settings link in sidebar for fork creators PR #10166 grants a fork's creator developer-management access to the workspace settings page, but every sidebar entry point to /workspace_settings stayed gated on is_admin || superadmin, so a non-admin fork creator could only reach the page by typing the URL. Widen the four sidebar guards (SettingsMenu, SidebarContent, WorkspaceMenu, WorkspaceScopeHeader) to also admit fork owners, via a shared isForkOwner() helper mirroring the fork-owner check already on the settings page and the backend authorize_fork_owner_add_user grant. Fixes WIN-2210 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(forks): check fork ownership against the settings entry's target workspace In session mode SettingsMenu's entry targets workspaceSettingsTarget, which can differ from the active workspace, but the fork-owner guard was checked against the active workspace — hiding the entry when the session targets the user's fork, or exposing a dead link when only the active workspace is theirs. Check ownership against the workspace the entry actually points at. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -29,7 +29,7 @@
|
||||
import DropdownV2 from '$lib/components/DropdownV2.svelte'
|
||||
import ConfirmationModal from '../common/confirmationModal/ConfirmationModal.svelte'
|
||||
import DeleteForkedWorkspaceModal from './DeleteForkedWorkspaceModal.svelte'
|
||||
import { workspaceIsFork } from '$lib/utils/workspaceHierarchy'
|
||||
import { workspaceIsFork, isForkOwner } from '$lib/utils/workspaceHierarchy'
|
||||
import DarkModeObserver from '../DarkModeObserver.svelte'
|
||||
import DiscordIcon from '../icons/brands/Discord.svelte'
|
||||
import MenuLink from './MenuLink.svelte'
|
||||
@@ -69,9 +69,6 @@
|
||||
} = $props()
|
||||
|
||||
const currentWs = $derived($userWorkspaces?.find((w) => w.id === $workspaceStore))
|
||||
const canManageWorkspace = $derived($userStore?.is_admin || $superadmin)
|
||||
// Fork/dev workspaces are detected by their parent link, not the `wm-fork-` id prefix.
|
||||
const currentWsIsFork = $derived(workspaceIsFork($workspaceStore, $userWorkspaces ?? []))
|
||||
|
||||
const settingsTargetWs = $derived(
|
||||
workspaceSettingsTarget
|
||||
@@ -79,6 +76,17 @@
|
||||
: undefined
|
||||
)
|
||||
|
||||
// The fork-owner grant must be checked against the workspace the settings entry
|
||||
// actually points at — the session target when in session mode, else the active
|
||||
// workspace — otherwise ownership of the wrong workspace could hide the entry or
|
||||
// expose a dead link to a fork the user doesn't own.
|
||||
const settingsWs = $derived(workspaceSettingsTarget ? settingsTargetWs : currentWs)
|
||||
const canManageWorkspace = $derived(
|
||||
$userStore?.is_admin || $superadmin || isForkOwner(settingsWs, $userStore?.email)
|
||||
)
|
||||
// Fork/dev workspaces are detected by their parent link, not the `wm-fork-` id prefix.
|
||||
const currentWsIsFork = $derived(workspaceIsFork($workspaceStore, $userWorkspaces ?? []))
|
||||
|
||||
let leaveWorkspaceModal = $state(false)
|
||||
let deleteForkModal = $state<DeleteForkedWorkspaceModal>()
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
superadmin,
|
||||
usedTriggerKinds,
|
||||
userStore,
|
||||
userWorkspaces,
|
||||
workspaceStore,
|
||||
isCriticalAlertsUIOpen,
|
||||
enterpriseLicense,
|
||||
@@ -11,6 +12,7 @@
|
||||
tutorialsToDo,
|
||||
skippedAll
|
||||
} from '$lib/stores'
|
||||
import { isForkOwner } from '$lib/utils/workspaceHierarchy'
|
||||
import { syncTutorialsTodos } from '$lib/tutorialUtils'
|
||||
import { SIDEBAR_SHOW_SCHEDULES } from '$lib/consts'
|
||||
import {
|
||||
@@ -400,6 +402,12 @@
|
||||
return !page.url.pathname.includes(link.href) && !$usedTriggerKinds.includes(link.kind)
|
||||
})
|
||||
)
|
||||
// Admins, superadmins, and fork creators reach workspace settings (the latter
|
||||
// for the fork members screen; see isForkOwner / backend authorize_fork_owner_add_user).
|
||||
const currentWs = $derived($userWorkspaces?.find((w) => w.id === $workspaceStore))
|
||||
const canManageWorkspace = $derived(
|
||||
$userStore?.is_admin || $superadmin || isForkOwner(currentWs, $userStore?.email)
|
||||
)
|
||||
let secondaryMenuLinks = $derived([
|
||||
// {
|
||||
// label: 'Workspace',
|
||||
@@ -422,7 +430,7 @@
|
||||
aiDescription: 'Button to navigate to account settings',
|
||||
faIcon: undefined
|
||||
},
|
||||
...($userStore?.is_admin || $superadmin
|
||||
...(canManageWorkspace
|
||||
? [
|
||||
{
|
||||
label: 'Workspace',
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
import { workspaceAIClients } from '../copilot/lib'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
import type { MenubarBuilders } from '@melt-ui/svelte'
|
||||
import { buildWorkspaceHierarchy } from '$lib/utils/workspaceHierarchy'
|
||||
import { buildWorkspaceHierarchy, isForkOwner } from '$lib/utils/workspaceHierarchy'
|
||||
import { canCreateFork } from '$lib/utils/editInFork'
|
||||
import { getContrastTextColor } from '$lib/utils'
|
||||
import { workspaceRootId } from '$lib/components/sessions/sessionScope.svelte'
|
||||
@@ -167,7 +167,9 @@
|
||||
|
||||
// The active workspace itself (fork included) — names the settings entry.
|
||||
const activeWorkspace = $derived($userWorkspaces?.find((w) => w.id === $workspaceStore))
|
||||
const canManageWorkspace = $derived($userStore?.is_admin || $superadmin)
|
||||
const canManageWorkspace = $derived(
|
||||
$userStore?.is_admin || $superadmin || isForkOwner(activeWorkspace, $userStore?.email)
|
||||
)
|
||||
|
||||
// font-normal is explicit: href-less MenuItems render as <button>, which the
|
||||
// global stylesheet makes semibold, unlike the <a> the href entries get.
|
||||
|
||||
@@ -12,7 +12,11 @@
|
||||
import { workspaceAIClients } from '$lib/components/copilot/lib'
|
||||
import WorkspaceFamilyPicker from '$lib/components/sessions/WorkspaceFamilyPicker.svelte'
|
||||
import WorkspaceScopeTrigger from '$lib/components/WorkspaceScopeTrigger.svelte'
|
||||
import { findWorkspaceRoot, findWorkspaceDescendants } from '$lib/utils/workspaceHierarchy'
|
||||
import {
|
||||
findWorkspaceRoot,
|
||||
findWorkspaceDescendants,
|
||||
isForkOwner
|
||||
} from '$lib/utils/workspaceHierarchy'
|
||||
import { useForkableWorkspaces } from '$lib/utils/useForkableWorkspaces.svelte'
|
||||
|
||||
let { isCollapsed = false }: { isCollapsed?: boolean } = $props()
|
||||
@@ -38,9 +42,11 @@
|
||||
})
|
||||
const rootLabel = $derived(`${forkCount} fork${forkCount === 1 ? '' : 's'}`)
|
||||
|
||||
// Settings link at the bottom of the picker — admin/superadmin only, scoped
|
||||
// to the active workspace (fork or root).
|
||||
const canManageWorkspace = $derived($userStore?.is_admin || $superadmin)
|
||||
// Settings link at the bottom of the picker — admins, superadmins, and fork
|
||||
// creators, scoped to the active workspace (fork or root).
|
||||
const canManageWorkspace = $derived(
|
||||
$userStore?.is_admin || $superadmin || isForkOwner(currentWs, $userStore?.email)
|
||||
)
|
||||
const settingsHref = $derived(canManageWorkspace ? `${base}/workspace_settings` : undefined)
|
||||
const settingsLabel = $derived(`${currentWs?.name ?? effectiveId ?? 'Workspace'} settings`)
|
||||
|
||||
|
||||
@@ -138,6 +138,21 @@ export function workspaceIsFork(
|
||||
return allWorkspaces.find((w) => w.id === workspaceId)?.parent_workspace_id != null
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether `userEmail` is the creator of a fork workspace. The fork creator gets workspace-settings
|
||||
* access (the fork members screen) even when they are not an admin of it: forking as an ordinary
|
||||
* developer copies their parent `usr` row, leaving them otherwise unable to bring collaborators in.
|
||||
* Mirrors the backend `authorize_fork_owner_add_user` grant.
|
||||
*/
|
||||
export function isForkOwner(
|
||||
workspace: UserWorkspace | undefined,
|
||||
userEmail: string | null | undefined
|
||||
): boolean {
|
||||
return (
|
||||
Boolean(workspace?.parent_workspace_id) && !!userEmail && workspace?.created_by === userEmail
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* The canonical dev workspace of a prod workspace, if any (at most one per prod). Used to redirect
|
||||
* edits from a locked prod workspace into its dev workspace. Disabled dev workspaces are excluded:
|
||||
|
||||
Reference in New Issue
Block a user