mirror of
https://github.com/Unleash/unleash.git
synced 2025-01-20 00:08:02 +01:00
chore: SCIM settings UI (#6800)
https://linear.app/unleash/issue/2-1981/ui-allow-users-to-set-up-scim Adds UI for Provisioning (SCIM) settings. ![image](https://github.com/Unleash/unleash/assets/14320932/e24cf4dd-09d5-459d-bf8a-dd75a966d8eb) ![image](https://github.com/Unleash/unleash/assets/14320932/090a8279-1e98-4d4a-8e21-98cf311f1721) ![image](https://github.com/Unleash/unleash/assets/14320932/aca67619-6748-4848-8f1f-4de1deb90860)
This commit is contained in:
parent
48b8df8f4e
commit
032419aa76
@ -11,11 +11,15 @@ import { ADMIN } from '@server/types/permissions';
|
|||||||
import { PremiumFeature } from 'component/common/PremiumFeature/PremiumFeature';
|
import { PremiumFeature } from 'component/common/PremiumFeature/PremiumFeature';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { TabPanel } from 'component/common/TabNav/TabPanel/TabPanel';
|
import { TabPanel } from 'component/common/TabNav/TabPanel/TabPanel';
|
||||||
|
import { useUiFlag } from 'hooks/useUiFlag';
|
||||||
|
import { ScimSettings } from './ScimSettings/ScimSettings';
|
||||||
|
|
||||||
export const AuthSettings = () => {
|
export const AuthSettings = () => {
|
||||||
const { authenticationType } = useUiConfig().uiConfig;
|
const { authenticationType } = useUiConfig().uiConfig;
|
||||||
const { uiConfig } = useUiConfig();
|
const { uiConfig } = useUiConfig();
|
||||||
|
|
||||||
|
const scimEnabled = useUiFlag('scimApi');
|
||||||
|
|
||||||
const tabs = [
|
const tabs = [
|
||||||
{
|
{
|
||||||
label: 'OpenID Connect',
|
label: 'OpenID Connect',
|
||||||
@ -36,6 +40,14 @@ export const AuthSettings = () => {
|
|||||||
].filter(
|
].filter(
|
||||||
(item) => uiConfig.flags?.googleAuthEnabled || item.label !== 'Google',
|
(item) => uiConfig.flags?.googleAuthEnabled || item.label !== 'Google',
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (scimEnabled) {
|
||||||
|
tabs.push({
|
||||||
|
label: 'Provisioning (SCIM)',
|
||||||
|
component: <ScimSettings />,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const [activeTab, setActiveTab] = useState(0);
|
const [activeTab, setActiveTab] = useState(0);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
144
frontend/src/component/admin/auth/ScimSettings/ScimSettings.tsx
Normal file
144
frontend/src/component/admin/auth/ScimSettings/ScimSettings.tsx
Normal file
@ -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 (
|
||||||
|
<>
|
||||||
|
<Grid container sx={{ mb: 3 }}>
|
||||||
|
<Grid item md={12}>
|
||||||
|
<Alert severity='info'>
|
||||||
|
Please read the{' '}
|
||||||
|
<a
|
||||||
|
href='https://docs.getunleash.io/reference/scim'
|
||||||
|
target='_blank'
|
||||||
|
rel='noreferrer'
|
||||||
|
>
|
||||||
|
documentation
|
||||||
|
</a>{' '}
|
||||||
|
to learn how to integrate with specific SCIM clients
|
||||||
|
(Microsoft Entra, Okta, etc). <br />
|
||||||
|
SCIM API URL: <code>{uiConfig.unleashUrl}/scim</code>
|
||||||
|
</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>
|
||||||
|
|
||||||
|
<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>
|
||||||
|
</form>
|
||||||
|
<ScimTokenGenerationDialog
|
||||||
|
open={tokenGenerationDialog}
|
||||||
|
setOpen={setTokenGenerationDialog}
|
||||||
|
onConfirm={onGenerateNewTokenConfirm}
|
||||||
|
/>
|
||||||
|
<ScimTokenDialog
|
||||||
|
open={tokenDialog}
|
||||||
|
setOpen={setTokenDialog}
|
||||||
|
token={newToken}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
@ -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<React.SetStateAction<boolean>>;
|
||||||
|
token?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ScimTokenDialog = ({
|
||||||
|
open,
|
||||||
|
setOpen,
|
||||||
|
token,
|
||||||
|
}: IScimTokenDialogProps) => (
|
||||||
|
<Dialogue
|
||||||
|
open={open}
|
||||||
|
secondaryButtonText='Close'
|
||||||
|
onClose={(_, muiCloseReason?: string) => {
|
||||||
|
if (!muiCloseReason) {
|
||||||
|
setOpen(false);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
title='SCIM API token created'
|
||||||
|
>
|
||||||
|
<StyledAlert severity='info'>
|
||||||
|
Make sure to copy your SCIM API token now. You won't be able to see
|
||||||
|
it again!
|
||||||
|
</StyledAlert>
|
||||||
|
<Typography variant='body1'>Your token:</Typography>
|
||||||
|
<UserToken token={token || ''} />
|
||||||
|
</Dialogue>
|
||||||
|
);
|
@ -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<React.SetStateAction<boolean>>;
|
||||||
|
onConfirm: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ScimTokenGenerationDialog = ({
|
||||||
|
open,
|
||||||
|
setOpen,
|
||||||
|
onConfirm,
|
||||||
|
}: IScimTokenGenerationDialogProps) => (
|
||||||
|
<Dialogue
|
||||||
|
open={open}
|
||||||
|
secondaryButtonText='Close'
|
||||||
|
onClose={(_, muiCloseReason?: string) => {
|
||||||
|
if (!muiCloseReason) {
|
||||||
|
setOpen(false);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
primaryButtonText='Generate new token'
|
||||||
|
onClick={onConfirm}
|
||||||
|
title='Generate new SCIM API token?'
|
||||||
|
>
|
||||||
|
<StyledAlert severity='error'>
|
||||||
|
Generating a new token will <strong>immediately revoke</strong> the
|
||||||
|
current one, which may break any existing provision integrations
|
||||||
|
currently using it.
|
||||||
|
</StyledAlert>
|
||||||
|
</Dialogue>
|
||||||
|
);
|
@ -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<ScimSettings, 'hasToken'>;
|
||||||
|
|
||||||
|
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<string> => {
|
||||||
|
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,
|
||||||
|
};
|
||||||
|
};
|
@ -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<ScimSettings>(
|
||||||
|
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());
|
||||||
|
};
|
@ -81,6 +81,7 @@ export type UiFlags = {
|
|||||||
variantDependencies?: boolean;
|
variantDependencies?: boolean;
|
||||||
projectOverviewRefactorFeedback?: boolean;
|
projectOverviewRefactorFeedback?: boolean;
|
||||||
featureLifecycle?: boolean;
|
featureLifecycle?: boolean;
|
||||||
|
scimApi?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface IVersionInfo {
|
export interface IVersionInfo {
|
||||||
|
Loading…
Reference in New Issue
Block a user