diff --git a/frontend/src/lib/components/sidebar/SettingsMenu.svelte b/frontend/src/lib/components/sidebar/SettingsMenu.svelte index d44c3b0364..8b5213da24 100644 --- a/frontend/src/lib/components/sidebar/SettingsMenu.svelte +++ b/frontend/src/lib/components/sidebar/SettingsMenu.svelte @@ -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() diff --git a/frontend/src/lib/components/sidebar/SidebarContent.svelte b/frontend/src/lib/components/sidebar/SidebarContent.svelte index abfd23f751..524c11072a 100644 --- a/frontend/src/lib/components/sidebar/SidebarContent.svelte +++ b/frontend/src/lib/components/sidebar/SidebarContent.svelte @@ -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', diff --git a/frontend/src/lib/components/sidebar/WorkspaceMenu.svelte b/frontend/src/lib/components/sidebar/WorkspaceMenu.svelte index e84f62daef..38eddb2b95 100644 --- a/frontend/src/lib/components/sidebar/WorkspaceMenu.svelte +++ b/frontend/src/lib/components/sidebar/WorkspaceMenu.svelte @@ -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