Skip to content
Merged
43 changes: 43 additions & 0 deletions site/src/api/queries/organizations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@ import {
type OrganizationPermissions,
organizationPermissionChecks,
} from "modules/permissions/organizations";
import {
type WorkspacePermissionName,
type WorkspacePermissions,
workspacePermissionChecks,
} from "modules/permissions/workspaces";
import type { QueryClient } from "react-query";
import { meKey } from "./users";

Expand Down Expand Up @@ -299,6 +304,44 @@ export const organizationsPermissions = (
};
};

export const workspacePermissionsByOrganization = (
organizationIds: string[] | undefined,
) => {
if (!organizationIds) {
return { enabled: false };
}

return {
queryKey: ["workspaces", organizationIds.sort(), "permissions"],
queryFn: async () => {
const prefixedChecks = organizationIds.flatMap((orgId) =>
Object.entries(workspacePermissionChecks(orgId)).map(([key, val]) => [
`${orgId}.${key}`,
val,
]),
);

const response = await API.checkAuthorization({
checks: Object.fromEntries(prefixedChecks),
});

return Object.entries(response).reduce(
(acc, [key, value]) => {
const index = key.indexOf(".");
const orgId = key.substring(0, index);
const perm = key.substring(index + 1);
if (!acc[orgId]) {
acc[orgId] = {};
}
acc[orgId][perm as WorkspacePermissionName] = value;
return acc;
},
{} as Record<string, Partial<WorkspacePermissions>>,
) as Record<string, WorkspacePermissions>;
},
};
};

