Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 097f739

Browse files
authored
feat: add organization-scoped permission checks to deployment settings (#14063)
* s/readAllUsers/viewAllUsers Other frontend variables use the `view` syntax. Arguably we should use `read` to match the backend, but `view` does seem more UI-like. * Check license for organizations All the checks now require both the experiment and license. I also renamed the variable canViewOrganizations everywhere for consistency. * Allow any auditor to view the audit log * Use fine-grained permissions on settings page Since in addition to deployment settings this page now also includes users, audit logs, groups, and orgs. Since you might not be able to fetch deployment values, move all the loaders to the individual pages instead of in the wrapping layout. * Add stories for organization members page Needed to break it out into a separate view to do this. * Add stories for multi-org sidebar * Remove multi-org check from management settings layout We only use this layout when multi-org is enabled, so no need to run the check a second time. * Add more stories for deployment dropdown
1 parent 0ad5f60 commit 097f739

29 files changed

+1250
-645
lines changed

site/src/api/queries/organizations.ts

+61
Original file line numberDiff line numberDiff line change
@@ -120,3 +120,64 @@ export const provisionerDaemons = (organization: string) => {
120120
queryFn: () => API.getProvisionerDaemonsByOrganization(organization),
121121
};
122122
};
123+
124+
/**
125+
* Fetch permissions for a single organization.
126+
*
127+
* If the ID is undefined, return a disabled query.
128+
*/
129+
export const organizationPermissions = (organizationId: string | undefined) => {
130+
if (!organizationId) {
131+
return { enabled: false };
132+
}
133+
return {
134+
queryKey: ["organization", organizationId, "permissions"],
135+
queryFn: () =>
136+
API.checkAuthorization({
137+
checks: {
138+
viewMembers: {
139+
object: {
140+
resource_type: "organization_member",
141+
organization_id: organizationId,
142+
},
143+
action: "read",
144+
},
145+
editMembers: {
146+
object: {
147+
resource_type: "organization_member",
148+
organization_id: organizationId,
149+
},
150+
action: "update",
151+
},
152+
createGroup: {
153+
object: {
154+
resource_type: "group",
155+
organization_id: organizationId,
156+
},
157+
action: "create",
158+
},
159+
viewGroups: {
160+
object: {
161+
resource_type: "group",
162+
organization_id: organizationId,
163+
},
164+
action: "read",
165+
},
166+
editOrganization: {
167+
object: {
168+
resource_type: "organization",
169+
organization_id: organizationId,
170+
},
171+
action: "update",
172+
},
173+
auditOrganization: {
174+
object: {
175+
resource_type: "audit_log",
176+
organization_id: organizationId,
177+
},
178+
action: "read",
179+
},
180+
},
181+
}),
182+
};
183+
};

site/src/contexts/auth/permissions.tsx

+46-8
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,26 @@
11
export const checks = {
2-
readAllUsers: "readAllUsers",
2+
viewAllUsers: "viewAllUsers",
33
updateUsers: "updateUsers",
44
createUser: "createUser",
55
createTemplates: "createTemplates",
66
updateTemplates: "updateTemplates",
77
deleteTemplates: "deleteTemplates",
8-
viewAuditLog: "viewAuditLog",
8+
viewAnyAuditLog: "viewAnyAuditLog",
99
viewDeploymentValues: "viewDeploymentValues",
10-
createGroup: "createGroup",
10+
editDeploymentValues: "editDeploymentValues",
1111
viewUpdateCheck: "viewUpdateCheck",
1212
viewExternalAuthConfig: "viewExternalAuthConfig",
1313
viewDeploymentStats: "viewDeploymentStats",
1414
editWorkspaceProxies: "editWorkspaceProxies",
15+
createOrganization: "createOrganization",
16+
editAnyOrganization: "editAnyOrganization",
17+
viewAnyGroup: "viewAnyGroup",
18+
createGroup: "createGroup",
19+
viewAllLicenses: "viewAllLicenses",
1520
} as const;
1621

