Skip to content

feat: add organization-scoped permission checks to deployment settings #14063

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 20 commits into from
Aug 6, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
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.
  • Loading branch information
code-asher committed Aug 1, 2024
commit 6dba3216bd82a20e54766c8caecd321e6c879df0
35 changes: 35 additions & 0 deletions site/src/api/queries/organizations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,13 +117,48 @@ export const organizationPermissions = (organizationId: string) => {
queryFn: () =>
API.checkAuthorization({
checks: {
viewUsers: {
object: {
resource_type: "user",
organization_id: organizationId,
},
action: "read",
},
editUsers: {
object: {
resource_type: "user",
organization_id: organizationId,
},
action: "update",
},
createGroup: {
object: {
resource_type: "group",
organization_id: organizationId,
},
action: "create",
},
viewGroups: {
object: {
resource_type: "group",
organization_id: organizationId,
},
action: "read",
},
editOrganization: {
object: {
resource_type: "organization",
organization_id: organizationId,
},
action: "update",
},
auditOrganization: {
object: {
resource_type: "audit_log",
organization_id: organizationId,
},
action: "read",
},
},
}),
};
Expand Down
30 changes: 30 additions & 0 deletions site/src/contexts/auth/permissions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,14 @@ export const checks = {
deleteTemplates: "deleteTemplates",
viewAnyAuditLog: "viewAnyAuditLog",
viewDeploymentValues: "viewDeploymentValues",
editDeploymentValues: "editDeploymentValues",
viewUpdateCheck: "viewUpdateCheck",
viewExternalAuthConfig: "viewExternalAuthConfig",
viewDeploymentStats: "viewDeploymentStats",
editWorkspaceProxies: "editWorkspaceProxies",
createOrganization: "createOrganization",
editAnyOrganization: "editAnyOrganization",
viewAnyGroup: "viewAnyGroup",
} as const;

export const permissionsToCheck = {
Expand Down Expand Up @@ -63,6 +67,12 @@ export const permissionsToCheck = {
},
action: "read",
},
[checks.editDeploymentValues]: {
object: {
resource_type: "deployment_config",
},
action: "update",
},
[checks.viewUpdateCheck]: {
object: {
resource_type: "deployment_config",
Expand All @@ -87,6 +97,26 @@ export const permissionsToCheck = {
},
action: "create",
},
[checks.createOrganization]: {
object: {
resource_type: "organization",
},
action: "create",
},
[checks.editAnyOrganization]: {
object: {
resource_type: "organization",
any_org: true,
},
action: "update",
},
[checks.viewAnyGroup]: {
object: {
resource_type: "group",
org_id: "any",
},
action: "read",
},
} as const;

