1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-01-20 00:08:02 +01:00
Nuno Góis 2024-04-09 15:26:35 +01:00 committed by GitHub
parent 48b8df8f4e
commit 032419aa76
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 325 additions and 0 deletions

View File

@ -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: <ScimSettings />,
});
}
const [activeTab, setActiveTab] = useState(0);
return (

View 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}
/>
</>
);
};

View File

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

View File

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

View File

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

View File

@ -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());
};

View File

@ -81,6 +81,7 @@ export type UiFlags = {
variantDependencies?: boolean;
projectOverviewRefactorFeedback?: boolean;
featureLifecycle?: boolean;
scimApi?: boolean;
};
export interface IVersionInfo {