1
0
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:
David Leek 2024-04-25 15:39:56 +02:00 committed by GitHub
parent 19055b1e33
commit d1bb65bebd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 259 additions and 108 deletions

View File

@ -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 (

View File

@ -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

View File

@ -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}

View File

@ -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}

View File

@ -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 = () => {

View 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,
};
};