From 55ed6535847c136718a4f1dc4e7958f3701e7130 Mon Sep 17 00:00:00 2001 From: Jaayden Halko Date: Thu, 3 Apr 2025 13:22:05 +0000 Subject: [PATCH 1/6] fix: fix permissions for workspace creation --- site/src/api/queries/organizations.ts | 3 +- site/src/components/Checkbox/Checkbox.tsx | 30 +++++++++++++++++++ site/src/modules/permissions/workspaces.ts | 6 ++-- .../CreateWorkspacePage.tsx | 2 +- .../CreateWorkspacePageView.stories.tsx | 2 +- .../CreateWorkspacePageView.tsx | 4 +-- .../src/pages/TemplatePage/TemplateLayout.tsx | 4 ++- .../TemplatePageHeader.stories.tsx | 4 +-- .../pages/TemplatePage/TemplatePageHeader.tsx | 2 +- .../src/pages/TemplatesPage/TemplatesPage.tsx | 3 +- .../TemplatesPageView.stories.tsx | 4 +-- .../pages/TemplatesPage/TemplatesPageView.tsx | 2 +- .../pages/WorkspacesPage/WorkspacesPage.tsx | 5 ++-- 13 files changed, 53 insertions(+), 18 deletions(-) create mode 100644 site/src/components/Checkbox/Checkbox.tsx diff --git a/site/src/api/queries/organizations.ts b/site/src/api/queries/organizations.ts index b0e25a985bd0f..4e58bc412c8b5 100644 --- a/site/src/api/queries/organizations.ts +++ b/site/src/api/queries/organizations.ts @@ -306,6 +306,7 @@ export const organizationsPermissions = ( export const workspacePermissionsByOrganization = ( organizationIds: string[] | undefined, + userId: string, ) => { if (!organizationIds) { return { enabled: false }; @@ -315,7 +316,7 @@ export const workspacePermissionsByOrganization = ( queryKey: ["workspaces", organizationIds.sort(), "permissions"], queryFn: async () => { const prefixedChecks = organizationIds.flatMap((orgId) => - Object.entries(workspacePermissionChecks(orgId)).map(([key, val]) => [ + Object.entries(workspacePermissionChecks(orgId, userId)).map(([key, val]) => [ `${orgId}.${key}`, val, ]), diff --git a/site/src/components/Checkbox/Checkbox.tsx b/site/src/components/Checkbox/Checkbox.tsx new file mode 100644 index 0000000000000..64cf894a86c49 --- /dev/null +++ b/site/src/components/Checkbox/Checkbox.tsx @@ -0,0 +1,30 @@ +"use client" + +import * as React from "react" +import * as CheckboxPrimitive from "@radix-ui/react-checkbox" +import { Check } from "lucide-react" + +import { cn } from "utils/cn" + +const Checkbox = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + + + +)) +Checkbox.displayName = CheckboxPrimitive.Root.displayName + +export { Checkbox } diff --git a/site/src/modules/permissions/workspaces.ts b/site/src/modules/permissions/workspaces.ts index 9ebb75d4790de..6567ee6d5e893 100644 --- a/site/src/modules/permissions/workspaces.ts +++ b/site/src/modules/permissions/workspaces.ts @@ -1,10 +1,10 @@ -export const workspacePermissionChecks = (organizationId: string) => +export const workspacePermissionChecks = (organizationId: string, userId: string) => ({ - createWorkspaceForUser: { + createWorkspace: { object: { resource_type: "workspace", organization_id: organizationId, - owner_id: "*", + owner_id: userId, }, action: "create", }, diff --git a/site/src/pages/CreateWorkspacePage/CreateWorkspacePage.tsx b/site/src/pages/CreateWorkspacePage/CreateWorkspacePage.tsx index 26f1808b83152..d15d651fd4b5d 100644 --- a/site/src/pages/CreateWorkspacePage/CreateWorkspacePage.tsx +++ b/site/src/pages/CreateWorkspacePage/CreateWorkspacePage.tsx @@ -67,7 +67,7 @@ const CreateWorkspacePage: FC = () => { const permissionsQuery = useQuery( templateQuery.data ? checkAuthorization({ - checks: workspacePermissionChecks(templateQuery.data.organization_id), + checks: workspacePermissionChecks(templateQuery.data.organization_id, me.id), }) : { enabled: false }, ); diff --git a/site/src/pages/CreateWorkspacePage/CreateWorkspacePageView.stories.tsx b/site/src/pages/CreateWorkspacePage/CreateWorkspacePageView.stories.tsx index 47d1198765452..12c8b9e2c6671 100644 --- a/site/src/pages/CreateWorkspacePage/CreateWorkspacePageView.stories.tsx +++ b/site/src/pages/CreateWorkspacePage/CreateWorkspacePageView.stories.tsx @@ -27,7 +27,7 @@ const meta: Meta = { hasAllRequiredExternalAuth: true, mode: "form", permissions: { - createWorkspaceForUser: true, + createWorkspace: true, }, onCancel: action("onCancel"), }, diff --git a/site/src/pages/CreateWorkspacePage/CreateWorkspacePageView.tsx b/site/src/pages/CreateWorkspacePage/CreateWorkspacePageView.tsx index 660580b5b80b8..8abbdc5f1a24a 100644 --- a/site/src/pages/CreateWorkspacePage/CreateWorkspacePageView.tsx +++ b/site/src/pages/CreateWorkspacePage/CreateWorkspacePageView.tsx @@ -256,7 +256,7 @@ export const CreateWorkspacePageView: FC = ({ = ({ - {permissions.createWorkspaceForUser && ( + {permissions.createWorkspace && ( { diff --git a/site/src/pages/TemplatePage/TemplateLayout.tsx b/site/src/pages/TemplatePage/TemplateLayout.tsx index 93d25d6f591db..68f70937b2c6c 100644 --- a/site/src/pages/TemplatePage/TemplateLayout.tsx +++ b/site/src/pages/TemplatePage/TemplateLayout.tsx @@ -6,6 +6,7 @@ import { Loader } from "components/Loader/Loader"; import { Margins } from "components/Margins/Margins"; import { TabLink, Tabs, TabsList } from "components/Tabs/Tabs"; import { workspacePermissionChecks } from "modules/permissions/workspaces"; +import { useAuthenticated } from "contexts/auth/RequireAuth"; import { type FC, type PropsWithChildren, @@ -73,6 +74,7 @@ export const TemplateLayout: FC = ({ children = , }) => { const navigate = useNavigate(); + const { user: me } = useAuthenticated(); const { organization: organizationName = "default", template: templateName } = useParams() as { organization?: string; template: string }; const { data, error, isLoading } = useQuery({ @@ -81,7 +83,7 @@ export const TemplateLayout: FC = ({ }); const workspacePermissionsQuery = useQuery( checkAuthorization({ - checks: workspacePermissionChecks(organizationName), + checks: workspacePermissionChecks(organizationName, me.id), }), ); diff --git a/site/src/pages/TemplatePage/TemplatePageHeader.stories.tsx b/site/src/pages/TemplatePage/TemplatePageHeader.stories.tsx index 4acd28446631f..b70dd4204b1ba 100644 --- a/site/src/pages/TemplatePage/TemplatePageHeader.stories.tsx +++ b/site/src/pages/TemplatePage/TemplatePageHeader.stories.tsx @@ -14,7 +14,7 @@ const meta: Meta = { canUpdateTemplate: true, }, workspacePermissions: { - createWorkspaceForUser: true, + createWorkspace: true, }, }, }; @@ -35,7 +35,7 @@ export const CanNotUpdate: Story = { export const CannotCreateWorkspace: Story = { args: { workspacePermissions: { - createWorkspaceForUser: false, + createWorkspace: false, }, }, }; diff --git a/site/src/pages/TemplatePage/TemplatePageHeader.tsx b/site/src/pages/TemplatePage/TemplatePageHeader.tsx index 1d70379e75f43..9dd072b8275b0 100644 --- a/site/src/pages/TemplatePage/TemplatePageHeader.tsx +++ b/site/src/pages/TemplatePage/TemplatePageHeader.tsx @@ -180,7 +180,7 @@ export const TemplatePageHeader: FC = ({ actions={ <> {!template.deprecated && - workspacePermissions.createWorkspaceForUser && ( + workspacePermissions.createWorkspace && ( - )} + {!template.deprecated && workspacePermissions.createWorkspace && ( + + )} {permissions.canUpdateTemplate && ( Date: Thu, 3 Apr 2025 13:33:18 +0000 Subject: [PATCH 3/6] fix: remove checkbox component --- site/src/components/Checkbox/Checkbox.tsx | 30 ----------------------- 1 file changed, 30 deletions(-) delete mode 100644 site/src/components/Checkbox/Checkbox.tsx diff --git a/site/src/components/Checkbox/Checkbox.tsx b/site/src/components/Checkbox/Checkbox.tsx deleted file mode 100644 index 64cf894a86c49..0000000000000 --- a/site/src/components/Checkbox/Checkbox.tsx +++ /dev/null @@ -1,30 +0,0 @@ -"use client" - -import * as React from "react" -import * as CheckboxPrimitive from "@radix-ui/react-checkbox" -import { Check } from "lucide-react" - -import { cn } from "utils/cn" - -const Checkbox = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - - - - - -)) -Checkbox.displayName = CheckboxPrimitive.Root.displayName - -export { Checkbox } From 49951a6e075df64dca1bd528bd12a56af1bac5fc Mon Sep 17 00:00:00 2001 From: Jaayden Halko Date: Thu, 3 Apr 2025 14:10:49 +0000 Subject: [PATCH 4/6] fix: revert create createWorkspaceForUser permissions --- .../CreateWorkspacePage/CreateWorkspacePage.tsx | 12 +++--------- .../CreateWorkspacePageView.stories.tsx | 2 +- .../CreateWorkspacePageView.tsx | 9 ++++----- .../src/pages/CreateWorkspacePage/permissions.ts | 16 ++++++++++++++++ 4 files changed, 24 insertions(+), 15 deletions(-) create mode 100644 site/src/pages/CreateWorkspacePage/permissions.ts diff --git a/site/src/pages/CreateWorkspacePage/CreateWorkspacePage.tsx b/site/src/pages/CreateWorkspacePage/CreateWorkspacePage.tsx index a53670afa2076..150a79bd69487 100644 --- a/site/src/pages/CreateWorkspacePage/CreateWorkspacePage.tsx +++ b/site/src/pages/CreateWorkspacePage/CreateWorkspacePage.tsx @@ -17,10 +17,6 @@ import { Loader } from "components/Loader/Loader"; import { useAuthenticated } from "contexts/auth/RequireAuth"; import { useEffectEvent } from "hooks/hookPolyfills"; import { useDashboard } from "modules/dashboard/useDashboard"; -import { - type WorkspacePermissions, - workspacePermissionChecks, -} from "modules/permissions/workspaces"; import { generateWorkspaceName } from "modules/workspaces/generateWorkspaceName"; import { type FC, useCallback, useEffect, useRef, useState } from "react"; import { Helmet } from "react-helmet-async"; @@ -30,6 +26,7 @@ import { pageTitle } from "utils/page"; import type { AutofillBuildParameter } from "utils/richParameters"; import { paramsUsedToCreateWorkspace } from "utils/workspace"; import { CreateWorkspacePageView } from "./CreateWorkspacePageView"; +import { type CreateWSPermissions, createWorkspaceChecks } from "./permissions"; export const createWorkspaceModes = ["form", "auto", "duplicate"] as const; export type CreateWorkspaceMode = (typeof createWorkspaceModes)[number]; @@ -67,10 +64,7 @@ const CreateWorkspacePage: FC = () => { const permissionsQuery = useQuery( templateQuery.data ? checkAuthorization({ - checks: workspacePermissionChecks( - templateQuery.data.organization_id, - me.id, - ), + checks: createWorkspaceChecks(templateQuery.data.organization_id), }) : { enabled: false }, ); @@ -212,7 +206,7 @@ const CreateWorkspacePage: FC = () => { externalAuthPollingState={externalAuthPollingState} startPollingExternalAuth={startPollingExternalAuth} hasAllRequiredExternalAuth={hasAllRequiredExternalAuth} - permissions={permissionsQuery.data as WorkspacePermissions} + permissions={permissionsQuery.data as CreateWSPermissions} parameters={realizedParameters as TemplateVersionParameter[]} presets={templateVersionPresetsQuery.data ?? []} creatingWorkspace={createWorkspaceMutation.isLoading} diff --git a/site/src/pages/CreateWorkspacePage/CreateWorkspacePageView.stories.tsx b/site/src/pages/CreateWorkspacePage/CreateWorkspacePageView.stories.tsx index 12c8b9e2c6671..47d1198765452 100644 --- a/site/src/pages/CreateWorkspacePage/CreateWorkspacePageView.stories.tsx +++ b/site/src/pages/CreateWorkspacePage/CreateWorkspacePageView.stories.tsx @@ -27,7 +27,7 @@ const meta: Meta = { hasAllRequiredExternalAuth: true, mode: "form", permissions: { - createWorkspace: true, + createWorkspaceForUser: true, }, onCancel: action("onCancel"), }, diff --git a/site/src/pages/CreateWorkspacePage/CreateWorkspacePageView.tsx b/site/src/pages/CreateWorkspacePage/CreateWorkspacePageView.tsx index 8abbdc5f1a24a..656e18563eb60 100644 --- a/site/src/pages/CreateWorkspacePage/CreateWorkspacePageView.tsx +++ b/site/src/pages/CreateWorkspacePage/CreateWorkspacePageView.tsx @@ -28,7 +28,6 @@ import { Stack } from "components/Stack/Stack"; import { Switch } from "components/Switch/Switch"; import { UserAutocomplete } from "components/UserAutocomplete/UserAutocomplete"; import { type FormikContextType, useFormik } from "formik"; -import type { WorkspacePermissions } from "modules/permissions/workspaces"; import { generateWorkspaceName } from "modules/workspaces/generateWorkspaceName"; import { type FC, useCallback, useEffect, useMemo, useState } from "react"; import { @@ -47,7 +46,7 @@ import type { ExternalAuthPollingState, } from "./CreateWorkspacePage"; import { ExternalAuthButton } from "./ExternalAuthButton"; - +import type { CreateWSPermissions } from "./permissions"; export const Language = { duplicationWarning: "Duplicating a workspace only copies its parameters. No state from the old workspace is copied over.", @@ -69,7 +68,7 @@ export interface CreateWorkspacePageViewProps { parameters: TypesGen.TemplateVersionParameter[]; autofillParameters: AutofillBuildParameter[]; presets: TypesGen.Preset[]; - permissions: WorkspacePermissions; + permissions: CreateWSPermissions; creatingWorkspace: boolean; onCancel: () => void; onSubmit: ( @@ -256,7 +255,7 @@ export const CreateWorkspacePageView: FC = ({ = ({ - {permissions.createWorkspace && ( + {permissions.createWorkspaceForUser && ( { diff --git a/site/src/pages/CreateWorkspacePage/permissions.ts b/site/src/pages/CreateWorkspacePage/permissions.ts new file mode 100644 index 0000000000000..07bad5031ddc2 --- /dev/null +++ b/site/src/pages/CreateWorkspacePage/permissions.ts @@ -0,0 +1,16 @@ +export const createWorkspaceChecks = (organizationId: string) => + ({ + createWorkspaceForUser: { + object: { + resource_type: "workspace", + organization_id: organizationId, + owner_id: "*", + }, + action: "create", + }, + }) as const; + +export type CreateWSPermissions = Record< + keyof ReturnType, + boolean +>; From 0b13269669355c242a2289323aef2b73b5163d19 Mon Sep 17 00:00:00 2001 From: Jaayden Halko Date: Thu, 3 Apr 2025 14:44:59 +0000 Subject: [PATCH 5/6] fix: get org id for permission check on template layout --- site/src/pages/TemplatePage/TemplateLayout.tsx | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/site/src/pages/TemplatePage/TemplateLayout.tsx b/site/src/pages/TemplatePage/TemplateLayout.tsx index 78b8822b6fa42..b8360506fdeeb 100644 --- a/site/src/pages/TemplatePage/TemplateLayout.tsx +++ b/site/src/pages/TemplatePage/TemplateLayout.tsx @@ -6,6 +6,7 @@ import { Loader } from "components/Loader/Loader"; import { Margins } from "components/Margins/Margins"; import { TabLink, Tabs, TabsList } from "components/Tabs/Tabs"; import { useAuthenticated } from "contexts/auth/RequireAuth"; +import { useDashboard } from "modules/dashboard/useDashboard"; import { workspacePermissionChecks } from "modules/permissions/workspaces"; import { type FC, @@ -77,15 +78,18 @@ export const TemplateLayout: FC = ({ const { user: me } = useAuthenticated(); const { organization: organizationName = "default", template: templateName } = useParams() as { organization?: string; template: string }; + const { organizations } = useDashboard(); + const organization = organizations.find((o) => o.name === organizationName); const { data, error, isLoading } = useQuery({ queryKey: ["template", templateName], queryFn: () => fetchTemplate(organizationName, templateName), }); - const workspacePermissionsQuery = useQuery( - checkAuthorization({ - checks: workspacePermissionChecks(organizationName, me.id), + const workspacePermissionsQuery = useQuery({ + ...checkAuthorization({ + checks: workspacePermissionChecks(organization?.id ?? "", me.id), }), - ); + enabled: organization !== undefined, + }); const location = useLocation(); const paths = location.pathname.split("/"); From d1f7eb0f53c2c6b0a8351b13d3627a38343ed700 Mon Sep 17 00:00:00 2001 From: Jaayden Halko Date: Thu, 3 Apr 2025 15:32:51 +0000 Subject: [PATCH 6/6] Revert "fix: get org id for permission check on template layout" This reverts commit 0b13269669355c242a2289323aef2b73b5163d19. --- site/src/pages/TemplatePage/TemplateLayout.tsx | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/site/src/pages/TemplatePage/TemplateLayout.tsx b/site/src/pages/TemplatePage/TemplateLayout.tsx index b8360506fdeeb..78b8822b6fa42 100644 --- a/site/src/pages/TemplatePage/TemplateLayout.tsx +++ b/site/src/pages/TemplatePage/TemplateLayout.tsx @@ -6,7 +6,6 @@ import { Loader } from "components/Loader/Loader"; import { Margins } from "components/Margins/Margins"; import { TabLink, Tabs, TabsList } from "components/Tabs/Tabs"; import { useAuthenticated } from "contexts/auth/RequireAuth"; -import { useDashboard } from "modules/dashboard/useDashboard"; import { workspacePermissionChecks } from "modules/permissions/workspaces"; import { type FC, @@ -78,18 +77,15 @@ export const TemplateLayout: FC = ({ const { user: me } = useAuthenticated(); const { organization: organizationName = "default", template: templateName } = useParams() as { organization?: string; template: string }; - const { organizations } = useDashboard(); - const organization = organizations.find((o) => o.name === organizationName); const { data, error, isLoading } = useQuery({ queryKey: ["template", templateName], queryFn: () => fetchTemplate(organizationName, templateName), }); - const workspacePermissionsQuery = useQuery({ - ...checkAuthorization({ - checks: workspacePermissionChecks(organization?.id ?? "", me.id), + const workspacePermissionsQuery = useQuery( + checkAuthorization({ + checks: workspacePermissionChecks(organizationName, me.id), }), - enabled: organization !== undefined, - }); + ); const location = useLocation(); const paths = location.pathname.split("/");