1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-07-31 13:47:02 +02:00

fix: proper error handling for auth-settings being stored (#293)

Co-authored-by: Fredrik Strand Oseberg <fredrik.no@gmail.com>
This commit is contained in:
Ivar Conradi Østhus 2021-05-10 13:15:44 +02:00 committed by GitHub
parent 91860744f8
commit 06d7f9b609
10 changed files with 320 additions and 8 deletions

View File

@ -1,3 +1,4 @@
export const P = 'P';
export const C = 'C';
export const RBAC = 'RBAC';
export const OIDC = 'OIDC';

View File

@ -4,11 +4,12 @@ import AdminMenu from '../admin-menu';
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 TabNav from '../../../component/common/TabNav/TabNav';
import PageContent from '../../../component/common/PageContent/PageContent';
import ConditionallyRender from '../../../component/common/ConditionallyRender/ConditionallyRender';
function AdminAuthPage({ authenticationType, history }) {
function AdminAuthPage({ authenticationType, history, enableOIDC }) {
const tabs = [
{
label: 'SAML 2.0',
@ -20,6 +21,13 @@ function AdminAuthPage({ authenticationType, history }) {
},
];
if(enableOIDC) {
tabs.unshift( {
label: 'OpenID Connect',
component: <OidcAuth />,
},)
}
return (
<div>
<AdminMenu history={history} />
@ -63,6 +71,7 @@ AdminAuthPage.propTypes = {
match: PropTypes.object.isRequired,
history: PropTypes.object.isRequired,
authenticationType: PropTypes.string,
enableOIDC: PropTypes.bool,
};
export default AdminAuthPage;

View File

@ -115,10 +115,11 @@ function GoogleAuth({
label="Client ID"
name="clientId"
placeholder=""
value={data.clientId}
value={data.clientId || ''}
style={{ width: '400px' }}
variant="outlined"
size="small"
required
/>
</Grid>
</Grid>
@ -135,11 +136,12 @@ function GoogleAuth({
onChange={updateField}
label="Client Secret"
name="clientSecret"
value={data.clientSecret}
value={data.clientSecret || ''}
placeholder=""
style={{ width: '400px' }}
variant="outlined"
size="small"
required
/>
</Grid>
</Grid>
@ -163,7 +165,7 @@ function GoogleAuth({
label="Unleash Hostname"
name="unleashHostname"
placeholder=""
value={data.unleashHostname}
value={data.unleashHostname || ''}
style={{ width: '400px' }}
variant="outlined"
size="small"

View File

@ -1,8 +1,10 @@
import { connect } from 'react-redux';
import component from './authentication';
import { OIDC } from '../../../component/common/flags';
const mapStateToProps = state => ({
authenticationType: state.uiConfig.toJS().authenticationType,
enableOIDC: !!state.uiConfig.toJS().flags[OIDC],
});
const Container = connect(mapStateToProps, { })(component);

View File

@ -0,0 +1,12 @@
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

@ -0,0 +1,225 @@
import React, { useState, useEffect, useContext } from 'react';
import PropTypes from 'prop-types';
import { Button, Grid, Switch, TextField } from '@material-ui/core';
import { Alert } from '@material-ui/lab';
import PageContent from '../../../component/common/PageContent/PageContent';
import AccessContext from '../../../contexts/AccessContext';
import { ADMIN } from '../../../component/AccessProvider/permissions';
const initialState = {
enabled: false,
autoCreate: false,
unleashHostname: location.hostname,
};
function OidcAuth({ config, getOidcConfig, updateOidcConfig, unleashUrl }) {
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
}, []);
useEffect(() => {
if (config.discoverUrl) {
setData(config);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [config]);
if (!hasAccess(ADMIN)) {
return (
<Alert severity="error">
You need to be a root admin to access this section.
</Alert>
);
}
const updateField = e => {
setData({
...data,
[e.target.name]: e.target.value,
});
};
const updateEnabled = () => {
setData({ ...data, enabled: !data.enabled });
};
const updateAutoCreate = () => {
setData({ ...data, autoCreate: !data.autoCreate });
};
const onSubmit = async e => {
e.preventDefault();
setInfo('...saving');
setError('');
try {
await updateOidcConfig(data);
setInfo('Settings stored');
setTimeout(() => setInfo(''), 2000);
} catch (e) {
setInfo('');
setError(e.message);
}
};
return (
<PageContent>
<Grid container style={{ marginBottom: '1rem' }}>
<Grid item md={12}>
<Alert severity="info">
Please read the{' '}
<a
href="https://www.unleash-hosted.com/docs/enterprise-authentication"
target="_blank"
rel="noreferrer"
>
documentation
</a>{' '}
to learn how to integrate with specific Open Id Connect
providers (Okta, Keycloak, Google, etc). <br />
Callback URL:{' '}
<code>{unleashUrl}/auth/oidc/callback</code>
</Alert>
</Grid>
</Grid>
<form onSubmit={onSubmit}>
<Grid container spacing={3}>
<Grid item md={5}>
<strong>Enable</strong>
<p>Enable Open Id Connect Authentication.</p>
</Grid>
<Grid item md={6}>
<Switch
onChange={updateEnabled}
value={data.enabled}
name="enabled"
checked={data.enabled}
>
{data.enabled ? 'Enabled' : 'Disabled'}
</Switch>
</Grid>
</Grid>
<Grid container spacing={3}>
<Grid item md={5}>
<strong>Discover URL</strong>
<p>(Required) Issuer discover metadata URL</p>
</Grid>
<Grid item md={6}>
<TextField
onChange={updateField}
label="Discover URL"
name="discoverUrl"
value={data.discoverUrl || ''}
style={{ width: '400px' }}
variant="outlined"
size="small"
/>
</Grid>
</Grid>
<Grid container spacing={3}>
<Grid item md={5}>
<strong>Client ID</strong>
<p>(Required) Client ID of your OpenID application</p>
</Grid>
<Grid item md={6}>
<TextField
onChange={updateField}
label="Client ID"
name="clientId"
value={data.clientId || ''}
style={{ width: '400px' }}
variant="outlined"
size="small"
required
/>
</Grid>
</Grid>
<Grid container spacing={3}>
<Grid item md={5}>
<strong>Client secret</strong>
<p>(Required) Client secret of your OpenID application. </p>
</Grid>
<Grid item md={6}>
<TextField
onChange={updateField}
label="Client Secret"
name="secret"
value={data.secret || ''}
style={{ width: '400px' }}
variant="outlined"
size="small"
required
/>
</Grid>
</Grid>
<Grid container spacing={3}>
<Grid item md={5}>
<strong>Auto-create users</strong>
<p>
Enable automatic creation of new users when signing
in with Open ID connect.
</p>
</Grid>
<Grid item md={6} style={{ padding: '20px' }}>
<Switch
onChange={updateAutoCreate}
name="enabled"
checked={data.autoCreate}
>
Auto-create users
</Switch>
</Grid>
</Grid>
<Grid container spacing={3}>
<Grid item md={5}>
<strong>Email domains</strong>
<p>
(Optional) 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"
value={data.emailDomains || ''}
placeholder="@company.com, @anotherCompany.com"
style={{ width: '400px' }}
rows={2}
variant="outlined"
size="small"
/>
</Grid>
</Grid>
<Grid container spacing={3}>
<Grid item md={12}>
<Button
variant="contained"
color="primary"
type="submit"
>
Save
</Button>{' '}
<small>{info}</small>
<small style={{color: 'red'}}>{error}</small>
</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

@ -114,6 +114,7 @@ function SamlAuth({ config, getSamlConfig, updateSamlConfig, unleashUrl }) {
style={{ width: '400px' }}
variant="outlined"
size="small"
required
/>
</Grid>
</Grid>
@ -134,6 +135,7 @@ function SamlAuth({ config, getSamlConfig, updateSamlConfig, unleashUrl }) {
style={{ width: '400px' }}
variant="outlined"
size="small"
required
/>
</Grid>
</Grid>
@ -161,6 +163,7 @@ function SamlAuth({ config, getSamlConfig, updateSamlConfig, unleashUrl }) {
rowsMax={14}
variant="outlined"
size="small"
required
/>
</Grid>
</Grid>

View File

@ -8,6 +8,10 @@ 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');
@ -30,7 +34,10 @@ export function updateGoogleConfig(data) {
api
.updateGoogleConfig(data)
.then(config => dispatch({ type: UPDATE_GOOGLE_AUTH, config }))
.catch(dispatchError(dispatch, UPDATE_GOOGLE_AUTH_ERROR));
.catch(e => {
dispatchError(dispatch, UPDATE_GOOGLE_AUTH_ERROR)(e);
throw e;
});
}
export function getSamlConfig() {
@ -52,5 +59,33 @@ export function updateSamlConfig(data) {
api
.updateSamlConfig(data)
.then(config => dispatch({ type: UPDATE_SAML_AUTH, config }))
.catch(dispatchError(dispatch, UPDATE_SAML_AUTH_ERROR));
.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

@ -3,6 +3,7 @@ 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' })
@ -38,9 +39,28 @@ function updateSamlConfig(data) {
.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,7 +1,7 @@
import { Map as $Map } from 'immutable';
import { RECIEVE_GOOGLE_CONFIG, UPDATE_GOOGLE_AUTH, RECIEVE_SAML_CONFIG, UPDATE_SAML_AUTH } from './actions';
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: {} }), action) => {
const store = (state = new $Map({ google: {}, saml: {}, oidc: {} }), action) => {
switch (action.type) {
case UPDATE_GOOGLE_AUTH:
case RECIEVE_GOOGLE_CONFIG:
@ -9,6 +9,9 @@ const store = (state = new $Map({ google: {}, saml: {} }), action) => {
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;
}