mirror of
https://github.com/Unleash/unleash.git
synced 2025-02-14 00:19:16 +01:00
feat: scim assume control UI - move scim into sso configs (#6929)
- Adds support for the configuration option for SCIM taking over control of users and groups - Moves SCIM settings into SSO config pages (OIDC and SAML). SCIM registers a callback to be invoked when saving in a parent SSO config page
This commit is contained in:
parent
19055b1e33
commit
d1bb65bebd
@ -11,15 +11,11 @@ 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',
|
||||
@ -41,13 +37,6 @@ export const AuthSettings = () => {
|
||||
(item) => uiConfig.flags?.googleAuthEnabled || item.label !== 'Google',
|
||||
);
|
||||
|
||||
if (scimEnabled) {
|
||||
tabs.push({
|
||||
label: 'Provisioning (SCIM)',
|
||||
component: <ScimSettings />,
|
||||
});
|
||||
}
|
||||
|
||||
const [activeTab, setActiveTab] = useState(0);
|
||||
|
||||
return (
|
||||
|
@ -21,6 +21,10 @@ import { formatUnknownError } from 'utils/formatUnknownError';
|
||||
import { removeEmptyStringFields } from 'utils/removeEmptyStringFields';
|
||||
import { SsoGroupSettings } from '../SsoGroupSettings';
|
||||
import type { IRole } from 'interfaces/role';
|
||||
import { useUiFlag } from 'hooks/useUiFlag';
|
||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||
import { useScim } from 'hooks/useScim';
|
||||
import { ScimConfigSettings } from '../ScimSettings/ScimSettings';
|
||||
|
||||
const initialState = {
|
||||
enabled: false,
|
||||
@ -85,6 +89,22 @@ export const OidcAuth = () => {
|
||||
});
|
||||
};
|
||||
|
||||
const {
|
||||
settings,
|
||||
enabled,
|
||||
setEnabled,
|
||||
assumeControlOfExisting,
|
||||
setAssumeControlOfExisting,
|
||||
newToken,
|
||||
tokenGenerationDialog,
|
||||
setTokenGenerationDialog,
|
||||
tokenDialog,
|
||||
setTokenDialog,
|
||||
loading: scimLoading,
|
||||
saveScimSettings,
|
||||
onGenerateNewTokenConfirm,
|
||||
} = useScim();
|
||||
|
||||
const onSubmit = async (event: React.SyntheticEvent) => {
|
||||
event.preventDefault();
|
||||
|
||||
@ -94,11 +114,14 @@ export const OidcAuth = () => {
|
||||
title: 'Settings stored',
|
||||
type: 'success',
|
||||
});
|
||||
saveScimSettings();
|
||||
} catch (error: unknown) {
|
||||
setToastApiError(formatUnknownError(error));
|
||||
}
|
||||
};
|
||||
|
||||
const scimEnabled = useUiFlag('scimApi');
|
||||
|
||||
return (
|
||||
<>
|
||||
<Grid container sx={{ mb: 3 }}>
|
||||
@ -255,6 +278,32 @@ export const OidcAuth = () => {
|
||||
data={data}
|
||||
setValue={setValue}
|
||||
/>
|
||||
|
||||
<ConditionallyRender
|
||||
condition={scimEnabled}
|
||||
show={
|
||||
<ScimConfigSettings
|
||||
disabled={!data.enabled}
|
||||
settings={settings}
|
||||
enabled={enabled}
|
||||
setEnabled={setEnabled}
|
||||
assumeControlOfExisting={assumeControlOfExisting}
|
||||
setAssumeControlOfExisting={
|
||||
setAssumeControlOfExisting
|
||||
}
|
||||
newToken={newToken}
|
||||
tokenGenerationDialog={tokenGenerationDialog}
|
||||
setTokenGenerationDialog={setTokenGenerationDialog}
|
||||
tokenDialog={tokenDialog}
|
||||
setTokenDialog={setTokenDialog}
|
||||
loading={scimLoading}
|
||||
onGenerateNewTokenConfirm={
|
||||
onGenerateNewTokenConfirm
|
||||
}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
<AutoCreateForm
|
||||
data={data}
|
||||
setValue={setValue}
|
||||
@ -296,6 +345,7 @@ export const OidcAuth = () => {
|
||||
</FormControl>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
<Grid container spacing={3}>
|
||||
<Grid item md={12}>
|
||||
<Button
|
||||
|
@ -17,6 +17,10 @@ import { formatUnknownError } from 'utils/formatUnknownError';
|
||||
import { removeEmptyStringFields } from 'utils/removeEmptyStringFields';
|
||||
import { SsoGroupSettings } from '../SsoGroupSettings';
|
||||
import type { IRole } from 'interfaces/role';
|
||||
import { useUiFlag } from 'hooks/useUiFlag';
|
||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||
import { useScim } from 'hooks/useScim';
|
||||
import { ScimConfigSettings } from '../ScimSettings/ScimSettings';
|
||||
|
||||
const initialState = {
|
||||
enabled: false,
|
||||
@ -76,6 +80,22 @@ export const SamlAuth = () => {
|
||||
});
|
||||
};
|
||||
|
||||
const {
|
||||
settings,
|
||||
enabled,
|
||||
setEnabled,
|
||||
assumeControlOfExisting,
|
||||
setAssumeControlOfExisting,
|
||||
newToken,
|
||||
tokenGenerationDialog,
|
||||
setTokenGenerationDialog,
|
||||
tokenDialog,
|
||||
setTokenDialog,
|
||||
loading: scimLoading,
|
||||
saveScimSettings,
|
||||
onGenerateNewTokenConfirm,
|
||||
} = useScim();
|
||||
|
||||
const onSubmit = async (event: React.SyntheticEvent) => {
|
||||
event.preventDefault();
|
||||
|
||||
@ -85,11 +105,14 @@ export const SamlAuth = () => {
|
||||
title: 'Settings stored',
|
||||
type: 'success',
|
||||
});
|
||||
saveScimSettings();
|
||||
} catch (error: unknown) {
|
||||
setToastApiError(formatUnknownError(error));
|
||||
}
|
||||
};
|
||||
|
||||
const scimEnabled = useUiFlag('scimApi');
|
||||
|
||||
return (
|
||||
<>
|
||||
<Grid container sx={{ mb: 3 }}>
|
||||
@ -263,6 +286,31 @@ export const SamlAuth = () => {
|
||||
setValue={setValue}
|
||||
/>
|
||||
|
||||
<ConditionallyRender
|
||||
condition={scimEnabled}
|
||||
show={
|
||||
<ScimConfigSettings
|
||||
disabled={!data.enabled}
|
||||
settings={settings}
|
||||
enabled={enabled}
|
||||
setEnabled={setEnabled}
|
||||
assumeControlOfExisting={assumeControlOfExisting}
|
||||
setAssumeControlOfExisting={
|
||||
setAssumeControlOfExisting
|
||||
}
|
||||
newToken={newToken}
|
||||
tokenGenerationDialog={tokenGenerationDialog}
|
||||
setTokenGenerationDialog={setTokenGenerationDialog}
|
||||
tokenDialog={tokenDialog}
|
||||
setTokenDialog={setTokenDialog}
|
||||
loading={scimLoading}
|
||||
onGenerateNewTokenConfirm={
|
||||
onGenerateNewTokenConfirm
|
||||
}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
<AutoCreateForm
|
||||
data={data}
|
||||
setValue={setValue}
|
||||
|
@ -1,67 +1,51 @@
|
||||
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';
|
||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||
import type { ScimSettings } from 'hooks/api/getters/useScimSettings/useScimSettings';
|
||||
|
||||
export const ScimSettings = () => {
|
||||
const { setToastData, setToastApiError } = useToast();
|
||||
export interface IScimSettingsParameters {
|
||||
disabled: boolean;
|
||||
loading: boolean;
|
||||
enabled: boolean;
|
||||
setEnabled: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
assumeControlOfExisting: boolean;
|
||||
setAssumeControlOfExisting: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
newToken: string;
|
||||
settings: ScimSettings;
|
||||
tokenGenerationDialog: boolean;
|
||||
setTokenGenerationDialog: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
onGenerateNewTokenConfirm: () => void;
|
||||
tokenDialog: boolean;
|
||||
setTokenDialog: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
}
|
||||
|
||||
export const ScimConfigSettings = ({
|
||||
disabled,
|
||||
loading,
|
||||
enabled,
|
||||
setEnabled,
|
||||
assumeControlOfExisting,
|
||||
setAssumeControlOfExisting,
|
||||
newToken,
|
||||
settings,
|
||||
tokenGenerationDialog,
|
||||
setTokenGenerationDialog,
|
||||
onGenerateNewTokenConfirm,
|
||||
tokenDialog,
|
||||
setTokenDialog,
|
||||
}: IScimSettingsParameters) => {
|
||||
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 (
|
||||
<>
|
||||
<h3>SCIM Provisioning</h3>
|
||||
<Grid container sx={{ mb: 3 }}>
|
||||
<Grid item md={12}>
|
||||
<Alert severity='info'>
|
||||
@ -79,56 +63,68 @@ export const ScimSettings = () => {
|
||||
</Alert>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<form onSubmit={onSubmit}>
|
||||
<Grid container spacing={3}>
|
||||
<Grid item md={5} mb={2}>
|
||||
<strong>Enable</strong>
|
||||
<p>Enable SCIM provisioning.</p>
|
||||
</Grid>
|
||||
<Grid item md={6}>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
onChange={(_, enabled) =>
|
||||
setEnabled(enabled)
|
||||
}
|
||||
value={enabled}
|
||||
name='enabled'
|
||||
checked={enabled}
|
||||
/>
|
||||
}
|
||||
label={enabled ? 'Enabled' : 'Disabled'}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid container spacing={3}>
|
||||
<Grid item md={5} mb={2}>
|
||||
<strong>Enable</strong>
|
||||
<p>Enable SCIM provisioning.</p>
|
||||
</Grid>
|
||||
<Grid item md={6}>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
onChange={(_, enabled) => setEnabled(enabled)}
|
||||
value={enabled}
|
||||
name='enabled'
|
||||
checked={enabled}
|
||||
disabled={disabled}
|
||||
/>
|
||||
}
|
||||
label={enabled ? 'Enabled' : 'Disabled'}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
<Grid container spacing={3}>
|
||||
<Grid item md={5}>
|
||||
<Button
|
||||
variant='contained'
|
||||
color='primary'
|
||||
type='submit'
|
||||
disabled={loading}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
<ConditionallyRender
|
||||
condition={Boolean(settings.hasToken)}
|
||||
show={
|
||||
<Button
|
||||
variant='outlined'
|
||||
color='error'
|
||||
disabled={loading}
|
||||
onClick={onGenerateNewToken}
|
||||
sx={{ ml: 1 }}
|
||||
>
|
||||
Generate new token
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid container spacing={3}>
|
||||
<Grid item md={5} mb={2}>
|
||||
<strong>Assume control</strong>
|
||||
<p>Assumes control of users and groups</p>
|
||||
</Grid>
|
||||
</form>
|
||||
<Grid item md={6}>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
onChange={(_, set_enabled) =>
|
||||
setAssumeControlOfExisting(set_enabled)
|
||||
}
|
||||
value={assumeControlOfExisting}
|
||||
name='assumeControlOfExisting'
|
||||
checked={assumeControlOfExisting}
|
||||
disabled={disabled}
|
||||
/>
|
||||
}
|
||||
label={assumeControlOfExisting ? 'Enabled' : 'Disabled'}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
<Grid container spacing={3}>
|
||||
<Grid item md={5}>
|
||||
<ConditionallyRender
|
||||
condition={Boolean(settings.hasToken)}
|
||||
show={
|
||||
<Button
|
||||
variant='outlined'
|
||||
color='error'
|
||||
disabled={loading}
|
||||
onClick={onGenerateNewToken}
|
||||
sx={{ ml: 1 }}
|
||||
>
|
||||
Generate new token
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<ScimTokenGenerationDialog
|
||||
open={tokenGenerationDialog}
|
||||
setOpen={setTokenGenerationDialog}
|
||||
|
@ -10,11 +10,13 @@ const ENDPOINT = 'api/admin/scim-settings';
|
||||
export type ScimSettings = {
|
||||
enabled: boolean;
|
||||
hasToken: boolean;
|
||||
assumeControlOfExisting: boolean;
|
||||
};
|
||||
|
||||
const DEFAULT_DATA: ScimSettings = {
|
||||
enabled: false,
|
||||
hasToken: false,
|
||||
assumeControlOfExisting: false,
|
||||
};
|
||||
|
||||
export const useScimSettings = () => {
|
||||
|
66
frontend/src/hooks/useScim.ts
Normal file
66
frontend/src/hooks/useScim.ts
Normal file
@ -0,0 +1,66 @@
|
||||
import { useScimSettingsApi } from 'hooks/api/actions/useScimSettingsApi/useScimSettingsApi';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { formatUnknownError } from 'utils/formatUnknownError';
|
||||
import useToast from 'hooks/useToast';
|
||||
import { useScimSettings } from './api/getters/useScimSettings/useScimSettings';
|
||||
|
||||
export const useScim = () => {
|
||||
const [newToken, setNewToken] = useState('');
|
||||
const [enabled, setEnabled] = useState(false);
|
||||
const [tokenGenerationDialog, setTokenGenerationDialog] = useState(false);
|
||||
const [tokenDialog, setTokenDialog] = useState(false);
|
||||
const [assumeControlOfExisting, setAssumeControlOfExisting] =
|
||||
useState(false);
|
||||
|
||||
const { saveSettings, generateNewToken, errors, loading } =
|
||||
useScimSettingsApi();
|
||||
|
||||
const { settings, refetch } = useScimSettings();
|
||||
const { setToastData, setToastApiError } = useToast();
|
||||
const saveScimSettings = async () => {
|
||||
try {
|
||||
await saveSettings({ enabled, assumeControlOfExisting });
|
||||
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 onGenerateNewTokenConfirm = async () => {
|
||||
setTokenGenerationDialog(false);
|
||||
const token = await generateNewToken();
|
||||
setNewToken(token);
|
||||
setTokenDialog(true);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setEnabled(settings.enabled ?? false);
|
||||
setAssumeControlOfExisting(settings.assumeControlOfExisting ?? false);
|
||||
}, [settings]);
|
||||
|
||||
return {
|
||||
settings,
|
||||
enabled,
|
||||
setEnabled,
|
||||
assumeControlOfExisting,
|
||||
setAssumeControlOfExisting,
|
||||
newToken,
|
||||
tokenGenerationDialog,
|
||||
setTokenGenerationDialog,
|
||||
tokenDialog,
|
||||
setTokenDialog,
|
||||
loading,
|
||||
saveScimSettings,
|
||||
onGenerateNewTokenConfirm,
|
||||
};
|
||||
};
|
Loading…
Reference in New Issue
Block a user