1722
export const permissionsToCheck = {
18-
[checks.readAllUsers]: {
23+
[checks.viewAllUsers]: {
1924
object: {
2025
resource_type: "user",
2126
},
@@ -51,9 +56,10 @@ export const permissionsToCheck = {
5156
},
5257
action: "delete",
5358
},
54-
[checks.viewAuditLog]: {
59+
[checks.viewAnyAuditLog]: {
5560
object: {
5661
resource_type: "audit_log",
62+
any_org: true,
5763
},
5864
action: "read",
5965
},
@@ -63,11 +69,11 @@ export const permissionsToCheck = {
6369
},
6470
action: "read",
6571
},
66-
[checks.createGroup]: {
72+
[checks.editDeploymentValues]: {
6773
object: {
68-
resource_type: "group",
74+
resource_type: "deployment_config",
6975
},
70-
action: "create",
76+
action: "update",
7177
},
7278
[checks.viewUpdateCheck]: {
7379
object: {
@@ -93,6 +99,38 @@ export const permissionsToCheck = {
9399
},
94100
action: "create",
95101
},
102+
[checks.createOrganization]: {
103+
object: {
104+
resource_type: "organization",
105+
},
106+
action: "create",
107+
},
108+
[checks.editAnyOrganization]: {
109+
object: {
110+
resource_type: "organization",
111+
any_org: true,
112+
},
113+
action: "update",
114+
},
115+
[checks.viewAnyGroup]: {
116+
object: {
117+
resource_type: "group",
118+
org_id: "any",
119+
},
120+
action: "read",
121+
},
122+
[checks.createGroup]: {
123+
object: {
124+
resource_type: "group",
125+
},
126+
action: "create",
127+
},
128+
[checks.viewAllLicenses]: {
129+
object: {
130+
resource_type: "license",
131+
},
132+
action: "read",
133+
},
96134
} as const;
97135

98136
export type Permissions = Record<keyof typeof permissionsToCheck, boolean>;

site/src/modules/dashboard/Navbar/Navbar.tsx

+3-2
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,13 @@ export const Navbar: FC = () => {
1616
const { user: me, permissions, signOut } = useAuthenticated();
1717
const featureVisibility = useFeatureVisibility();
1818
const canViewAuditLog =
19-
featureVisibility["audit_log"] && Boolean(permissions.viewAuditLog);
19+
featureVisibility.audit_log && Boolean(permissions.viewAnyAuditLog);
2020
const canViewDeployment = Boolean(permissions.viewDeploymentValues);
2121
const canViewOrganizations =
22+
Boolean(permissions.editAnyOrganization) &&
2223
featureVisibility.multiple_organizations &&
2324
experiments.includes("multi-organization");
24-
const canViewAllUsers = Boolean(permissions.readAllUsers);
25+
const canViewAllUsers = Boolean(permissions.viewAllUsers);
2526
const proxyContextValue = useProxy();
2627
const canViewHealth = canViewDeployment;
2728

site/src/modules/dashboard/Navbar/NavbarView.stories.tsx

+41-3
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type { Meta, StoryObj } from "@storybook/react";
2+
import { within, userEvent } from "@storybook/test";
23
import { chromaticWithTablet } from "testHelpers/chromatic";
34
import { MockUser, MockUser2 } from "testHelpers/entities";
45
import { withDashboardProvider } from "testHelpers/storybook";
@@ -10,26 +11,63 @@ const meta: Meta<typeof NavbarView> = {
1011
component: NavbarView,
1112
args: {
1213
user: MockUser,
14+
canViewAllUsers: true,
1315
canViewAuditLog: true,
1416
canViewDeployment: true,
15-
canViewAllUsers: true,
1617
canViewHealth: true,
18+
canViewOrganizations: true,
1719
},
1820
decorators: [withDashboardProvider],
1921
};
2022

2123
export default meta;
2224
type Story = StoryObj<typeof NavbarView>;
2325

24-
export const ForAdmin: Story = {};
26+
export const ForAdmin: Story = {
27+
play: async ({ canvasElement }) => {
28+
const canvas = within(canvasElement);
29+
await userEvent.click(canvas.getByRole("button", { name: "Deployment" }));
30+
},
31+
};
32+
33+
export const ForAuditor: Story = {
34+
args: {
35+
user: MockUser2,
36+
canViewAllUsers: false,
37+
canViewAuditLog: true,
38+
canViewDeployment: false,
39+
canViewHealth: false,
40+
canViewOrganizations: false,
41+
},
42+
play: async ({ canvasElement }) => {
43+
const canvas = within(canvasElement);
44+
await userEvent.click(canvas.getByRole("button", { name: "Deployment" }));
45+
},
46+
};
47+
48+
export const ForOrgAdmin: Story = {
49+
args: {
50+
user: MockUser2,
51+
canViewAllUsers: false,
52+
canViewAuditLog: true,
53+
canViewDeployment: false,
54+
canViewHealth: false,
55+
canViewOrganizations: true,
56+
},
57+
play: async ({ canvasElement }) => {
58+
const canvas = within(canvasElement);
59+
await userEvent.click(canvas.getByRole("button", { name: "Deployment" }));
60+
},
61+
};
2562

2663
export const ForMember: Story = {
2764
args: {
2865
user: MockUser2,
66+
canViewAllUsers: false,
2967
canViewAuditLog: false,
3068
canViewDeployment: false,
31-
canViewAllUsers: false,
3269
canViewHealth: false,
70+
canViewOrganizations: false,
3371
},
3472
};
3573

site/src/pages/AuditPage/AuditPage.tsx

+7-6
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,9 @@ import {
1717
import { AuditPageView } from "./AuditPageView";
1818

1919
const AuditPage: FC = () => {
20-
const { audit_log: isAuditLogVisible } = useFeatureVisibility();
20+
const feats = useFeatureVisibility();
2121
const { experiments } = useDashboard();
2222
const location = useLocation();
23-
const isMultiOrg = experiments.includes("multi-organization");
2423

2524
/**
2625
* There is an implicit link between auditsQuery and filter via the
@@ -75,7 +74,9 @@ const AuditPage: FC = () => {
7574
// TODO: Once multi-org is stable, we should place this redirect into the
7675
// router directly, if we still need to maintain it (for users who are
7776
// typing the old URL manually or have it bookmarked).
78-
if (isMultiOrg && location.pathname !== "/deployment/audit") {
77+
const canViewOrganizations =
78+
feats.multiple_organizations && experiments.includes("multi-organization");
79+
if (canViewOrganizations && location.pathname !== "/deployment/audit") {
7980
return <Navigate to={`/deployment/audit${location.search}`} replace />;
8081
}
8182

@@ -88,18 +89,18 @@ const AuditPage: FC = () => {
8889
<AuditPageView
8990
auditLogs={auditsQuery.data?.audit_logs}
9091
isNonInitialPage={isNonInitialPage(searchParams)}
91-
isAuditLogVisible={isAuditLogVisible}
92+
isAuditLogVisible={feats.audit_log}
9293
auditsQuery={auditsQuery}
9394
error={auditsQuery.error}
94-
showOrgDetails={isMultiOrg}
95+
showOrgDetails={canViewOrganizations}
9596
filterProps={{
9697
filter,
9798
error: auditsQuery.error,
9899
menus: {
99100
user: userMenu,
100101
action: actionMenu,
101102
resourceType: resourceTypeMenu,
102-
organization: isMultiOrg ? organizationsMenu : undefined,
103+
organization: canViewOrganizations ? organizationsMenu : undefined,
103104
},
104105
}}
105106
/>

site/src/pages/DeploySettingsPage/DeploySettingsLayout.tsx

+15-16
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,12 @@ import { Stack } from "components/Stack/Stack";
99
import { useAuthenticated } from "contexts/auth/RequireAuth";
1010
import { RequirePermission } from "contexts/auth/RequirePermission";
1111
import { useDashboard } from "modules/dashboard/useDashboard";
12+
import { useFeatureVisibility } from "modules/dashboard/useFeatureVisibility";
1213
import { ManagementSettingsLayout } from "pages/ManagementSettingsPage/ManagementSettingsLayout";
1314
import { Sidebar } from "./Sidebar";
1415

1516
type DeploySettingsContextValue = {
16-
deploymentValues: DeploymentConfig;
17+
deploymentValues: DeploymentConfig | undefined;
1718
};
1819

1920
export const DeploySettingsContext = createContext<
@@ -33,9 +34,11 @@ export const useDeploySettings = (): DeploySettingsContextValue => {
3334
export const DeploySettingsLayout: FC = () => {
3435
const { experiments } = useDashboard();
3536

36-
const multiOrgExperimentEnabled = experiments.includes("multi-organization");
37+
const feats = useFeatureVisibility();
38+
const canViewOrganizations =
39+
feats.multiple_organizations && experiments.includes("multi-organization");
3740

38-
return multiOrgExperimentEnabled ? (
41+
return canViewOrganizations ? (
3942
<ManagementSettingsLayout />
4043
) : (
4144
<DeploySettingsLayoutInner />
@@ -52,19 +55,15 @@ const DeploySettingsLayoutInner: FC = () => {
5255
<Stack css={{ padding: "48px 0" }} direction="row" spacing={6}>
5356
<Sidebar />
5457
<main css={{ maxWidth: 800, width: "100%" }}>
55-
{deploymentConfigQuery.data ? (
56-
<DeploySettingsContext.Provider
57-
value={{
58-
deploymentValues: deploymentConfigQuery.data,
59-
}}
60-
>
61-
<Suspense fallback={<Loader />}>
62-
<Outlet />
63-
</Suspense>
64-
</DeploySettingsContext.Provider>
65-
) : (
66-
<Loader />
67-
)}
58+
<DeploySettingsContext.Provider
59+
value={{
60+
deploymentValues: deploymentConfigQuery.data,
61+
}}
62+
>
63+
<Suspense fallback={<Loader />}>
64+
<Outlet />
65+
</Suspense>
66+
</DeploySettingsContext.Provider>
6867
</main>
6968
</Stack>
7069
</Margins>

site/src/pages/DeploySettingsPage/ExternalAuthSettingsPage/ExternalAuthSettingsPage.tsx

+6-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type { FC } from "react";
22
import { Helmet } from "react-helmet-async";
3+
import { Loader } from "components/Loader/Loader";
34
import { pageTitle } from "utils/page";
45
import { useDeploySettings } from "../DeploySettingsLayout";
56
import { ExternalAuthSettingsPageView } from "./ExternalAuthSettingsPageView";
@@ -13,7 +14,11 @@ const ExternalAuthSettingsPage: FC = () => {
1314
<title>{pageTitle("External Authentication Settings")}</title>
1415
</Helmet>
1516

16-
<ExternalAuthSettingsPageView config={deploymentValues.config} />
17+
{deploymentValues ? (
18+
<ExternalAuthSettingsPageView config={deploymentValues.config} />
19+
) : (
20+
<Loader />
21+
)}
1722
</>
1823
);
1924
};

0 commit comments

Comments
 (0)