From 2bdf80d452d7c0ad1ca64e482850a547f40cf46a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kayla=20=E3=81=AF=E3=81=AA?= Date: Mon, 2 Mar 2026 19:04:55 -0700 Subject: [PATCH] fix: disable sharing ui when sharing is unavailable (#22390) Currently the sharing UI is only hidden under certain circumstances, rather than on a permission basis. This makes it permissions based, and makes some backend changes to make sure permissions are correct. --- coderd/coderd.go | 5 +- coderd/rbac/roles.go | 56 +++++++++++++------ coderd/workspaces_test.go | 4 ++ site/src/api/queries/organizations.ts | 2 +- site/src/api/queries/workspaces.ts | 2 +- .../WorkspaceSharingForm.tsx | 44 +++++++++++++++ site/src/modules/workspaces/permissions.ts | 16 +++--- .../pages/WorkspacePage/Workspace.stories.tsx | 2 +- site/src/pages/WorkspacePage/Workspace.tsx | 2 - .../WorkspaceActions/ShareButton.tsx | 1 + .../WorkspaceActions.stories.tsx | 8 +-- .../WorkspaceActions/WorkspaceActions.tsx | 11 ++-- .../WorkspaceNotifications.stories.tsx | 4 +- .../WorkspacePage/WorkspacePage.jest.tsx | 4 +- .../src/pages/WorkspacePage/WorkspacePage.tsx | 8 --- .../WorkspacePage/WorkspaceReadyPage.tsx | 3 - .../WorkspacePage/WorkspaceTopbar.stories.tsx | 4 +- .../pages/WorkspacePage/WorkspaceTopbar.tsx | 3 - .../pages/WorkspaceSettingsPage/Sidebar.tsx | 19 ++----- .../WorkspaceParametersExperimentRouter.tsx | 2 +- .../WorkspaceParametersPage.tsx | 2 +- .../WorkspaceParametersPageExperimental.tsx | 2 +- .../WorkspaceSchedulePage.stories.tsx | 23 +++----- .../WorkspaceSchedulePage.tsx | 22 +------- .../WorkspaceSettingsLayout.tsx | 54 ++++++++++-------- .../WorkspaceSettingsPage.tsx | 2 +- .../WorkspaceSharingPage.tsx | 4 +- .../WorkspaceSharingPageView.stories.tsx | 9 +++ .../WorkspaceSharingPageView.tsx | 1 + 29 files changed, 181 insertions(+), 138 deletions(-) diff --git a/coderd/coderd.go b/coderd/coderd.go index 5cffb32319..353ae7e0e4 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -333,9 +333,10 @@ func New(options *Options) *API { panic("developer error: options.PrometheusRegistry is nil and not running a unit test") } - if options.DeploymentValues.DisableOwnerWorkspaceExec { + if options.DeploymentValues.DisableOwnerWorkspaceExec || options.DeploymentValues.DisableWorkspaceSharing { rbac.ReloadBuiltinRoles(&rbac.RoleOptions{ - NoOwnerWorkspaceExec: true, + NoOwnerWorkspaceExec: bool(options.DeploymentValues.DisableOwnerWorkspaceExec), + NoWorkspaceSharing: bool(options.DeploymentValues.DisableWorkspaceSharing), }) } diff --git a/coderd/rbac/roles.go b/coderd/rbac/roles.go index 2b55f0ad26..1789b57786 100644 --- a/coderd/rbac/roles.go +++ b/coderd/rbac/roles.go @@ -244,6 +244,7 @@ func SystemRoleName(name string) bool { type RoleOptions struct { NoOwnerWorkspaceExec bool + NoWorkspaceSharing bool } // ReservedRoleName exists because the database should only allow unique role @@ -267,12 +268,23 @@ func ReloadBuiltinRoles(opts *RoleOptions) { opts = &RoleOptions{} } + denyPermissions := []Permission{} + if opts.NoWorkspaceSharing { + denyPermissions = append(denyPermissions, Permission{ + Negate: true, + ResourceType: ResourceWorkspace.Type, + Action: policy.ActionShare, + }) + } + ownerWorkspaceActions := ResourceWorkspace.AvailableActions() if opts.NoOwnerWorkspaceExec { // Remove ssh and application connect from the owner role. This // prevents owners from have exec access to all workspaces. - ownerWorkspaceActions = slice.Omit(ownerWorkspaceActions, - policy.ActionApplicationConnect, policy.ActionSSH) + ownerWorkspaceActions = slice.Omit( + ownerWorkspaceActions, + policy.ActionApplicationConnect, policy.ActionSSH, + ) } // Static roles that never change should be allocated in a closure. @@ -295,7 +307,8 @@ func ReloadBuiltinRoles(opts *RoleOptions) { // Explicitly setting PrebuiltWorkspace permissions for clarity. // Note: even without PrebuiltWorkspace permissions, access is still granted via Workspace permissions. ResourcePrebuiltWorkspace.Type: {policy.ActionUpdate, policy.ActionDelete}, - })...), + })..., + ), User: []Permission{}, ByOrgID: map[string]OrgPermissions{}, }.withCachedRegoValue() @@ -303,13 +316,17 @@ func ReloadBuiltinRoles(opts *RoleOptions) { memberRole := Role{ Identifier: RoleMember(), DisplayName: "Member", - Site: Permissions(map[string][]policy.Action{ - ResourceAssignRole.Type: {policy.ActionRead}, - // All users can see OAuth2 provider applications. - ResourceOauth2App.Type: {policy.ActionRead}, - ResourceWorkspaceProxy.Type: {policy.ActionRead}, - }), - User: append(allPermsExcept(ResourceWorkspaceDormant, ResourcePrebuiltWorkspace, ResourceWorkspace, ResourceUser, ResourceOrganizationMember, ResourceOrganizationMember, ResourceBoundaryUsage), + Site: append( + Permissions(map[string][]policy.Action{ + ResourceAssignRole.Type: {policy.ActionRead}, + // All users can see OAuth2 provider applications. + ResourceOauth2App.Type: {policy.ActionRead}, + ResourceWorkspaceProxy.Type: {policy.ActionRead}, + }), + denyPermissions..., + ), + User: append( + allPermsExcept(ResourceWorkspaceDormant, ResourcePrebuiltWorkspace, ResourceWorkspace, ResourceUser, ResourceOrganizationMember, ResourceOrganizationMember, ResourceBoundaryUsage), Permissions(map[string][]policy.Action{ // Users cannot do create/update/delete on themselves, but they // can read their own details. @@ -433,14 +450,17 @@ func ReloadBuiltinRoles(opts *RoleOptions) { ByOrgID: map[string]OrgPermissions{ // Org admins should not have workspace exec perms. organizationID.String(): { - Org: append(allPermsExcept(ResourceWorkspace, ResourceWorkspaceDormant, ResourcePrebuiltWorkspace, ResourceAssignRole, ResourceUserSecret, ResourceBoundaryUsage), Permissions(map[string][]policy.Action{ - ResourceWorkspaceDormant.Type: {policy.ActionRead, policy.ActionDelete, policy.ActionCreate, policy.ActionUpdate, policy.ActionWorkspaceStop, policy.ActionCreateAgent, policy.ActionDeleteAgent, policy.ActionUpdateAgent}, - ResourceWorkspace.Type: slice.Omit(ResourceWorkspace.AvailableActions(), policy.ActionApplicationConnect, policy.ActionSSH), - // PrebuiltWorkspaces are a subset of Workspaces. - // Explicitly setting PrebuiltWorkspace permissions for clarity. - // Note: even without PrebuiltWorkspace permissions, access is still granted via Workspace permissions. - ResourcePrebuiltWorkspace.Type: {policy.ActionUpdate, policy.ActionDelete}, - })...), + Org: append( + allPermsExcept(ResourceWorkspace, ResourceWorkspaceDormant, ResourcePrebuiltWorkspace, ResourceAssignRole, ResourceUserSecret, ResourceBoundaryUsage), + Permissions(map[string][]policy.Action{ + ResourceWorkspace.Type: slice.Omit(ResourceWorkspace.AvailableActions(), policy.ActionApplicationConnect, policy.ActionSSH), + ResourceWorkspaceDormant.Type: {policy.ActionRead, policy.ActionDelete, policy.ActionCreate, policy.ActionUpdate, policy.ActionWorkspaceStop, policy.ActionCreateAgent, policy.ActionDeleteAgent, policy.ActionUpdateAgent}, + // PrebuiltWorkspaces are a subset of Workspaces. + // Explicitly setting PrebuiltWorkspace permissions for clarity. + // Note: even without PrebuiltWorkspace permissions, access is still granted via Workspace permissions. + ResourcePrebuiltWorkspace.Type: {policy.ActionUpdate, policy.ActionDelete}, + })..., + ), Member: []Permission{}, }, }, diff --git a/coderd/workspaces_test.go b/coderd/workspaces_test.go index c3b80396b9..cc7b6b5543 100644 --- a/coderd/workspaces_test.go +++ b/coderd/workspaces_test.go @@ -5572,6 +5572,10 @@ func TestWorkspaceSharingDisabled(t *testing.T) { }) t.Run("NoAccessWhenDisabled", func(t *testing.T) { + t.Cleanup(func() { + rbac.ReloadBuiltinRoles(nil) + }) + var ( client, db = coderdtest.NewWithDatabase(t, &coderdtest.Options{ DeploymentValues: coderdtest.DeploymentValues(t, func(dv *codersdk.DeploymentValues) { diff --git a/site/src/api/queries/organizations.ts b/site/src/api/queries/organizations.ts index 86478c87e6..f727e4387f 100644 --- a/site/src/api/queries/organizations.ts +++ b/site/src/api/queries/organizations.ts @@ -248,7 +248,7 @@ export const patchRoleSyncSettings = ( }; }; -const getWorkspaceSharingSettingsKey = (organization: string) => [ +export const getWorkspaceSharingSettingsKey = (organization: string) => [ "organization", organization, "workspaceSharingSettings", diff --git a/site/src/api/queries/workspaces.ts b/site/src/api/queries/workspaces.ts index cb14916586..237fed6a2f 100644 --- a/site/src/api/queries/workspaces.ts +++ b/site/src/api/queries/workspaces.ts @@ -489,7 +489,7 @@ export const workspacePermissions = (workspace?: Workspace) => { checks: workspace ? workspaceChecks(workspace) : {}, }), queryKey: ["workspaces", workspace?.id, "permissions"], - enabled: !!workspace, + enabled: Boolean(workspace), staleTime: Number.POSITIVE_INFINITY, }; }; diff --git a/site/src/modules/workspaces/WorkspaceSharingForm/WorkspaceSharingForm.tsx b/site/src/modules/workspaces/WorkspaceSharingForm/WorkspaceSharingForm.tsx index 196ff53332..50a24441ee 100644 --- a/site/src/modules/workspaces/WorkspaceSharingForm/WorkspaceSharingForm.tsx +++ b/site/src/modules/workspaces/WorkspaceSharingForm/WorkspaceSharingForm.tsx @@ -1,3 +1,4 @@ +import { workspaceSharingSettings } from "api/queries/organizations"; import type { Group, WorkspaceACL, @@ -37,6 +38,7 @@ import { TableLoader } from "components/TableLoader/TableLoader"; import { EllipsisVertical, UserPlusIcon } from "lucide-react"; import { getGroupSubtitle } from "modules/groups"; import type { FC, ReactNode } from "react"; +import { useQuery } from "react-query"; interface RoleSelectProps { value: WorkspaceRole; @@ -139,6 +141,7 @@ export const RoleSelectField: FC = ({ }; interface WorkspaceSharingFormProps { + organizationId: string; workspaceACL: WorkspaceACL | undefined; canUpdatePermissions: boolean; isTaskWorkspace: boolean; @@ -155,6 +158,7 @@ interface WorkspaceSharingFormProps { } export const WorkspaceSharingForm: FC = ({ + organizationId, workspaceACL, canUpdatePermissions, isTaskWorkspace, @@ -169,6 +173,46 @@ export const WorkspaceSharingForm: FC = ({ isCompact, showRestartWarning, }) => { + const sharingSettingsQuery = useQuery( + workspaceSharingSettings(organizationId), + ); + + if (sharingSettingsQuery.isLoading) { + return ( + + + + ); + } + + if (!sharingSettingsQuery.data) { + return ( + + + + + + + + ); + } + + if (sharingSettingsQuery.data.sharing_disabled) { + return ( + + + + + + + + ); + } + const isEmpty = Boolean( workspaceACL && workspaceACL.users.length === 0 && diff --git a/site/src/modules/workspaces/permissions.ts b/site/src/modules/workspaces/permissions.ts index 07c4e612cd..0b79581d36 100644 --- a/site/src/modules/workspaces/permissions.ts +++ b/site/src/modules/workspaces/permissions.ts @@ -10,6 +10,14 @@ export const workspaceChecks = (workspace: Workspace) => }, action: "read", }, + shareWorkspace: { + object: { + resource_type: "workspace", + resource_id: workspace.id, + owner_id: workspace.owner_id, + }, + action: "share", + }, updateWorkspace: { object: { resource_type: "workspace", @@ -34,14 +42,6 @@ export const workspaceChecks = (workspace: Workspace) => }, action: "update", }, - // To run a build in debug mode we need to be able to read the deployment - // config (enable_terraform_debug_mode). - deploymentConfig: { - object: { - resource_type: "deployment_config", - }, - action: "read", - }, }) satisfies Record; export type WorkspacePermissions = Record< diff --git a/site/src/pages/WorkspacePage/Workspace.stories.tsx b/site/src/pages/WorkspacePage/Workspace.stories.tsx index 8ae0c1b309..b57a9a6100 100644 --- a/site/src/pages/WorkspacePage/Workspace.stories.tsx +++ b/site/src/pages/WorkspacePage/Workspace.stories.tsx @@ -24,9 +24,9 @@ const createTimestamp = ( const permissions: WorkspacePermissions = { readWorkspace: true, + shareWorkspace: true, updateWorkspace: true, updateWorkspaceVersion: true, - deploymentConfig: true, deleteFailedWorkspace: true, }; diff --git a/site/src/pages/WorkspacePage/Workspace.tsx b/site/src/pages/WorkspacePage/Workspace.tsx index 0b98ed6c17..353a5bf847 100644 --- a/site/src/pages/WorkspacePage/Workspace.tsx +++ b/site/src/pages/WorkspacePage/Workspace.tsx @@ -57,7 +57,6 @@ export const Workspace: FC = ({ latestVersion, permissions, timings, - sharingDisabled, handleStart, handleStop, handleRestart, @@ -111,7 +110,6 @@ export const Workspace: FC = ({ latestVersion={latestVersion} isUpdating={isUpdating} isRestarting={isRestarting} - sharingDisabled={sharingDisabled} handleStart={handleStart} handleStop={handleStop} handleRestart={handleRestart} diff --git a/site/src/pages/WorkspacePage/WorkspaceActions/ShareButton.tsx b/site/src/pages/WorkspacePage/WorkspaceActions/ShareButton.tsx index de225be488..6050371c51 100644 --- a/site/src/pages/WorkspacePage/WorkspaceActions/ShareButton.tsx +++ b/site/src/pages/WorkspacePage/WorkspaceActions/ShareButton.tsx @@ -38,6 +38,7 @@ export const ShareButton: FC = ({ = { args: { isUpdating: false, permissions: { - deleteFailedWorkspace: true, - deploymentConfig: true, readWorkspace: true, + shareWorkspace: true, updateWorkspace: true, updateWorkspaceVersion: true, + deleteFailedWorkspace: true, }, }, decorators: [withDashboardProvider, withDesktopViewport, withAuthProvider], @@ -172,11 +172,11 @@ export const FailedWithDebug: Story = { args: { workspace: Mocks.MockFailedWorkspace, permissions: { - deploymentConfig: true, - deleteFailedWorkspace: true, readWorkspace: true, + shareWorkspace: true, updateWorkspace: true, updateWorkspaceVersion: true, + deleteFailedWorkspace: true, }, }, }; diff --git a/site/src/pages/WorkspacePage/WorkspaceActions/WorkspaceActions.tsx b/site/src/pages/WorkspacePage/WorkspaceActions/WorkspaceActions.tsx index 6af5b2230a..db724792b5 100644 --- a/site/src/pages/WorkspacePage/WorkspaceActions/WorkspaceActions.tsx +++ b/site/src/pages/WorkspacePage/WorkspaceActions/WorkspaceActions.tsx @@ -29,7 +29,6 @@ interface WorkspaceActionsProps { isUpdating: boolean; isRestarting: boolean; permissions: WorkspacePermissions; - sharingDisabled?: boolean; handleToggleFavorite: () => void; handleStart: (buildParameters?: WorkspaceBuildParameter[]) => void; handleStop: () => void; @@ -46,7 +45,6 @@ export const WorkspaceActions: FC = ({ isUpdating, isRestarting, permissions, - sharingDisabled, handleToggleFavorite, handleStart, handleStop, @@ -57,10 +55,13 @@ export const WorkspaceActions: FC = ({ handleDebug, handleDormantActivate, }) => { - const { user } = useAuthenticated(); + const { + permissions: { viewDeploymentConfig }, + user, + } = useAuthenticated(); const { data: deployment } = useQuery({ ...deploymentConfig(), - enabled: permissions.deploymentConfig, + enabled: viewDeploymentConfig, }); const { actions, canCancel, canAcceptJobs } = abilitiesByWorkspaceStatus( workspace, @@ -191,7 +192,7 @@ export const WorkspaceActions: FC = ({ onToggle={handleToggleFavorite} /> - {!sharingDisabled && ( + {permissions.shareWorkspace && ( { server.use( http.post("/api/v2/authcheck", async () => { const permissions: WorkspacePermissions = { - deleteFailedWorkspace: true, - deploymentConfig: true, readWorkspace: true, + shareWorkspace: true, updateWorkspace: true, updateWorkspaceVersion: true, + deleteFailedWorkspace: true, }; return HttpResponse.json(permissions); }), diff --git a/site/src/pages/WorkspacePage/WorkspacePage.tsx b/site/src/pages/WorkspacePage/WorkspacePage.tsx index 70393eaee0..5095c923bd 100644 --- a/site/src/pages/WorkspacePage/WorkspacePage.tsx +++ b/site/src/pages/WorkspacePage/WorkspacePage.tsx @@ -1,5 +1,4 @@ import { watchWorkspace } from "api/api"; -import { workspaceSharingSettings } from "api/queries/organizations"; import { template as templateQueryOptions } from "api/queries/templates"; import { workspaceBuildsKey } from "api/queries/workspaceBuilds"; import { @@ -45,12 +44,6 @@ const WorkspacePage: FC = () => { const permissionsQuery = useQuery(workspacePermissions(workspace)); const permissions = permissionsQuery.data; - const sharingSettingsQuery = useQuery({ - ...workspaceSharingSettings(workspace?.organization_id ?? ""), - enabled: !!workspace, - }); - const sharingDisabled = sharingSettingsQuery.data?.sharing_disabled ?? false; - // Watch workspace changes const updateWorkspaceData = useEffectEvent( async (newWorkspaceData: Workspace) => { @@ -124,7 +117,6 @@ const WorkspacePage: FC = () => { workspace={workspace} template={template} permissions={permissions} - sharingDisabled={sharingDisabled} /> ); }; diff --git a/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx b/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx index 739ced4467..daa83eab32 100644 --- a/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx +++ b/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx @@ -39,14 +39,12 @@ interface WorkspaceReadyPageProps { template: TypesGen.Template; workspace: TypesGen.Workspace; permissions: WorkspacePermissions; - sharingDisabled?: boolean; } export const WorkspaceReadyPage: FC = ({ workspace, template, permissions, - sharingDisabled, }) => { const queryClient = useQueryClient(); @@ -298,7 +296,6 @@ export const WorkspaceReadyPage: FC = ({ template={template} buildLogs={buildLogs} timings={timingsQuery.data} - sharingDisabled={sharingDisabled} handleStart={async (buildParameters) => { const { hasEphemeral, ephemeralParameters } = await checkEphemeralParameters(buildParameters); diff --git a/site/src/pages/WorkspacePage/WorkspaceTopbar.stories.tsx b/site/src/pages/WorkspacePage/WorkspaceTopbar.stories.tsx index dae0874e7c..487f0f202d 100644 --- a/site/src/pages/WorkspacePage/WorkspaceTopbar.stories.tsx +++ b/site/src/pages/WorkspacePage/WorkspaceTopbar.stories.tsx @@ -35,9 +35,9 @@ const meta: Meta = { latestVersion: MockTemplateVersion, permissions: { readWorkspace: true, - updateWorkspaceVersion: true, + shareWorkspace: true, updateWorkspace: true, - deploymentConfig: true, + updateWorkspaceVersion: true, deleteFailedWorkspace: true, }, }, diff --git a/site/src/pages/WorkspacePage/WorkspaceTopbar.tsx b/site/src/pages/WorkspacePage/WorkspaceTopbar.tsx index 0a4d9b881b..b64352da65 100644 --- a/site/src/pages/WorkspacePage/WorkspaceTopbar.tsx +++ b/site/src/pages/WorkspacePage/WorkspaceTopbar.tsx @@ -44,7 +44,6 @@ interface WorkspaceProps { template: TypesGen.Template; permissions: WorkspacePermissions; latestVersion?: TypesGen.TemplateVersion; - sharingDisabled?: boolean; handleStart: (buildParameters?: TypesGen.WorkspaceBuildParameter[]) => void; handleStop: () => void; handleRestart: (buildParameters?: TypesGen.WorkspaceBuildParameter[]) => void; @@ -63,7 +62,6 @@ export const WorkspaceTopbar: FC = ({ permissions, isUpdating, isRestarting, - sharingDisabled, handleStart, handleStop, handleRestart, @@ -238,7 +236,6 @@ export const WorkspaceTopbar: FC = ({ permissions={permissions} isUpdating={isUpdating} isRestarting={isRestarting} - sharingDisabled={sharingDisabled} handleStart={handleStart} handleStop={handleStop} handleRestart={handleRestart} diff --git a/site/src/pages/WorkspaceSettingsPage/Sidebar.tsx b/site/src/pages/WorkspaceSettingsPage/Sidebar.tsx index 2305923aa5..50ba48f2d8 100644 --- a/site/src/pages/WorkspaceSettingsPage/Sidebar.tsx +++ b/site/src/pages/WorkspaceSettingsPage/Sidebar.tsx @@ -1,4 +1,3 @@ -import type { Workspace } from "api/typesGenerated"; import { Avatar } from "components/Avatar/Avatar"; import { FeatureStageBadge } from "components/FeatureStageBadge/FeatureStageBadge"; import { @@ -12,19 +11,11 @@ import { TimerIcon as ScheduleIcon, Users as SharingIcon, } from "lucide-react"; -import type { FC } from "react"; +import { useWorkspaceSettings } from "./WorkspaceSettingsLayout"; -interface SidebarProps { - username: string; - workspace: Workspace; - sharingDisabled?: boolean; -} +export const Sidebar: React.FC = () => { + const { owner, workspace, permissions } = useWorkspaceSettings(); -export const Sidebar: FC = ({ - username, - workspace, - sharingDisabled, -}) => { return ( = ({ /> } title={workspace.name} - linkTo={`/@${username}/${workspace.name}`} + linkTo={`/@${owner}/${workspace.name}`} subtitle={workspace.template_display_name ?? workspace.template_name} /> @@ -49,7 +40,7 @@ export const Sidebar: FC = ({ Schedule - {!sharingDisabled && ( + {permissions?.shareWorkspace && ( Sharing diff --git a/site/src/pages/WorkspaceSettingsPage/WorkspaceParametersPage/WorkspaceParametersExperimentRouter.tsx b/site/src/pages/WorkspaceSettingsPage/WorkspaceParametersPage/WorkspaceParametersExperimentRouter.tsx index 0a01c9907b..b3a2da30b6 100644 --- a/site/src/pages/WorkspaceSettingsPage/WorkspaceParametersPage/WorkspaceParametersExperimentRouter.tsx +++ b/site/src/pages/WorkspaceSettingsPage/WorkspaceParametersPage/WorkspaceParametersExperimentRouter.tsx @@ -4,7 +4,7 @@ import WorkspaceParametersPage from "./WorkspaceParametersPage"; import WorkspaceParametersPageExperimental from "./WorkspaceParametersPageExperimental"; const WorkspaceParametersExperimentRouter: FC = () => { - const workspace = useWorkspaceSettings(); + const { workspace } = useWorkspaceSettings(); return ( <> diff --git a/site/src/pages/WorkspaceSettingsPage/WorkspaceParametersPage/WorkspaceParametersPage.tsx b/site/src/pages/WorkspaceSettingsPage/WorkspaceParametersPage/WorkspaceParametersPage.tsx index db67b52429..6ef647faa9 100644 --- a/site/src/pages/WorkspaceSettingsPage/WorkspaceParametersPage/WorkspaceParametersPage.tsx +++ b/site/src/pages/WorkspaceSettingsPage/WorkspaceParametersPage/WorkspaceParametersPage.tsx @@ -29,7 +29,7 @@ import { } from "./WorkspaceParametersForm"; const WorkspaceParametersPage: FC = () => { - const workspace = useWorkspaceSettings(); + const { workspace } = useWorkspaceSettings(); const build = workspace.latest_build; const { data: templateVersionParameters } = useQuery( richParameters(build.template_version_id), diff --git a/site/src/pages/WorkspaceSettingsPage/WorkspaceParametersPage/WorkspaceParametersPageExperimental.tsx b/site/src/pages/WorkspaceSettingsPage/WorkspaceParametersPage/WorkspaceParametersPageExperimental.tsx index 0178a4e6a2..9faf13bb54 100644 --- a/site/src/pages/WorkspaceSettingsPage/WorkspaceParametersPage/WorkspaceParametersPageExperimental.tsx +++ b/site/src/pages/WorkspaceSettingsPage/WorkspaceParametersPage/WorkspaceParametersPageExperimental.tsx @@ -33,7 +33,7 @@ import { useWorkspaceSettings } from "../WorkspaceSettingsLayout"; import { WorkspaceParametersPageViewExperimental } from "./WorkspaceParametersPageViewExperimental"; const WorkspaceParametersPageExperimental: FC = () => { - const workspace = useWorkspaceSettings(); + const { workspace } = useWorkspaceSettings(); const navigate = useNavigate(); const [searchParams] = useSearchParams(); const templateVersionId = searchParams.get("templateVersionId") ?? undefined; diff --git a/site/src/pages/WorkspaceSettingsPage/WorkspaceSchedulePage/WorkspaceSchedulePage.stories.tsx b/site/src/pages/WorkspaceSettingsPage/WorkspaceSchedulePage/WorkspaceSchedulePage.stories.tsx index 7503c439a3..6625e092f7 100644 --- a/site/src/pages/WorkspaceSettingsPage/WorkspaceSchedulePage/WorkspaceSchedulePage.stories.tsx +++ b/site/src/pages/WorkspaceSettingsPage/WorkspaceSchedulePage/WorkspaceSchedulePage.stories.tsx @@ -6,10 +6,10 @@ import { } from "testHelpers/entities"; import { withAuthProvider, withDashboardProvider } from "testHelpers/storybook"; import type { Meta, StoryObj } from "@storybook/react-vite"; -import { getAuthorizationKey } from "api/queries/authCheck"; import { templateByNameKey } from "api/queries/templates"; import { workspaceByOwnerAndNameKey } from "api/queries/workspaces"; import type { Workspace } from "api/typesGenerated"; +import type { WorkspacePermissions } from "modules/workspaces/permissions"; import { reactRouterOutlet, reactRouterParameters, @@ -68,19 +68,14 @@ function workspaceQueries(workspace: Workspace) { data: workspace, }, { - key: getAuthorizationKey({ - checks: { - updateWorkspace: { - object: { - resource_type: "workspace", - resource_id: MockWorkspace.id, - owner_id: MockWorkspace.owner_id, - }, - action: "update", - }, - }, - }), - data: { updateWorkspace: true }, + key: ["workspaces", workspace.id, "permissions"], + data: { + readWorkspace: true, + shareWorkspace: true, + updateWorkspace: true, + updateWorkspaceVersion: true, + deleteFailedWorkspace: true, + } satisfies WorkspacePermissions, }, { key: templateByNameKey( diff --git a/site/src/pages/WorkspaceSettingsPage/WorkspaceSchedulePage/WorkspaceSchedulePage.tsx b/site/src/pages/WorkspaceSettingsPage/WorkspaceSchedulePage/WorkspaceSchedulePage.tsx index 05d9bdfaac..257ee85166 100644 --- a/site/src/pages/WorkspaceSettingsPage/WorkspaceSchedulePage/WorkspaceSchedulePage.tsx +++ b/site/src/pages/WorkspaceSettingsPage/WorkspaceSchedulePage/WorkspaceSchedulePage.tsx @@ -1,6 +1,5 @@ import { API } from "api/api"; import { getErrorDetail } from "api/errors"; -import { checkAuthorization } from "api/queries/authCheck"; import { templateByName } from "api/queries/templates"; import { workspaceByOwnerAndNameKey } from "api/queries/workspaces"; import type * as TypesGen from "api/typesGenerated"; @@ -29,28 +28,13 @@ import { } from "./formToRequest"; import { WorkspaceScheduleForm } from "./WorkspaceScheduleForm"; -const permissionsToCheck = (workspace: TypesGen.Workspace) => - ({ - updateWorkspace: { - object: { - resource_type: "workspace", - resource_id: workspace.id, - owner_id: workspace.owner_id, - }, - action: "update", - }, - }) as const; - const WorkspaceSchedulePage: FC = () => { const params = useParams() as { username: string; workspace: string }; const navigate = useNavigate(); const username = params.username.replace("@", ""); const workspaceName = params.workspace; const queryClient = useQueryClient(); - const workspace = useWorkspaceSettings(); - const { data: permissions, error: checkPermissionsError } = useQuery( - checkAuthorization({ checks: permissionsToCheck(workspace) }), - ); + const { permissions, workspace } = useWorkspaceSettings(); const { data: template, error: getTemplateError } = useQuery( templateByName(workspace.organization_id, workspace.template_name), ); @@ -75,8 +59,8 @@ const WorkspaceSchedulePage: FC = () => { }, ), }); - const error = checkPermissionsError || getTemplateError; - const isLoading = !template || !permissions; + const error = getTemplateError; + const isLoading = !template; const [isConfirmingApply, setIsConfirmingApply] = useState(false); const { mutate: updateWorkspace } = useMutation({ diff --git a/site/src/pages/WorkspaceSettingsPage/WorkspaceSettingsLayout.tsx b/site/src/pages/WorkspaceSettingsPage/WorkspaceSettingsLayout.tsx index e818a62fc5..6f8a0921ef 100644 --- a/site/src/pages/WorkspaceSettingsPage/WorkspaceSettingsLayout.tsx +++ b/site/src/pages/WorkspaceSettingsPage/WorkspaceSettingsLayout.tsx @@ -1,17 +1,28 @@ -import { workspaceSharingSettings } from "api/queries/organizations"; -import { workspaceByOwnerAndName } from "api/queries/workspaces"; +import { + workspaceByOwnerAndName, + workspacePermissions, +} from "api/queries/workspaces"; import type { Workspace } from "api/typesGenerated"; import { ErrorAlert } from "components/Alert/ErrorAlert"; import { Loader } from "components/Loader/Loader"; import { Margins } from "components/Margins/Margins"; import { Stack } from "components/Stack/Stack"; +import type { WorkspacePermissions } from "modules/workspaces/permissions"; import { createContext, type FC, Suspense, useContext } from "react"; import { useQuery } from "react-query"; import { Outlet, useParams } from "react-router"; import { pageTitle } from "utils/page"; import { Sidebar } from "./Sidebar"; -const WorkspaceSettings = createContext(undefined); +type WorkspaceSettingsContext = { + owner: string; + workspace: Workspace; + permissions?: WorkspacePermissions; +}; + +const WorkspaceSettings = createContext( + undefined, +); export function useWorkspaceSettings() { const value = useContext(WorkspaceSettings); @@ -31,39 +42,36 @@ export const WorkspaceSettingsLayout: FC = () => { }; const workspaceName = params.workspace; const username = params.username.replace("@", ""); - const { - data: workspace, - error, - isLoading, - isError, - } = useQuery(workspaceByOwnerAndName(username, workspaceName)); + const workspaceQuery = useQuery( + workspaceByOwnerAndName(username, workspaceName), + ); - const sharingSettingsQuery = useQuery({ - ...workspaceSharingSettings(workspace?.organization_id ?? ""), - enabled: !!workspace, - }); - const sharingDisabled = sharingSettingsQuery.data?.sharing_disabled ?? false; + const permissionsQuery = useQuery(workspacePermissions(workspaceQuery.data)); - if (isLoading) { + if (workspaceQuery.isLoading) { return ; } + const error = workspaceQuery.error || permissionsQuery.error; + return ( <> {pageTitle(workspaceName, "Settings")} - {isError ? ( + {error ? ( ) : ( - workspace && ( - - + workspaceQuery.data && ( + + }>
diff --git a/site/src/pages/WorkspaceSettingsPage/WorkspaceSettingsPage.tsx b/site/src/pages/WorkspaceSettingsPage/WorkspaceSettingsPage.tsx index 83050692f9..efea1b164e 100644 --- a/site/src/pages/WorkspaceSettingsPage/WorkspaceSettingsPage.tsx +++ b/site/src/pages/WorkspaceSettingsPage/WorkspaceSettingsPage.tsx @@ -15,7 +15,7 @@ const WorkspaceSettingsPage: FC = () => { }; const workspaceName = params.workspace; const username = params.username.replace("@", ""); - const workspace = useWorkspaceSettings(); + const { workspace } = useWorkspaceSettings(); const navigate = useNavigate(); const mutation = useMutation({ diff --git a/site/src/pages/WorkspaceSettingsPage/WorkspaceSharingPage/WorkspaceSharingPage.tsx b/site/src/pages/WorkspaceSettingsPage/WorkspaceSharingPage/WorkspaceSharingPage.tsx index 8ae8cf72df..ccd145d78a 100644 --- a/site/src/pages/WorkspaceSettingsPage/WorkspaceSharingPage/WorkspaceSharingPage.tsx +++ b/site/src/pages/WorkspaceSettingsPage/WorkspaceSharingPage/WorkspaceSharingPage.tsx @@ -11,7 +11,7 @@ import { useWorkspaceSettings } from "../WorkspaceSettingsLayout"; import { WorkspaceSharingPageView } from "./WorkspaceSharingPageView"; const WorkspaceSharingPage: FC = () => { - const workspace = useWorkspaceSettings(); + const { workspace } = useWorkspaceSettings(); const sharing = useWorkspaceSharing(workspace); const checks = workspaceChecks(workspace); @@ -25,7 +25,7 @@ const WorkspaceSharingPage: FC = () => { sharing.error ?? permissionsQuery.error ?? sharing.mutationError; return ( -
+
{pageTitle(workspace.name, "Sharing")}
diff --git a/site/src/pages/WorkspaceSettingsPage/WorkspaceSharingPage/WorkspaceSharingPageView.stories.tsx b/site/src/pages/WorkspaceSettingsPage/WorkspaceSharingPage/WorkspaceSharingPageView.stories.tsx index 2d53d42de6..f40f4215d6 100644 --- a/site/src/pages/WorkspaceSettingsPage/WorkspaceSharingPage/WorkspaceSharingPageView.stories.tsx +++ b/site/src/pages/WorkspaceSettingsPage/WorkspaceSharingPage/WorkspaceSharingPageView.stories.tsx @@ -7,6 +7,7 @@ import { mockApiError, } from "testHelpers/entities"; import type { Meta, StoryObj } from "@storybook/react-vite"; +import { getWorkspaceSharingSettingsKey } from "api/queries/organizations"; import type { WorkspaceACL, WorkspaceGroup, @@ -63,6 +64,14 @@ const aclWithUsersAndGroups: WorkspaceACL = { const meta: Meta = { title: "pages/WorkspaceSharingPageView", component: WorkspaceSharingPageView, + parameters: { + queries: [ + { + key: getWorkspaceSharingSettingsKey(MockWorkspace.organization_id), + data: { sharing_disabled: false }, + }, + ], + }, args: { workspace: MockWorkspace, workspaceACL: emptyACL, diff --git a/site/src/pages/WorkspaceSettingsPage/WorkspaceSharingPage/WorkspaceSharingPageView.tsx b/site/src/pages/WorkspaceSettingsPage/WorkspaceSharingPage/WorkspaceSharingPageView.tsx index 73a16d3976..7b8c93fc15 100644 --- a/site/src/pages/WorkspaceSettingsPage/WorkspaceSharingPage/WorkspaceSharingPageView.tsx +++ b/site/src/pages/WorkspaceSettingsPage/WorkspaceSharingPage/WorkspaceSharingPageView.tsx @@ -52,6 +52,7 @@ export const WorkspaceSharingPageView: FC = ({ }) => { return (