export type Permissions = Record<keyof typeof permissionsToCheck, boolean>;
1 change: 1 addition & 0 deletions site/src/modules/dashboard/Navbar/Navbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export const Navbar: FC = () => {
featureVisibility.audit_log && Boolean(permissions.viewAnyAuditLog);
const canViewDeployment = Boolean(permissions.viewDeploymentValues);
const canViewOrganizations =
Boolean(permissions.editAnyOrganization) &&
featureVisibility.multiple_organizations &&
experiments.includes("multi-organization");
const canViewAllUsers = Boolean(permissions.viewAllUsers);
Expand Down
24 changes: 10 additions & 14 deletions site/src/pages/DeploySettingsPage/DeploySettingsLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { ManagementSettingsLayout } from "pages/ManagementSettingsPage/Managemen
import { Sidebar } from "./Sidebar";

type DeploySettingsContextValue = {
deploymentValues: DeploymentConfig;
deploymentValues: DeploymentConfig | undefined;
};

export const DeploySettingsContext = createContext<
Expand Down Expand Up @@ -55,19 +55,15 @@ const DeploySettingsLayoutInner: FC = () => {
<Stack css={{ padding: "48px 0" }} direction="row" spacing={6}>
<Sidebar />
<main css={{ maxWidth: 800, width: "100%" }}>
{deploymentConfigQuery.data ? (
<DeploySettingsContext.Provider
value={{
deploymentValues: deploymentConfigQuery.data,
}}
>
<Suspense fallback={<Loader />}>
<Outlet />
</Suspense>
</DeploySettingsContext.Provider>
) : (
<Loader />
)}
<DeploySettingsContext.Provider
value={{
deploymentValues: deploymentConfigQuery.data,
}}
>
<Suspense fallback={<Loader />}>
<Outlet />
</Suspense>
</DeploySettingsContext.Provider>
</main>
</Stack>
</Margins>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { FC } from "react";
import { Helmet } from "react-helmet-async";
import { Loader } from "components/Loader/Loader";
import { pageTitle } from "utils/page";
import { useDeploySettings } from "../DeploySettingsLayout";
import { ExternalAuthSettingsPageView } from "./ExternalAuthSettingsPageView";
Expand All @@ -13,7 +14,11 @@ const ExternalAuthSettingsPage: FC = () => {
<title>{pageTitle("External Authentication Settings")}</title>
</Helmet>

<ExternalAuthSettingsPageView config={deploymentValues.config} />
{deploymentValues ? (
<ExternalAuthSettingsPageView config={deploymentValues.config} />
) : (
<Loader />
)}
</>
);
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { useQuery } from "react-query";
import { deploymentDAUs } from "api/queries/deployment";
import { entitlements } from "api/queries/entitlements";
import { availableExperiments, experiments } from "api/queries/experiments";
import { Loader } from "components/Loader/Loader";
import { useEmbeddedMetadata } from "hooks/useEmbeddedMetadata";
import { pageTitle } from "utils/page";
import { useDeploySettings } from "../DeploySettingsLayout";
Expand All @@ -29,14 +30,18 @@ const GeneralSettingsPage: FC = () => {
<Helmet>
<title>{pageTitle("General Settings")}</title>
</Helmet>
<GeneralSettingsPageView
deploymentOptions={deploymentValues.options}
deploymentDAUs={deploymentDAUsQuery.data}
deploymentDAUsError={deploymentDAUsQuery.error}
entitlements={entitlementsQuery.data}
invalidExperiments={invalidExperiments}
safeExperiments={safeExperiments}
/>
{deploymentValues ? (
<GeneralSettingsPageView
deploymentOptions={deploymentValues.options}
deploymentDAUs={deploymentDAUsQuery.data}
deploymentDAUsError={deploymentDAUsQuery.error}
entitlements={entitlementsQuery.data}
invalidExperiments={invalidExperiments}
safeExperiments={safeExperiments}
/>
) : (
<Loader />
)}
</>
);
};
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { FC } from "react";
import { Helmet } from "react-helmet-async";
import { Loader } from "components/Loader/Loader";
import { pageTitle } from "utils/page";
import { useDeploySettings } from "../DeploySettingsLayout";
import { NetworkSettingsPageView } from "./NetworkSettingsPageView";
Expand All @@ -13,7 +14,11 @@ const NetworkSettingsPage: FC = () => {
<title>{pageTitle("Network Settings")}</title>
</Helmet>

<NetworkSettingsPageView options={deploymentValues.options} />
{deploymentValues ? (
<NetworkSettingsPageView options={deploymentValues.options} />
) : (
<Loader />
)}
</>
);
};
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { FC } from "react";
import { Helmet } from "react-helmet-async";
import { Loader } from "components/Loader/Loader";
import { useDashboard } from "modules/dashboard/useDashboard";
import { pageTitle } from "utils/page";
import { useDeploySettings } from "../DeploySettingsLayout";
Expand All @@ -15,10 +16,14 @@ const ObservabilitySettingsPage: FC = () => {
<title>{pageTitle("Observability Settings")}</title>
</Helmet>

<ObservabilitySettingsPageView
options={deploymentValues.options}
featureAuditLogEnabled={entitlements.features["audit_log"].enabled}
/>
{deploymentValues ? (
<ObservabilitySettingsPageView
options={deploymentValues.options}
featureAuditLogEnabled={entitlements.features["audit_log"].enabled}
/>
) : (
<Loader />
)}
</>
);
};
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { FC } from "react";
import { Helmet } from "react-helmet-async";
import { Loader } from "components/Loader/Loader";
import { useDashboard } from "modules/dashboard/useDashboard";
import { pageTitle } from "utils/page";
import { useDeploySettings } from "../DeploySettingsLayout";
Expand All @@ -15,12 +16,16 @@ const SecuritySettingsPage: FC = () => {
<title>{pageTitle("Security Settings")}</title>
</Helmet>

<SecuritySettingsPageView
options={deploymentValues.options}
featureBrowserOnlyEnabled={
entitlements.features["browser_only"].enabled
}
/>
{deploymentValues ? (
<SecuritySettingsPageView
options={deploymentValues.options}
featureBrowserOnlyEnabled={
entitlements.features["browser_only"].enabled
}
/>
) : (
<Loader />
)}
</>
);
};
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { FC } from "react";
import { Helmet } from "react-helmet-async";
import { Loader } from "components/Loader/Loader";
import { pageTitle } from "utils/page";
import { useDeploySettings } from "../DeploySettingsLayout";
import { UserAuthSettingsPageView } from "./UserAuthSettingsPageView";
Expand All @@ -13,7 +14,11 @@ const UserAuthSettingsPage: FC = () => {
<title>{pageTitle("User Authentication Settings")}</title>
</Helmet>

<UserAuthSettingsPageView options={deploymentValues.options} />
{deploymentValues ? (
<UserAuthSettingsPageView options={deploymentValues.options} />
) : (
<Loader />
)}
</>
);
};
Expand Down
61 changes: 34 additions & 27 deletions site/src/pages/ManagementSettingsPage/ManagementSettingsLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import { DeploySettingsContext } from "../DeploySettingsPage/DeploySettingsLayou
import { Sidebar } from "./Sidebar";

