mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
fix: fix deployment settings navigation issues (#16780)
This commit is contained in:
@@ -0,0 +1,157 @@
|
||||
import { type Page, expect, test } from "@playwright/test";
|
||||
import {
|
||||
createOrganization,
|
||||
createOrganizationMember,
|
||||
setupApiCalls,
|
||||
} from "../api";
|
||||
import { license, users } from "../constants";
|
||||
import { login, requiresLicense } from "../helpers";
|
||||
import { beforeCoderTest } from "../hooks";
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
beforeCoderTest(page);
|
||||
});
|
||||
|
||||
type AdminSetting = (typeof adminSettings)[number];
|
||||
|
||||
const adminSettings = [
|
||||
"Deployment",
|
||||
"Organizations",
|
||||
"Healthcheck",
|
||||
"Audit Logs",
|
||||
] as const;
|
||||
|
||||
async function hasAccessToAdminSettings(page: Page, settings: AdminSetting[]) {
|
||||
// Organizations and Audit Logs both require a license to be visible
|
||||
const visibleSettings = license
|
||||
? settings
|
||||
: settings.filter((it) => it !== "Organizations" && it !== "Audit Logs");
|
||||
const adminSettingsButton = page.getByRole("button", {
|
||||
name: "Admin settings",
|
||||
});
|
||||
if (visibleSettings.length < 1) {
|
||||
await expect(adminSettingsButton).not.toBeVisible();
|
||||
return;
|
||||
}
|
||||
|
||||
await adminSettingsButton.click();
|
||||
|
||||
for (const name of visibleSettings) {
|
||||
await expect(page.getByText(name, { exact: true })).toBeVisible();
|
||||
}
|
||||
|
||||
const hiddenSettings = adminSettings.filter(
|
||||
(it) => !visibleSettings.includes(it),
|
||||
);
|
||||
for (const name of hiddenSettings) {
|
||||
await expect(page.getByText(name, { exact: true })).not.toBeVisible();
|
||||
}
|
||||
}
|
||||
|
||||
test.describe("roles admin settings access", () => {
|
||||
test("member cannot see admin settings", async ({ page }) => {
|
||||
await login(page, users.member);
|
||||
await page.goto("/", { waitUntil: "domcontentloaded" });
|
||||
|
||||
// None, "Admin settings" button should not be visible
|
||||
await hasAccessToAdminSettings(page, []);
|
||||
});
|
||||
|
||||
test("template admin can see admin settings", async ({ page }) => {
|
||||
await login(page, users.templateAdmin);
|
||||
await page.goto("/", { waitUntil: "domcontentloaded" });
|
||||
|
||||
await hasAccessToAdminSettings(page, ["Deployment", "Organizations"]);
|
||||
});
|
||||
|
||||
test("user admin can see admin settings", async ({ page }) => {
|
||||
await login(page, users.userAdmin);
|
||||
await page.goto("/", { waitUntil: "domcontentloaded" });
|
||||
|
||||
await hasAccessToAdminSettings(page, ["Deployment", "Organizations"]);
|
||||
});
|
||||
|
||||
test("auditor can see admin settings", async ({ page }) => {
|
||||
await login(page, users.auditor);
|
||||
await page.goto("/", { waitUntil: "domcontentloaded" });
|
||||
|
||||
await hasAccessToAdminSettings(page, [
|
||||
"Deployment",
|
||||
"Organizations",
|
||||
"Audit Logs",
|
||||
]);
|
||||
});
|
||||
|
||||
test("admin can see admin settings", async ({ page }) => {
|
||||
await login(page, users.admin);
|
||||
await page.goto("/", { waitUntil: "domcontentloaded" });
|
||||
|
||||
await hasAccessToAdminSettings(page, [
|
||||
"Deployment",
|
||||
"Organizations",
|
||||
"Healthcheck",
|
||||
"Audit Logs",
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("org-scoped roles admin settings access", () => {
|
||||
requiresLicense();
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await login(page);
|
||||
await setupApiCalls(page);
|
||||
});
|
||||
|
||||
test("org template admin can see admin settings", async ({ page }) => {
|
||||
const org = await createOrganization();
|
||||
const orgTemplateAdmin = await createOrganizationMember({
|
||||
[org.id]: ["organization-template-admin"],
|
||||
});
|
||||
|
||||
await login(page, orgTemplateAdmin);
|
||||
await page.goto("/", { waitUntil: "domcontentloaded" });
|
||||
|
||||
await hasAccessToAdminSettings(page, ["Organizations"]);
|
||||
});
|
||||
|
||||
test("org user admin can see admin settings", async ({ page }) => {
|
||||
const org = await createOrganization();
|
||||
const orgUserAdmin = await createOrganizationMember({
|
||||
[org.id]: ["organization-user-admin"],
|
||||
});
|
||||
|
||||
await login(page, orgUserAdmin);
|
||||
await page.goto("/", { waitUntil: "domcontentloaded" });
|
||||
|
||||
await hasAccessToAdminSettings(page, ["Deployment", "Organizations"]);
|
||||
});
|
||||
|
||||
test("org auditor can see admin settings", async ({ page }) => {
|
||||
const org = await createOrganization();
|
||||
const orgAuditor = await createOrganizationMember({
|
||||
[org.id]: ["organization-auditor"],
|
||||
});
|
||||
|
||||
await login(page, orgAuditor);
|
||||
await page.goto("/", { waitUntil: "domcontentloaded" });
|
||||
|
||||
await hasAccessToAdminSettings(page, ["Organizations", "Audit Logs"]);
|
||||
});
|
||||
|
||||
test("org admin can see admin settings", async ({ page }) => {
|
||||
const org = await createOrganization();
|
||||
const orgAdmin = await createOrganizationMember({
|
||||
[org.id]: ["organization-admin"],
|
||||
});
|
||||
|
||||
await login(page, orgAdmin);
|
||||
await page.goto("/", { waitUntil: "domcontentloaded" });
|
||||
|
||||
await hasAccessToAdminSettings(page, [
|
||||
"Deployment",
|
||||
"Organizations",
|
||||
"Audit Logs",
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -6,10 +6,8 @@ import type {
|
||||
UpdateOrganizationRequest,
|
||||
} from "api/typesGenerated";
|
||||
import {
|
||||
type AnyOrganizationPermissions,
|
||||
type OrganizationPermissionName,
|
||||
type OrganizationPermissions,
|
||||
anyOrganizationPermissionChecks,
|
||||
organizationPermissionChecks,
|
||||
} from "modules/management/organizationPermissions";
|
||||
import type { QueryClient } from "react-query";
|
||||
@@ -266,21 +264,6 @@ export const organizationsPermissions = (
|
||||
};
|
||||
};
|
||||
|
||||
export const anyOrganizationPermissionsKey = [
|
||||
"authorization",
|
||||
"anyOrganization",
|
||||
];
|
||||
|
||||
export const anyOrganizationPermissions = () => {
|
||||
return {
|
||||
queryKey: anyOrganizationPermissionsKey,
|
||||
queryFn: () =>
|
||||
API.checkAuthorization({
|
||||
checks: anyOrganizationPermissionChecks,
|
||||
}) as Promise<AnyOrganizationPermissions>,
|
||||
};
|
||||
};
|
||||
|
||||
export const getOrganizationIdpSyncClaimFieldValuesKey = (
|
||||
organization: string,
|
||||
field: string,
|
||||
|
||||
@@ -18,7 +18,7 @@ import {
|
||||
useContext,
|
||||
} from "react";
|
||||
import { useMutation, useQuery, useQueryClient } from "react-query";
|
||||
import { type Permissions, permissionsToCheck } from "./permissions";
|
||||
import { type Permissions, permissionChecks } from "./permissions";
|
||||
|
||||
export type AuthContextValue = {
|
||||
isLoading: boolean;
|
||||
@@ -50,13 +50,13 @@ export const AuthProvider: FC<PropsWithChildren> = ({ children }) => {
|
||||
const hasFirstUserQuery = useQuery(hasFirstUser(userMetadataState));
|
||||
|
||||
const permissionsQuery = useQuery({
|
||||
...checkAuthorization({ checks: permissionsToCheck }),
|
||||
...checkAuthorization({ checks: permissionChecks }),
|
||||
enabled: userQuery.data !== undefined,
|
||||
});
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
const loginMutation = useMutation(
|
||||
login({ checks: permissionsToCheck }, queryClient),
|
||||
login({ checks: permissionChecks }, queryClient),
|
||||
);
|
||||
|
||||
const logoutMutation = useMutation(logout(queryClient));
|
||||
|
||||
@@ -1,156 +1,205 @@
|
||||
import type { AuthorizationCheck } from "api/typesGenerated";
|
||||
|
||||
export const checks = {
|
||||
viewAllUsers: "viewAllUsers",
|
||||
updateUsers: "updateUsers",
|
||||
createUser: "createUser",
|
||||
createTemplates: "createTemplates",
|
||||
updateTemplates: "updateTemplates",
|
||||
deleteTemplates: "deleteTemplates",
|
||||
viewAnyAuditLog: "viewAnyAuditLog",
|
||||
viewDeploymentValues: "viewDeploymentValues",
|
||||
editDeploymentValues: "editDeploymentValues",
|
||||
viewUpdateCheck: "viewUpdateCheck",
|
||||
viewExternalAuthConfig: "viewExternalAuthConfig",
|
||||
viewDeploymentStats: "viewDeploymentStats",
|
||||
readWorkspaceProxies: "readWorkspaceProxies",
|
||||
editWorkspaceProxies: "editWorkspaceProxies",
|
||||
createOrganization: "createOrganization",
|
||||
viewAnyGroup: "viewAnyGroup",
|
||||
createGroup: "createGroup",
|
||||
viewAllLicenses: "viewAllLicenses",
|
||||
viewNotificationTemplate: "viewNotificationTemplate",
|
||||
viewOrganizationIDPSyncSettings: "viewOrganizationIDPSyncSettings",
|
||||
} as const satisfies Record<string, string>;
|
||||
export type Permissions = {
|
||||
[k in PermissionName]: boolean;
|
||||
};
|
||||
|
||||
// Type expression seems a little redundant (`keyof typeof checks` has the same
|
||||
// result), just because each key-value pair is currently symmetrical; this may
|
||||
// change down the line
|
||||
type PermissionValue = (typeof checks)[keyof typeof checks];
|
||||
export type PermissionName = keyof typeof permissionChecks;
|
||||
|
||||
export const permissionsToCheck = {
|
||||
[checks.viewAllUsers]: {
|
||||
export const permissionChecks = {
|
||||
viewAllUsers: {
|
||||
object: {
|
||||
resource_type: "user",
|
||||
},
|
||||
action: "read",
|
||||
},
|
||||
[checks.updateUsers]: {
|
||||
updateUsers: {
|
||||
object: {
|
||||
resource_type: "user",
|
||||
},
|
||||
action: "update",
|
||||
},
|
||||
[checks.createUser]: {
|
||||
createUser: {
|
||||
object: {
|
||||
resource_type: "user",
|
||||
},
|
||||
action: "create",
|
||||
},
|
||||
[checks.createTemplates]: {
|
||||
createTemplates: {
|
||||
object: {
|
||||
resource_type: "template",
|
||||
any_org: true,
|
||||
},
|
||||
action: "update",
|
||||
},
|
||||
[checks.updateTemplates]: {
|
||||
updateTemplates: {
|
||||
object: {
|
||||
resource_type: "template",
|
||||
},
|
||||
action: "update",
|
||||
},
|
||||
[checks.deleteTemplates]: {
|
||||
deleteTemplates: {
|
||||
object: {
|
||||
resource_type: "template",
|
||||
},
|
||||
action: "delete",
|
||||
},
|
||||
[checks.viewAnyAuditLog]: {
|
||||
viewDeploymentValues: {
|
||||
object: {
|
||||
resource_type: "deployment_config",
|
||||
},
|
||||
action: "read",
|
||||
},
|
||||
editDeploymentValues: {
|
||||
object: {
|
||||
resource_type: "deployment_config",
|
||||
},
|
||||
action: "update",
|
||||
},
|
||||
viewUpdateCheck: {
|
||||
object: {
|
||||
resource_type: "deployment_config",
|
||||
},
|
||||
action: "read",
|
||||
},
|
||||
viewExternalAuthConfig: {
|
||||
object: {
|
||||
resource_type: "deployment_config",
|
||||
},
|
||||
action: "read",
|
||||
},
|
||||
viewDeploymentStats: {
|
||||
object: {
|
||||
resource_type: "deployment_stats",
|
||||
},
|
||||
action: "read",
|
||||
},
|
||||
readWorkspaceProxies: {
|
||||
object: {
|
||||
resource_type: "workspace_proxy",
|
||||
},
|
||||
action: "read",
|
||||
},
|
||||
editWorkspaceProxies: {
|
||||
object: {
|
||||
resource_type: "workspace_proxy",
|
||||
},
|
||||
action: "create",
|
||||
},
|
||||
createOrganization: {
|
||||
object: {
|
||||
resource_type: "organization",
|
||||
},
|
||||
action: "create",
|
||||
},
|
||||
viewAnyGroup: {
|
||||
object: {
|
||||
resource_type: "group",
|
||||
},
|
||||
action: "read",
|
||||
},
|
||||
createGroup: {
|
||||
object: {
|
||||
resource_type: "group",
|
||||
},
|
||||
action: "create",
|
||||
},
|
||||
viewAllLicenses: {
|
||||
object: {
|
||||
resource_type: "license",
|
||||
},
|
||||
action: "read",
|
||||
},
|
||||
viewNotificationTemplate: {
|
||||
object: {
|
||||
resource_type: "notification_template",
|
||||
},
|
||||
action: "read",
|
||||
},
|
||||
viewOrganizationIDPSyncSettings: {
|
||||
object: {
|
||||
resource_type: "idpsync_settings",
|
||||
},
|
||||
action: "read",
|
||||
},
|
||||
|
||||
viewAnyMembers: {
|
||||
object: {
|
||||
resource_type: "organization_member",
|
||||
any_org: true,
|
||||
},
|
||||
action: "read",
|
||||
},
|
||||
editAnyGroups: {
|
||||
object: {
|
||||
resource_type: "group",
|
||||
any_org: true,
|
||||
},
|
||||
action: "update",
|
||||
},
|
||||
assignAnyRoles: {
|
||||
object: {
|
||||
resource_type: "assign_org_role",
|
||||
any_org: true,
|
||||
},
|
||||
action: "assign",
|
||||
},
|
||||
viewAnyIdpSyncSettings: {
|
||||
object: {
|
||||
resource_type: "idpsync_settings",
|
||||
any_org: true,
|
||||
},
|
||||
action: "read",
|
||||
},
|
||||
editAnySettings: {
|
||||
object: {
|
||||
resource_type: "organization",
|
||||
any_org: true,
|
||||
},
|
||||
action: "update",
|
||||
},
|
||||
viewAnyAuditLog: {
|
||||
object: {
|
||||
resource_type: "audit_log",
|
||||
any_org: true,
|
||||
},
|
||||
action: "read",
|
||||
},
|
||||
[checks.viewDeploymentValues]: {
|
||||
viewDebugInfo: {
|
||||
object: {
|
||||
resource_type: "deployment_config",
|
||||
resource_type: "debug_info",
|
||||
},
|
||||
action: "read",
|
||||
},
|
||||
[checks.editDeploymentValues]: {
|
||||
object: {
|
||||
resource_type: "deployment_config",
|
||||
},
|
||||
action: "update",
|
||||
},
|
||||
[checks.viewUpdateCheck]: {
|
||||
object: {
|
||||
resource_type: "deployment_config",
|
||||
},
|
||||
action: "read",
|
||||
},
|
||||
[checks.viewExternalAuthConfig]: {
|
||||
object: {
|
||||
resource_type: "deployment_config",
|
||||
},
|
||||
action: "read",
|
||||
},
|
||||
[checks.viewDeploymentStats]: {
|
||||
object: {
|
||||
resource_type: "deployment_stats",
|
||||
},
|
||||
action: "read",
|
||||
},
|
||||
[checks.readWorkspaceProxies]: {
|
||||
object: {
|
||||
resource_type: "workspace_proxy",
|
||||
},
|
||||
action: "read",
|
||||
},
|
||||
[checks.editWorkspaceProxies]: {
|
||||
object: {
|
||||
resource_type: "workspace_proxy",
|
||||
},
|
||||
action: "create",
|
||||
},
|
||||
[checks.createOrganization]: {
|
||||
object: {
|
||||
resource_type: "organization",
|
||||
},
|
||||
action: "create",
|
||||
},
|
||||
[checks.viewAnyGroup]: {
|
||||
object: {
|
||||
resource_type: "group",
|
||||
},
|
||||
action: "read",
|
||||
},
|
||||
[checks.createGroup]: {
|
||||
object: {
|
||||
resource_type: "group",
|
||||
},
|
||||
action: "create",
|
||||
},
|
||||
[checks.viewAllLicenses]: {
|
||||
object: {
|
||||
resource_type: "license",
|
||||
},
|
||||
action: "read",
|
||||
},
|
||||
[checks.viewNotificationTemplate]: {
|
||||
object: {
|
||||
resource_type: "notification_template",
|
||||
},
|
||||
action: "read",
|
||||
},
|
||||
[checks.viewOrganizationIDPSyncSettings]: {
|
||||
object: {
|
||||
resource_type: "idpsync_settings",
|
||||
},
|
||||
action: "read",
|
||||
},
|
||||
} as const satisfies Record<PermissionValue, AuthorizationCheck>;
|
||||
} as const satisfies Record<string, AuthorizationCheck>;
|
||||
|
||||
export type Permissions = Record<PermissionValue, boolean>;
|
||||
export const canViewDeploymentSettings = (
|
||||
permissions: Permissions | undefined,
|
||||
): permissions is Permissions => {
|
||||
return (
|
||||
permissions !== undefined &&
|
||||
(permissions.viewDeploymentValues ||
|
||||
permissions.viewAllLicenses ||
|
||||
permissions.viewAllUsers ||
|
||||
permissions.viewAnyGroup ||
|
||||
permissions.viewNotificationTemplate ||
|
||||
permissions.viewOrganizationIDPSyncSettings)
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks if the user can view or edit members or groups for the organization
|
||||
* that produced the given OrganizationPermissions.
|
||||
*/
|
||||
export const canViewAnyOrganization = (
|
||||
permissions: Permissions | undefined,
|
||||
): permissions is Permissions => {
|
||||
return (
|
||||
permissions !== undefined &&
|
||||
(permissions.viewAnyMembers ||
|
||||
permissions.editAnyGroups ||
|
||||
permissions.assignAnyRoles ||
|
||||
permissions.viewAnyIdpSyncSettings ||
|
||||
permissions.editAnySettings)
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
import { appearance } from "api/queries/appearance";
|
||||
import { entitlements } from "api/queries/entitlements";
|
||||
import { experiments } from "api/queries/experiments";
|
||||
import {
|
||||
anyOrganizationPermissions,
|
||||
organizations,
|
||||
} from "api/queries/organizations";
|
||||
import { organizations } from "api/queries/organizations";
|
||||
import type {
|
||||
AppearanceConfig,
|
||||
Entitlements,
|
||||
@@ -13,8 +10,9 @@ import type {
|
||||
} from "api/typesGenerated";
|
||||
import { ErrorAlert } from "components/Alert/ErrorAlert";
|
||||
import { Loader } from "components/Loader/Loader";
|
||||
import { useAuthenticated } from "contexts/auth/RequireAuth";
|
||||
import { canViewAnyOrganization } from "contexts/auth/permissions";
|
||||
import { useEmbeddedMetadata } from "hooks/useEmbeddedMetadata";
|
||||
import { canViewAnyOrganization } from "modules/management/organizationPermissions";
|
||||
import { type FC, type PropsWithChildren, createContext } from "react";
|
||||
import { useQuery } from "react-query";
|
||||
import { selectFeatureVisibility } from "./entitlements";
|
||||
@@ -34,20 +32,17 @@ export const DashboardContext = createContext<DashboardValue | undefined>(
|
||||
|
||||
export const DashboardProvider: FC<PropsWithChildren> = ({ children }) => {
|
||||
const { metadata } = useEmbeddedMetadata();
|
||||
const { permissions } = useAuthenticated();
|
||||
const entitlementsQuery = useQuery(entitlements(metadata.entitlements));
|
||||
const experimentsQuery = useQuery(experiments(metadata.experiments));
|
||||
const appearanceQuery = useQuery(appearance(metadata.appearance));
|
||||
const organizationsQuery = useQuery(organizations());
|
||||
const anyOrganizationPermissionsQuery = useQuery(
|
||||
anyOrganizationPermissions(),
|
||||
);
|
||||
|
||||
const error =
|
||||
entitlementsQuery.error ||
|
||||
appearanceQuery.error ||
|
||||
experimentsQuery.error ||
|
||||
organizationsQuery.error ||
|
||||
anyOrganizationPermissionsQuery.error;
|
||||
organizationsQuery.error;
|
||||
|
||||
if (error) {
|
||||
return <ErrorAlert error={error} />;
|
||||
@@ -57,8 +52,7 @@ export const DashboardProvider: FC<PropsWithChildren> = ({ children }) => {
|
||||
!entitlementsQuery.data ||
|
||||
!appearanceQuery.data ||
|
||||
!experimentsQuery.data ||
|
||||
!organizationsQuery.data ||
|
||||
!anyOrganizationPermissionsQuery.data;
|
||||
!organizationsQuery.data;
|
||||
|
||||
if (isLoading) {
|
||||
return <Loader fullscreen />;
|
||||
@@ -79,8 +73,7 @@ export const DashboardProvider: FC<PropsWithChildren> = ({ children }) => {
|
||||
organizations: organizationsQuery.data,
|
||||
showOrganizations,
|
||||
canViewOrganizationSettings:
|
||||
showOrganizations &&
|
||||
canViewAnyOrganization(anyOrganizationPermissionsQuery.data),
|
||||
showOrganizations && canViewAnyOrganization(permissions),
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
|
||||
@@ -82,7 +82,7 @@ const DeploymentDropdownContent: FC<DeploymentDropdownProps> = ({
|
||||
{canViewDeployment && (
|
||||
<MenuItem
|
||||
component={NavLink}
|
||||
to="/deployment/general"
|
||||
to="/deployment"
|
||||
css={styles.menuItem}
|
||||
onClick={onPopoverClose}
|
||||
>
|
||||
|
||||
@@ -220,7 +220,7 @@ const AdminSettingsSub: FC<MobileMenuPermissions> = ({
|
||||
asChild
|
||||
className={cn(itemStyles.default, itemStyles.sub)}
|
||||
>
|
||||
<Link to="/deployment/general">Deployment</Link>
|
||||
<Link to="/deployment">Deployment</Link>
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
{canViewOrganizations && (
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { buildInfo } from "api/queries/buildInfo";
|
||||
import { useProxy } from "contexts/ProxyContext";
|
||||
import { useAuthenticated } from "contexts/auth/RequireAuth";
|
||||
import { canViewDeploymentSettings } from "contexts/auth/permissions";
|
||||
import { useEmbeddedMetadata } from "hooks/useEmbeddedMetadata";
|
||||
import { useDashboard } from "modules/dashboard/useDashboard";
|
||||
import type { FC } from "react";
|
||||
@@ -11,16 +12,16 @@ import { NavbarView } from "./NavbarView";
|
||||
export const Navbar: FC = () => {
|
||||
const { metadata } = useEmbeddedMetadata();
|
||||
const buildInfoQuery = useQuery(buildInfo(metadata["build-info"]));
|
||||
|
||||
const { appearance, canViewOrganizationSettings } = useDashboard();
|
||||
const { user: me, permissions, signOut } = useAuthenticated();
|
||||
const featureVisibility = useFeatureVisibility();
|
||||
const proxyContextValue = useProxy();
|
||||
|
||||
const canViewDeployment = canViewDeploymentSettings(permissions);
|
||||
const canViewOrganizations = canViewOrganizationSettings;
|
||||
const canViewHealth = permissions.viewDebugInfo;
|
||||
const canViewAuditLog =
|
||||
featureVisibility.audit_log && permissions.viewAnyAuditLog;
|
||||
const canViewDeployment = permissions.viewDeploymentValues;
|
||||
const canViewOrganizations = canViewOrganizationSettings;
|
||||
const proxyContextValue = useProxy();
|
||||
const canViewHealth = canViewDeployment;
|
||||
|
||||
return (
|
||||
<NavbarView
|
||||
|
||||
@@ -90,6 +90,6 @@ describe("NavbarView", () => {
|
||||
await userEvent.click(deploymentMenu);
|
||||
const deploymentSettingsLink =
|
||||
await screen.findByText<HTMLAnchorElement>(/deployment/i);
|
||||
expect(deploymentSettingsLink.href).toContain("/deployment/general");
|
||||
expect(deploymentSettingsLink.href).toContain("/deployment");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,7 +3,7 @@ import { fn, userEvent, within } from "@storybook/test";
|
||||
import { getAuthorizationKey } from "api/queries/authCheck";
|
||||
import { getPreferredProxy } from "contexts/ProxyContext";
|
||||
import { AuthProvider } from "contexts/auth/AuthProvider";
|
||||
import { permissionsToCheck } from "contexts/auth/permissions";
|
||||
import { permissionChecks } from "contexts/auth/permissions";
|
||||
import {
|
||||
MockAuthMethodsAll,
|
||||
MockPermissions,
|
||||
@@ -45,7 +45,7 @@ const meta: Meta<typeof ProxyMenu> = {
|
||||
{ key: ["authMethods"], data: MockAuthMethodsAll },
|
||||
{ key: ["hasFirstUser"], data: true },
|
||||
{
|
||||
key: getAuthorizationKey({ checks: permissionsToCheck }),
|
||||
key: getAuthorizationKey({ checks: permissionChecks }),
|
||||
data: MockPermissions,
|
||||
},
|
||||
],
|
||||
|
||||
@@ -8,19 +8,31 @@ import {
|
||||
import { Loader } from "components/Loader/Loader";
|
||||
import { useAuthenticated } from "contexts/auth/RequireAuth";
|
||||
import { RequirePermission } from "contexts/auth/RequirePermission";
|
||||
import { canViewDeploymentSettings } from "contexts/auth/permissions";
|
||||
import { type FC, Suspense } from "react";
|
||||
import { Outlet } from "react-router-dom";
|
||||
import { Navigate, Outlet, useLocation } from "react-router-dom";
|
||||
import { DeploymentSidebar } from "./DeploymentSidebar";
|
||||
|
||||
const DeploymentSettingsLayout: FC = () => {
|
||||
const { permissions } = useAuthenticated();
|
||||
const location = useLocation();
|
||||
|
||||
// The deployment settings page also contains users, audit logs, and groups
|
||||
// so this page must be visible if you can see any of these.
|
||||
const canViewDeploymentSettingsPage =
|
||||
permissions.viewDeploymentValues ||
|
||||
permissions.viewAllUsers ||
|
||||
permissions.viewAnyAuditLog;
|
||||
if (location.pathname === "/deployment") {
|
||||
return (
|
||||
<Navigate
|
||||
to={
|
||||
permissions.viewDeploymentValues
|
||||
? "/deployment/general"
|
||||
: "/deployment/users"
|
||||
}
|
||||
replace
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// The deployment settings page also contains users and groups and more so
|
||||
// this page must be visible if you can see any of these.
|
||||
const canViewDeploymentSettingsPage = canViewDeploymentSettings(permissions);
|
||||
|
||||
return (
|
||||
<RequirePermission isFeatureVisible={canViewDeploymentSettingsPage}>
|
||||
|
||||
@@ -2,8 +2,6 @@ import type { DeploymentConfig } from "api/api";
|
||||
import { deploymentConfig } from "api/queries/deployment";
|
||||
import { ErrorAlert } from "components/Alert/ErrorAlert";
|
||||
import { Loader } from "components/Loader/Loader";
|
||||
import { useAuthenticated } from "contexts/auth/RequireAuth";
|
||||
import { RequirePermission } from "contexts/auth/RequirePermission";
|
||||
import { type FC, createContext, useContext } from "react";
|
||||
import { useQuery } from "react-query";
|
||||
import { Outlet } from "react-router-dom";
|
||||
@@ -28,19 +26,8 @@ export const useDeploymentSettings = (): DeploymentSettingsValue => {
|
||||
};
|
||||
|
||||
const DeploymentSettingsProvider: FC = () => {
|
||||
const { permissions } = useAuthenticated();
|
||||
const deploymentConfigQuery = useQuery(deploymentConfig());
|
||||
|
||||
// The deployment settings page also contains users, audit logs, and groups
|
||||
// so this page must be visible if you can see any of these.
|
||||
const canViewDeploymentSettingsPage =
|
||||
permissions.viewDeploymentValues ||
|
||||
permissions.viewAllUsers ||
|
||||
permissions.viewAnyAuditLog;
|
||||
|
||||
// Not a huge problem to unload the content in the event of an error,
|
||||
// because the sidebar rendering isn't tied to this. Even if the user hits
|
||||
// a 403 error, they'll still have navigation options
|
||||
if (deploymentConfigQuery.error) {
|
||||
return <ErrorAlert error={deploymentConfigQuery.error} />;
|
||||
}
|
||||
@@ -50,13 +37,11 @@ const DeploymentSettingsProvider: FC = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<RequirePermission isFeatureVisible={canViewDeploymentSettingsPage}>
|
||||
<DeploymentSettingsContext.Provider
|
||||
value={{ deploymentConfig: deploymentConfigQuery.data }}
|
||||
>
|
||||
<Outlet />
|
||||
</DeploymentSettingsContext.Provider>
|
||||
</RequirePermission>
|
||||
<DeploymentSettingsContext.Provider
|
||||
value={{ deploymentConfig: deploymentConfigQuery.data }}
|
||||
>
|
||||
<Outlet />
|
||||
</DeploymentSettingsContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -135,65 +135,3 @@ export const canEditOrganization = (
|
||||
permissions.createOrgRoles)
|
||||
);
|
||||
};
|
||||
|
||||
export type AnyOrganizationPermissions = {
|
||||
[k in AnyOrganizationPermissionName]: boolean;
|
||||
};
|
||||
|
||||
export type AnyOrganizationPermissionName =
|
||||
keyof typeof anyOrganizationPermissionChecks;
|
||||
|
||||
export const anyOrganizationPermissionChecks = {
|
||||
viewAnyMembers: {
|
||||
object: {
|
||||
resource_type: "organization_member",
|
||||
any_org: true,
|
||||
},
|
||||
action: "read",
|
||||
},
|
||||
editAnyGroups: {
|
||||
object: {
|
||||
resource_type: "group",
|
||||
any_org: true,
|
||||
},
|
||||
action: "update",
|
||||
},
|
||||
assignAnyRoles: {
|
||||
object: {
|
||||
resource_type: "assign_org_role",
|
||||
any_org: true,
|
||||
},
|
||||
action: "assign",
|
||||
},
|
||||
viewAnyIdpSyncSettings: {
|
||||
object: {
|
||||
resource_type: "idpsync_settings",
|
||||
any_org: true,
|
||||
},
|
||||
action: "read",
|
||||
},
|
||||
editAnySettings: {
|
||||
object: {
|
||||
resource_type: "organization",
|
||||
any_org: true,
|
||||
},
|
||||
action: "update",
|
||||
},
|
||||
} as const satisfies Record<string, AuthorizationCheck>;
|
||||
|
||||
/**
|
||||
* Checks if the user can view or edit members or groups for the organization
|
||||
* that produced the given OrganizationPermissions.
|
||||
*/
|
||||
export const canViewAnyOrganization = (
|
||||
permissions: AnyOrganizationPermissions | undefined,
|
||||
): permissions is AnyOrganizationPermissions => {
|
||||
return (
|
||||
permissions !== undefined &&
|
||||
(permissions.viewAnyMembers ||
|
||||
permissions.editAnyGroups ||
|
||||
permissions.assignAnyRoles ||
|
||||
permissions.viewAnyIdpSyncSettings ||
|
||||
permissions.editAnySettings)
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { getAuthorizationKey } from "api/queries/authCheck";
|
||||
import { anyOrganizationPermissionsKey } from "api/queries/organizations";
|
||||
import { workspaceByOwnerAndNameKey } from "api/queries/workspaces";
|
||||
import type { Workspace, WorkspaceAgentLifecycle } from "api/typesGenerated";
|
||||
import { AuthProvider } from "contexts/auth/AuthProvider";
|
||||
import { RequireAuth } from "contexts/auth/RequireAuth";
|
||||
import { permissionsToCheck } from "contexts/auth/permissions";
|
||||
import { permissionChecks } from "contexts/auth/permissions";
|
||||
import {
|
||||
reactRouterOutlet,
|
||||
reactRouterParameters,
|
||||
@@ -74,10 +73,9 @@ const meta = {
|
||||
{ key: ["appearance"], data: MockAppearanceConfig },
|
||||
{ key: ["organizations"], data: [MockDefaultOrganization] },
|
||||
{
|
||||
key: getAuthorizationKey({ checks: permissionsToCheck }),
|
||||
key: getAuthorizationKey({ checks: permissionChecks }),
|
||||
data: { editWorkspaceProxies: true },
|
||||
},
|
||||
{ key: anyOrganizationPermissionsKey, data: {} },
|
||||
],
|
||||
chromatic: { delay: 300 },
|
||||
},
|
||||
|
||||
+3
-2
@@ -453,8 +453,6 @@ export const router = createBrowserRouter(
|
||||
path="notifications"
|
||||
element={<DeploymentNotificationsPage />}
|
||||
/>
|
||||
<Route path="idp-org-sync" element={<IdpOrgSyncPage />} />
|
||||
<Route path="premium" element={<PremiumPage />} />
|
||||
</Route>
|
||||
|
||||
<Route path="licenses">
|
||||
@@ -476,6 +474,9 @@ export const router = createBrowserRouter(
|
||||
<Route path="users/create" element={<CreateUserPage />} />
|
||||
|
||||
{groupsRouter()}
|
||||
|
||||
<Route path="idp-org-sync" element={<IdpOrgSyncPage />} />
|
||||
<Route path="premium" element={<PremiumPage />} />
|
||||
</Route>
|
||||
|
||||
<Route path="/settings" element={<UserSettingsLayout />}>
|
||||
|
||||
@@ -2856,6 +2856,41 @@ export const MockPermissions: Permissions = {
|
||||
viewAllLicenses: true,
|
||||
viewNotificationTemplate: true,
|
||||
viewOrganizationIDPSyncSettings: true,
|
||||
viewDebugInfo: true,
|
||||
assignAnyRoles: true,
|
||||
editAnyGroups: true,
|
||||
editAnySettings: true,
|
||||
viewAnyIdpSyncSettings: true,
|
||||
viewAnyMembers: true,
|
||||
};
|
||||
|
||||
export const MockNoPermissions: Permissions = {
|
||||
createTemplates: false,
|
||||
createUser: false,
|
||||
deleteTemplates: false,
|
||||
updateTemplates: false,
|
||||
viewAllUsers: false,
|
||||
updateUsers: false,
|
||||
viewAnyAuditLog: false,
|
||||
viewDeploymentValues: false,
|
||||
editDeploymentValues: false,
|
||||
viewUpdateCheck: false,
|
||||
viewDeploymentStats: false,
|
||||
viewExternalAuthConfig: false,
|
||||
readWorkspaceProxies: false,
|
||||
editWorkspaceProxies: false,
|
||||
createOrganization: false,
|
||||
viewAnyGroup: false,
|
||||
createGroup: false,
|
||||
viewAllLicenses: false,
|
||||
viewNotificationTemplate: false,
|
||||
viewOrganizationIDPSyncSettings: false,
|
||||
viewDebugInfo: false,
|
||||
assignAnyRoles: false,
|
||||
editAnyGroups: false,
|
||||
editAnySettings: false,
|
||||
viewAnyIdpSyncSettings: false,
|
||||
viewAnyMembers: false,
|
||||
};
|
||||
|
||||
export const MockOrganizationPermissions: OrganizationPermissions = {
|
||||
@@ -2890,29 +2925,6 @@ export const MockNoOrganizationPermissions: OrganizationPermissions = {
|
||||
editIdpSyncSettings: false,
|
||||
};
|
||||
|
||||
export const MockNoPermissions: Permissions = {
|
||||
createTemplates: false,
|
||||
createUser: false,
|
||||
deleteTemplates: false,
|
||||
updateTemplates: false,
|
||||
viewAllUsers: false,
|
||||
updateUsers: false,
|
||||
viewAnyAuditLog: false,
|
||||
viewDeploymentValues: false,
|
||||
editDeploymentValues: false,
|
||||
viewUpdateCheck: false,
|
||||
viewDeploymentStats: false,
|
||||
viewExternalAuthConfig: false,
|
||||
readWorkspaceProxies: false,
|
||||
editWorkspaceProxies: false,
|
||||
createOrganization: false,
|
||||
viewAnyGroup: false,
|
||||
createGroup: false,
|
||||
viewAllLicenses: false,
|
||||
viewNotificationTemplate: false,
|
||||
viewOrganizationIDPSyncSettings: false,
|
||||
};
|
||||
|
||||
export const MockDeploymentConfig: DeploymentConfig = {
|
||||
config: {
|
||||
enable_terraform_debug_mode: true,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import type { CreateWorkspaceBuildRequest } from "api/typesGenerated";
|
||||
import { permissionsToCheck } from "contexts/auth/permissions";
|
||||
import { permissionChecks } from "contexts/auth/permissions";
|
||||
import { http, HttpResponse } from "msw";
|
||||
import * as M from "./entities";
|
||||
import { MockGroup, MockWorkspaceQuota } from "./entities";
|
||||
@@ -173,7 +173,7 @@ export const handlers = [
|
||||
}),
|
||||
http.post("/api/v2/authcheck", () => {
|
||||
const permissions = [
|
||||
...Object.keys(permissionsToCheck),
|
||||
...Object.keys(permissionChecks),
|
||||
"canUpdateTemplate",
|
||||
"updateWorkspace",
|
||||
];
|
||||
|
||||
@@ -6,7 +6,7 @@ import { hasFirstUserKey, meKey } from "api/queries/users";
|
||||
import type { Entitlements } from "api/typesGenerated";
|
||||
import { GlobalSnackbar } from "components/GlobalSnackbar/GlobalSnackbar";
|
||||
import { AuthProvider } from "contexts/auth/AuthProvider";
|
||||
import { permissionsToCheck } from "contexts/auth/permissions";
|
||||
import { permissionChecks } from "contexts/auth/permissions";
|
||||
import { DashboardContext } from "modules/dashboard/DashboardProvider";
|
||||
import { DeploymentSettingsContext } from "modules/management/DeploymentSettingsProvider";
|
||||
import { OrganizationSettingsContext } from "modules/management/OrganizationSettingsLayout";
|
||||
@@ -114,7 +114,7 @@ export const withAuthProvider = (Story: FC, { parameters }: StoryContext) => {
|
||||
queryClient.setQueryData(meKey, parameters.user);
|
||||
queryClient.setQueryData(hasFirstUserKey, true);
|
||||
queryClient.setQueryData(
|
||||
getAuthorizationKey({ checks: permissionsToCheck }),
|
||||
getAuthorizationKey({ checks: permissionChecks }),
|
||||
parameters.permissions ?? {},
|
||||
);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user