From e6955804152989657d126dba9cdac51f428a1150 Mon Sep 17 00:00:00 2001 From: Jaayden Halko Date: Mon, 10 Mar 2025 21:53:29 +0000 Subject: [PATCH 01/12] chore: hide workspace creation UI for users without permission --- site/src/modules/permissions/organizations.ts | 8 +++++++ .../src/pages/TemplatesPage/TemplatesPage.tsx | 12 +++++++++- .../pages/TemplatesPage/TemplatesPageView.tsx | 15 +++++++++--- .../pages/WorkspacesPage/WorkspacesPage.tsx | 24 +++++++++++++++++-- 4 files changed, 53 insertions(+), 6 deletions(-) diff --git a/site/src/modules/permissions/organizations.ts b/site/src/modules/permissions/organizations.ts index 0a7cb505c2a4b..79e7616e2c137 100644 --- a/site/src/modules/permissions/organizations.ts +++ b/site/src/modules/permissions/organizations.ts @@ -115,6 +115,14 @@ export const organizationPermissionChecks = (organizationId: string) => }, action: "update", }, + createWorkspaces: { + object: { + resource_type: "workspace", + organization_id: organizationId, + owner_id: "*", + }, + action: "create", + }, }) as const satisfies Record; /** diff --git a/site/src/pages/TemplatesPage/TemplatesPage.tsx b/site/src/pages/TemplatesPage/TemplatesPage.tsx index de09956d44d1d..11779c0f1629b 100644 --- a/site/src/pages/TemplatesPage/TemplatesPage.tsx +++ b/site/src/pages/TemplatesPage/TemplatesPage.tsx @@ -1,3 +1,4 @@ +import { organizationsPermissions } from "api/queries/organizations"; import { templateExamples, templates } from "api/queries/templates"; import { useFilter } from "components/Filter/Filter"; import { useAuthenticated } from "contexts/auth/RequireAuth"; @@ -25,7 +26,15 @@ export const TemplatesPage: FC = () => { ...templateExamples(), enabled: permissions.createTemplates, }); - const error = templatesQuery.error || examplesQuery.error; + + const orgPermissionsQuery = useQuery( + organizationsPermissions( + templatesQuery.data?.map((template) => template.organization_id), + ), + ); + + const error = + templatesQuery.error || examplesQuery.error || orgPermissionsQuery.error; return ( <> @@ -39,6 +48,7 @@ export const TemplatesPage: FC = () => { canCreateTemplates={permissions.createTemplates} examples={examplesQuery.data} templates={templatesQuery.data} + orgPermissions={orgPermissionsQuery.data} /> ); diff --git a/site/src/pages/TemplatesPage/TemplatesPageView.tsx b/site/src/pages/TemplatesPage/TemplatesPageView.tsx index 5d2a512980d8e..54c0da8bb3c12 100644 --- a/site/src/pages/TemplatesPage/TemplatesPageView.tsx +++ b/site/src/pages/TemplatesPage/TemplatesPageView.tsx @@ -39,6 +39,7 @@ import { } from "components/TableLoader/TableLoader"; import { useClickableTableRow } from "hooks/useClickableTableRow"; import { PlusIcon } from "lucide-react"; +import type { OrganizationPermissions } from "modules/management/organizationPermissions"; import { linkToTemplate, useLinks } from "modules/navigation"; import type { FC } from "react"; import { Link, useNavigate } from "react-router-dom"; @@ -87,9 +88,14 @@ const TemplateHelpTooltip: FC = () => { interface TemplateRowProps { showOrganizations: boolean; template: Template; + orgPermissions: Record | undefined; } -const TemplateRow: FC = ({ showOrganizations, template }) => { +const TemplateRow: FC = ({ + showOrganizations, + template, + orgPermissions, +}) => { const getLink = useLinks(); const templatePageLink = getLink( linkToTemplate(template.organization_name, template.name), @@ -153,7 +159,7 @@ const TemplateRow: FC = ({ showOrganizations, template }) => { {template.deprecated ? ( - ) : ( + ) : orgPermissions?.[template.organization_id]?.createWorkspaces ? ( = ({ showOrganizations, template }) => { > Create Workspace - )} + ) : null} ); @@ -180,6 +186,7 @@ export interface TemplatesPageViewProps { canCreateTemplates: boolean; examples: TemplateExample[] | undefined; templates: Template[] | undefined; + orgPermissions: Record | undefined; } export const TemplatesPageView: FC = ({ @@ -189,6 +196,7 @@ export const TemplatesPageView: FC = ({ canCreateTemplates, examples, templates, + orgPermissions, }) => { const isLoading = !templates; const isEmpty = templates && templates.length === 0; @@ -250,6 +258,7 @@ export const TemplatesPageView: FC = ({ key={template.id} showOrganizations={showOrganizations} template={template} + orgPermissions={orgPermissions} /> )) )} diff --git a/site/src/pages/WorkspacesPage/WorkspacesPage.tsx b/site/src/pages/WorkspacesPage/WorkspacesPage.tsx index e94ccbbd86605..d8a999710cf37 100644 --- a/site/src/pages/WorkspacesPage/WorkspacesPage.tsx +++ b/site/src/pages/WorkspacesPage/WorkspacesPage.tsx @@ -1,3 +1,4 @@ +import { organizationsPermissions } from "api/queries/organizations"; import { templates } from "api/queries/templates"; import type { Workspace } from "api/typesGenerated"; import { useFilter } from "components/Filter/Filter"; @@ -7,7 +8,7 @@ import { useEffectEvent } from "hooks/hookPolyfills"; import { usePagination } from "hooks/usePagination"; import { useDashboard } from "modules/dashboard/useDashboard"; import { useOrganizationsFilterMenu } from "modules/tableFiltering/options"; -import { type FC, useEffect, useState } from "react"; +import { type FC, useEffect, useMemo, useState } from "react"; import { Helmet } from "react-helmet-async"; import { useQuery } from "react-query"; import { useSearchParams } from "react-router-dom"; @@ -44,6 +45,25 @@ const WorkspacesPage: FC = () => { const templatesQuery = useQuery(templates()); + // Check if user can create workspaces in each organization + const orgPermissionsQuery = useQuery( + organizationsPermissions( + templatesQuery.data?.map((template) => template.organization_id), + ), + ); + + // Filter templates based on workspace creation permission + const filteredTemplates = useMemo(() => { + if (!templatesQuery.data || !orgPermissionsQuery.data) { + return templatesQuery.data; + } + + return templatesQuery.data.filter((template) => { + const orgPermission = orgPermissionsQuery.data[template.organization_id]; + return orgPermission?.createWorkspaces; + }); + }, [templatesQuery.data, orgPermissionsQuery.data]); + const filterProps = useWorkspacesFilter({ searchParamsResult, onFilterChange: () => pagination.goToPage(1), @@ -90,7 +110,7 @@ const WorkspacesPage: FC = () => { checkedWorkspaces={checkedWorkspaces} onCheckChange={setCheckedWorkspaces} canCheckWorkspaces={canCheckWorkspaces} - templates={templatesQuery.data} + templates={filteredTemplates} templatesFetchStatus={templatesQuery.status} workspaces={data?.workspaces} error={error} From 1da44737b8ad07918bb395698fe4e6b5d25449e2 Mon Sep 17 00:00:00 2001 From: Jaayden Halko Date: Mon, 10 Mar 2025 22:46:44 +0000 Subject: [PATCH 02/12] fix: fix import --- site/src/pages/TemplatesPage/TemplatesPageView.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/site/src/pages/TemplatesPage/TemplatesPageView.tsx b/site/src/pages/TemplatesPage/TemplatesPageView.tsx index 54c0da8bb3c12..0b2ede6f08536 100644 --- a/site/src/pages/TemplatesPage/TemplatesPageView.tsx +++ b/site/src/pages/TemplatesPage/TemplatesPageView.tsx @@ -39,7 +39,7 @@ import { } from "components/TableLoader/TableLoader"; import { useClickableTableRow } from "hooks/useClickableTableRow"; import { PlusIcon } from "lucide-react"; -import type { OrganizationPermissions } from "modules/management/organizationPermissions"; +import type { OrganizationPermissions } from "modules/permissions/organizations"; import { linkToTemplate, useLinks } from "modules/navigation"; import type { FC } from "react"; import { Link, useNavigate } from "react-router-dom"; From b9ec2489f0156cb7ec8400c0d6c3003143fa9563 Mon Sep 17 00:00:00 2001 From: Jaayden Halko Date: Tue, 11 Mar 2025 17:24:52 +0000 Subject: [PATCH 03/12] fix: update entities --- site/src/testHelpers/entities.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/site/src/testHelpers/entities.ts b/site/src/testHelpers/entities.ts index a298dea4ffd9d..344a2d0735f8d 100644 --- a/site/src/testHelpers/entities.ts +++ b/site/src/testHelpers/entities.ts @@ -2922,6 +2922,7 @@ export const MockOrganizationPermissions: OrganizationPermissions = { viewProvisionerJobs: true, viewIdpSyncSettings: true, editIdpSyncSettings: true, + createWorkspaces: true, }; export const MockNoOrganizationPermissions: OrganizationPermissions = { @@ -2940,6 +2941,7 @@ export const MockNoOrganizationPermissions: OrganizationPermissions = { viewProvisionerJobs: false, viewIdpSyncSettings: false, editIdpSyncSettings: false, + createWorkspaces: false, }; export const MockDeploymentConfig: DeploymentConfig = { From d5033d1a74fe02ac89b9cddb7546870183c5142d Mon Sep 17 00:00:00 2001 From: Jaayden Halko Date: Tue, 11 Mar 2025 18:15:58 +0000 Subject: [PATCH 04/12] fix: format --- site/src/pages/TemplatesPage/TemplatesPageView.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/site/src/pages/TemplatesPage/TemplatesPageView.tsx b/site/src/pages/TemplatesPage/TemplatesPageView.tsx index 0b2ede6f08536..2710765499b34 100644 --- a/site/src/pages/TemplatesPage/TemplatesPageView.tsx +++ b/site/src/pages/TemplatesPage/TemplatesPageView.tsx @@ -39,8 +39,8 @@ import { } from "components/TableLoader/TableLoader"; import { useClickableTableRow } from "hooks/useClickableTableRow"; import { PlusIcon } from "lucide-react"; -import type { OrganizationPermissions } from "modules/permissions/organizations"; import { linkToTemplate, useLinks } from "modules/navigation"; +import type { OrganizationPermissions } from "modules/permissions/organizations"; import type { FC } from "react"; import { Link, useNavigate } from "react-router-dom"; import { createDayString } from "utils/createDayString"; From ff77fe9886883b7e10d64ae1838b3092c25835ff Mon Sep 17 00:00:00 2001 From: Jaayden Halko Date: Wed, 26 Mar 2025 11:34:40 +0000 Subject: [PATCH 05/12] chore: hide create workspace button on template header --- site/src/pages/TemplatePage/TemplateLayout.tsx | 11 +++++++++-- site/src/pages/TemplatePage/TemplatePageHeader.tsx | 5 ++++- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/site/src/pages/TemplatePage/TemplateLayout.tsx b/site/src/pages/TemplatePage/TemplateLayout.tsx index cf1c3f84ddd55..68dccd0218ae0 100644 --- a/site/src/pages/TemplatePage/TemplateLayout.tsx +++ b/site/src/pages/TemplatePage/TemplateLayout.tsx @@ -1,4 +1,5 @@ import { API } from "api/api"; +import { organizationsPermissions } from "api/queries/organizations"; import type { AuthorizationRequest } from "api/typesGenerated"; import { ErrorAlert } from "components/Alert/ErrorAlert"; import { Loader } from "components/Loader/Loader"; @@ -77,6 +78,9 @@ export const TemplateLayout: FC = ({ queryKey: ["template", templateName], queryFn: () => fetchTemplate(organizationName, templateName), }); + const orgPermissionsQuery = useQuery( + organizationsPermissions([organizationName]), + ); const location = useLocation(); const paths = location.pathname.split("/"); const activeTab = paths.at(-1) === templateName ? "summary" : paths.at(-1)!; @@ -85,7 +89,7 @@ export const TemplateLayout: FC = ({ const shouldShowInsights = data?.permissions?.canUpdateTemplate || data?.permissions?.canReadInsights; - if (error) { + if (error || orgPermissionsQuery.error) { return (
@@ -93,16 +97,19 @@ export const TemplateLayout: FC = ({ ); } - if (isLoading || !data) { + if (isLoading || !data || !orgPermissionsQuery.data) { return ; } + const orgPermissions = orgPermissionsQuery.data?.[organizationName]; + return ( <> { navigate("/templates"); }} diff --git a/site/src/pages/TemplatePage/TemplatePageHeader.tsx b/site/src/pages/TemplatePage/TemplatePageHeader.tsx index 7bb1d9e54a4c2..761c40aa047fd 100644 --- a/site/src/pages/TemplatePage/TemplatePageHeader.tsx +++ b/site/src/pages/TemplatePage/TemplatePageHeader.tsx @@ -31,6 +31,7 @@ import { import { Pill } from "components/Pill/Pill"; import { Stack } from "components/Stack/Stack"; import { linkToTemplate, useLinks } from "modules/navigation"; +import type { OrganizationPermissions } from "modules/permissions/organizations"; import type { FC } from "react"; import { useQuery } from "react-query"; import { Link as RouterLink, useNavigate } from "react-router-dom"; @@ -158,6 +159,7 @@ export type TemplatePageHeaderProps = { template: Template; activeVersion: TemplateVersion; permissions: AuthorizationResponse; + orgPermissions: OrganizationPermissions; onDeleteTemplate: () => void; }; @@ -165,6 +167,7 @@ export const TemplatePageHeader: FC = ({ template, activeVersion, permissions, + orgPermissions, onDeleteTemplate, }) => { const getLink = useLinks(); @@ -177,7 +180,7 @@ export const TemplatePageHeader: FC = ({ - {!template.deprecated && ( + {!template.deprecated && orgPermissions.createWorkspaces && ( - )} + {!template.deprecated && + workspacePermissions.createWorkspaceForUser && ( + + )} {permissions.canUpdateTemplate && ( { enabled: permissions.createTemplates, }); - const orgPermissionsQuery = useQuery( - organizationsPermissions( + const workspacePermissionsQuery = useQuery( + workspacePermissionsByOrganization( templatesQuery.data?.map((template) => template.organization_id), ), ); const error = - templatesQuery.error || examplesQuery.error || orgPermissionsQuery.error; + templatesQuery.error || + examplesQuery.error || + workspacePermissionsQuery.error; return ( <> @@ -48,7 +50,7 @@ export const TemplatesPage: FC = () => { canCreateTemplates={permissions.createTemplates} examples={examplesQuery.data} templates={templatesQuery.data} - orgPermissions={orgPermissionsQuery.data} + workspacePermissions={workspacePermissionsQuery.data} /> ); diff --git a/site/src/pages/TemplatesPage/TemplatesPageView.tsx b/site/src/pages/TemplatesPage/TemplatesPageView.tsx index 2710765499b34..fbf3743043c08 100644 --- a/site/src/pages/TemplatesPage/TemplatesPageView.tsx +++ b/site/src/pages/TemplatesPage/TemplatesPageView.tsx @@ -40,7 +40,7 @@ import { import { useClickableTableRow } from "hooks/useClickableTableRow"; import { PlusIcon } from "lucide-react"; import { linkToTemplate, useLinks } from "modules/navigation"; -import type { OrganizationPermissions } from "modules/permissions/organizations"; +import type { WorkspacePermissions } from "modules/permissions/workspaces"; import type { FC } from "react"; import { Link, useNavigate } from "react-router-dom"; import { createDayString } from "utils/createDayString"; @@ -88,13 +88,13 @@ const TemplateHelpTooltip: FC = () => { interface TemplateRowProps { showOrganizations: boolean; template: Template; - orgPermissions: Record | undefined; + workspacePermissions: Record | undefined; } const TemplateRow: FC = ({ showOrganizations, template, - orgPermissions, + workspacePermissions, }) => { const getLink = useLinks(); const templatePageLink = getLink( @@ -159,7 +159,8 @@ const TemplateRow: FC = ({ {template.deprecated ? ( - ) : orgPermissions?.[template.organization_id]?.createWorkspaces ? ( + ) : workspacePermissions?.[template.organization_id] + ?.createWorkspaceForUser ? ( | undefined; + workspacePermissions: Record | undefined; } export const TemplatesPageView: FC = ({ @@ -196,7 +197,7 @@ export const TemplatesPageView: FC = ({ canCreateTemplates, examples, templates, - orgPermissions, + workspacePermissions, }) => { const isLoading = !templates; const isEmpty = templates && templates.length === 0; @@ -258,7 +259,7 @@ export const TemplatesPageView: FC = ({ key={template.id} showOrganizations={showOrganizations} template={template} - orgPermissions={orgPermissions} + workspacePermissions={workspacePermissions} /> )) )} diff --git a/site/src/pages/WorkspacesPage/WorkspacesPage.tsx b/site/src/pages/WorkspacesPage/WorkspacesPage.tsx index c22de18f4ed4d..62fb8c1132200 100644 --- a/site/src/pages/WorkspacesPage/WorkspacesPage.tsx +++ b/site/src/pages/WorkspacesPage/WorkspacesPage.tsx @@ -1,4 +1,4 @@ -import { organizationsPermissions } from "api/queries/organizations"; +import { workspacePermissionsByOrganization } from "api/queries/organizations"; import { templates } from "api/queries/templates"; import type { Workspace } from "api/typesGenerated"; import { useFilter } from "components/Filter/Filter"; @@ -46,7 +46,7 @@ const WorkspacesPage: FC = () => { const templatesQuery = useQuery(templates()); const orgPermissionsQuery = useQuery( - organizationsPermissions( + workspacePermissionsByOrganization( templatesQuery.data?.map((template) => template.organization_id), ), ); @@ -59,7 +59,7 @@ const WorkspacesPage: FC = () => { return templatesQuery.data.filter((template) => { const orgPermission = orgPermissionsQuery.data[template.organization_id]; - return orgPermission?.createWorkspaces; + return orgPermission?.createWorkspaceForUser; }); }, [templatesQuery.data, orgPermissionsQuery.data]); diff --git a/site/src/testHelpers/entities.ts b/site/src/testHelpers/entities.ts index 344a2d0735f8d..a298dea4ffd9d 100644 --- a/site/src/testHelpers/entities.ts +++ b/site/src/testHelpers/entities.ts @@ -2922,7 +2922,6 @@ export const MockOrganizationPermissions: OrganizationPermissions = { viewProvisionerJobs: true, viewIdpSyncSettings: true, editIdpSyncSettings: true, - createWorkspaces: true, }; export const MockNoOrganizationPermissions: OrganizationPermissions = { @@ -2941,7 +2940,6 @@ export const MockNoOrganizationPermissions: OrganizationPermissions = { viewProvisionerJobs: false, viewIdpSyncSettings: false, editIdpSyncSettings: false, - createWorkspaces: false, }; export const MockDeploymentConfig: DeploymentConfig = { From 3ecc9d9a3f1314e986280db003b240a0d247302f Mon Sep 17 00:00:00 2001 From: Jaayden Halko Date: Wed, 2 Apr 2025 10:14:18 +0000 Subject: [PATCH 12/12] fix: update storybook testsw --- .../TemplatePage/TemplatePageHeader.stories.tsx | 11 +++++++++++ .../TemplatesPage/TemplatesPageView.stories.tsx | 16 ++++++++++++++++ 2 files changed, 27 insertions(+) diff --git a/site/src/pages/TemplatePage/TemplatePageHeader.stories.tsx b/site/src/pages/TemplatePage/TemplatePageHeader.stories.tsx index e7bf7db389666..4acd28446631f 100644 --- a/site/src/pages/TemplatePage/TemplatePageHeader.stories.tsx +++ b/site/src/pages/TemplatePage/TemplatePageHeader.stories.tsx @@ -13,6 +13,9 @@ const meta: Meta = { permissions: { canUpdateTemplate: true, }, + workspacePermissions: { + createWorkspaceForUser: true, + }, }, }; @@ -29,6 +32,14 @@ export const CanNotUpdate: Story = { }, }; +export const CannotCreateWorkspace: Story = { + args: { + workspacePermissions: { + createWorkspaceForUser: false, + }, + }, +}; + export const Deprecated: Story = { args: { template: { diff --git a/site/src/pages/TemplatesPage/TemplatesPageView.stories.tsx b/site/src/pages/TemplatesPage/TemplatesPageView.stories.tsx index 7572f39b4b365..bed6b72c5d719 100644 --- a/site/src/pages/TemplatesPage/TemplatesPageView.stories.tsx +++ b/site/src/pages/TemplatesPage/TemplatesPageView.stories.tsx @@ -74,6 +74,11 @@ export const WithTemplates: Story = { }, ], examples: [], + workspacePermissions: { + [MockTemplate.organization_id]: { + createWorkspaceForUser: true, + }, + }, }, }; @@ -84,6 +89,17 @@ export const MultipleOrganizations: Story = { }, }; +export const CannotCreateWorkspaces: Story = { + args: { + ...WithTemplates.args, + workspacePermissions: { + [MockTemplate.organization_id]: { + createWorkspaceForUser: false, + }, + }, + }, +}; + export const WithFilteredAllTemplates: Story = { args: { ...WithTemplates.args,