1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-02-23 00:22:19 +01:00

refactor: port auth admin to TS/SWR (#675)

* refactor: format files

* refactor: add missing RE UI config flag

* refactor: port admin auth index to TS/SWR

* refactor: port GoogleAuth to TS/SWR

* refactor: port OidcAuth to TS/SWR

* refactor: port SamlAuth to TS/SWR

* refactor: remove unused e-admin-auth store

* refactor: make AutoCreateForm an explicit export

* refactor: improve auth settings dir structure

* refactor: destructure authenticationType from uiConfig

* refactor: use setToastApiError to show errors

* refactor: format files

* refactor: remove invalid string fields from requests

Co-authored-by: Fredrik Strand Oseberg <fredrik.no@gmail.com>
This commit is contained in:
olav 2022-02-08 11:44:41 +01:00 committed by GitHub
parent 234bab6cb4
commit f4d5ed03aa
26 changed files with 347 additions and 518 deletions

View File

@ -1,16 +1,18 @@
import React from 'react';
import PropTypes from 'prop-types';
import AdminMenu from '../menu/AdminMenu';
import { Alert } from '@material-ui/lab';
import GoogleAuth from './google-auth-container';
import SamlAuth from './saml-auth-container';
import OidcAuth from './oidc-auth-container';
import PasswordAuthSettings from './PasswordAuthSettings';
import TabNav from '../../common/TabNav/TabNav';
import PageContent from '../../common/PageContent/PageContent';
import ConditionallyRender from '../../common/ConditionallyRender/ConditionallyRender';
import useUiConfig from '../../../hooks/api/getters/useUiConfig/useUiConfig';
import { OidcAuth } from './OidcAuth/OidcAuth';
import { SamlAuth } from './SamlAuth/SamlAuth';
import { PasswordAuth } from './PasswordAuth/PasswordAuth';
import { GoogleAuth } from './GoogleAuth/GoogleAuth';
export const AuthSettings = () => {
const { authenticationType } = useUiConfig().uiConfig;
function AdminAuthPage({ authenticationType, history }) {
const tabs = [
{
label: 'OpenID Connect',
@ -22,7 +24,7 @@ function AdminAuthPage({ authenticationType, history }) {
},
{
label: 'Password',
component: <PasswordAuthSettings />,
component: <PasswordAuth />,
},
{
label: 'Google',
@ -32,7 +34,7 @@ function AdminAuthPage({ authenticationType, history }) {
return (
<div>
<AdminMenu history={history} />
<AdminMenu />
<PageContent headerContent="Single Sign-On">
<ConditionallyRender
condition={authenticationType === 'enterprise'}
@ -80,12 +82,4 @@ function AdminAuthPage({ authenticationType, history }) {
</PageContent>
</div>
);
}
AdminAuthPage.propTypes = {
match: PropTypes.object.isRequired,
history: PropTypes.object.isRequired,
authenticationType: PropTypes.string,
};
export default AdminAuthPage;

View File

@ -1,102 +1,119 @@
import React, { ChangeEvent, Fragment } from 'react';
import { FormControl, Grid, MenuItem, Switch, TextField, Select, InputLabel, FormControlLabel } from '@material-ui/core';
import {
FormControl,
FormControlLabel,
Grid,
InputLabel,
MenuItem,
Select,
Switch,
TextField,
} from '@material-ui/core';
interface Props {
interface Props {
data?: {
enabled: boolean;
autoCreate: boolean;
defaultRootRole?: string;
emailDomains?: string;
};
setValue: (name: string, value: string | boolean) => void;
}
function AutoCreateForm({ data = { enabled: false, autoCreate: false }, setValue }: Props) {
export const AutoCreateForm = ({
data = { enabled: false, autoCreate: false },
setValue,
}: Props) => {
const updateAutoCreate = () => {
setValue('autoCreate', !data.autoCreate);
}
};
const updateDefaultRootRole = (evt: ChangeEvent<{ name?: string; value: unknown }>) => {
const updateDefaultRootRole = (
evt: ChangeEvent<{ name?: string; value: unknown }>
) => {
setValue('defaultRootRole', evt.target.value as string);
}
};
const updateField = (e: ChangeEvent<HTMLInputElement>) => {
setValue(e.target.name, e.target.value);
}
};
return (
<Fragment>
<Grid container spacing={3}>
<Grid item md={5}>
<strong>Auto-create users</strong>
<p>
Enable automatic creation of new users when signing in.
</p>
return (
<Fragment>
<Grid container spacing={3}>
<Grid item md={5}>
<strong>Auto-create users</strong>
<p>
Enable automatic creation of new users when signing in.
</p>
</Grid>
<Grid item md={6} style={{ padding: '20px' }}>
<FormControlLabel
control={
<Switch
onChange={updateAutoCreate}
name="enabled"
checked={data.autoCreate}
disabled={!data.enabled}
/>
}
label="Auto-create users"
/>
</Grid>
</Grid>
<Grid item md={6} style={{ padding: '20px' }}>
<FormControlLabel
control={ <Switch
onChange={updateAutoCreate}
name="enabled"
checked={data.autoCreate}
disabled={!data.enabled}
/>}
label="Auto-create users"
/>
<Grid container spacing={3}>
<Grid item md={5}>
<strong>Default Root Role</strong>
<p>
Choose which root role the user should get when no
explicit role mapping exists.
</p>
</Grid>
<Grid item md={6}>
<FormControl style={{ minWidth: '200px' }}>
<InputLabel id="defaultRootRole-label">
Default Role
</InputLabel>
<Select
labelId="defaultRootRole-label"
id="defaultRootRole"
name="defaultRootRole"
disabled={!data.autoCreate || !data.enabled}
value={data.defaultRootRole || 'Editor'}
onChange={updateDefaultRootRole}
>
{/*consider these from API or constants. */}
<MenuItem value="Viewer">Viewer</MenuItem>
<MenuItem value="Editor">Editor</MenuItem>
<MenuItem value="Admin">Admin</MenuItem>
</Select>
</FormControl>
</Grid>
</Grid>
</Grid>
<Grid container spacing={3}>
<Grid item md={5}>
<strong>Default Root Role</strong>
<p>
Choose which root role the user should get when no explicit role mapping exists.
</p>
<Grid container spacing={3}>
<Grid item md={5}>
<strong>Email domains</strong>
<p>
Comma separated list of email domains that should be
allowed to sign in.
</p>
</Grid>
<Grid item md={6}>
<TextField
onChange={updateField}
label="Email domains"
name="emailDomains"
disabled={!data.autoCreate || !data.enabled}
required={!!data.autoCreate}
value={data.emailDomains || ''}
placeholder="@company.com, @anotherCompany.com"
style={{ width: '400px' }}
rows={2}
variant="outlined"
size="small"
/>
</Grid>
</Grid>
<Grid item md={6}>
<FormControl style={{minWidth: '200px'}}>
<InputLabel id="defaultRootRole-label">Default Role</InputLabel>
<Select
labelId="defaultRootRole-label"
id="defaultRootRole"
name="defaultRootRole"
disabled={!data.autoCreate || !data.enabled}
value={data.defaultRootRole || 'Editor'}
onChange={updateDefaultRootRole}
>
{/*consider these from API or constants. */}
<MenuItem value='Viewer'>Viewer</MenuItem>
<MenuItem value='Editor'>Editor</MenuItem>
<MenuItem value='Admin'>Admin</MenuItem>
</Select>
</FormControl>
</Grid>
</Grid>
<Grid container spacing={3}>
<Grid item md={5}>
<strong>Email domains</strong>
<p>
Comma separated list of email domains
that should be allowed to sign in.
</p>
</Grid>
<Grid item md={6}>
<TextField
onChange={updateField}
label="Email domains"
name="emailDomains"
disabled={!data.autoCreate || !data.enabled}
required={!!data.autoCreate}
value={data.emailDomains || ''}
placeholder="@company.com, @anotherCompany.com"
style={{ width: '400px' }}
rows={2}
variant="outlined"
size="small"
/>
</Grid>
</Grid>
</Fragment>);
}
export default AutoCreateForm;
</Fragment>
);
};

View File

@ -1,5 +1,4 @@
import React, { useState, useEffect, useContext } from 'react';
import PropTypes from 'prop-types';
import React, { useContext, useEffect, useState } from 'react';
import {
Button,
FormControlLabel,
@ -8,30 +7,32 @@ import {
TextField,
} from '@material-ui/core';
import { Alert } from '@material-ui/lab';
import PageContent from '../../common/PageContent/PageContent';
import AccessContext from '../../../contexts/AccessContext';
import { ADMIN } from '../../providers/AccessProvider/permissions';
import PageContent from '../../../common/PageContent/PageContent';
import AccessContext from '../../../../contexts/AccessContext';
import { ADMIN } from '../../../providers/AccessProvider/permissions';
import useUiConfig from '../../../../hooks/api/getters/useUiConfig/useUiConfig';
import useAuthSettings from '../../../../hooks/api/getters/useAuthSettings/useAuthSettings';
import useAuthSettingsApi from '../../../../hooks/api/actions/useAuthSettingsApi/useAuthSettingsApi';
import useToast from '../../../../hooks/useToast';
import { formatUnknownError } from '../../../../utils/format-unknown-error';
import { removeEmptyStringFields } from '../../../../utils/remove-empty-string-fields';
const initialState = {
enabled: false,
autoCreate: false,
unleashHostname: location.hostname,
clientId: '',
clientSecret: '',
emailDomains: '',
};
function GoogleAuth({
config,
getGoogleConfig,
updateGoogleConfig,
unleashUrl,
}) {
export const GoogleAuth = () => {
const { setToastData, setToastApiError } = useToast();
const { uiConfig } = useUiConfig();
const [data, setData] = useState(initialState);
const [info, setInfo] = useState();
const { hasAccess } = useContext(AccessContext);
useEffect(() => {
getGoogleConfig();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const { config } = useAuthSettings('google');
const { updateSettings, errors, loading } = useAuthSettingsApi('google');
useEffect(() => {
if (config.clientId) {
@ -43,10 +44,10 @@ function GoogleAuth({
return <span>You need admin privileges to access this section.</span>;
}
const updateField = e => {
const updateField = (event: React.ChangeEvent<HTMLInputElement>) => {
setData({
...data,
[e.target.name]: e.target.value,
[event.target.name]: event.target.value,
});
};
@ -58,19 +59,22 @@ function GoogleAuth({
setData({ ...data, autoCreate: !data.autoCreate });
};
const onSubmit = async e => {
e.preventDefault();
setInfo('...saving');
const onSubmit = async (event: React.SyntheticEvent) => {
event.preventDefault();
try {
await updateGoogleConfig(data);
setInfo('Settings stored');
setTimeout(() => setInfo(''), 2000);
} catch (e) {
setInfo(e.message);
await updateSettings(removeEmptyStringFields(data));
setToastData({
title: 'Settings stored',
type: 'success',
});
} catch (err) {
setToastApiError(formatUnknownError(err));
}
};
return (
<PageContent>
<PageContent headerContent="">
<Grid container style={{ marginBottom: '1rem' }}>
<Grid item xs={12}>
<Alert severity="info">
@ -84,7 +88,7 @@ function GoogleAuth({
</a>{' '}
to learn how to integrate with Google OAuth 2.0. <br />
Callback URL:{' '}
<code>{unleashUrl}/auth/google/callback</code>
<code>{uiConfig.unleashUrl}/auth/google/callback</code>
</Alert>
</Grid>
</Grid>
@ -125,7 +129,7 @@ function GoogleAuth({
label="Client ID"
name="clientId"
placeholder=""
value={data.clientId || ''}
value={data.clientId}
style={{ width: '400px' }}
variant="outlined"
size="small"
@ -146,7 +150,7 @@ function GoogleAuth({
onChange={updateField}
label="Client Secret"
name="clientSecret"
value={data.clientSecret || ''}
value={data.clientSecret}
placeholder=""
style={{ width: '400px' }}
variant="outlined"
@ -195,9 +199,7 @@ function GoogleAuth({
onChange={updateAutoCreate}
name="enabled"
checked={data.autoCreate}
>
Auto-create users
</Switch>
/>
</Grid>
</Grid>
<Grid container spacing={3}>
@ -229,22 +231,18 @@ function GoogleAuth({
variant="contained"
color="primary"
type="submit"
disabled={loading}
>
Save
</Button>{' '}
<small>{info}</small>
<p>
<small style={{ color: 'red' }}>
{errors?.message}
</small>
</p>
</Grid>
</Grid>
</form>
</PageContent>
);
}
GoogleAuth.propTypes = {
config: PropTypes.object,
unleashUrl: PropTypes.string,
getGoogleConfig: PropTypes.func.isRequired,
updateGoogleConfig: PropTypes.func.isRequired,
};
export default GoogleAuth;

View File

@ -1,5 +1,4 @@
import React, { useState, useEffect, useContext } from 'react';
import PropTypes from 'prop-types';
import React, { useContext, useEffect, useState } from 'react';
import {
Button,
FormControlLabel,
@ -8,34 +7,40 @@ import {
TextField,
} from '@material-ui/core';
import { Alert } from '@material-ui/lab';
import PageContent from '../../common/PageContent/PageContent';
import AccessContext from '../../../contexts/AccessContext';
import { ADMIN } from '../../providers/AccessProvider/permissions';
import AutoCreateForm from './AutoCreateForm/AutoCreateForm';
import PageContent from '../../../common/PageContent/PageContent';
import AccessContext from '../../../../contexts/AccessContext';
import { ADMIN } from '../../../providers/AccessProvider/permissions';
import { AutoCreateForm } from '../AutoCreateForm/AutoCreateForm';
import useUiConfig from '../../../../hooks/api/getters/useUiConfig/useUiConfig';
import useAuthSettingsApi from '../../../../hooks/api/actions/useAuthSettingsApi/useAuthSettingsApi';
import useAuthSettings from '../../../../hooks/api/getters/useAuthSettings/useAuthSettings';
import useToast from '../../../../hooks/useToast';
import { formatUnknownError } from '../../../../utils/format-unknown-error';
import { removeEmptyStringFields } from '../../../../utils/remove-empty-string-fields';
const initialState = {
enabled: false,
enableSingleSignOut: false,
autoCreate: false,
unleashHostname: location.hostname,
clientId: '',
discoverUrl: '',
secret: '',
acrValues: '',
};
function OidcAuth({ config, getOidcConfig, updateOidcConfig, unleashUrl }) {
export const OidcAuth = () => {
const { setToastData, setToastApiError } = useToast();
const { uiConfig } = useUiConfig();
const [data, setData] = useState(initialState);
const [info, setInfo] = useState();
const [error, setError] = useState();
const { hasAccess } = useContext(AccessContext);
useEffect(() => {
getOidcConfig();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const { config } = useAuthSettings('oidc');
const { updateSettings, errors, loading } = useAuthSettingsApi('oidc');
useEffect(() => {
if (config.discoverUrl) {
setData(config);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [config]);
if (!hasAccess(ADMIN)) {
@ -46,8 +51,8 @@ function OidcAuth({ config, getOidcConfig, updateOidcConfig, unleashUrl }) {
);
}
const updateField = e => {
setValue(e.target.name, e.target.value);
const updateField = (event: React.ChangeEvent<HTMLInputElement>) => {
setValue(event.target.name, event.target.value);
};
const updateEnabled = () => {
@ -58,28 +63,29 @@ function OidcAuth({ config, getOidcConfig, updateOidcConfig, unleashUrl }) {
setData({ ...data, enableSingleSignOut: !data.enableSingleSignOut });
};
const setValue = (field, value) => {
const setValue = (name: string, value: string | boolean) => {
setData({
...data,
[field]: value,
[name]: value,
});
};
const onSubmit = async e => {
e.preventDefault();
setInfo('...saving');
setError('');
const onSubmit = async (event: React.SyntheticEvent) => {
event.preventDefault();
try {
await updateOidcConfig(data);
setInfo('Settings stored');
setTimeout(() => setInfo(''), 2000);
} catch (e) {
setInfo('');
setError(e.message);
await updateSettings(removeEmptyStringFields(data));
setToastData({
title: 'Settings stored',
type: 'success',
});
} catch (err) {
setToastApiError(formatUnknownError(err));
}
};
return (
<PageContent>
<PageContent headerContent="">
<Grid container style={{ marginBottom: '1rem' }}>
<Grid item md={12}>
<Alert severity="info">
@ -94,7 +100,7 @@ function OidcAuth({ config, getOidcConfig, updateOidcConfig, unleashUrl }) {
to learn how to integrate with specific Open Id Connect
providers (Okta, Keycloak, Google, etc). <br />
Callback URL:{' '}
<code>{unleashUrl}/auth/oidc/callback</code>
<code>{uiConfig.unleashUrl}/auth/oidc/callback</code>
</Alert>
</Grid>
</Grid>
@ -128,7 +134,7 @@ function OidcAuth({ config, getOidcConfig, updateOidcConfig, unleashUrl }) {
onChange={updateField}
label="Discover URL"
name="discoverUrl"
value={data.discoverUrl || ''}
value={data.discoverUrl}
disabled={!data.enabled}
style={{ width: '400px' }}
variant="outlined"
@ -146,7 +152,7 @@ function OidcAuth({ config, getOidcConfig, updateOidcConfig, unleashUrl }) {
onChange={updateField}
label="Client ID"
name="clientId"
value={data.clientId || ''}
value={data.clientId}
disabled={!data.enabled}
style={{ width: '400px' }}
variant="outlined"
@ -167,7 +173,7 @@ function OidcAuth({ config, getOidcConfig, updateOidcConfig, unleashUrl }) {
onChange={updateField}
label="Client Secret"
name="secret"
value={data.secret || ''}
value={data.secret}
disabled={!data.enabled}
style={{ width: '400px' }}
variant="outlined"
@ -180,7 +186,10 @@ function OidcAuth({ config, getOidcConfig, updateOidcConfig, unleashUrl }) {
<Grid container spacing={3}>
<Grid item md={5}>
<strong>Enable Single Sign-Out</strong>
<p>If you enable Single Sign-Out Unleash will redirect the user to the IDP as part of the Sign-out process.</p>
<p>
If you enable Single Sign-Out Unleash will redirect
the user to the IDP as part of the Sign-out process.
</p>
</Grid>
<Grid item md={6} style={{ padding: '20px' }}>
<FormControlLabel
@ -204,15 +213,21 @@ function OidcAuth({ config, getOidcConfig, updateOidcConfig, unleashUrl }) {
<Grid container spacing={3}>
<Grid item md={5}>
<strong>ACR Values</strong>
<p>Requested Authentication Context Class Reference values. If multiple values are specified they should be "space" separated. Will be sent as "acr_values" as
part of the authentication request. Unleash will validate the acr value in the id token claims against the list of acr values.</p>
<p>
Requested Authentication Context Class Reference
values. If multiple values are specified they should
be "space" separated. Will be sent as "acr_values"
as part of the authentication request. Unleash will
validate the acr value in the id token claims
against the list of acr values.
</p>
</Grid>
<Grid item md={6}>
<TextField
onChange={updateField}
label="ACR Values"
name="acrValues"
value={data.acrValues || ''}
value={data.acrValues}
disabled={!data.enabled}
style={{ width: '400px' }}
variant="outlined"
@ -229,23 +244,18 @@ function OidcAuth({ config, getOidcConfig, updateOidcConfig, unleashUrl }) {
variant="contained"
color="primary"
type="submit"
disabled={loading}
>
Save
</Button>{' '}
<small>{info}</small>
<small style={{ color: 'red' }}>{error}</small>
<p>
<small style={{ color: 'red' }}>
{errors?.message}
</small>
</p>
</Grid>
</Grid>
</form>
</PageContent>
);
}
OidcAuth.propTypes = {
config: PropTypes.object,
unleash: PropTypes.string,
getOidcConfig: PropTypes.func.isRequired,
updateOidcConfig: PropTypes.func.isRequired,
};
export default OidcAuth;

View File

@ -1,30 +1,28 @@
import React, { useState, useContext, useEffect } from 'react';
import {
Button,
FormControlLabel,
Grid,
Switch,
} from '@material-ui/core';
import React, { useContext, useEffect, useState } from 'react';
import { Button, FormControlLabel, Grid, Switch } from '@material-ui/core';
import { Alert } from '@material-ui/lab';
import PageContent from '../../common/PageContent/PageContent';
import AccessContext from '../../../contexts/AccessContext';
import { ADMIN } from '../../providers/AccessProvider/permissions';
import useAuthSettings from '../../../hooks/api/getters/useAuthSettings/useAuthSettings';
import useAuthSettingsApi, {ISimpleAuthSettings } from '../../../hooks/api/actions/useAuthSettingsApi/useAuthSettingsApi';
import useToast from '../../../hooks/useToast';
import PageContent from '../../../common/PageContent/PageContent';
import AccessContext from '../../../../contexts/AccessContext';
import { ADMIN } from '../../../providers/AccessProvider/permissions';
import useAuthSettings from '../../../../hooks/api/getters/useAuthSettings/useAuthSettings';
import useAuthSettingsApi, {
ISimpleAuthSettings,
} from '../../../../hooks/api/actions/useAuthSettingsApi/useAuthSettingsApi';
import useToast from '../../../../hooks/useToast';
import { formatUnknownError } from '../../../../utils/format-unknown-error';
const PasswordAuthSettings = () => {
const { setToastData } = useToast();
export const PasswordAuth = () => {
const { setToastData, setToastApiError } = useToast();
const { config } = useAuthSettings('simple');
const [disablePasswordAuth, setDisablePasswordAuth] = useState<boolean>(false);
const { updateSettings, errors, loading } = useAuthSettingsApi<ISimpleAuthSettings>('simple')
const [disablePasswordAuth, setDisablePasswordAuth] =
useState<boolean>(false);
const { updateSettings, errors, loading } =
useAuthSettingsApi<ISimpleAuthSettings>('simple');
const { hasAccess } = useContext(AccessContext);
useEffect(() => {
setDisablePasswordAuth(!!config.disabled);
}, [ config.disabled ]);
}, [config.disabled]);
if (!hasAccess(ADMIN)) {
return (
@ -38,12 +36,13 @@ const PasswordAuthSettings = () => {
setDisablePasswordAuth(!disablePasswordAuth);
};
const onSubmit = async evt => {
evt.preventDefault();
const onSubmit = async (event: React.SyntheticEvent) => {
event.preventDefault();
try {
const settings: ISimpleAuthSettings = { disabled: disablePasswordAuth };
const settings: ISimpleAuthSettings = {
disabled: disablePasswordAuth,
};
await updateSettings(settings);
setToastData({
title: 'Successfully saved',
@ -52,20 +51,13 @@ const PasswordAuthSettings = () => {
type: 'success',
show: true,
});
} catch (err: any) {
setToastData({
title: 'Could not store settings',
text: err?.message,
autoHideDuration: 4000,
type: 'error',
show: true,
});
setDisablePasswordAuth(config.disabled)
} catch (err) {
setToastApiError(formatUnknownError(err));
setDisablePasswordAuth(config.disabled);
}
};
return (
<PageContent headerContent=''>
<PageContent headerContent="">
<form onSubmit={onSubmit}>
<Grid container spacing={3}>
<Grid item md={5}>
@ -82,7 +74,9 @@ const PasswordAuthSettings = () => {
checked={!disablePasswordAuth}
/>
}
label={!disablePasswordAuth ? 'Enabled' : 'Disabled'}
label={
!disablePasswordAuth ? 'Enabled' : 'Disabled'
}
/>
</Grid>
</Grid>
@ -96,12 +90,14 @@ const PasswordAuthSettings = () => {
>
Save
</Button>{' '}
<p><small style={{ color: 'red' }}>{errors?.message}</small></p>
<p>
<small style={{ color: 'red' }}>
{errors?.message}
</small>
</p>
</Grid>
</Grid>
</form>
</PageContent>
);
}
export default PasswordAuthSettings;
};

View File

@ -1,5 +1,4 @@
import React, { useState, useEffect, useContext } from 'react';
import PropTypes from 'prop-types';
import React, { useContext, useEffect, useState } from 'react';
import {
Button,
FormControlLabel,
@ -8,32 +7,40 @@ import {
TextField,
} from '@material-ui/core';
import { Alert } from '@material-ui/lab';
import PageContent from '../../common/PageContent/PageContent';
import AccessContext from '../../../contexts/AccessContext';
import { ADMIN } from '../../providers/AccessProvider/permissions';
import AutoCreateForm from './AutoCreateForm/AutoCreateForm';
import PageContent from '../../../common/PageContent/PageContent';
import AccessContext from '../../../../contexts/AccessContext';
import { ADMIN } from '../../../providers/AccessProvider/permissions';
import { AutoCreateForm } from '../AutoCreateForm/AutoCreateForm';
import useToast from '../../../../hooks/useToast';
import useUiConfig from '../../../../hooks/api/getters/useUiConfig/useUiConfig';
import useAuthSettings from '../../../../hooks/api/getters/useAuthSettings/useAuthSettings';
import useAuthSettingsApi from '../../../../hooks/api/actions/useAuthSettingsApi/useAuthSettingsApi';
import { formatUnknownError } from '../../../../utils/format-unknown-error';
import { removeEmptyStringFields } from '../../../../utils/remove-empty-string-fields';
const initialState = {
enabled: false,
autoCreate: false,
unleashHostname: location.hostname,
entityId: '',
signOnUrl: '',
certificate: '',
signOutUrl: '',
spCertificate: '',
};
function SamlAuth({ config, getSamlConfig, updateSamlConfig, unleashUrl }) {
export const SamlAuth = () => {
const { setToastData, setToastApiError } = useToast();
const { uiConfig } = useUiConfig();
const [data, setData] = useState(initialState);
const [info, setInfo] = useState();
const { hasAccess } = useContext(AccessContext);
useEffect(() => {
getSamlConfig();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const { config } = useAuthSettings('saml');
const { updateSettings, errors, loading } = useAuthSettingsApi('saml');
useEffect(() => {
if (config.entityId) {
setData(config);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [config]);
if (!hasAccess(ADMIN)) {
@ -44,34 +51,37 @@ function SamlAuth({ config, getSamlConfig, updateSamlConfig, unleashUrl }) {
);
}
const updateField = e => {
setValue(e.target.name, e.target.value);
const updateField = (event: React.ChangeEvent<HTMLInputElement>) => {
setValue(event.target.name, event.target.value);
};
const updateEnabled = () => {
setData({ ...data, enabled: !data.enabled });
};
const setValue = (field, value) => {
const setValue = (name: string, value: string | boolean) => {
setData({
...data,
[field]: value,
[name]: value,
});
};
const onSubmit = async e => {
e.preventDefault();
setInfo('...saving');
const onSubmit = async (event: React.SyntheticEvent) => {
event.preventDefault();
try {
await updateSamlConfig(data);
setInfo('Settings stored');
setTimeout(() => setInfo(''), 2000);
} catch (e) {
setInfo(e.message);
await updateSettings(removeEmptyStringFields(data));
setToastData({
title: 'Settings stored',
type: 'success',
});
} catch (err) {
setToastApiError(formatUnknownError(err));
}
};
return (
<PageContent>
<PageContent headerContent="">
<Grid container style={{ marginBottom: '1rem' }}>
<Grid item md={12}>
<Alert severity="info">
@ -86,7 +96,7 @@ function SamlAuth({ config, getSamlConfig, updateSamlConfig, unleashUrl }) {
to learn how to integrate with specific SAML 2.0
providers (Okta, Keycloak, etc). <br />
Callback URL:{' '}
<code>{unleashUrl}/auth/saml/callback</code>
<code>{uiConfig.unleashUrl}/auth/saml/callback</code>
</Alert>
</Grid>
</Grid>
@ -120,7 +130,7 @@ function SamlAuth({ config, getSamlConfig, updateSamlConfig, unleashUrl }) {
onChange={updateField}
label="Entity ID"
name="entityId"
value={data.entityId || ''}
value={data.entityId}
disabled={!data.enabled}
style={{ width: '400px' }}
variant="outlined"
@ -142,7 +152,7 @@ function SamlAuth({ config, getSamlConfig, updateSamlConfig, unleashUrl }) {
onChange={updateField}
label="Single Sign-On URL"
name="signOnUrl"
value={data.signOnUrl || ''}
value={data.signOnUrl}
disabled={!data.enabled}
style={{ width: '400px' }}
variant="outlined"
@ -164,7 +174,7 @@ function SamlAuth({ config, getSamlConfig, updateSamlConfig, unleashUrl }) {
onChange={updateField}
label="X.509 Certificate"
name="certificate"
value={data.certificate || ''}
value={data.certificate}
disabled={!data.enabled}
style={{ width: '100%' }}
InputProps={{
@ -196,7 +206,7 @@ function SamlAuth({ config, getSamlConfig, updateSamlConfig, unleashUrl }) {
onChange={updateField}
label="Single Sign-out URL"
name="signOutUrl"
value={data.signOutUrl || ''}
value={data.signOutUrl}
disabled={!data.enabled}
style={{ width: '400px' }}
variant="outlined"
@ -219,7 +229,7 @@ function SamlAuth({ config, getSamlConfig, updateSamlConfig, unleashUrl }) {
onChange={updateField}
label="X.509 Certificate"
name="spCertificate"
value={data.spCertificate || ''}
value={data.spCertificate}
disabled={!data.enabled}
style={{ width: '100%' }}
InputProps={{
@ -243,22 +253,18 @@ function SamlAuth({ config, getSamlConfig, updateSamlConfig, unleashUrl }) {
variant="contained"
color="primary"
type="submit"
disabled={loading}
>
Save
</Button>{' '}
<small>{info}</small>
<p>
<small style={{ color: 'red' }}>
{errors?.message}
</small>
</p>
</Grid>
</Grid>
</form>
</PageContent>
);
}
SamlAuth.propTypes = {
config: PropTypes.object,
unleash: PropTypes.string,
getSamlConfig: PropTypes.func.isRequired,
updateSamlConfig: PropTypes.func.isRequired,
};
export default SamlAuth;

View File

@ -1,12 +0,0 @@
import { connect } from 'react-redux';
import GoogleAuth from './google-auth';
import { getGoogleConfig, updateGoogleConfig } from '../../../store/e-admin-auth/actions';
const mapStateToProps = state => ({
config: state.authAdmin.get('google'),
unleashUrl: state.uiConfig.toJS().unleashUrl,
});
const Container = connect(mapStateToProps, { getGoogleConfig, updateGoogleConfig })(GoogleAuth);
export default Container;

View File

@ -1,10 +0,0 @@
import { connect } from 'react-redux';
import component from './authentication';
const mapStateToProps = state => ({
authenticationType: state.uiConfig.toJS().authenticationType,
});
const Container = connect(mapStateToProps, { })(component);
export default Container;

View File

@ -1,12 +0,0 @@
import { connect } from 'react-redux';
import OidcAuth from './oidc-auth';
import { getOidcConfig, updateOidcConfig } from '../../../store/e-admin-auth/actions';
const mapStateToProps = state => ({
config: state.authAdmin.get('oidc'),
unleashUrl: state.uiConfig.toJS().unleashUrl,
});
const OidcContainer = connect(mapStateToProps, { getOidcConfig, updateOidcConfig })(OidcAuth);
export default OidcContainer;

View File

@ -1,12 +0,0 @@
import { connect } from 'react-redux';
import SamlAuth from './saml-auth';
import { getSamlConfig, updateSamlConfig } from '../../../store/e-admin-auth/actions';
const mapStateToProps = state => ({
config: state.authAdmin.get('saml'),
unleashUrl: state.uiConfig.toJS().unleashUrl,
});
const Container = connect(mapStateToProps, { getSamlConfig, updateSamlConfig })(SamlAuth);
export default Container;

View File

@ -1,6 +1,6 @@
import React from 'react';
import { NavLink } from 'react-router-dom';
import { Paper, Tabs, Tab } from '@material-ui/core';
import { NavLink, useLocation } from 'react-router-dom';
import { Paper, Tab, Tabs } from '@material-ui/core';
import useUiConfig from '../../../hooks/api/getters/useUiConfig/useUiConfig';
const navLinkStyle = {
@ -13,18 +13,17 @@ const navLinkStyle = {
padding: '0.8rem 1.5rem',
};
const activeNavLinkStyle = {
const activeNavLinkStyle: React.CSSProperties = {
fontWeight: 'bold',
borderRadius: '3px',
padding: '0.8rem 1.5rem',
};
function AdminMenu({ history }) {
function AdminMenu() {
const { uiConfig } = useUiConfig();
const { pathname } = useLocation();
const { flags } = uiConfig;
const { location } = history;
const { pathname } = location;
return (
<Paper
style={{
@ -45,7 +44,7 @@ function AdminMenu({ history }) {
<span>Users</span>
</NavLink>
}
></Tab>
/>
{flags.RE && (
<Tab
value="/admin/roles"
@ -58,7 +57,7 @@ function AdminMenu({ history }) {
<span>PROJECT ROLES</span>
</NavLink>
}
></Tab>
/>
)}
<Tab
@ -72,7 +71,7 @@ function AdminMenu({ history }) {
API Access
</NavLink>
}
></Tab>
/>
<Tab
value="/admin/auth"
label={
@ -84,7 +83,7 @@ function AdminMenu({ history }) {
Single Sign-On
</NavLink>
}
></Tab>
/>
</Tabs>
</Paper>
);

View File

@ -61,6 +61,7 @@ const TabNav = ({ tabData, className, navClass, startingTab = 0 }) => {
TabNav.propTypes = {
tabData: PropTypes.array.isRequired,
navClass: PropTypes.string,
className: PropTypes.string,
startingTab: PropTypes.number,
};

View File

@ -398,12 +398,7 @@ Array [
"type": "protected",
},
Object {
"component": Object {
"$$typeof": Symbol(react.memo),
"WrappedComponent": [Function],
"compare": null,
"type": [Function],
},
"component": [Function],
"layout": "main",
"menu": Object {
"adminSettings": true,

View File

@ -16,7 +16,7 @@ import Admin from '../admin';
import AdminApi from '../admin/api';
import AdminInvoice from '../admin/invoice/InvoiceAdminPage';
import AdminUsers from '../admin/users/UsersAdmin';
import AdminAuth from '../admin/auth';
import { AuthSettings } from '../admin/auth/AuthSettings';
import Login from '../user/Login/Login';
import { P, C, E, EEA, RE } from '../common/flags';
import NewUser from '../user/NewUser';
@ -446,7 +446,7 @@ export const routes = [
path: '/admin/auth',
parent: '/admin',
title: 'Single Sign-On',
component: AdminAuth,
component: AuthSettings,
type: 'protected',
layout: 'main',
menu: { adminSettings: true },

View File

@ -46,7 +46,7 @@ const useAPI = ({
handleUnauthorized,
propagateErrors = false,
}: IUseAPI) => {
const [errors, setErrors] = useState({});
const [errors, setErrors] = useState<Record<string, string>>({});
const [loading, setLoading] = useState(false);
const defaultOptions: RequestInit = {

View File

@ -12,7 +12,6 @@ export const handleBadRequest = async (
if (!setErrors) return;
if (res) {
const data = await res.json();
setErrors({message: data.message});
throw new Error(data.message);
}

View File

@ -5,7 +5,7 @@ export const defaultValue = {
version: '3.x',
environment: '',
slogan: 'The enterprise ready feature toggle service.',
flags: { P: false, C: false, E: false },
flags: { P: false, C: false, E: false, RE: false },
links: [
{
value: 'Documentation',

View File

@ -15,6 +15,7 @@ export interface IFlags {
C: boolean;
P: boolean;
E: boolean;
RE: boolean;
}
export interface IVersionInfo {

View File

@ -1,91 +0,0 @@
import api from './api';
import { dispatchError } from '../util';
export const RECIEVE_GOOGLE_CONFIG = 'RECIEVE_GOOGLE_CONFIG';
export const RECIEVE_GOOGLE_CONFIG_ERROR = 'RECIEVE_GOOGLE_CONFIG_ERROR';
export const UPDATE_GOOGLE_AUTH = 'UPDATE_GOOGLE_AUTH';
export const UPDATE_GOOGLE_AUTH_ERROR = 'UPDATE_GOOGLE_AUTH_ERROR';
export const RECIEVE_SAML_CONFIG = 'RECIEVE_SAML_CONFIG';
export const RECIEVE_SAML_CONFIG_ERROR = 'RECIEVE_SAML_CONFIG_ERROR';
export const UPDATE_SAML_AUTH = 'UPDATE_SAML_AUTH';
export const UPDATE_SAML_AUTH_ERROR = 'UPDATE_SAML_AUTH_ERROR';
export const RECIEVE_OIDC_CONFIG = 'RECIEVE_OIDC_CONFIG';
export const RECIEVE_OIDC_CONFIG_ERROR = 'RECIEVE_OIDC_CONFIG_ERROR';
export const UPDATE_OIDC_AUTH = 'UPDATE_OIDC_AUTH';
export const UPDATE_OIDC_AUTH_ERROR = 'UPDATE_OIDC_AUTH_ERROR';
const debug = require('debug')('unleash:e-admin-auth-actions');
export function getGoogleConfig() {
debug('Start fetching google-auth config');
return dispatch =>
api
.getGoogleConfig()
.then(config =>
dispatch({
type: RECIEVE_GOOGLE_CONFIG,
config,
})
)
.catch(dispatchError(dispatch, RECIEVE_GOOGLE_CONFIG_ERROR));
}
export function updateGoogleConfig(data) {
return dispatch =>
api
.updateGoogleConfig(data)
.then(config => dispatch({ type: UPDATE_GOOGLE_AUTH, config }))
.catch(e => {
dispatchError(dispatch, UPDATE_GOOGLE_AUTH_ERROR)(e);
throw e;
});
}
export function getSamlConfig() {
debug('Start fetching Saml-auth config');
return dispatch =>
api
.getSamlConfig()
.then(config =>
dispatch({
type: RECIEVE_SAML_CONFIG,
config,
})
)
.catch(dispatchError(dispatch, RECIEVE_SAML_CONFIG_ERROR));
}
export function updateSamlConfig(data) {
return dispatch =>
api
.updateSamlConfig(data)
.then(config => dispatch({ type: UPDATE_SAML_AUTH, config }))
.catch(e => {
dispatchError(dispatch, UPDATE_SAML_AUTH_ERROR)(e);
throw e;
});
}
export function getOidcConfig() {
debug('Start fetching OIDC-auth config');
return dispatch =>
api
.getOidcConfig()
.then(config =>
dispatch({
type: RECIEVE_OIDC_CONFIG,
config,
})
)
.catch(dispatchError(dispatch, RECIEVE_OIDC_CONFIG_ERROR));
}
export function updateOidcConfig(data) {
return dispatch =>
api
.updateOidcConfig(data)
.then(config => dispatch({ type: UPDATE_OIDC_AUTH, config }))
.catch(e => {
dispatchError(dispatch, UPDATE_OIDC_AUTH_ERROR)(e);
throw e;
});
}

View File

@ -1,66 +0,0 @@
import { throwIfNotSuccess, headers } from '../api-helper';
import { formatApiPath } from '../../utils/format-path';
const GOOGLE_URI = formatApiPath('api/admin/auth/google/settings');
const SAML_URI = formatApiPath('api/admin/auth/saml/settings');
const OIDC_URI = formatApiPath('api/admin/auth/oidc/settings');
function getGoogleConfig() {
return fetch(GOOGLE_URI, { headers, credentials: 'include' })
.then(throwIfNotSuccess)
.then(response => response.json());
}
function updateGoogleConfig(data) {
return fetch(GOOGLE_URI, {
method: 'POST',
headers,
body: JSON.stringify(data),
credentials: 'include',
})
.then(throwIfNotSuccess)
.then(response => response.json());
}
function getSamlConfig() {
return fetch(SAML_URI, { headers, credentials: 'include' })
.then(throwIfNotSuccess)
.then(response => response.json());
}
function updateSamlConfig(data) {
return fetch(SAML_URI, {
method: 'POST',
headers,
body: JSON.stringify(data),
credentials: 'include',
})
.then(throwIfNotSuccess)
.then(response => response.json());
}
function getOidcConfig() {
return fetch(OIDC_URI, { headers, credentials: 'include' })
.then(throwIfNotSuccess)
.then(response => response.json());
}
function updateOidcConfig(data) {
return fetch(OIDC_URI, {
method: 'POST',
headers,
body: JSON.stringify(data),
credentials: 'include',
})
.then(throwIfNotSuccess)
.then(response => response.json());
}
export default {
getGoogleConfig,
updateGoogleConfig,
getSamlConfig,
updateSamlConfig,
getOidcConfig,
updateOidcConfig,
};

View File

@ -1,20 +0,0 @@
import { Map as $Map } from 'immutable';
import { RECIEVE_GOOGLE_CONFIG, UPDATE_GOOGLE_AUTH, RECIEVE_SAML_CONFIG, UPDATE_SAML_AUTH, UPDATE_OIDC_AUTH, RECIEVE_OIDC_CONFIG } from './actions';
const store = (state = new $Map({ google: {}, saml: {}, oidc: {} }), action) => {
switch (action.type) {
case UPDATE_GOOGLE_AUTH:
case RECIEVE_GOOGLE_CONFIG:
return state.set('google', action.config);
case UPDATE_SAML_AUTH:
case RECIEVE_SAML_CONFIG:
return state.set('saml', action.config);
case UPDATE_OIDC_AUTH:
case RECIEVE_OIDC_CONFIG:
return state.set('oidc', action.config);
default:
return state;
}
};
export default store;

View File

@ -15,7 +15,6 @@ import uiConfig from './ui-config';
import context from './context';
import projects from './project';
import addons from './addons';
import authAdmin from './e-admin-auth';
import apiCalls from './api-calls';
import invoiceAdmin from './e-admin-invoice';
import feedback from './feedback';
@ -37,7 +36,6 @@ const unleashStore = combineReducers({
context,
projects,
addons,
authAdmin,
apiCalls,
invoiceAdmin,
feedback,

View File

@ -0,0 +1,8 @@
import { formatUnknownError } from './format-unknown-error';
test('formatUnknownError', () => {
expect(formatUnknownError(1)).toEqual('Unknown error');
expect(formatUnknownError('1')).toEqual('1');
expect(formatUnknownError(new Error('1'))).toEqual('1');
expect(formatUnknownError(new Error())).toEqual('Error');
});

View File

@ -0,0 +1,10 @@
// Get a human-readable error message string from a caught value.
export const formatUnknownError = (error: unknown): string => {
if (error instanceof Error) {
return error.message || error.toString();
} else if (typeof error === 'string') {
return error;
} else {
return 'Unknown error';
}
};

View File

@ -0,0 +1,11 @@
import { removeEmptyStringFields } from './remove-empty-string-fields';
test('removeEmptyStringFields', () => {
expect(removeEmptyStringFields({})).toEqual({});
expect(removeEmptyStringFields({ a: undefined })).toEqual({ a: undefined });
expect(removeEmptyStringFields({ a: 0 })).toEqual({ a: 0 });
expect(removeEmptyStringFields({ a: 1 })).toEqual({ a: 1 });
expect(removeEmptyStringFields({ a: '1' })).toEqual({ a: '1' });
expect(removeEmptyStringFields({ a: '' })).toEqual({});
expect(removeEmptyStringFields({ a: '', b: '2' })).toEqual({ b: '2' });
});

View File

@ -0,0 +1,9 @@
// Remove fields from an object if their value is the empty string.
export const removeEmptyStringFields = (object: {
[key: string]: unknown;
}): { [key: string]: unknown } => {
const entries = Object.entries(object);
const filtered = entries.filter(([, v]) => v !== '');
return Object.fromEntries(filtered);
};