export const getOrganizationIdpSyncClaimFieldValuesKey = (
organization: string,
field: string,
Expand Down
20 changes: 20 additions & 0 deletions site/src/modules/permissions/workspaces.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
export const workspacePermissionChecks = (organizationId: string) =>
({
createWorkspaceForUser: {
object: {
resource_type: "workspace",
organization_id: organizationId,
owner_id: "*",
},
action: "create",
},
}) as const;

export type WorkspacePermissions = Record<
keyof ReturnType<typeof workspacePermissionChecks>,
boolean
>;

export type WorkspacePermissionName = keyof ReturnType<
typeof workspacePermissionChecks
>;
9 changes: 6 additions & 3 deletions site/src/pages/CreateWorkspacePage/CreateWorkspacePage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ 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";
Expand All @@ -26,7 +30,6 @@ 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];
Expand Down Expand Up @@ -64,7 +67,7 @@ const CreateWorkspacePage: FC = () => {
const permissionsQuery = useQuery(
templateQuery.data
? checkAuthorization({
checks: createWorkspaceChecks(templateQuery.data.organization_id),
checks: workspacePermissionChecks(templateQuery.data.organization_id),
})
: { enabled: false },
);
Expand Down Expand Up @@ -206,7 +209,7 @@ const CreateWorkspacePage: FC = () => {
externalAuthPollingState={externalAuthPollingState}
startPollingExternalAuth={startPollingExternalAuth}
hasAllRequiredExternalAuth={hasAllRequiredExternalAuth}
permissions={permissionsQuery.data as CreateWSPermissions}
permissions={permissionsQuery.data as WorkspacePermissions}
parameters={realizedParameters as TemplateVersionParameter[]}
presets={templateVersionPresetsQuery.data ?? []}
creatingWorkspace={createWorkspaceMutation.isLoading}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ 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 {
Expand All @@ -46,7 +47,6 @@ import type {
ExternalAuthPollingState,
} from "./CreateWorkspacePage";
import { ExternalAuthButton } from "./ExternalAuthButton";
import type { CreateWSPermissions } from "./permissions";

export const Language = {
duplicationWarning:
Expand All @@ -69,7 +69,7 @@ export interface CreateWorkspacePageViewProps {
parameters: TypesGen.TemplateVersionParameter[];
autofillParameters: AutofillBuildParameter[];
presets: TypesGen.Preset[];
permissions: CreateWSPermissions;
permissions: WorkspacePermissions;
creatingWorkspace: boolean;
onCancel: () => void;
onSubmit: (
Expand Down
16 changes: 0 additions & 16 deletions site/src/pages/CreateWorkspacePage/permissions.ts

This file was deleted.

13 changes: 11 additions & 2 deletions site/src/pages/TemplatePage/TemplateLayout.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { API } from "api/api";
import { checkAuthorization } from "api/queries/authCheck";
import type { AuthorizationRequest } from "api/typesGenerated";
import { ErrorAlert } from "components/Alert/ErrorAlert";
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 {
type FC,
type PropsWithChildren,
Expand Down Expand Up @@ -77,6 +79,12 @@ export const TemplateLayout: FC<PropsWithChildren> = ({
queryKey: ["template", templateName],
queryFn: () => fetchTemplate(organizationName, templateName),
});
const workspacePermissionsQuery = useQuery(
checkAuthorization({
checks: workspacePermissionChecks(organizationName),
}),
);

const location = useLocation();
const paths = location.pathname.split("/");
const activeTab = paths.at(-1) === templateName ? "summary" : paths.at(-1)!;
Expand All @@ -85,15 +93,15 @@ export const TemplateLayout: FC<PropsWithChildren> = ({
const shouldShowInsights =
data?.permissions?.canUpdateTemplate || data?.permissions?.canReadInsights;

if (error) {
if (error || workspacePermissionsQuery.error) {
return (
<div css={{ margin: 16 }}>
<ErrorAlert error={error} />
</div>
);
}

if (isLoading || !data) {
if (isLoading || !data || !workspacePermissionsQuery.data) {
return <Loader />;
}

Expand All @@ -103,6 +111,7 @@ export const TemplateLayout: FC<PropsWithChildren> = ({
template={data.template}
activeVersion={data.activeVersion}
permissions={data.permissions}
workspacePermissions={workspacePermissionsQuery.data}
onDeleteTemplate={() => {
navigate("/templates");
}}
Expand Down
11 changes: 11 additions & 0 deletions site/src/pages/TemplatePage/TemplatePageHeader.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ const meta: Meta<typeof TemplatePageHeader> = {
permissions: {
canUpdateTemplate: true,
},
workspacePermissions: {
createWorkspaceForUser: true,
},
},
};

Expand All @@ -29,6 +32,14 @@ export const CanNotUpdate: Story = {
},
};

export const CannotCreateWorkspace: Story = {
args: {
workspacePermissions: {
createWorkspaceForUser: false,
},
},
};

export const Deprecated: Story = {
args: {
template: {
Expand Down
23 changes: 13 additions & 10 deletions site/src/pages/TemplatePage/TemplatePageHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -158,13 +158,15 @@ export type TemplatePageHeaderProps = {
template: Template;
activeVersion: TemplateVersion;
permissions: AuthorizationResponse;
workspacePermissions: AuthorizationResponse;
onDeleteTemplate: () => void;
};

export const TemplatePageHeader: FC<TemplatePageHeaderProps> = ({
template,
activeVersion,
permissions,
workspacePermissions,
onDeleteTemplate,
}) => {
const getLink = useLinks();
Expand All @@ -177,16 +179,17 @@ export const TemplatePageHeader: FC<TemplatePageHeaderProps> = ({
<PageHeader
actions={
<>
{!template.deprecated && (
<Button
variant="contained"
startIcon={<AddIcon />}
component={RouterLink}
to={`${templateLink}/workspace`}
>
Create Workspace
</Button>
)}
{!template.deprecated &&
workspacePermissions.createWorkspaceForUser && (
<Button
variant="contained"
startIcon={<AddIcon />}
component={RouterLink}
to={`${templateLink}/workspace`}
>
Create Workspace
</Button>
)}

{permissions.canUpdateTemplate && (
<TemplateMenu
Expand Down
14 changes: 13 additions & 1 deletion site/src/pages/TemplatesPage/TemplatesPage.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { workspacePermissionsByOrganization } from "api/queries/organizations";
import { templateExamples, templates } from "api/queries/templates";
import { useFilter } from "components/Filter/Filter";
import { useAuthenticated } from "contexts/auth/RequireAuth";
Expand Down Expand Up @@ -25,7 +26,17 @@ export const TemplatesPage: FC = () => {
...templateExamples(),
enabled: permissions.createTemplates,
});
const error = templatesQuery.error || examplesQuery.error;

const workspacePermissionsQuery = useQuery(
workspacePermissionsByOrganization(
templatesQuery.data?.map((template) => template.organization_id),
),
);

const error =
templatesQuery.error ||
examplesQuery.error ||
workspacePermissionsQuery.error;

return (
<>
Expand All @@ -39,6 +50,7 @@ export const TemplatesPage: FC = () => {
canCreateTemplates={permissions.createTemplates}
examples={examplesQuery.data}
templates={templatesQuery.data}
workspacePermissions={workspacePermissionsQuery.data}
/>
</>
);
Expand Down
16 changes: 16 additions & 0 deletions site/src/pages/TemplatesPage/TemplatesPageView.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,11 @@ export const WithTemplates: Story = {
},
],
examples: [],
workspacePermissions: {
[MockTemplate.organization_id]: {
createWorkspaceForUser: true,
},
},
},
};

Expand All @@ -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,
Expand Down
Loading
Loading