diff --git a/frontend/src/component/admin/auth/AuthSettings.tsx b/frontend/src/component/admin/auth/AuthSettings.tsx
index a3343150a4b0..db1eb4aaa93b 100644
--- a/frontend/src/component/admin/auth/AuthSettings.tsx
+++ b/frontend/src/component/admin/auth/AuthSettings.tsx
@@ -11,11 +11,15 @@ import { ADMIN } from '@server/types/permissions';
import { PremiumFeature } from 'component/common/PremiumFeature/PremiumFeature';
import { useState } from 'react';
import { TabPanel } from 'component/common/TabNav/TabPanel/TabPanel';
+import { useUiFlag } from 'hooks/useUiFlag';
+import { ScimSettings } from './ScimSettings/ScimSettings';
export const AuthSettings = () => {
const { authenticationType } = useUiConfig().uiConfig;
const { uiConfig } = useUiConfig();
+ const scimEnabled = useUiFlag('scimApi');
+
const tabs = [
{
label: 'OpenID Connect',
@@ -36,6 +40,14 @@ export const AuthSettings = () => {
].filter(
(item) => uiConfig.flags?.googleAuthEnabled || item.label !== 'Google',
);
+
+ if (scimEnabled) {
+ tabs.push({
+ label: 'Provisioning (SCIM)',
+ component: ,
+ });
+ }
+
const [activeTab, setActiveTab] = useState(0);
return (
diff --git a/frontend/src/component/admin/auth/ScimSettings/ScimSettings.tsx b/frontend/src/component/admin/auth/ScimSettings/ScimSettings.tsx
new file mode 100644
index 000000000000..7b386c45e582
--- /dev/null
+++ b/frontend/src/component/admin/auth/ScimSettings/ScimSettings.tsx
@@ -0,0 +1,144 @@
+import type React from 'react';
+import { useEffect, useState } from 'react';
+import { Button, FormControlLabel, Grid, Switch } from '@mui/material';
+import { Alert } from '@mui/material';
+import useToast from 'hooks/useToast';
+import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
+import { useScimSettings } from 'hooks/api/getters/useScimSettings/useScimSettings';
+import { useScimSettingsApi } from 'hooks/api/actions/useScimSettingsApi/useScimSettingsApi';
+import { formatUnknownError } from 'utils/formatUnknownError';
+import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
+import { ScimTokenGenerationDialog } from './ScimTokenGenerationDialog';
+import { ScimTokenDialog } from './ScimTokenDialog';
+
+export const ScimSettings = () => {
+ const { setToastData, setToastApiError } = useToast();
+ const { uiConfig } = useUiConfig();
+ const { settings, refetch } = useScimSettings();
+ const { saveSettings, generateNewToken, errors, loading } =
+ useScimSettingsApi();
+
+ const [enabled, setEnabled] = useState(false);
+
+ const [tokenGenerationDialog, setTokenGenerationDialog] = useState(false);
+ const [tokenDialog, setTokenDialog] = useState(false);
+ const [newToken, setNewToken] = useState('');
+
+ useEffect(() => {
+ setEnabled(settings.enabled ?? false);
+ }, [settings]);
+
+ const onSubmit = async (event: React.SyntheticEvent) => {
+ event.preventDefault();
+
+ try {
+ await saveSettings({ enabled });
+ if (enabled && !settings.hasToken) {
+ const token = await generateNewToken();
+ setNewToken(token);
+ setTokenDialog(true);
+ }
+
+ setToastData({
+ title: 'Settings stored',
+ type: 'success',
+ });
+ refetch();
+ } catch (error: unknown) {
+ setToastApiError(formatUnknownError(error));
+ }
+ };
+
+ const onGenerateNewToken = async () => {
+ setTokenGenerationDialog(true);
+ };
+
+ const onGenerateNewTokenConfirm = async () => {
+ setTokenGenerationDialog(false);
+ const token = await generateNewToken();
+ setNewToken(token);
+ setTokenDialog(true);
+ };
+
+ return (
+ <>
+
+
+
+ Please read the{' '}
+
+ documentation
+ {' '}
+ to learn how to integrate with specific SCIM clients
+ (Microsoft Entra, Okta, etc).
+ SCIM API URL: {uiConfig.unleashUrl}/scim
+
+
+
+
+
+
+ >
+ );
+};
diff --git a/frontend/src/component/admin/auth/ScimSettings/ScimTokenDialog.tsx b/frontend/src/component/admin/auth/ScimSettings/ScimTokenDialog.tsx
new file mode 100644
index 000000000000..6558bc9fc6fe
--- /dev/null
+++ b/frontend/src/component/admin/auth/ScimSettings/ScimTokenDialog.tsx
@@ -0,0 +1,37 @@
+import { Alert, styled, Typography } from '@mui/material';
+import { UserToken } from 'component/admin/apiToken/ConfirmToken/UserToken/UserToken';
+import { Dialogue } from 'component/common/Dialogue/Dialogue';
+
+const StyledAlert = styled(Alert)(({ theme }) => ({
+ marginBottom: theme.spacing(3),
+}));
+
+interface IScimTokenDialogProps {
+ open: boolean;
+ setOpen: React.Dispatch>;
+ token?: string;
+}
+
+export const ScimTokenDialog = ({
+ open,
+ setOpen,
+ token,
+}: IScimTokenDialogProps) => (
+ {
+ if (!muiCloseReason) {
+ setOpen(false);
+ }
+ }}
+ title='SCIM API token created'
+ >
+
+ Make sure to copy your SCIM API token now. You won't be able to see
+ it again!
+
+ Your token:
+
+
+);
diff --git a/frontend/src/component/admin/auth/ScimSettings/ScimTokenGenerationDialog.tsx b/frontend/src/component/admin/auth/ScimSettings/ScimTokenGenerationDialog.tsx
new file mode 100644
index 000000000000..92a23330bd27
--- /dev/null
+++ b/frontend/src/component/admin/auth/ScimSettings/ScimTokenGenerationDialog.tsx
@@ -0,0 +1,37 @@
+import { Alert, styled } from '@mui/material';
+import { Dialogue } from 'component/common/Dialogue/Dialogue';
+
+const StyledAlert = styled(Alert)(({ theme }) => ({
+ marginBottom: theme.spacing(3),
+}));
+
+interface IScimTokenGenerationDialogProps {
+ open: boolean;
+ setOpen: React.Dispatch>;
+ onConfirm: () => void;
+}
+
+export const ScimTokenGenerationDialog = ({
+ open,
+ setOpen,
+ onConfirm,
+}: IScimTokenGenerationDialogProps) => (
+ {
+ if (!muiCloseReason) {
+ setOpen(false);
+ }
+ }}
+ primaryButtonText='Generate new token'
+ onClick={onConfirm}
+ title='Generate new SCIM API token?'
+ >
+
+ Generating a new token will immediately revoke the
+ current one, which may break any existing provision integrations
+ currently using it.
+
+
+);
diff --git a/frontend/src/hooks/api/actions/useScimSettingsApi/useScimSettingsApi.ts b/frontend/src/hooks/api/actions/useScimSettingsApi/useScimSettingsApi.ts
new file mode 100644
index 000000000000..72a511131444
--- /dev/null
+++ b/frontend/src/hooks/api/actions/useScimSettingsApi/useScimSettingsApi.ts
@@ -0,0 +1,48 @@
+import useAPI from '../useApi/useApi';
+import type { ScimSettings } from 'hooks/api/getters/useScimSettings/useScimSettings';
+
+const ENDPOINT = 'api/admin/scim-settings';
+
+export type ScimSettingsPayload = Omit;
+
+export const useScimSettingsApi = () => {
+ const { loading, makeRequest, createRequest, errors } = useAPI({
+ propagateErrors: true,
+ });
+
+ const saveSettings = async (scimSettings: ScimSettingsPayload) => {
+ const requestId = 'saveSettings';
+ const req = createRequest(
+ ENDPOINT,
+ {
+ method: 'POST',
+ body: JSON.stringify(scimSettings),
+ },
+ requestId,
+ );
+
+ await makeRequest(req.caller, req.id);
+ };
+
+ const generateNewToken = async (): Promise => {
+ const requestId = 'generateNewToken';
+ const req = createRequest(
+ `${ENDPOINT}/generate-new-token`,
+ {
+ method: 'POST',
+ },
+ requestId,
+ );
+
+ const response = await makeRequest(req.caller, req.id);
+ const { token } = await response.json();
+ return token;
+ };
+
+ return {
+ saveSettings,
+ generateNewToken,
+ errors,
+ loading,
+ };
+};
diff --git a/frontend/src/hooks/api/getters/useScimSettings/useScimSettings.ts b/frontend/src/hooks/api/getters/useScimSettings/useScimSettings.ts
new file mode 100644
index 000000000000..4b366ae2a7d0
--- /dev/null
+++ b/frontend/src/hooks/api/getters/useScimSettings/useScimSettings.ts
@@ -0,0 +1,46 @@
+import { useMemo } from 'react';
+import { formatApiPath } from 'utils/formatPath';
+import handleErrorResponses from '../httpErrorResponseHandler';
+import { useConditionalSWR } from '../useConditionalSWR/useConditionalSWR';
+import useUiConfig from '../useUiConfig/useUiConfig';
+import { useUiFlag } from 'hooks/useUiFlag';
+
+const ENDPOINT = 'api/admin/scim-settings';
+
+export type ScimSettings = {
+ enabled: boolean;
+ hasToken: boolean;
+};
+
+const DEFAULT_DATA: ScimSettings = {
+ enabled: false,
+ hasToken: false,
+};
+
+export const useScimSettings = () => {
+ const { isEnterprise } = useUiConfig();
+ const scimEnabled = useUiFlag('scimApi');
+
+ const { data, error, mutate } = useConditionalSWR(
+ isEnterprise() && scimEnabled,
+ DEFAULT_DATA,
+ formatApiPath(ENDPOINT),
+ fetcher,
+ );
+
+ return useMemo(
+ () => ({
+ settings: data ?? DEFAULT_DATA,
+ loading: !error && !data,
+ refetch: () => mutate(),
+ error,
+ }),
+ [data, error, mutate],
+ );
+};
+
+const fetcher = (path: string) => {
+ return fetch(path)
+ .then(handleErrorResponses('SCIM settings'))
+ .then((res) => res.json());
+};
diff --git a/frontend/src/interfaces/uiConfig.ts b/frontend/src/interfaces/uiConfig.ts
index 188980fd4e0d..c127d5d5cff6 100644
--- a/frontend/src/interfaces/uiConfig.ts
+++ b/frontend/src/interfaces/uiConfig.ts
@@ -81,6 +81,7 @@ export type UiFlags = {
variantDependencies?: boolean;
projectOverviewRefactorFeedback?: boolean;
featureLifecycle?: boolean;
+ scimApi?: boolean;
};
export interface IVersionInfo {