Skip to content

Commit f4a2d6f

Browse files
authored
Add Okta SCIM integration (#361)
* Add Okta integration (wip) * Update okta setup dialog * Add okta integration images * Add error handling for 500 status codes * Add okta integration * Fix lint warnings * Update azures last sync time * Remove 'on' from step, disable copy for HTTP Header * Update text for custom IDP
1 parent cb922b4 commit f4a2d6f

File tree

15 files changed

+1014
-14
lines changed

15 files changed

+1014
-14
lines changed

src/assets/integrations/okta.png

9.37 KB
Loading

src/components/ui/MinimalList.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@ import * as React from "react";
55
type Props = {
66
data: {
77
label: string;
8-
value: string;
8+
value: string | React.ReactNode;
9+
noCopy?: boolean;
10+
tooltip?: boolean;
911
}[];
1012
className?: string;
1113
};
@@ -16,10 +18,11 @@ export const MinimalList = ({ data, className }: Props) => {
1618
{data.map((item, index) => {
1719
return (
1820
<Card.ListItem
19-
copy
21+
copy={!item.noCopy}
2022
label={item.label}
2123
value={item.value}
2224
key={index}
25+
tooltip={item.tooltip !== false}
2326
/>
2427
);
2528
})}

src/interfaces/IdentityProvider.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,14 @@ export interface AzureADIntegration {
1717
user_group_prefixes: string[];
1818
}
1919

20+
export interface OktaIntegration {
21+
id: string;
22+
enabled: boolean;
23+
group_prefixes: string[];
24+
user_group_prefixes: string[];
25+
auth_token: string;
26+
}
27+
2028
export interface IdentityProviderLog {
2129
id: number;
2230
level: string;

src/modules/integrations/idp-sync/IdentityProviderTab.tsx

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import IntegrationIcon from "@/assets/icons/IntegrationIcon";
1010
import { useAccount } from "@/modules/account/useAccount";
1111
import { AzureAD } from "@/modules/integrations/idp-sync/azure-ad/AzureAD";
1212
import { GoogleWorkspace } from "@/modules/integrations/idp-sync/google-workspace/GoogleWorkspace";
13+
import { Okta } from "@/modules/integrations/idp-sync/okta-scim/Okta";
1314
import { useIntegrations } from "@/modules/integrations/idp-sync/useIntegrations";
1415

1516
export default function IdentityProviderTab() {
@@ -40,7 +41,10 @@ export default function IdentityProviderTab() {
4041
</Paragraph>
4142
<Paragraph>
4243
Learn more about{" "}
43-
<InlineLink href={"#"} target={"_blank"}>
44+
<InlineLink
45+
href={"https://docs.netbird.io/how-to/idp-sync"}
46+
target={"_blank"}
47+
>
4448
Identity Provider
4549
<ExternalLinkIcon size={12} />
4650
</InlineLink>
@@ -51,23 +55,24 @@ export default function IdentityProviderTab() {
5155
<>
5256
<SkeletonIntegration loadingHeight={196} />
5357
<SkeletonIntegration loadingHeight={196} />
58+
<SkeletonIntegration loadingHeight={196} />
5459
</>
5560
) : (
5661
<>
5762
<GoogleWorkspace />
5863
<AzureAD />
64+
<Okta />
5965
</>
6066
)}
6167
</div>
62-
<div className={"flex flex-col gap-6 max-w-md mt-10"}>
68+
<div className={"flex flex-col gap-6 max-w-lg mt-10"}>
6369
<div
6470
className={
6571
"bg-netbird-950 px-6 py-4 rounded-md border border-netbird-500 "
6672
}
6773
>
6874
<Label className={"!text-netbird-100 text-md"}>
69-
Looking to enable a custom Identity Provider like Okta or
70-
Jumpcloud?
75+
Looking to enable a custom IDP like Jumpcloud?
7176
</Label>
7277
<p className={"!text-netbird-200 mt-2"}>
7378
Please contact us at{" "}

src/modules/integrations/idp-sync/azure-ad/AzureAD.tsx

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,10 @@ import { notify } from "@components/Notification";
44
import { SkeletonIntegration } from "@components/skeletons/SkeletonIntegration";
55
import useFetchApi, { useApiCall } from "@utils/api";
66
import dayjs from "dayjs";
7+
import { isEmpty } from "lodash";
78
import { RefreshCw, Settings } from "lucide-react";
89
import * as React from "react";
9-
import { useEffect, useState } from "react";
10+
import { useEffect, useMemo, useState } from "react";
1011
import { useSWRConfig } from "swr";
1112
import integrationImage from "@/assets/integrations/entra-id.png";
1213
import {
@@ -116,6 +117,11 @@ const ConfigurationButton = ({ config }: ConfigurationProps) => {
116117
});
117118
};
118119

120+
const lastSync = useMemo(() => {
121+
if (isEmpty(logs)) return "Not synchronized";
122+
return "Synced " + dayjs().to(logs?.[0]?.timestamp);
123+
}, [logs]);
124+
119125
return (
120126
<>
121127
<div className={"flex gap-2"}>
@@ -137,7 +143,7 @@ const ConfigurationButton = ({ config }: ConfigurationProps) => {
137143
disabled={!config.enabled}
138144
>
139145
<RefreshCw size={14} />
140-
Synced {dayjs().to(logs?.[0]?.timestamp)}
146+
{lastSync}
141147
</Button>
142148
</FullTooltip>
143149

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
import Button from "@components/Button";
2+
import { notify } from "@components/Notification";
3+
import { SkeletonIntegration } from "@components/skeletons/SkeletonIntegration";
4+
import useFetchApi, { useApiCall } from "@utils/api";
5+
import dayjs from "dayjs";
6+
import { RefreshCw, Settings } from "lucide-react";
7+
import * as React from "react";
8+
import { useEffect, useState } from "react";
9+
import { useSWRConfig } from "swr";
10+
import integrationImage from "@/assets/integrations/okta.png";
11+
import {
12+
IdentityProviderLog,
13+
OktaIntegration,
14+
} from "@/interfaces/IdentityProvider";
15+
import OktaConfiguration from "@/modules/integrations/idp-sync/okta-scim/OktaConfiguration";
16+
import OktaSetup from "@/modules/integrations/idp-sync/okta-scim/OktaSetup";
17+
import { useIntegrations } from "@/modules/integrations/idp-sync/useIntegrations";
18+
import { IntegrationCard } from "@/modules/integrations/IntegrationCard";
19+
20+
export const Okta = () => {
21+
const { mutate } = useSWRConfig();
22+
const [setupModal, setSetupModal] = useState(false);
23+
24+
const {
25+
okta: integration,
26+
isAnyIntegrationEnabled,
27+
isOktaLoading,
28+
} = useIntegrations();
29+
const oktaRequest = useApiCall<OktaIntegration>(
30+
"/integrations/okta-scim-idp",
31+
);
32+
33+
const [enabled, setEnabled] = useState(
34+
integration ? integration.enabled : false,
35+
);
36+
37+
useEffect(() => {
38+
setEnabled(integration?.enabled ?? false);
39+
}, [integration]);
40+
41+
const toggleSwitch = async (state: boolean) => {
42+
if (!integration) return setSetupModal(true);
43+
44+
notify({
45+
title: "Okta Integration",
46+
description: `Okta was successfully ${state ? "enabled" : "disabled"}`,
47+
promise: oktaRequest
48+
.put(
49+
{
50+
enabled: state,
51+
},
52+
"/" + integration.id,
53+
)
54+
.then(() => {
55+
mutate("/integrations/okta-scim-idp");
56+
setEnabled(state);
57+
}),
58+
loadingMessage: "Updating integration...",
59+
});
60+
};
61+
62+
return isOktaLoading ? (
63+
<SkeletonIntegration loadingHeight={197} />
64+
) : (
65+
<>
66+
<IntegrationCard
67+
name="Okta"
68+
description="Okta is a platform to provision and manage user accounts in cloud-based applications."
69+
url={{
70+
title: "okta.com",
71+
href: "https://www.okta.com/",
72+
}}
73+
image={integrationImage}
74+
data={integration}
75+
disabled={enabled ? false : isAnyIntegrationEnabled}
76+
switchState={enabled}
77+
onEnabledChange={toggleSwitch}
78+
onSetup={() => setSetupModal(true)}
79+
>
80+
{integration && <ConfigurationButton config={integration} />}
81+
</IntegrationCard>
82+
<OktaSetup
83+
open={setupModal}
84+
onOpenChange={setSetupModal}
85+
onSuccess={() => {
86+
setEnabled(true);
87+
mutate("/integrations/okta-scim-idp");
88+
}}
89+
/>
90+
</>
91+
);
92+
};
93+
94+
type ConfigurationProps = {
95+
config: OktaIntegration;
96+
};
97+
const ConfigurationButton = ({ config }: ConfigurationProps) => {
98+
const { data: logs } = useFetchApi<IdentityProviderLog[]>(
99+
`/integrations/okta-scim-idp/${config.id}/logs`,
100+
);
101+
102+
const [configModal, setConfigModal] = useState(false);
103+
104+
return (
105+
<>
106+
<div className={"flex gap-2"}>
107+
<Button
108+
variant={"default-outline"}
109+
size={"xs"}
110+
className={"w-full items-center pointer-events-none"}
111+
disabled={!config.enabled}
112+
>
113+
<RefreshCw size={14} />
114+
Synced {dayjs().to(logs?.[0]?.timestamp)}
115+
</Button>
116+
117+
<Button
118+
variant={"secondary"}
119+
size={"xs"}
120+
className={"items-center"}
121+
onClick={() => {
122+
setConfigModal(true);
123+
}}
124+
>
125+
<Settings size={14} />
126+
</Button>
127+
</div>
128+
<OktaConfiguration open={configModal} onOpenChange={setConfigModal} />
129+
</>
130+
);
131+
};

0 commit comments

Comments
 (0)