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
This commit is contained in:
Eduard Gert
2024-03-27 15:55:56 +01:00
committed by GitHub
parent cb922b46b7
commit f4a2d6fae8
15 changed files with 1014 additions and 14 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

View File

@@ -5,7 +5,9 @@ import * as React from "react";
type Props = {
data: {
label: string;
value: string;
value: string | React.ReactNode;
noCopy?: boolean;
tooltip?: boolean;
}[];
className?: string;
};
@@ -16,10 +18,11 @@ export const MinimalList = ({ data, className }: Props) => {
{data.map((item, index) => {
return (
<Card.ListItem
copy
copy={!item.noCopy}
label={item.label}
value={item.value}
key={index}
tooltip={item.tooltip !== false}
/>
);
})}

View File

@@ -17,6 +17,14 @@ export interface AzureADIntegration {
user_group_prefixes: string[];
}
export interface OktaIntegration {
id: string;
enabled: boolean;
group_prefixes: string[];
user_group_prefixes: string[];
auth_token: string;
}
export interface IdentityProviderLog {
id: number;
level: string;

View File

@@ -10,6 +10,7 @@ import IntegrationIcon from "@/assets/icons/IntegrationIcon";
import { useAccount } from "@/modules/account/useAccount";
import { AzureAD } from "@/modules/integrations/idp-sync/azure-ad/AzureAD";
import { GoogleWorkspace } from "@/modules/integrations/idp-sync/google-workspace/GoogleWorkspace";
import { Okta } from "@/modules/integrations/idp-sync/okta-scim/Okta";
import { useIntegrations } from "@/modules/integrations/idp-sync/useIntegrations";
export default function IdentityProviderTab() {
@@ -40,7 +41,10 @@ export default function IdentityProviderTab() {
</Paragraph>
<Paragraph>
Learn more about{" "}
<InlineLink href={"#"} target={"_blank"}>
<InlineLink
href={"https://docs.netbird.io/how-to/idp-sync"}
target={"_blank"}
>
Identity Provider
<ExternalLinkIcon size={12} />
</InlineLink>
@@ -51,23 +55,24 @@ export default function IdentityProviderTab() {
<>
<SkeletonIntegration loadingHeight={196} />
<SkeletonIntegration loadingHeight={196} />
<SkeletonIntegration loadingHeight={196} />
</>
) : (
<>
<GoogleWorkspace />
<AzureAD />
<Okta />
</>
)}
</div>
<div className={"flex flex-col gap-6 max-w-md mt-10"}>
<div className={"flex flex-col gap-6 max-w-lg mt-10"}>
<div
className={
"bg-netbird-950 px-6 py-4 rounded-md border border-netbird-500 "
}
>
<Label className={"!text-netbird-100 text-md"}>
Looking to enable a custom Identity Provider like Okta or
Jumpcloud?
Looking to enable a custom IDP like Jumpcloud?
</Label>
<p className={"!text-netbird-200 mt-2"}>
Please contact us at{" "}

View File

@@ -4,9 +4,10 @@ import { notify } from "@components/Notification";
import { SkeletonIntegration } from "@components/skeletons/SkeletonIntegration";
import useFetchApi, { useApiCall } from "@utils/api";
import dayjs from "dayjs";
import { isEmpty } from "lodash";
import { RefreshCw, Settings } from "lucide-react";
import * as React from "react";
import { useEffect, useState } from "react";
import { useEffect, useMemo, useState } from "react";
import { useSWRConfig } from "swr";
import integrationImage from "@/assets/integrations/entra-id.png";
import {
@@ -116,6 +117,11 @@ const ConfigurationButton = ({ config }: ConfigurationProps) => {
});
};
const lastSync = useMemo(() => {
if (isEmpty(logs)) return "Not synchronized";
return "Synced " + dayjs().to(logs?.[0]?.timestamp);
}, [logs]);
return (
<>
<div className={"flex gap-2"}>
@@ -137,7 +143,7 @@ const ConfigurationButton = ({ config }: ConfigurationProps) => {
disabled={!config.enabled}
>
<RefreshCw size={14} />
Synced {dayjs().to(logs?.[0]?.timestamp)}
{lastSync}
</Button>
</FullTooltip>

View File

@@ -0,0 +1,131 @@
import Button from "@components/Button";
import { notify } from "@components/Notification";
import { SkeletonIntegration } from "@components/skeletons/SkeletonIntegration";
import useFetchApi, { useApiCall } from "@utils/api";
import dayjs from "dayjs";
import { RefreshCw, Settings } from "lucide-react";
import * as React from "react";
import { useEffect, useState } from "react";
import { useSWRConfig } from "swr";
import integrationImage from "@/assets/integrations/okta.png";
import {
IdentityProviderLog,
OktaIntegration,
} from "@/interfaces/IdentityProvider";
import OktaConfiguration from "@/modules/integrations/idp-sync/okta-scim/OktaConfiguration";
import OktaSetup from "@/modules/integrations/idp-sync/okta-scim/OktaSetup";
import { useIntegrations } from "@/modules/integrations/idp-sync/useIntegrations";
import { IntegrationCard } from "@/modules/integrations/IntegrationCard";
export const Okta = () => {
const { mutate } = useSWRConfig();
const [setupModal, setSetupModal] = useState(false);
const {
okta: integration,
isAnyIntegrationEnabled,
isOktaLoading,
} = useIntegrations();
const oktaRequest = useApiCall<OktaIntegration>(
"/integrations/okta-scim-idp",
);
const [enabled, setEnabled] = useState(
integration ? integration.enabled : false,
);
useEffect(() => {
setEnabled(integration?.enabled ?? false);
}, [integration]);
const toggleSwitch = async (state: boolean) => {
if (!integration) return setSetupModal(true);
notify({
title: "Okta Integration",
description: `Okta was successfully ${state ? "enabled" : "disabled"}`,
promise: oktaRequest
.put(
{
enabled: state,
},
"/" + integration.id,
)
.then(() => {
mutate("/integrations/okta-scim-idp");
setEnabled(state);
}),
loadingMessage: "Updating integration...",
});
};
return isOktaLoading ? (
<SkeletonIntegration loadingHeight={197} />
) : (
<>
<IntegrationCard
name="Okta"
description="Okta is a platform to provision and manage user accounts in cloud-based applications."
url={{
title: "okta.com",
href: "https://www.okta.com/",
}}
image={integrationImage}
data={integration}
disabled={enabled ? false : isAnyIntegrationEnabled}
switchState={enabled}
onEnabledChange={toggleSwitch}
onSetup={() => setSetupModal(true)}
>
{integration && <ConfigurationButton config={integration} />}
</IntegrationCard>
<OktaSetup
open={setupModal}
onOpenChange={setSetupModal}
onSuccess={() => {
setEnabled(true);
mutate("/integrations/okta-scim-idp");
}}
/>
</>
);
};
type ConfigurationProps = {
config: OktaIntegration;
};
const ConfigurationButton = ({ config }: ConfigurationProps) => {
const { data: logs } = useFetchApi<IdentityProviderLog[]>(
`/integrations/okta-scim-idp/${config.id}/logs`,
);
const [configModal, setConfigModal] = useState(false);
return (
<>
<div className={"flex gap-2"}>
<Button
variant={"default-outline"}
size={"xs"}
className={"w-full items-center pointer-events-none"}
disabled={!config.enabled}
>
<RefreshCw size={14} />
Synced {dayjs().to(logs?.[0]?.timestamp)}
</Button>
<Button
variant={"secondary"}
size={"xs"}
className={"items-center"}
onClick={() => {
setConfigModal(true);
}}
>
<Settings size={14} />
</Button>
</div>
<OktaConfiguration open={configModal} onOpenChange={setConfigModal} />
</>
);
};

View File

@@ -0,0 +1,333 @@
import Button from "@components/Button";
import Card from "@components/Card";
import HelpText from "@components/HelpText";
import { Label } from "@components/Label";
import {
Modal,
ModalClose,
ModalContent,
ModalFooter,
} from "@components/modal/Modal";
import { notify } from "@components/Notification";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@components/Tabs";
import { GradientFadedBackground } from "@components/ui/GradientFadedBackground";
import { useHasChanges } from "@hooks/useHasChanges";
import { useApiCall } from "@utils/api";
import { cn } from "@utils/helpers";
import {
AlertOctagon,
Cog,
FolderGit2,
KeyRound,
RefreshCcw,
UserCircle,
} from "lucide-react";
import React, { useState } from "react";
import { useSWRConfig } from "swr";
import integrationImage from "@/assets/integrations/entra-id.png";
import { useDialog } from "@/contexts/DialogProvider";
import { OktaIntegration } from "@/interfaces/IdentityProvider";
import { GroupPrefixInput } from "@/modules/integrations/idp-sync/GroupPrefixInput";
import { useIntegrations } from "@/modules/integrations/idp-sync/useIntegrations";
import { IntegrationModalHeader } from "@/modules/integrations/IntegrationModalHeader";
type Props = {
open: boolean;
onOpenChange: (open: boolean) => void;
onSuccess?: () => void;
};
export default function OktaConfiguration({
open,
onOpenChange,
onSuccess,
}: Props) {
const { okta } = useIntegrations();
return (
<>
<Modal open={open} onOpenChange={onOpenChange} key={open ? 1 : 0}>
{okta && (
<ConfigurationContent
onSuccess={() => {
onOpenChange(false);
onSuccess && onSuccess();
}}
config={okta}
/>
)}
</Modal>
</>
);
}
type ModalProps = {
onSuccess: () => void;
config: OktaIntegration;
};
export function ConfigurationContent({ onSuccess, config }: ModalProps) {
const { mutate } = useSWRConfig();
const { confirm } = useDialog();
const [tab, setTab] = useState<string>("settings");
const oktaRequest = useApiCall<OktaIntegration>(
"/integrations/okta-scim-idp",
);
const clientSecretPlaceholder = "******************************";
const [authToken, setAuthToken] = useState(
config.auth_token || clientSecretPlaceholder,
);
const [groupPrefixes, setGroupPrefixes] = useState<string[]>(
config.group_prefixes || [],
);
const [userGroupPrefixes, setUserGroupPrefixes] = useState<string[]>(
config.user_group_prefixes || [],
);
const { hasChanges, updateRef } = useHasChanges([
authToken,
groupPrefixes,
userGroupPrefixes,
]);
const regenerateAuthToken = async () => {
const choice = await confirm({
title: `Regenerate Auth Token?`,
description:
"Are you sure you want to regenerate the auth token? You will need to update the token in your Okta configuration.",
confirmText: "Regenerate",
cancelText: "Cancel",
type: "default",
});
if (!choice) return;
notify({
title: "Okta Integration",
description: `Auth token for Okta was successfully regenerated`,
promise: oktaRequest.post({}, `/${config.id}/token`).then((r) => {
mutate("/integrations/okta-scim-idp");
setAuthToken(r.auth_token);
updateRef([r.auth_token, groupPrefixes, userGroupPrefixes]);
}),
loadingMessage: "Updating your auth token...",
});
};
const deleteIntegration = async () => {
const choice = await confirm({
title: `Delete integration?`,
description: "Are you sure you want to delete this integration?",
confirmText: "Delete",
cancelText: "Cancel",
type: "danger",
});
if (!choice) return;
notify({
title: "Okta Integration",
description: `Okta was successfully deleted`,
promise: oktaRequest.del({}, `/${config.id}`).then(() => {
mutate("/integrations/okta-scim-idp");
onSuccess();
}),
loadingMessage: "Deleting integration...",
});
};
const updateIntegration = async () => {
notify({
title: "Okta Integration",
description: `Okta was successfully updated`,
promise: oktaRequest
.put(
{
group_prefixes: groupPrefixes || [],
user_group_prefixes: userGroupPrefixes || [],
},
`/${config.id}`,
)
.then(() => {
mutate("/integrations/okta-scim-idp");
onSuccess();
}),
loadingMessage: "Updating integration...",
});
};
return (
<ModalContent
maxWidthClass={cn("relative max-w-xl")}
showClose={true}
className={""}
autoFocus={false}
>
<GradientFadedBackground />
<IntegrationModalHeader
image={integrationImage}
title={"Okta Configuration"}
description={"Sync your users and groups from Okta to NetBird."}
/>
<Tabs
defaultValue={tab}
onValueChange={(v) => setTab(v)}
className={"mt-6"}
>
<TabsList justify={"start"} className={"px-8"}>
<TabsTrigger value={"settings"}>
<Cog
size={16}
className={
"text-nb-gray-500 group-data-[state=active]/trigger:text-netbird transition-all"
}
/>
Settings
</TabsTrigger>
<TabsTrigger value={"group-sync"}>
<FolderGit2
size={16}
className={
"text-nb-gray-500 group-data-[state=active]/trigger:text-netbird transition-all"
}
/>
Group Sync
</TabsTrigger>
<TabsTrigger value={"user-sync"}>
<UserCircle
size={16}
className={
"text-nb-gray-500 group-data-[state=active]/trigger:text-netbird transition-all"
}
/>
User Sync
</TabsTrigger>
<TabsTrigger value={"danger"}>
<AlertOctagon
size={16}
className={
"text-nb-gray-500 group-data-[state=active]/trigger:text-netbird transition-all"
}
/>
Danger Zone
</TabsTrigger>
</TabsList>
<TabsContent value={"settings"} className={"px-8 text-sm"}>
<div className={"flex-col gap-3 flex"}>
<Card className={"w-full"}>
<Card.List>
<Card.ListItem
copy={!authToken.includes("*")}
copyText={"Auth token"}
label={
<>
<KeyRound size={16} />
Auth Token
</>
}
value={authToken}
/>
</Card.List>
</Card>
<Button variant={"secondary"} onClick={regenerateAuthToken}>
<RefreshCcw size={16} />
Regenerate Auth Token
</Button>
</div>
</TabsContent>
<TabsContent value={"group-sync"} className={"px-8"}>
<div>
<Label>
<div className={"flex gap-2 items-center"}>
<FolderGit2 size={16} />
Synchronize Groups
</div>
</Label>
<HelpText className={"max-w-lg mt-2"}>
By default,{" "}
<span className={"text-netbird font-semibold"}>All Groups</span>{" "}
will be synchronized from your IdP to NetBird. <br />
If you want to synchronize only groups that start with a specific
prefix, you can add them below.
</HelpText>
</div>
<GroupPrefixInput value={groupPrefixes} onChange={setGroupPrefixes} />
</TabsContent>
<TabsContent value={"user-sync"} className={"px-8"}>
<div>
<Label>
<div className={"flex gap-2 items-center"}>
<UserCircle size={16} />
Synchronize Users
</div>
</Label>
<HelpText className={"max-w-lg mt-2"}>
By default,{" "}
<span className={"text-netbird font-semibold"}>All Users</span>{" "}
will be synchronized from your IdP to NetBird. <br />
If you want to synchronize only users that belong to a specific
group, you can add them below.
</HelpText>
</div>
<GroupPrefixInput
addText={"Add user group filter"}
text={"User group starts with..."}
value={userGroupPrefixes}
onChange={setUserGroupPrefixes}
/>
</TabsContent>
<TabsContent value={"danger"} className={"px-8"}>
<div>
<Label>
<div className={"flex gap-2 items-center"}>
<AlertOctagon size={16} />
Delete Integration
</div>
</Label>
<HelpText className={"max-w-lg mt-2"}>
Deleting this integration will remove the ability to sync users
and groups from your IdP to NetBird. If you delete the integration
you will need to reconfigure it again to enable the
synchronization.
</HelpText>
</div>
<Button
variant={"danger"}
size={"xs"}
className={"mt-3"}
onClick={deleteIntegration}
>
Delete Integration
</Button>
</TabsContent>
</Tabs>
<div className={"h-6"}></div>
<ModalFooter className={"items-center gap-4"}>
<ModalClose asChild={true}>
<Button variant={"secondary"} className={"w-full"}>
Cancel
</Button>
</ModalClose>
<Button
variant={"primary"}
className={"w-full"}
disabled={!hasChanges}
onClick={updateIntegration}
>
Save
</Button>
</ModalFooter>
</ModalContent>
);
}

View File

@@ -0,0 +1,491 @@
import Button from "@components/Button";
import { Modal, ModalContent, ModalFooter } from "@components/modal/Modal";
import Steps from "@components/Steps";
import { GradientFadedBackground } from "@components/ui/GradientFadedBackground";
import { Lightbox } from "@components/ui/Lightbox";
import { Mark } from "@components/ui/Mark";
import { MinimalList } from "@components/ui/MinimalList";
import {
IconArrowLeft,
IconArrowRight,
IconCirclePlus,
} from "@tabler/icons-react";
import { useApiCall } from "@utils/api";
import { cn } from "@utils/helpers";
import { isEmpty } from "lodash";
import {
Box,
Clock4,
FolderGit2,
PlusCircle,
Settings2,
Share2,
Shield,
UserCircle,
} from "lucide-react";
import React, { useEffect, useState } from "react";
import integrationImage from "@/assets/integrations/okta.png";
import { OktaIntegration } from "@/interfaces/IdentityProvider";
import oktaGroupsAssignments from "@/modules/integrations/idp-sync/okta-scim/images/okta-groups-assignments.png";
import oktaSAMLConfig from "@/modules/integrations/idp-sync/okta-scim/images/okta-saml-configuration.png";
import oktaSCIMProvisioning from "@/modules/integrations/idp-sync/okta-scim/images/okta-scim-provisioning-enabled.png";
import oktaSCIMToApp from "@/modules/integrations/idp-sync/okta-scim/images/okta-scim-to-app-sync-enabled.png";
import oktaSyncGroups from "@/modules/integrations/idp-sync/okta-scim/images/okta-sync-groups.png";
import { IntegrationModalHeader } from "@/modules/integrations/IntegrationModalHeader";
type Props = {
open: boolean;
onOpenChange: (open: boolean) => void;
onSuccess?: () => void;
};
export default function OktaSetup({ open, onOpenChange, onSuccess }: Props) {
const [authToken, setAuthToken] = useState("");
return (
<>
<Modal open={open} onOpenChange={onOpenChange}>
{open && (
<SetupContent
authToken={authToken}
setAuthToken={setAuthToken}
onSuccess={() => {
onOpenChange(false);
onSuccess && onSuccess();
}}
/>
)}
</Modal>
</>
);
}
type ModalProps = {
onSuccess: () => void;
authToken: string;
setAuthToken: (token: string) => void;
};
export function SetupContent({
onSuccess,
authToken,
setAuthToken,
}: ModalProps) {
const integrationsRequest = useApiCall<OktaIntegration[]>(
"/integrations/okta-scim-idp",
);
const authTokenRequest = useApiCall<OktaIntegration>(
"/integrations/okta-scim-idp",
).post;
const [step, setStep] = useState(0);
const maxSteps = 7;
// const [groupPrefixes, setGroupPrefixes] = useState<string[]>([]);
// const [userGroupPrefixes, setUserGroupPrefixes] = useState<string[]>([]);
useEffect(() => {
const getAuthToken = async () => {
const integration = await integrationsRequest.get();
if (!isEmpty(integration)) {
const integrationId = integration[0].id;
if (authToken != "") return authToken;
const okta = await authTokenRequest({}, `${integrationId}/token`);
if (!okta) return "";
return okta.auth_token;
} else {
const okta = await authTokenRequest({});
if (!okta) return "";
return okta.auth_token;
}
};
getAuthToken().then((t) => setAuthToken(t));
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return (
<ModalContent
maxWidthClass={cn(
"relative",
step == 0 ? "max-w-md" : step == 4 ? "max-w-3xl" : "max-w-2xl",
)}
showClose={true}
className={""}
onEscapeKeyDown={(e) => step > 0 && e.preventDefault()}
onInteractOutside={(e) => step > 0 && e.preventDefault()}
onPointerDownOutside={(e) => step > 0 && e.preventDefault()}
>
<GradientFadedBackground />
{step > 0 && (
<div className={"flex gap-2 w-full items-center justify-center mb-4"}>
{Array.from({ length: maxSteps }).map((_, index) => (
<div
key={index}
className={cn(
"w-8 h-1 rounded-full bg-nb-gray-800",
step >= index + 1 && "bg-netbird",
)}
/>
))}
</div>
)}
<IntegrationModalHeader
image={integrationImage}
title={"Connect NetBird with Okta"}
description={
"Start syncing your users and groups from Okta to NetBird. Follow the steps below to get started."
}
/>
{step == 0 && (
<div
className={
"px-8 py-3 flex z-0 flex-col gap-0 text-sm mb-3 text-center justify-center items-center"
}
>
<div
className={
"mt-6 text-base font-medium text-nb-gray-100 flex gap-2 items-center justify-center"
}
>
<Shield size={16} />
Required Permissions
</div>
<p className={"mt-2 !text-nb-gray-300 !leading-[1.5]"}>
Ensure that you have an an{" "}
<span className={"text-nb-gray-100 font-semibold"}>
Okta user account
</span>{" "}
with the following{" "}
<span className={"text-nb-gray-100 font-semibold"}>
permissions
</span>
.{" "}
{
"If you don't have the required permissions, ask your Okta administrator to grant them to you."
}
</p>
<div
className={
"flex items-center flex-col gap-0 mt-2 w-full justify-center max-w-lg"
}
>
<div
className={
"py-2 px-6 flex items-center gap-2 rounded-md w-full justify-center bg-nb-gray-930/0 text-nb-gray-200"
}
>
<PlusCircle size={14} className={"text-sky-500"} />
Add Okta applications
</div>
<div
className={
"py-2 px-6 flex items-center gap-2 rounded-md w-full justify-center bg-nb-gray-930/0 text-nb-gray-200"
}
>
<Settings2 size={14} className={"text-sky-500"} />
Configure Okta applications
</div>
</div>
</div>
)}
{step == 1 && (
<div className={"px-8 py-3 flex flex-col gap-0 mt-4"}>
<p className={"font-medium flex gap-3 items-center text-base"}>
<Box size={20} />
Create and configure Okta application
</p>
<Steps>
<Steps.Step step={1}>
<p>Navigate to your Okta Admin Dashboard</p>
</Steps.Step>
<Steps.Step step={2}>
<p className={"font-normal"}>
Click <Mark>Applications</Mark> in the left menu then click on
<Mark>Applications</Mark>
</p>
</Steps.Step>
<Steps.Step step={3} line={false}>
<p className={"font-normal"}>
Click <Mark>Create App Integration</Mark>, select{" "}
<Mark>SAML 2.0</Mark> and click <Mark>Next</Mark>
</p>
</Steps.Step>
</Steps>
</div>
)}
{step == 2 && (
<div className={"px-8 py-3 flex flex-col gap-0 mt-4"}>
<p className={"font-medium flex gap-3 items-center text-base"}>
<IconCirclePlus size={20} />
Create SAML Integration
</p>
<Steps>
<Steps.Step step={1}>
<p className={"font-normal"}>
Enter <Mark copy>NetBird SCIM</Mark> as the{" "}
<Mark>App name</Mark> and click <Mark>Next</Mark>
</p>
</Steps.Step>
<Steps.Step step={2}>
<p className={"font-normal"}>
Enter <Mark copy>http://localhost</Mark> as the Single sign-on
URL and Audience URI (SP Entity ID) and click <Mark>Next</Mark>
</p>
<Lightbox image={oktaSAMLConfig} />
</Steps.Step>
<Steps.Step step={3} line={false}>
<p className={"font-normal"}>
Select App type as
<Mark>This is an internal app that we have created</Mark>
and click <Mark>Finish</Mark>
</p>
</Steps.Step>
</Steps>
</div>
)}
{step == 3 && (
<div className={"px-8 py-3 flex flex-col gap-0 mt-4"}>
<p className={"font-medium flex gap-3 items-center text-base"}>
<Share2 size={20} />
Enable and configure SCIM provisioning
</p>
<Steps>
<Steps.Step step={1}>
<p>Navigate to your Okta Admin Dashboard</p>
</Steps.Step>
<Steps.Step step={2}>
<p className={"font-normal"}>
Click <Mark>Applications</Mark> in the left menu then click on
<Mark>Applications</Mark>
</p>
</Steps.Step>
<Steps.Step step={3}>
<p className={"font-normal"}>
Select the <Mark>NetBird SCIM</Mark> application we created
earlier
</p>
</Steps.Step>
<Steps.Step step={4}>
<p className={"font-normal"}>
Click <Mark>General</Mark> tab and in <Mark>App Settings</Mark>{" "}
click <Mark>Edit</Mark> to update the settings
</p>
</Steps.Step>
<Steps.Step step={5} line={false}>
<p className={"font-normal"}>
Tick <Mark>Enable SCIM provisioning</Mark> and click{" "}
<Mark>Save</Mark>
</p>
<Lightbox image={oktaSCIMProvisioning} />
</Steps.Step>
</Steps>
</div>
)}
{step == 4 && (
<div className={"px-8 py-3 flex flex-col gap-0 mt-4"}>
<p className={"font-medium flex gap-3 items-center text-base"}>
<Share2 size={20} />
Enable and configure SCIM provisioning
</p>
<Steps>
<Steps.Step step={1}>
<p className={"font-normal"}>
Click <Mark>Provisioning</Mark> tab and under{" "}
<Mark>SCIM connection</Mark> click
<Mark>Edit</Mark>
</p>
</Steps.Step>
<Steps.Step step={2}>
<p className={"font-normal"}>
Fill in the form with the following details
</p>
<MinimalList
data={[
{
label: "SCIM connector base URL",
value: "https://api.netbird.io/api/scim/v2",
},
{
label: "Unique identifier field for users",
value: "userName",
},
{
label: "Supported provisioning actions",
value: (
<div className={"text-right"}>
Push New Users <br />
Push Profile Updates <br />
Push Groups
</div>
),
noCopy: true,
},
{
label: "Authentication Mode",
value: "HTTP Header",
noCopy: true,
},
{
label: "Authorization (Bearer)",
value: authToken,
},
]}
/>
</Steps.Step>
<Steps.Step step={3}>
<p className={"font-normal"}>
Click on <Mark>Test Connector Configuration</Mark> to verify if
the SCIM configuration is working. After the test is completed,
make sure <Mark>Create Users</Mark>,{" "}
<Mark>Update User Attributes</Mark>, and{" "}
<Mark>Push Groups</Mark> were successful.
</p>
</Steps.Step>
<Steps.Step step={4} line={false}>
<p className={"font-normal"}>
Click <Mark>Save</Mark>
</p>
</Steps.Step>
</Steps>
</div>
)}
{step == 5 && (
<div className={"px-8 py-3 flex flex-col gap-0 mt-4"}>
<p className={"font-medium flex gap-3 items-center text-base"}>
<Box size={20} />
Configure SCIM provisioning to NetBird
</p>
<Steps>
<Steps.Step step={1}>
<p>
Go to the <Mark>Provisioning</Mark> tab, and select the{" "}
<Mark>To App</Mark> settings and click <Mark>Edit</Mark>
</p>
</Steps.Step>
<Steps.Step step={2} line={false}>
<p className={"font-normal"}>
Enable <Mark>Create Users</Mark>,{" "}
<Mark>Update User Attributes</Mark>, and{" "}
<Mark>Deactivate Users</Mark> and click <Mark>Save</Mark>
</p>
<Lightbox image={oktaSCIMToApp} />
</Steps.Step>
</Steps>
</div>
)}
{step == 6 && (
<div className={"px-8 py-3 flex flex-col gap-0 mt-4"}>
<p className={"font-medium flex gap-3 items-center text-base"}>
<UserCircle size={20} />
Assign members to NetBird
</p>
<Steps>
<Steps.Step step={1}>
<p>
Go to the <Mark>Assignments</Mark> tab, select the{" "}
<Mark>Assign</Mark> and click <Mark>Assign to Groups</Mark>
</p>
<Lightbox image={oktaGroupsAssignments} />
</Steps.Step>
<Steps.Step step={2}>
<p className={"font-normal"}>
Select the groups you want to provision, and then select{" "}
<Mark>Assign</Mark> and click <Mark>Save and Go Back</Mark>
</p>
</Steps.Step>
<Steps.Step step={3} line={false}>
<p className={"font-normal"}>
Select <Mark>Done</Mark> after you have finished assigning
groups. At this point, all members of the groups assigned to the
application will be synced to NetBird.
</p>
</Steps.Step>
</Steps>
</div>
)}
{step == 7 && (
<div className={"px-8 py-3 flex flex-col gap-0 mt-4"}>
<p className={"font-medium flex gap-3 items-center text-base"}>
<FolderGit2 size={20} />
Assign groups to NetBird
</p>
<Steps>
<Steps.Step step={1}>
<p>
Go to the <Mark>Push Groups</Mark> tab, select{" "}
<Mark>Push Groups</Mark> and click{" "}
<Mark>Find groups by name</Mark>
</p>
<Lightbox image={oktaSyncGroups} />
</Steps.Step>
<Steps.Step step={2} line={false}>
<p className={"font-normal"}>
Search groups to push and then click <Mark>Save</Mark>. The
selected groups will then be synced to NetBird.
</p>
</Steps.Step>
</Steps>
</div>
)}
<ModalFooter className={"items-center gap-4"}>
{step > 0 && (
<Button
variant={"secondary"}
className={"w-full"}
onClick={() => setStep(step - 1)}
>
<IconArrowLeft size={16} />
Back
</Button>
)}
{step >= 0 && step < maxSteps && (
<Button
variant={"primary"}
className={"w-full"}
onClick={() => setStep(step + 1)}
disabled={authToken == ""}
>
{step == 0 ? "Get Started" : "Continue"}
<IconArrowRight size={16} />
</Button>
)}
{step == maxSteps && (
<Button
variant={"primary"}
className={"w-full"}
onClick={() => {
onSuccess();
}}
>
Finish Setup
</Button>
)}
</ModalFooter>
{step == 0 && (
<div
className={
"text-center z-0 mt-2.5 text-xs text-nb-gray-300 flex items-center justify-center gap-2 font-normal"
}
>
<Clock4 size={12} />
<div>
Estimated setup time:
<span className={"font-medium"}> 10-20 Minutes</span>
</div>
</div>
)}
</ModalContent>
);
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 93 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

View File

@@ -2,6 +2,7 @@ import useFetchApi from "@utils/api";
import {
AzureADIntegration,
GoogleWorkspaceIntegration,
OktaIntegration,
} from "@/interfaces/IdentityProvider";
export const useIntegrations = () => {
@@ -11,17 +12,24 @@ export const useIntegrations = () => {
const { data: googleIntegrations, isLoading: isGoogleLoading } = useFetchApi<
GoogleWorkspaceIntegration[]
>("/integrations/google-idp");
const { data: oktaIntegration, isLoading: isOktaLoading } = useFetchApi<
OktaIntegration[]
>("/integrations/okta-scim-idp");
const azure = azureIntegrations?.[0];
const google = googleIntegrations?.[0];
const okta = oktaIntegration?.[0];
const isAnyIntegrationEnabled = azure?.enabled || google?.enabled;
const isAnyIntegrationEnabled =
azure?.enabled || google?.enabled || okta?.enabled;
return {
azure,
google,
okta,
isAnyIntegrationEnabled,
isAzureLoading,
isGoogleLoading,
isOktaLoading,
};
};

View File

@@ -26,16 +26,28 @@ async function apiRequest<T>(
data?: any,
) {
const origin = config.apiOrigin;
const res = await oidcFetch(`${origin}/api${url}`, {
method,
body: JSON.stringify(data),
});
if (!res.ok) {
const error = (await res.json()) as ErrorResponse;
return Promise.reject(error);
}
return (await res.json()) as T;
try {
if (!res.ok) {
const error = (await res.json()) as ErrorResponse;
return Promise.reject(error);
}
return (await res.json()) as T;
} catch (e) {
if (!res.ok) {
const error = {
code: res.status,
message: res.statusText,
} as ErrorResponse;
return Promise.reject(error);
}
return res;
}
}
export function useNetBirdFetch(ignoreError: boolean = false) {
@@ -160,6 +172,9 @@ export function useApiErrorHandling(ignoreError = false) {
if (err.code == 500 && err.message == "internal server error") {
return setError(err);
}
if (err.code > 400 && err.code <= 500) {
return setError(err);
}
return Promise.reject(err);
};