feat: integrate backend with idp sync page (#14755)

* feat: idp sync initial commit

* fix: hookup backend data for groups and roles

* chore: cleanup

* feat: separate groups and roles into tabs

* feat: implement export policy button

* feat: handle missing groups

* chore: add story for missing groups

* chore: add stories for export policy button

* fix: updates for PR review

* chore: update tests

* chore: document uuid regex

* chore: remove unused

* fix: fix stories
This commit is contained in:
Jaayden Halko
2024-09-23 22:07:46 -04:00
committed by GitHub
parent b4f54f3eea
commit a3ebcd7a1e
12 changed files with 654 additions and 207 deletions
+24
View File
@@ -704,6 +704,30 @@ class ApiMethods {
return response.data;
};
/**
* @param organization Can be the organization's ID or name
*/
getGroupIdpSyncSettingsByOrganization = async (
organization: string,
): Promise<TypesGen.GroupSyncSettings> => {
const response = await this.axios.get<TypesGen.GroupSyncSettings>(
`/api/v2/organizations/${organization}/settings/idpsync/groups`,
);
return response.data;
};
/**
* @param organization Can be the organization's ID or name
*/
getRoleIdpSyncSettingsByOrganization = async (
organization: string,
): Promise<TypesGen.RoleSyncSettings> => {
const response = await this.axios.get<TypesGen.RoleSyncSettings>(
`/api/v2/organizations/${organization}/settings/idpsync/roles`,
);
return response.data;
};
getTemplate = async (templateId: string): Promise<TypesGen.Template> => {
const response = await this.axios.get<TypesGen.Template>(
`/api/v2/templates/${templateId}`,
+33
View File
@@ -141,6 +141,32 @@ export const provisionerDaemonGroups = (organization: string) => {
};
};
export const getGroupIdpSyncSettingsKey = (organization: string) => [
"organizations",
organization,
"groupIdpSyncSettings",
];
export const groupIdpSyncSettings = (organization: string) => {
return {
queryKey: getGroupIdpSyncSettingsKey(organization),
queryFn: () => API.getGroupIdpSyncSettingsByOrganization(organization),
};
};
export const getRoleIdpSyncSettingsKey = (organization: string) => [
"organizations",
organization,
"roleIdpSyncSettings",
];
export const roleIdpSyncSettings = (organization: string) => {
return {
queryKey: getRoleIdpSyncSettingsKey(organization),
queryFn: () => API.getRoleIdpSyncSettingsByOrganization(organization),
};
};
/**
* Fetch permissions for a single organization.
*
@@ -243,6 +269,13 @@ export const organizationsPermissions = (
},
action: "read",
},
viewIdpSyncSettings: {
object: {
resource_type: "idpsync_settings",
organization_id: organizationId,
},
action: "read",
},
});
// The endpoint takes a flat array, so to avoid collisions prepend each
@@ -74,7 +74,7 @@ export const AppearanceSettingsPageView: FC<
<PopoverContent css={{ transform: "translateY(-28px)" }}>
<PopoverPaywall
message="Appearance"
description="With a Premium license, you can customize the appearance of your deployment."
description="With a Premium license, you can customize the appearance and branding of your deployment."
documentationLink="https://coder.com/docs/admin/appearance"
/>
</PopoverContent>
@@ -29,12 +29,12 @@ import { Link as RouterLink, useNavigate } from "react-router-dom";
import { docs } from "utils/docs";
import { PermissionPillsList } from "./PermissionPillsList";
export type CustomRolesPageViewProps = {
interface CustomRolesPageViewProps {
roles: Role[] | undefined;
onDeleteRole: (role: Role) => void;
canAssignOrgRole: boolean;
isCustomRolesEnabled: boolean;
};
}
export const CustomRolesPageView: FC<CustomRolesPageViewProps> = ({
roles,
@@ -0,0 +1,75 @@
import type { Meta, StoryObj } from "@storybook/react";
import { expect, fn, userEvent, waitFor, within } from "@storybook/test";
import {
MockGroupSyncSettings,
MockOrganization,
MockRoleSyncSettings,
} from "testHelpers/entities";
import { ExportPolicyButton } from "./ExportPolicyButton";
const meta: Meta<typeof ExportPolicyButton> = {
title: "modules/resources/ExportPolicyButton",
component: ExportPolicyButton,
args: {
syncSettings: MockGroupSyncSettings,
type: "groups",
organization: MockOrganization,
},
};
export default meta;
type Story = StoryObj<typeof ExportPolicyButton>;
export const Default: Story = {};
export const ClickExportGroupPolicy: Story = {
args: {
syncSettings: MockGroupSyncSettings,
type: "groups",
organization: MockOrganization,
download: fn(),
},
play: async ({ canvasElement, args }) => {
const canvas = within(canvasElement);
await userEvent.click(
canvas.getByRole("button", { name: "Export Policy" }),
);
await waitFor(() =>
expect(args.download).toHaveBeenCalledWith(
expect.anything(),
`${MockOrganization.name}_groups-policy.json`,
),
);
const blob: Blob = (args.download as jest.Mock).mock.lastCall[0];
await expect(blob.type).toEqual("application/json");
await expect(await blob.text()).toEqual(
JSON.stringify(MockGroupSyncSettings, null, 2),
);
},
};
export const ClickExportRolePolicy: Story = {
args: {
syncSettings: MockRoleSyncSettings,
type: "roles",
organization: MockOrganization,
download: fn(),
},
play: async ({ canvasElement, args }) => {
const canvas = within(canvasElement);
await userEvent.click(
canvas.getByRole("button", { name: "Export Policy" }),
);
await waitFor(() =>
expect(args.download).toHaveBeenCalledWith(
expect.anything(),
`${MockOrganization.name}_roles-policy.json`,
),
);
const blob: Blob = (args.download as jest.Mock).mock.lastCall[0];
await expect(blob.type).toEqual("application/json");
await expect(await blob.text()).toEqual(
JSON.stringify(MockRoleSyncSettings, null, 2),
);
},
};
@@ -0,0 +1,57 @@
import DownloadOutlined from "@mui/icons-material/DownloadOutlined";
import Button from "@mui/material/Button";
import type {
GroupSyncSettings,
Organization,
RoleSyncSettings,
} from "api/typesGenerated";
import { displayError } from "components/GlobalSnackbar/utils";
import { saveAs } from "file-saver";
import { type FC, useMemo, useState } from "react";
interface DownloadPolicyButtonProps {
syncSettings: RoleSyncSettings | GroupSyncSettings | undefined;
type: "groups" | "roles";
organization: Organization;
download?: (file: Blob, filename: string) => void;
}
export const ExportPolicyButton: FC<DownloadPolicyButtonProps> = ({
syncSettings,
type,
organization,
download = saveAs,
}) => {
const [isDownloading, setIsDownloading] = useState(false);
const policyJSON = useMemo(() => {
return syncSettings?.field && syncSettings.mapping
? JSON.stringify(syncSettings, null, 2)
: null;
}, [syncSettings]);
return (
<Button
startIcon={<DownloadOutlined />}
disabled={!policyJSON || isDownloading}
onClick={async () => {
if (policyJSON) {
try {
setIsDownloading(true);
const file = new Blob([policyJSON], {
type: "application/json",
});
download(file, `${organization.name}_${type}-policy.json`);
} catch (e) {
console.error(e);
displayError("Failed to export policy json");
} finally {
setIsDownloading(false);
}
}
}}
>
Export Policy
</Button>
);
};
@@ -0,0 +1,106 @@
import { type Interpolation, type Theme, useTheme } from "@emotion/react";
import Stack from "@mui/material/Stack";
import { Pill } from "components/Pill/Pill";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "components/Popover/Popover";
import type { FC } from "react";
interface PillListProps {
roles: readonly string[];
}
// used to check if the role is a UUID
const UUID =
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
export const IdpPillList: FC<PillListProps> = ({ roles }) => {
return (
<Stack direction="row" spacing={1}>
{roles.length > 0 ? (
<Pill css={UUID.test(roles[0]) ? styles.errorPill : styles.pill}>
{roles[0]}
</Pill>
) : (
<p>None</p>
)}
{roles.length > 1 && <OverflowPill roles={roles.slice(1)} />}
</Stack>
);
};
interface OverflowPillProps {
roles: string[];
}
const OverflowPill: FC<OverflowPillProps> = ({ roles }) => {
const theme = useTheme();
return (
<Popover mode="hover">
<PopoverTrigger>
<Pill
css={{
backgroundColor: theme.palette.background.paper,
borderColor: theme.palette.divider,
}}
data-testid="overflow-pill"
>
+{roles.length} more
</Pill>
</PopoverTrigger>
<PopoverContent
disableRestoreFocus
disableScrollLock
css={{
".MuiPaper-root": {
display: "flex",
flexFlow: "column wrap",
columnGap: 8,
rowGap: 12,
padding: "12px 16px",
alignContent: "space-around",
minWidth: "auto",
backgroundColor: theme.palette.background.default,
},
}}
anchorOrigin={{
vertical: -4,
horizontal: "center",
}}
transformOrigin={{
vertical: "bottom",
horizontal: "center",
}}
>
{roles.map((role) => (
<Pill
key={role}
css={UUID.test(role) ? styles.errorPill : styles.pill}
>
{role}
</Pill>
))}
</PopoverContent>
</Popover>
);
};
const styles = {
pill: (theme) => ({
backgroundColor: theme.experimental.pillDefault.background,
borderColor: theme.experimental.pillDefault.outline,
color: theme.experimental.pillDefault.text,
width: "fit-content",
}),
errorPill: (theme) => ({
backgroundColor: theme.roles.error.background,
borderColor: theme.roles.error.outline,
color: theme.roles.error.text,
width: "fit-content",
}),
} satisfies Record<string, Interpolation<Theme>>;
@@ -1,63 +1,73 @@
import AddIcon from "@mui/icons-material/AddOutlined";
import LaunchOutlined from "@mui/icons-material/LaunchOutlined";
import Button from "@mui/material/Button";
import { groupsByOrganization } from "api/queries/groups";
import {
groupIdpSyncSettings,
roleIdpSyncSettings,
} from "api/queries/organizations";
import { ErrorAlert } from "components/Alert/ErrorAlert";
import { EmptyState } from "components/EmptyState/EmptyState";
import { FeatureStageBadge } from "components/FeatureStageBadge/FeatureStageBadge";
import { Loader } from "components/Loader/Loader";
import { SettingsHeader } from "components/SettingsHeader/SettingsHeader";
import { Stack } from "components/Stack/Stack";
import type { FC } from "react";
import { Helmet } from "react-helmet-async";
import { Link as RouterLink } from "react-router-dom";
import { useQueries } from "react-query";
import { useParams } from "react-router-dom";
import { docs } from "utils/docs";
import { pageTitle } from "utils/page";
import { useOrganizationSettings } from "../ManagementSettingsLayout";
import { IdpSyncHelpTooltip } from "./IdpSyncHelpTooltip";
import IdpSyncPageView from "./IdpSyncPageView";
const mockOIDCConfig = {
allow_signups: true,
client_id: "test",
client_secret: "test",
client_key_file: "test",
client_cert_file: "test",
email_domain: [],
issuer_url: "test",
scopes: [],
ignore_email_verified: true,
username_field: "",
name_field: "",
email_field: "",
auth_url_params: {},
ignore_user_info: true,
organization_field: "",
organization_mapping: {},
organization_assign_default: true,
group_auto_create: false,
group_regex_filter: "^Coder-.*$",
group_allow_list: [],
groups_field: "groups",
group_mapping: { group1: "developers", group2: "admin", group3: "auditors" },
user_role_field: "roles",
user_role_mapping: { role1: ["role1", "role2"] },
user_roles_default: [],
sign_in_text: "",
icon_url: "",
signups_disabled_text: "string",
skip_issuer_checks: true,
};
export const IdpSyncPage: FC = () => {
// feature visibility and permissions to be implemented when integrating with backend
// const feats = useFeatureVisibility();
// const { organization: organizationName } = useParams() as {
// organization: string;
// };
// const { organizations } = useOrganizationSettings();
// const organization = organizations?.find((o) => o.name === organizationName);
// const permissionsQuery = useQuery(organizationPermissions(organization?.id));
// const permissions = permissionsQuery.data;
const { organization: organizationName } = useParams() as {
organization: string;
};
const { organizations } = useOrganizationSettings();
const organization = organizations?.find((o) => o.name === organizationName);
// if (!permissions) {
// return <Loader />;
// }
const [groupIdpSyncSettingsQuery, roleIdpSyncSettingsQuery, groupsQuery] =
useQueries({
queries: [
groupIdpSyncSettings(organizationName),
roleIdpSyncSettings(organizationName),
groupsByOrganization(organizationName),
],
});
if (!organization) {
return <EmptyState message="Organization not found" />;
}
if (
groupsQuery.isLoading ||
groupIdpSyncSettingsQuery.isLoading ||
roleIdpSyncSettingsQuery.isLoading
) {
return <Loader />;
}
const error =
groupIdpSyncSettingsQuery.error ||
roleIdpSyncSettingsQuery.error ||
groupsQuery.error;
if (
error ||
!groupIdpSyncSettingsQuery.data ||
!roleIdpSyncSettingsQuery.data ||
!groupsQuery.data
) {
return <ErrorAlert error={error} />;
}
const groupsMap = new Map<string, string>();
if (groupsQuery.data) {
for (const group of groupsQuery.data) {
groupsMap.set(group.id, group.display_name || group.name);
}
}
return (
<>
@@ -72,7 +82,7 @@ export const IdpSyncPage: FC = () => {
>
<SettingsHeader
title="IdP Sync"
description="Group and role sync mappings (configured outside Coder)."
description="Group and role sync mappings (configured using Coder CLI)."
tooltip={<IdpSyncHelpTooltip />}
badges={<FeatureStageBadge contentType="beta" size="lg" />}
/>
@@ -85,13 +95,16 @@ export const IdpSyncPage: FC = () => {
>
Setup IdP Sync
</Button>
<Button component={RouterLink} startIcon={<AddIcon />} to="export">
Export Policy
</Button>
</Stack>
</Stack>
<IdpSyncPageView oidcConfig={mockOIDCConfig} />
<IdpSyncPageView
groupSyncSettings={groupIdpSyncSettingsQuery.data}
roleSyncSettings={roleIdpSyncSettingsQuery.data}
groups={groupsQuery.data}
groupsMap={groupsMap}
organization={organization}
/>
</>
);
};
@@ -1,5 +1,11 @@
import type { Meta, StoryObj } from "@storybook/react";
import { MockOIDCConfig } from "testHelpers/entities";
import {
MockGroup,
MockGroup2,
MockGroupSyncSettings,
MockGroupSyncSettings2,
MockRoleSyncSettings,
} from "testHelpers/entities";
import { IdpSyncPageView } from "./IdpSyncPageView";
const meta: Meta<typeof IdpSyncPageView> = {
@@ -10,10 +16,32 @@ const meta: Meta<typeof IdpSyncPageView> = {
export default meta;
type Story = StoryObj<typeof IdpSyncPageView>;
const groupsMap = new Map<string, string>();
for (const group of [MockGroup, MockGroup2]) {
groupsMap.set(group.id, group.display_name || group.name);
}
export const Empty: Story = {
args: { oidcConfig: undefined },
args: {
groupSyncSettings: undefined,
roleSyncSettings: undefined,
groupsMap: undefined,
},
};
export const Default: Story = {
args: { oidcConfig: MockOIDCConfig },
args: {
groupSyncSettings: MockGroupSyncSettings,
roleSyncSettings: MockRoleSyncSettings,
groupsMap,
},
};
export const MissingGroups: Story = {
args: {
groupSyncSettings: MockGroupSyncSettings2,
roleSyncSettings: MockRoleSyncSettings,
groupsMap,
},
};
@@ -1,5 +1,4 @@
import type { Interpolation, Theme } from "@emotion/react";
import { useTheme } from "@emotion/react";
import LaunchOutlined from "@mui/icons-material/LaunchOutlined";
import Button from "@mui/material/Button";
import Skeleton from "@mui/material/Skeleton";
@@ -9,7 +8,12 @@ import TableCell from "@mui/material/TableCell";
import TableContainer from "@mui/material/TableContainer";
import TableHead from "@mui/material/TableHead";
import TableRow from "@mui/material/TableRow";
import type { OIDCConfig } from "api/typesGenerated";
import type {
Group,
GroupSyncSettings,
Organization,
RoleSyncSettings,
} from "api/typesGenerated";
import { ChooseOne, Cond } from "components/Conditionals/ChooseOne";
import { EmptyState } from "components/EmptyState/EmptyState";
import { Paywall } from "components/Paywall/Paywall";
@@ -19,22 +23,43 @@ import {
TableLoaderSkeleton,
TableRowSkeleton,
} from "components/TableLoader/TableLoader";
import { TabLink, Tabs, TabsList } from "components/Tabs/Tabs";
import type { FC } from "react";
import { useSearchParams } from "react-router-dom";
import { MONOSPACE_FONT_FAMILY } from "theme/constants";
import { docs } from "utils/docs";
import { ExportPolicyButton } from "./ExportPolicyButton";
import { IdpPillList } from "./IdpPillList";
export type IdpSyncPageViewProps = {
oidcConfig: OIDCConfig | undefined;
};
interface IdpSyncPageViewProps {
groupSyncSettings: GroupSyncSettings | undefined;
roleSyncSettings: RoleSyncSettings | undefined;
groups: Group[] | undefined;
groupsMap: Map<string, string>;
organization: Organization;
}
export const IdpSyncPageView: FC<IdpSyncPageViewProps> = ({
groupSyncSettings,
roleSyncSettings,
groupsMap,
organization,
}) => {
const [searchParams] = useSearchParams();
const getGroupNames = (groupIds: readonly string[]) => {
return groupIds.map((groupId) => groupsMap.get(groupId) || groupId);
};
const tab = searchParams.get("tab") || "groups";
const groupMappingCount = groupSyncSettings?.mapping
? Object.entries(groupSyncSettings.mapping).length
: 0;
const roleMappingCount = roleSyncSettings?.mapping
? Object.entries(roleSyncSettings.mapping).length
: 0;
export const IdpSyncPageView: FC<IdpSyncPageViewProps> = ({ oidcConfig }) => {
const theme = useTheme();
const {
groups_field,
user_role_field,
group_regex_filter,
group_auto_create,
} = oidcConfig || {};
return (
<>
<ChooseOne>
@@ -46,75 +71,112 @@ export const IdpSyncPageView: FC<IdpSyncPageViewProps> = ({ oidcConfig }) => {
/>
</Cond>
<Cond>
<Stack spacing={2} css={styles.fields}>
{/* Semantically fieldset is used for forms. In the future this screen will allow
updates to these fields in a form */}
<fieldset css={styles.box}>
<legend css={styles.legend}>Groups</legend>
<Stack direction={"row"} alignItems={"center"} spacing={8}>
<IdpField
name={"Sync Field"}
fieldText={groups_field}
showStatusIndicator
/>
<IdpField
name={"Regex Filter"}
fieldText={group_regex_filter}
/>
<IdpField
name={"Auto Create"}
fieldText={group_auto_create?.toString()}
/>
</Stack>
</fieldset>
<fieldset css={styles.box}>
<legend css={styles.legend}>Roles</legend>
<Stack direction={"row"} alignItems={"center"} spacing={3}>
<IdpField
name={"Sync Field"}
fieldText={user_role_field}
showStatusIndicator
/>
</Stack>
</fieldset>
</Stack>
<Stack spacing={6}>
<IdpMappingTable
type="Role"
isEmpty={Boolean(
!oidcConfig?.user_role_mapping ||
Object.entries(oidcConfig?.user_role_mapping).length === 0,
)}
>
{oidcConfig?.user_role_mapping &&
Object.entries(oidcConfig.user_role_mapping)
.sort()
.map(([idpRole, roles]) => (
<RoleRow
key={idpRole}
idpRole={idpRole}
coderRoles={roles}
<Stack spacing={2}>
<Tabs active={tab}>
<TabsList>
<TabLink to="?tab=groups" value="groups">
Group Sync Settings
</TabLink>
<TabLink to="?tab=roles" value="roles">
Role Sync Settings
</TabLink>
</TabsList>
</Tabs>
{tab === "groups" ? (
<>
<div css={styles.fields}>
<Stack direction={"row"} alignItems={"center"} spacing={6}>
<IdpField
name={"Sync Field"}
fieldText={groupSyncSettings?.field}
showDisabled
/>
))}
</IdpMappingTable>
<IdpMappingTable
type="Group"
isEmpty={Boolean(
!oidcConfig?.group_mapping ||
Object.entries(oidcConfig?.group_mapping).length === 0,
)}
>
{oidcConfig?.user_role_mapping &&
Object.entries(oidcConfig.group_mapping)
.sort()
.map(([idpGroup, group]) => (
<GroupRow
key={idpGroup}
idpGroup={idpGroup}
coderGroup={group}
<IdpField
name={"Regex Filter"}
fieldText={
typeof groupSyncSettings?.regex_filter === "string"
? groupSyncSettings.regex_filter
: "none"
}
/>
))}
</IdpMappingTable>
<IdpField
name={"Auto Create"}
fieldText={String(
groupSyncSettings?.auto_create_missing_groups || "n/a",
)}
/>
</Stack>
</div>
<Stack
direction="row"
alignItems="baseline"
justifyContent="space-between"
css={styles.tableInfo}
>
<TableRowCount count={groupMappingCount} type="groups" />
<ExportPolicyButton
syncSettings={groupSyncSettings}
organization={organization}
type="groups"
/>
</Stack>
<Stack spacing={6}>
<IdpMappingTable
type="Group"
isEmpty={Boolean(groupMappingCount === 0)}
>
{groupSyncSettings?.mapping &&
Object.entries(groupSyncSettings.mapping)
.sort()
.map(([idpGroup, groups]) => (
<GroupRow
key={idpGroup}
idpGroup={idpGroup}
coderGroup={getGroupNames(groups)}
/>
))}
</IdpMappingTable>
</Stack>
</>
) : (
<>
<div css={styles.fields}>
<IdpField
name={"Sync Field"}
fieldText={roleSyncSettings?.field}
showDisabled
/>
</div>
<Stack
direction="row"
alignItems="baseline"
justifyContent="space-between"
css={styles.tableInfo}
>
<TableRowCount count={roleMappingCount} type="roles" />
<ExportPolicyButton
syncSettings={roleSyncSettings}
organization={organization}
type="roles"
/>
</Stack>
<IdpMappingTable
type="Role"
isEmpty={Boolean(roleMappingCount === 0)}
>
{roleSyncSettings?.mapping &&
Object.entries(roleSyncSettings.mapping)
.sort()
.map(([idpRole, roles]) => (
<RoleRow
key={idpRole}
idpRole={idpRole}
coderRoles={roles}
/>
))}
</IdpMappingTable>
</>
)}
</Stack>
</Cond>
</ChooseOne>
@@ -125,37 +187,66 @@ export const IdpSyncPageView: FC<IdpSyncPageViewProps> = ({ oidcConfig }) => {
interface IdpFieldProps {
name: string;
fieldText: string | undefined;
showStatusIndicator?: boolean;
showDisabled?: boolean;
}
const IdpField: FC<IdpFieldProps> = ({
name,
fieldText,
showStatusIndicator = false,
showDisabled = false,
}) => {
return (
<span css={{ display: "flex", alignItems: "center", gap: "16px" }}>
<h4>{name}</h4>
<p css={styles.field}>
{fieldText ||
(showStatusIndicator && (
<div
css={{
display: "flex",
alignItems: "center",
gap: "8px",
height: 0,
}}
>
<StatusIndicator color="error" />
<p>disabled</p>
</div>
))}
</p>
<span
css={{
display: "flex",
alignItems: "center",
gap: "16px",
}}
>
<p css={styles.fieldLabel}>{name}</p>
{fieldText ? (
<p css={styles.fieldText}>{fieldText}</p>
) : (
showDisabled && (
<div
css={{
display: "flex",
alignItems: "center",
gap: "8px",
height: 0,
}}
>
<StatusIndicator color="error" />
<p>disabled</p>
</div>
)
)}
</span>
);
};
interface TableRowCountProps {
count: number;
type: string;
}
const TableRowCount: FC<TableRowCountProps> = ({ count, type }) => {
return (
<div
css={(theme) => ({
margin: 0,
fontSize: 13,
color: theme.palette.text.secondary,
"& strong": {
color: theme.palette.text.primary,
},
})}
>
Showing <strong>{count}</strong> {type}
</div>
);
};
interface IdpMappingTableProps {
type: "Role" | "Group";
isEmpty: boolean;
@@ -194,7 +285,9 @@ const IdpMappingTable: FC<IdpMappingTableProps> = ({
<Button
startIcon={<LaunchOutlined />}
component="a"
href={docs("/admin/auth#group-sync-enterprise")}
href={docs(
`/admin/auth#${type.toLowerCase()}-sync-enterprise`,
)}
target="_blank"
>
How to setup IdP {type} sync
@@ -215,28 +308,32 @@ const IdpMappingTable: FC<IdpMappingTableProps> = ({
interface GroupRowProps {
idpGroup: string;
coderGroup: string;
coderGroup: readonly string[];
}
const GroupRow: FC<GroupRowProps> = ({ idpGroup, coderGroup }) => {
return (
<TableRow data-testid={`group-${idpGroup}`}>
<TableCell>{idpGroup}</TableCell>
<TableCell>{coderGroup}</TableCell>
<TableCell>
<IdpPillList roles={coderGroup} />
</TableCell>
</TableRow>
);
};
interface RoleRowProps {
idpRole: string;
coderRoles: ReadonlyArray<string>;
coderRoles: readonly string[];
}
const RoleRow: FC<RoleRowProps> = ({ idpRole, coderRoles }) => {
return (
<TableRow data-testid={`role-${idpRole}`}>
<TableCell>{idpRole}</TableCell>
<TableCell>coderRoles Placeholder</TableCell>
<TableCell>
<IdpPillList roles={coderRoles} />
</TableCell>
</TableRow>
);
};
@@ -260,22 +357,20 @@ const TableLoader = () => {
};
const styles = {
field: (theme) => ({
color: theme.palette.text.secondary,
fieldText: (theme) => ({
fontFamily: MONOSPACE_FONT_FAMILY,
whiteSpace: "nowrap",
paddingBottom: ".02rem",
}),
fieldLabel: (theme) => ({
color: theme.palette.text.secondary,
}),
fields: () => ({
marginBottom: "60px",
marginLeft: 16,
fontSize: 14,
}),
legend: () => ({
padding: "0px 6px",
fontWeight: 600,
}),
box: (theme) => ({
border: "1px solid",
borderColor: theme.palette.divider,
padding: "0px 20px",
borderRadius: 8,
tableInfo: () => ({
marginBottom: 16,
}),
} satisfies Record<string, Interpolation<Theme>>;
@@ -297,7 +297,7 @@ const OrganizationSettingsNavigation: FC<
Provisioners
</SidebarNavSubItem>
)}
{organization.permissions.editMembers && (
{organization.permissions.viewIdpSyncSettings && (
<SidebarNavSubItem
href={urlForSubpage(organization.name, "idp-sync")}
>
+48 -32
View File
@@ -451,38 +451,6 @@ export const MockAssignableSiteRoles = [
assignableRole(MockAuditorRole, true),
];
export const MockOIDCConfig: TypesGen.OIDCConfig = {
allow_signups: true,
client_id: "test",
client_secret: "test",
client_key_file: "test",
client_cert_file: "test",
email_domain: [],
issuer_url: "test",
scopes: [],
ignore_email_verified: true,
username_field: "",
name_field: "",
email_field: "",
auth_url_params: {},
ignore_user_info: true,
organization_field: "",
organization_mapping: {},
organization_assign_default: true,
group_auto_create: false,
group_regex_filter: "^Coder-.*$",
group_allow_list: [],
groups_field: "groups",
group_mapping: { group1: "developers", group2: "admin", group3: "auditors" },
user_role_field: "roles",
user_role_mapping: { role1: ["role1", "role2"] },
user_roles_default: [],
sign_in_text: "",
icon_url: "",
signups_disabled_text: "string",
skip_issuer_checks: true,
};
export const MockMemberPermissions = {
viewAuditLog: false,
};
@@ -2632,6 +2600,40 @@ export const MockWorkspaceQuota: TypesGen.WorkspaceQuota = {
budget: 100,
};
export const MockGroupSyncSettings: TypesGen.GroupSyncSettings = {
field: "group-test",
mapping: {
"idp-group-1": [
"fbd2116a-8961-4954-87ae-e4575bd29ce0",
"13de3eb4-9b4f-49e7-b0f8-0c3728a0d2e2",
],
"idp-group-2": ["fbd2116a-8961-4954-87ae-e4575bd29ce0"],
},
regex_filter: "@[a-zA-Z0-9_]+",
auto_create_missing_groups: false,
};
export const MockGroupSyncSettings2: TypesGen.GroupSyncSettings = {
field: "group-test",
mapping: {
"idp-group-1": [
"fbd2116a-8961-4954-87ae-e4575bd29ce0",
"13de3eb4-9b4f-49e7-b0f8-0c3728a0d2e3",
],
"idp-group-2": ["fbd2116a-8961-4954-87ae-e4575bd29ce2"],
},
regex_filter: "@[a-zA-Z0-9_]+",
auto_create_missing_groups: false,
};
export const MockRoleSyncSettings: TypesGen.RoleSyncSettings = {
field: "role-test",
mapping: {
"idp-role-1": ["admin", "developer"],
"idp-role-2": ["auditor"],
},
};
export const MockGroup: TypesGen.Group = {
id: "fbd2116a-8961-4954-87ae-e4575bd29ce0",
name: "Front-End",
@@ -2646,6 +2648,20 @@ export const MockGroup: TypesGen.Group = {
total_member_count: 2,
};
export const MockGroup2: TypesGen.Group = {
id: "13de3eb4-9b4f-49e7-b0f8-0c3728a0d2e2",
name: "developer",
display_name: "",
avatar_url: "https://example.com",
organization_id: MockOrganization.id,
organization_name: MockOrganization.name,
organization_display_name: MockOrganization.display_name,
members: [MockUser, MockUser2],
quota_allowance: 5,
source: "user",
total_member_count: 2,
};
const MockEveryoneGroup: TypesGen.Group = {
// The "Everyone" group must have the same ID as a the organization it belongs
// to.