From 11bb37d7ca7fbd50cc66905f41851226a48e57fb Mon Sep 17 00:00:00 2001 From: Ruben Fiszel Date: Mon, 20 Jul 2026 20:39:48 +0200 Subject: [PATCH] fix(forks): show workspace settings link in sidebar for fork creators (#10216) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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) * 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) --------- Co-authored-by: Claude Opus 4.8 (1M context) --- .../lib/components/sidebar/SettingsMenu.svelte | 16 ++++++++++++---- .../lib/components/sidebar/SidebarContent.svelte | 10 +++++++++- .../lib/components/sidebar/WorkspaceMenu.svelte | 6 ++++-- .../sidebar/WorkspaceScopeHeader.svelte | 14 ++++++++++---- frontend/src/lib/utils/workspaceHierarchy.ts | 15 +++++++++++++++ 5 files changed, 50 insertions(+), 11 deletions(-) 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