type OrganizationSettingsContextValue = {
organizations: Organization[];
organizations: Organization[] | undefined;
};

const OrganizationSettingsContext = createContext<
Expand All @@ -37,7 +37,14 @@ export const ManagementSettingsLayout: FC = () => {
const { permissions } = useAuthenticated();
const { experiments } = useDashboard();
const feats = useFeatureVisibility();
const deploymentConfigQuery = useQuery(deploymentConfig());
const deploymentConfigQuery = useQuery({
...deploymentConfig(),
// TODO: This is probably normally fine because we will not show links to
// pages that need this data, but if you manually visit the page you
// will see an endless loader when maybe we should show a "permission
// denied" error or at least a 404 instead.
enabled: permissions.viewDeploymentValues,
});
const organizationsQuery = useQuery(organizations());

const canViewOrganizations =
Expand All @@ -47,34 +54,34 @@ export const ManagementSettingsLayout: FC = () => {
return <NotFoundPage />;
}

// The deployment settings page also contains users, audit logs, groups and
// organizations, so this page must be visible if you can see any of these.
const canViewDeploymentSettingsPage =
permissions.viewDeploymentValues ||
permissions.viewAllUsers ||
permissions.editAnyOrganization ||
permissions.viewAnyAuditLog;

return (
<RequirePermission isFeatureVisible={permissions.viewDeploymentValues}>
<RequirePermission isFeatureVisible={canViewDeploymentSettingsPage}>
<Margins>
<Stack css={{ padding: "48px 0" }} direction="row" spacing={6}>
{organizationsQuery.data ? (
<OrganizationSettingsContext.Provider
value={{ organizations: organizationsQuery.data }}
>
<Sidebar />
<main css={{ width: "100%" }}>
{deploymentConfigQuery.data ? (
<DeploySettingsContext.Provider
value={{
deploymentValues: deploymentConfigQuery.data,
}}
>
<Suspense fallback={<Loader />}>
<Outlet />
</Suspense>
</DeploySettingsContext.Provider>
) : (
<Loader />
)}
</main>
</OrganizationSettingsContext.Provider>
) : (
<Loader />
)}
<OrganizationSettingsContext.Provider
value={{ organizations: organizationsQuery.data }}
>
<Sidebar />
<main css={{ width: "100%" }}>
<DeploySettingsContext.Provider
value={{
deploymentValues: deploymentConfigQuery.data,
}}
>
<Suspense fallback={<Loader />}>
<Outlet />
</Suspense>
</DeploySettingsContext.Provider>
</main>
</OrganizationSettingsContext.Provider>
</Stack>
</Margins>
</RequirePermission>
Expand Down
Loading