Skip to content

Commit bcfeb72

Browse files
authored
feat: show warning on unrecognized idp org mapping claims (#16478)
1 parent 33a89ab commit bcfeb72

File tree

10 files changed

+113
-91
lines changed

10 files changed

+113
-91
lines changed

site/src/api/api.ts

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -698,7 +698,7 @@ class ApiMethods {
698698
}
699699

700700
const response = await this.axios.get<TypesGen.ProvisionerDaemon[]>(
701-
`/api/v2/organizations/${organization}/provisionerdaemons?${params.toString()}`,
701+
`/api/v2/organizations/${organization}/provisionerdaemons?${params}`,
702702
);
703703
return response.data;
704704
};
@@ -787,19 +787,25 @@ class ApiMethods {
787787
return response.data;
788788
};
789789

790-
getIdpSyncClaimFieldValues = async (claimField: string) => {
791-
const response = await this.axios.get<string[]>(
792-
`/api/v2/settings/idpsync/field-values?claimField=${claimField}`,
790+
getDeploymentIdpSyncFieldValues = async (
791+
field: string,
792+
): Promise<readonly string[]> => {
793+
const params = new URLSearchParams();
794+
params.set("claimField", field);
795+
const response = await this.axios.get<readonly string[]>(
796+
`/api/v2/settings/idpsync/field-values?${params}`,
793797
);
794798
return response.data;
795799
};
796800

797-
getIdpSyncClaimFieldValuesByOrganization = async (
801+
getOrganizationIdpSyncClaimFieldValues = async (
798802
organization: string,
799-
claimField: string,
803+
field: string,
800804
) => {
805+
const params = new URLSearchParams();
806+
params.set("claimField", field);
801807
const response = await this.axios.get<TypesGen.Response>(
802-
`/api/v2/organizations/${organization}/settings/idpsync/field-values?claimField=${claimField}`,
808+
`/api/v2/organizations/${organization}/settings/idpsync/field-values?${params}`,
803809
);
804810
return response.data;
805811
};

site/src/api/queries/deployment.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,3 +29,10 @@ export const deploymentSSHConfig = () => {
2929
queryFn: API.getDeploymentSSHConfig,
3030
};
3131
};
32+
33+
export const deploymentIdpSyncFieldValues = (field: string) => {
34+
return {
35+
queryKey: ["deployment", "idpSync", "fieldValues", field],
36+
queryFn: () => API.getDeploymentIdpSyncFieldValues(field),
37+
};
38+
};

site/src/api/queries/organizations.ts

Lines changed: 5 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -341,32 +341,16 @@ export const organizationsPermissions = (
341341

342342
export const getOrganizationIdpSyncClaimFieldValuesKey = (
343343
organization: string,
344-
claimField: string,
345-
) => [organization, claimField, "organizationIdpSyncClaimFieldValues"];
344+
field: string,
345+
) => [organization, "idpSync", "fieldValues", field];
346346

347347
export const organizationIdpSyncClaimFieldValues = (
348348
organization: string,
349-
claimField: string,
349+
field: string,
350350
) => {
351351
return {
352-
queryKey: getOrganizationIdpSyncClaimFieldValuesKey(
353-
organization,
354-
claimField,
355-
),
352+
queryKey: getOrganizationIdpSyncClaimFieldValuesKey(organization, field),
356353
queryFn: () =>
357-
API.getIdpSyncClaimFieldValuesByOrganization(organization, claimField),
358-
};
359-
};
360-
361-
export const getIdpSyncClaimFieldValuesKey = (claimField: string) => [
362-
claimField,
363-
"idpSyncClaimFieldValues",
364-
];
365-
366-
export const idpSyncClaimFieldValues = (claimField: string) => {
367-
return {
368-
queryKey: getIdpSyncClaimFieldValuesKey(claimField),
369-
queryFn: () => API.getIdpSyncClaimFieldValues(claimField),
370-
enabled: !!claimField,
354+
API.getOrganizationIdpSyncClaimFieldValues(organization, field),
371355
};
372356
};

site/src/components/Combobox/Combobox.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import { cn } from "utils/cn";
1818

1919
interface ComboboxProps {
2020
value: string;
21-
options?: string[];
21+
options?: readonly string[];
2222
placeholder?: string;
2323
open: boolean;
2424
onOpenChange: (open: boolean) => void;

site/src/components/Tooltip/Tooltip.stories.tsx

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -12,16 +12,12 @@ const meta: Meta<typeof TooltipProvider> = {
1212
component: TooltipProvider,
1313
args: {
1414
children: (
15-
<>
16-
<TooltipProvider>
17-
<Tooltip open>
18-
<TooltipTrigger asChild>
19-
<Button variant="outline">Hover</Button>
20-
</TooltipTrigger>
21-
<TooltipContent>Add to library</TooltipContent>
22-
</Tooltip>
23-
</TooltipProvider>
24-
</>
15+
<Tooltip open>
16+
<TooltipTrigger asChild>
17+
<Button variant="outline">Hover</Button>
18+
</TooltipTrigger>
19+
<TooltipContent>Add to library</TooltipContent>
20+
</Tooltip>
2521
),
2622
},
2723
};

site/src/pages/DeploymentSettingsPage/IdpOrgSyncPage/IdpOrgSyncPage.tsx

Lines changed: 19 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import { getErrorMessage } from "api/errors";
2+
import { deploymentIdpSyncFieldValues } from "api/queries/deployment";
23
import {
34
organizationIdpSyncSettings,
45
patchOrganizationSyncSettings,
56
} from "api/queries/idpsync";
6-
import { idpSyncClaimFieldValues } from "api/queries/organizations";
77
import { ChooseOne, Cond } from "components/Conditionals/ChooseOne";
88
import { displayError } from "components/GlobalSnackbar/utils";
99
import { displaySuccess } from "components/GlobalSnackbar/utils";
@@ -21,26 +21,23 @@ import { ExportPolicyButton } from "./ExportPolicyButton";
2121
import IdpOrgSyncPageView from "./IdpOrgSyncPageView";
2222

2323
export const IdpOrgSyncPage: FC = () => {
24-
const [claimField, setClaimField] = useState("");
2524
const queryClient = useQueryClient();
2625
// IdP sync does not have its own entitlement and is based on templace_rbac
2726
const { template_rbac: isIdpSyncEnabled } = useFeatureVisibility();
2827
const { organizations } = useDashboard();
29-
const {
30-
data: orgSyncSettingsData,
31-
isLoading,
32-
error,
33-
} = useQuery({
34-
...organizationIdpSyncSettings(isIdpSyncEnabled),
35-
onSuccess: (data) => {
36-
if (data?.field) {
37-
setClaimField(data.field);
38-
}
39-
},
40-
});
28+
const settingsQuery = useQuery(organizationIdpSyncSettings(isIdpSyncEnabled));
4129

42-
const { data: claimFieldValues } = useQuery(
43-
idpSyncClaimFieldValues(claimField),
30+
const [field, setField] = useState("");
31+
useEffect(() => {
32+
if (!settingsQuery.data) {
33+
return;
34+
}
35+
36+
setField(settingsQuery.data.field);
37+
}, [settingsQuery.data]);
38+
39+
const fieldValuesQuery = useQuery(
40+
field ? deploymentIdpSyncFieldValues(field) : { enabled: false },
4441
);
4542

4643
const patchOrganizationSyncSettingsMutation = useMutation(
@@ -58,14 +55,10 @@ export const IdpOrgSyncPage: FC = () => {
5855
}
5956
}, [patchOrganizationSyncSettingsMutation.error]);
6057

61-
if (isLoading) {
58+
if (settingsQuery.isLoading) {
6259
return <Loader />;
6360
}
6461

65-
const handleSyncFieldChange = (value: string) => {
66-
setClaimField(value);
67-
};
68-
6962
return (
7063
<>
7164
<Helmet>
@@ -84,7 +77,7 @@ export const IdpOrgSyncPage: FC = () => {
8477
</Link>
8578
</p>
8679
</div>
87-
<ExportPolicyButton syncSettings={orgSyncSettingsData} />
80+
<ExportPolicyButton syncSettings={settingsQuery.data} />
8881
</header>
8982
<ChooseOne>
9083
<Cond condition={!isIdpSyncEnabled}>
@@ -96,8 +89,10 @@ export const IdpOrgSyncPage: FC = () => {
9689
</Cond>
9790
<Cond>
9891
<IdpOrgSyncPageView
99-
organizationSyncSettings={orgSyncSettingsData}
92+
organizationSyncSettings={settingsQuery.data}
93+
claimFieldValues={fieldValuesQuery.data}
10094
organizations={organizations}
95+
onSyncFieldChange={setField}
10196
onSubmit={async (data) => {
10297
try {
10398
await patchOrganizationSyncSettingsMutation.mutateAsync(data);
@@ -111,9 +106,7 @@ export const IdpOrgSyncPage: FC = () => {
111106
);
112107
}
113108
}}
114-
onSyncFieldChange={handleSyncFieldChange}
115-
claimFieldValues={claimFieldValues}
116-
error={error || patchOrganizationSyncSettingsMutation.error}
109+
error={settingsQuery.error || fieldValuesQuery.error}
117110
/>
118111
</Cond>
119112
</ChooseOne>

site/src/pages/DeploymentSettingsPage/IdpOrgSyncPage/IdpOrgSyncPageView.stories.tsx

Lines changed: 17 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -5,48 +5,49 @@ import {
55
MockOrganization2,
66
MockOrganizationSyncSettings,
77
MockOrganizationSyncSettings2,
8+
MockOrganizationSyncSettingsEmpty,
89
} from "testHelpers/entities";
910
import { IdpOrgSyncPageView } from "./IdpOrgSyncPageView";
1011

1112
const meta: Meta<typeof IdpOrgSyncPageView> = {
1213
title: "pages/IdpOrgSyncPageView",
1314
component: IdpOrgSyncPageView,
15+
args: {
16+
organizationSyncSettings: MockOrganizationSyncSettings2,
17+
claimFieldValues: Object.keys(MockOrganizationSyncSettings2.mapping),
18+
organizations: [MockOrganization, MockOrganization2],
19+
error: undefined,
20+
},
1421
};
1522

1623
export default meta;
1724
type Story = StoryObj<typeof IdpOrgSyncPageView>;
1825

1926
export const Empty: Story = {
2027
args: {
21-
organizationSyncSettings: {
22-
field: "",
23-
mapping: {},
24-
organization_assign_default: true,
25-
},
26-
organizations: [MockOrganization, MockOrganization2],
27-
error: undefined,
28+
organizationSyncSettings: MockOrganizationSyncSettingsEmpty,
2829
},
2930
};
3031

31-
export const Default: Story = {
32-
args: {
33-
organizationSyncSettings: MockOrganizationSyncSettings2,
34-
organizations: [MockOrganization, MockOrganization2],
35-
error: undefined,
36-
},
37-
};
32+
export const Default: Story = {};
3833

3934
export const HasError: Story = {
4035
args: {
41-
...Default.args,
4236
error: "This is a test error",
4337
},
4438
};
4539

4640
export const MissingGroups: Story = {
4741
args: {
48-
...Default.args,
4942
organizationSyncSettings: MockOrganizationSyncSettings,
43+
claimFieldValues: Object.keys(MockOrganizationSyncSettings.mapping),
44+
organizations: [],
45+
},
46+
};
47+
48+
export const MissingClaim: Story = {
49+
args: {
50+
claimFieldValues: [],
5051
},
5152
};
5253

site/src/pages/DeploymentSettingsPage/IdpOrgSyncPage/IdpOrgSyncPageView.tsx

Lines changed: 37 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { TooltipProvider } from "@radix-ui/react-tooltip";
12
import type {
23
Organization,
34
OrganizationSyncSettings,
@@ -28,12 +29,8 @@ import {
2829
MultiSelectCombobox,
2930
type Option,
3031
} from "components/MultiSelectCombobox/MultiSelectCombobox";
31-
import {
32-
Popover,
33-
PopoverContent,
34-
PopoverTrigger,
35-
} from "components/Popover/Popover";
3632
import { Spinner } from "components/Spinner/Spinner";
33+
import { Stack } from "components/Stack/Stack";
3734
import { Switch } from "components/Switch/Switch";
3835
import {
3936
Table,
@@ -42,21 +39,25 @@ import {
4239
TableHeader,
4340
TableRow,
4441
} from "components/Table/Table";
42+
import {
43+
Tooltip,
44+
TooltipContent,
45+
TooltipTrigger,
46+
} from "components/Tooltip/Tooltip";
4547
import { useFormik } from "formik";
46-
import { Check, ChevronDown, CornerDownLeft, Plus, Trash } from "lucide-react";
48+
import { Plus, Trash, TriangleAlert } from "lucide-react";
4749
import { type FC, type KeyboardEventHandler, useId, useState } from "react";
48-
import { cn } from "utils/cn";
4950
import { docs } from "utils/docs";
5051
import { isUUID } from "utils/uuid";
5152
import * as Yup from "yup";
5253
import { OrganizationPills } from "./OrganizationPills";
5354

5455
interface IdpSyncPageViewProps {
5556
organizationSyncSettings: OrganizationSyncSettings | undefined;
57+
claimFieldValues: readonly string[] | undefined;
5658
organizations: readonly Organization[];
5759
onSubmit: (data: OrganizationSyncSettings) => void;
5860
onSyncFieldChange: (value: string) => void;
59-
claimFieldValues: string[] | undefined;
6061
error?: unknown;
6162
}
6263

@@ -84,10 +85,10 @@ const validationSchema = Yup.object({
8485

8586
export const IdpOrgSyncPageView: FC<IdpSyncPageViewProps> = ({
8687
organizationSyncSettings,
88+
claimFieldValues,
8789
organizations,
8890
onSubmit,
8991
onSyncFieldChange,
90-
claimFieldValues,
9192
error,
9293
}) => {
9394
const form = useFormik<OrganizationSyncSettings>({
@@ -313,6 +314,7 @@ export const IdpOrgSyncPageView: FC<IdpSyncPageViewProps> = ({
313314
idpOrg={idpOrg}
314315
coderOrgs={getOrgNames(organizations)}
315316
onDelete={handleDelete}
317+
exists={claimFieldValues?.includes(idpOrg)}
316318
/>
317319
))}
318320
</IdpMappingTable>
@@ -398,18 +400,43 @@ const IdpMappingTable: FC<IdpMappingTableProps> = ({ isEmpty, children }) => {
398400

399401
interface OrganizationRowProps {
400402
idpOrg: string;
403+
exists: boolean | undefined;
401404
coderOrgs: readonly string[];
402405
onDelete: (idpOrg: string) => void;
403406
}
404407

405408
const OrganizationRow: FC<OrganizationRowProps> = ({
406409
idpOrg,
410+
exists = true,
407411
coderOrgs,
408412
onDelete,
409413
}) => {
410414
return (
411415
<TableRow data-testid={`idp-org-${idpOrg}`}>
412-
<TableCell>{idpOrg}</TableCell>
416+
<TableCell>
417+
<div className="flex flex-row items-center gap-2 text-content-primary">
418+
{idpOrg}
419+
{!exists && (
420+
<TooltipProvider>
421+
<Tooltip>
422+
<TooltipTrigger asChild>
423+
<TriangleAlert className="size-icon-xs cursor-pointer text-content-warning" />
424+
</TooltipTrigger>
425+
<TooltipContent
426+
align="start"
427+
alignOffset={-8}
428+
sideOffset={8}
429+
className="p-2 text-xs text-content-secondary max-w-sm"
430+
>
431+
This value has not be seen in the specified claim field
432+
before. You might want to check your IdP configuration and
433+
ensure that this value is not misspelled.
434+
</TooltipContent>
435+
</Tooltip>
436+
</TooltipProvider>
437+
)}
438+
</div>
439+
</TableCell>
413440
<TableCell>
414441
<OrganizationPills organizations={coderOrgs} />
415442
</TableCell>

0 commit comments

Comments
 (0)