From 44869aba098b3a28776412502651626ee4ac184b Mon Sep 17 00:00:00 2001 From: McKayla Washburn Date: Mon, 5 Aug 2024 19:12:09 +0000 Subject: [PATCH 1/3] fix: only show valid organizations in `CreateTemplateForm` hide any organizations that the user might have view permissions for, but not permission to create a template --- .../OrganizationAutocomplete.tsx | 30 +++++++++++++++++-- site/src/contexts/auth/permissions.tsx | 1 + .../CreateTemplatePage/CreateTemplateForm.tsx | 4 +++ 3 files changed, 33 insertions(+), 2 deletions(-) diff --git a/site/src/components/OrganizationAutocomplete/OrganizationAutocomplete.tsx b/site/src/components/OrganizationAutocomplete/OrganizationAutocomplete.tsx index 2bbf73502a6c6..3b3e78d2ddb6d 100644 --- a/site/src/components/OrganizationAutocomplete/OrganizationAutocomplete.tsx +++ b/site/src/components/OrganizationAutocomplete/OrganizationAutocomplete.tsx @@ -9,8 +9,9 @@ import { useState, } from "react"; import { useQuery } from "react-query"; +import { checkAuthorization } from "api/queries/authCheck"; import { organizations } from "api/queries/organizations"; -import type { Organization } from "api/typesGenerated"; +import type { AuthorizationCheck, Organization } from "api/typesGenerated"; import { Avatar } from "components/Avatar/Avatar"; import { AvatarData } from "components/AvatarData/AvatarData"; import { useDebouncedFunction } from "hooks/debounce"; @@ -22,6 +23,7 @@ export type OrganizationAutocompleteProps = { className?: string; size?: ComponentProps["size"]; required?: boolean; + check?: AuthorizationCheck; }; export const OrganizationAutocomplete: FC = ({ @@ -31,6 +33,7 @@ export const OrganizationAutocomplete: FC = ({ className, size = "small", required, + check, }) => { const [autoComplete, setAutoComplete] = useState<{ value: string; @@ -41,6 +44,22 @@ export const OrganizationAutocomplete: FC = ({ }); const organizationsQuery = useQuery(organizations()); + const permissionsQuery = useQuery( + check && organizationsQuery.data + ? checkAuthorization({ + checks: Object.fromEntries( + organizationsQuery.data.map((org) => [ + org.id, + { + ...check, + object: { ...check.object, organization_id: org.id }, + }, + ]), + ), + }) + : { enabled: false }, + ); + const { debounced: debouncedInputOnChange } = useDebouncedFunction( (event: ChangeEvent) => { setAutoComplete((state) => ({ @@ -51,11 +70,18 @@ export const OrganizationAutocomplete: FC = ({ 750, ); + // If an authorization check was provided, filter the organizations based on + // the results of that check. + let options = organizationsQuery.data ?? []; + if (check && permissionsQuery.data) { + options = options.filter((org) => permissionsQuery.data[org.id]); + } + return ( = (props) => { void form.setFieldValue("organization", newValue?.name || ""); }} size="medium" + check={{ + object: { resource_type: "template" }, + action: "create", + }} /> )} From bb70b9a899c8571692e860920b46ae1be943fe97 Mon Sep 17 00:00:00 2001 From: McKayla Washburn Date: Mon, 5 Aug 2024 19:16:11 +0000 Subject: [PATCH 2/3] =?UTF-8?q?=F0=9F=A7=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../OrganizationAutocomplete/OrganizationAutocomplete.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/site/src/components/OrganizationAutocomplete/OrganizationAutocomplete.tsx b/site/src/components/OrganizationAutocomplete/OrganizationAutocomplete.tsx index 3b3e78d2ddb6d..26292d61b2010 100644 --- a/site/src/components/OrganizationAutocomplete/OrganizationAutocomplete.tsx +++ b/site/src/components/OrganizationAutocomplete/OrganizationAutocomplete.tsx @@ -73,8 +73,10 @@ export const OrganizationAutocomplete: FC = ({ // If an authorization check was provided, filter the organizations based on // the results of that check. let options = organizationsQuery.data ?? []; - if (check && permissionsQuery.data) { - options = options.filter((org) => permissionsQuery.data[org.id]); + if (check) { + options = permissionsQuery.data + ? options.filter((org) => permissionsQuery.data[org.id]) + : []; } return ( From 37b84eb0992e7ddc58254255f64dba640045a2bb Mon Sep 17 00:00:00 2001 From: McKayla Washburn Date: Mon, 5 Aug 2024 20:18:51 +0000 Subject: [PATCH 3/3] fix storybook --- .../CreateTemplateForm.stories.tsx | 65 +++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/site/src/pages/CreateTemplatePage/CreateTemplateForm.stories.tsx b/site/src/pages/CreateTemplatePage/CreateTemplateForm.stories.tsx index b6da0e8f127a1..61a0f09a3b647 100644 --- a/site/src/pages/CreateTemplatePage/CreateTemplateForm.stories.tsx +++ b/site/src/pages/CreateTemplatePage/CreateTemplateForm.stories.tsx @@ -61,6 +61,18 @@ export const StarterTemplateWithOrgPicker: Story = { }, }; +const canCreateTemplate = (organizationId: string) => { + return { + [organizationId]: { + object: { + resource_type: "template", + organization_id: organizationId, + }, + action: "create", + }, + }; +}; + export const StarterTemplateWithProvisionerWarning: Story = { parameters: { queries: [ @@ -68,6 +80,21 @@ export const StarterTemplateWithProvisionerWarning: Story = { key: organizationsKey, data: [MockDefaultOrganization, MockOrganization2], }, + { + key: [ + "authorization", + { + checks: { + ...canCreateTemplate(MockDefaultOrganization.id), + ...canCreateTemplate(MockOrganization2.id), + }, + }, + ], + data: { + [MockDefaultOrganization.id]: true, + [MockOrganization2.id]: true, + }, + }, { key: getProvisionerDaemonsKey(MockOrganization2.id), data: [], @@ -86,6 +113,44 @@ export const StarterTemplateWithProvisionerWarning: Story = { }, }; +export const StarterTemplatePermissionsCheck: Story = { + parameters: { + queries: [ + { + key: organizationsKey, + data: [MockDefaultOrganization, MockOrganization2], + }, + { + key: [ + "authorization", + { + checks: { + ...canCreateTemplate(MockDefaultOrganization.id), + ...canCreateTemplate(MockOrganization2.id), + }, + }, + ], + data: { + [MockDefaultOrganization.id]: true, + [MockOrganization2.id]: false, + }, + }, + { + key: getProvisionerDaemonsKey(MockOrganization2.id), + data: [], + }, + ], + }, + args: { + ...StarterTemplate.args, + showOrganizationPicker: true, + }, + play: async () => { + const organizationPicker = screen.getByPlaceholderText("Organization name"); + await userEvent.click(organizationPicker); + }, +}; + export const DuplicateTemplateWithVariables: Story = { args: { copiedTemplate: MockTemplate,