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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy