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

fix: make admin pages work for OSS and enterprise (#268)

* fix: make admin pages work for OSS and enterprise

* fix: more admin tuning

* fix: project mgm access
This commit is contained in:
Ivar Conradi Østhus 2021-04-16 11:31:47 +02:00 committed by GitHub
parent 2a0acd3fb2
commit b3436b5ae6
19 changed files with 236 additions and 275 deletions

View File

@ -93,6 +93,15 @@ Array [
"title": "Sign out",
"type": "protected",
},
Object {
"component": [Function],
"hidden": false,
"icon": "album",
"layout": "main",
"path": "/admin",
"title": "Admin",
"type": "protected",
},
]
`;
@ -260,39 +269,6 @@ Array [
"title": "Projects",
"type": "protected",
},
Object {
"component": [Function],
"layout": "main",
"parent": "/admin",
"path": "/admin/api",
"title": "API access",
"type": "protected",
},
Object {
"component": [Function],
"layout": "main",
"parent": "/admin",
"path": "/admin/users",
"title": "Users",
"type": "protected",
},
Object {
"component": [Function],
"layout": "main",
"parent": "/admin",
"path": "/admin/auth",
"title": "Authentication",
"type": "protected",
},
Object {
"component": [Function],
"hidden": true,
"icon": "album",
"layout": "main",
"path": "/admin",
"title": "Admin",
"type": "protected",
},
Object {
"component": [Function],
"layout": "main",
@ -389,5 +365,43 @@ Array [
"title": "Log in",
"type": "unprotected",
},
Object {
"component": [Function],
"layout": "main",
"parent": "/admin",
"path": "/admin/api",
"title": "API access",
"type": "protected",
},
Object {
"component": [Function],
"layout": "main",
"parent": "/admin",
"path": "/admin/users",
"title": "Users",
"type": "protected",
},
Object {
"component": Object {
"$$typeof": Symbol(react.memo),
"WrappedComponent": [Function],
"compare": null,
"type": [Function],
},
"layout": "main",
"parent": "/admin",
"path": "/admin/auth",
"title": "Authentication",
"type": "protected",
},
Object {
"component": [Function],
"hidden": false,
"icon": "album",
"layout": "main",
"path": "/admin",
"title": "Admin",
"type": "protected",
},
]
`;

View File

@ -6,7 +6,7 @@ test('returns all defined routes', () => {
});
test('returns all baseRoutes', () => {
expect(baseRoutes.length).toEqual(11);
expect(baseRoutes.length).toEqual(12);
expect(baseRoutes).toMatchSnapshot();
});

View File

@ -213,41 +213,6 @@ export const routes = [
layout: 'main',
},
// Admin
{
path: '/admin/api',
parent: '/admin',
title: 'API access',
component: AdminApi,
type: 'protected',
layout: 'main',
},
{
path: '/admin/users',
parent: '/admin',
title: 'Users',
component: AdminUsers,
type: 'protected',
layout: 'main',
},
{
path: '/admin/auth',
parent: '/admin',
title: 'Authentication',
component: AdminAuth,
type: 'protected',
layout: 'main',
},
{
path: '/admin',
title: 'Admin',
icon: 'album',
component: Admin,
hidden: true,
type: 'protected',
layout: 'main',
},
{
path: '/tag-types/create',
parent: '/tag-types',
@ -342,6 +307,40 @@ export const routes = [
hidden: true,
layout: 'standalone',
},
// Admin
{
path: '/admin/api',
parent: '/admin',
title: 'API access',
component: AdminApi,
type: 'protected',
layout: 'main',
},
{
path: '/admin/users',
parent: '/admin',
title: 'Users',
component: AdminUsers,
type: 'protected',
layout: 'main',
},
{
path: '/admin/auth',
parent: '/admin',
title: 'Authentication',
component: AdminAuth,
type: 'protected',
layout: 'main',
},
{
path: '/admin',
title: 'Admin',
icon: 'album',
component: Admin,
hidden: false,
type: 'protected',
layout: 'main',
},
];
export const getRoute = path => routes.find(route => route.path === path);

View File

@ -2,7 +2,7 @@ import PropTypes from 'prop-types';
import React, { useEffect, useState } from 'react';
import HeaderTitle from '../../common/HeaderTitle';
import ConditionallyRender from '../../common/ConditionallyRender/ConditionallyRender';
import { CREATE_PROJECT, DELETE_PROJECT } from '../../../permissions';
import { CREATE_PROJECT, DELETE_PROJECT, UPDATE_PROJECT } from '../../../permissions';
import { Icon, IconButton, List, ListItem, ListItemAvatar, ListItemText, Tooltip } from '@material-ui/core';
import { Link } from 'react-router-dom';
import ConfirmDialogue from '../../common/Dialogue';
@ -36,6 +36,16 @@ const ProjectList = ({ projects, fetchProjects, removeProject, history, hasPermi
</Link>
);
const mgmAccessButton = project => (
<Tooltip title="Manage access">
<Link to={`/projects/${project.id}/access`} style={{ color: 'black' }}>
<IconButton aria-label="manage_access" >
<Icon>supervised_user_circle</Icon>
</IconButton>
</Link>
</Tooltip>
);
const deleteProjectButton = project => (
<Tooltip title="Remove project">
<IconButton
@ -57,12 +67,16 @@ const ProjectList = ({ projects, fetchProjects, removeProject, history, hasPermi
<Icon>folder_open</Icon>
</ListItemAvatar>
<ListItemText primary={projectLink(project)} secondary={project.description} />
<ConditionallyRender
condition={hasPermission(UPDATE_PROJECT)}
show={mgmAccessButton(project)}
/>
<ConditionallyRender condition={hasPermission(DELETE_PROJECT)} show={deleteProjectButton(project)} />
</ListItem>
));
return (
<PageContent headerContent={<HeaderTitle title="Projects (beta)" actions={addProjectButton()} />}>
<PageContent headerContent={<HeaderTitle title="Projects" actions={addProjectButton()} />}>
<List>
<ConditionallyRender
condition={projects.length > 0}
@ -93,8 +107,6 @@ ProjectList.propTypes = {
removeProject: PropTypes.func.isRequired,
history: PropTypes.object.isRequired,
hasPermission: PropTypes.func.isRequired,
id: PropTypes.string.isRequired,
name: PropTypes.string.isRequired,
};
export default ProjectList;

View File

@ -1,16 +1,13 @@
import { connect } from 'react-redux';
import { fetchProjects, removeProject } from '../../../store/project/actions';
import { hasPermission } from '../../../permissions';
import { RBAC } from '../../common/flags';
import ProjectList from './ProjectList';
const mapStateToProps = state => {
const projects = state.projects.toJS();
const rbacEnabled = !!state.uiConfig.toJS().flags[RBAC];
return {
projects,
rbacEnabled,
hasPermission: hasPermission.bind(null, state.user.get('profile')),
};
};

View File

@ -1,96 +0,0 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { Link } from 'react-router-dom';
import { List, ListItem, IconButton, Icon, Paper, ListItemAvatar, ListItemText, Tooltip } from '@material-ui/core';
import { HeaderTitle, styles as commonStyles } from '../common';
import { CREATE_PROJECT, DELETE_PROJECT } from '../../permissions';
import ConditionallyRender from '../common/conditionally-render';
class ProjectListComponent extends Component {
static propTypes = {
projects: PropTypes.array.isRequired,
fetchProjects: PropTypes.func.isRequired,
removeProject: PropTypes.func.isRequired,
history: PropTypes.object.isRequired,
hasPermission: PropTypes.func.isRequired,
rbacEnabled: PropTypes.bool.isRequired,
};
componentDidMount() {
this.props.fetchProjects();
}
removeProject = (project, evt) => {
evt.preventDefault();
this.props.removeProject(project);
};
projectLink = ({ id, name }) => (
<Link to={`/projects/edit/${id}`}>
<strong>{name}</strong>
</Link>
);
deleteProjectButton = project => (
<Tooltip title="Remove project">
<IconButton aria-label="delete" onClick={this.removeProject.bind(this, project)}>
<Icon>delete</Icon>
</IconButton>
</Tooltip>
);
projectList = () => {
const { projects, hasPermission } = this.props;
return projects.map((project, i) => (
<ListItem key={i}>
<ListItemAvatar>
<Icon>folder_open</Icon>
</ListItemAvatar>
<ListItemText primary={this.projectLink(project)} secondary={project.description} />
<ConditionallyRender
condition={hasPermission(DELETE_PROJECT)}
show={this.deleteProjectButton(project)}
/>
</ListItem>
));
};
addProjectButton = () => {
const { hasPermission } = this.props;
return (
<ConditionallyRender
condition={hasPermission(CREATE_PROJECT)}
show={
<Tooltip title="Add new project">
<IconButton
aria-label="add-project"
onClick={() => this.props.history.push('/projects/create')}
>
<Icon>add</Icon>
</IconButton>
</Tooltip>
}
/>
);
};
render() {
const { projects } = this.props;
return (
<Paper shadow={0} className={commonStyles.fullwidth}>
<HeaderTitle title="Projects (beta)" actions={this.addProjectButton()} />
<List>
<ConditionallyRender
condition={projects.length > 0}
show={this.projectList()}
elseShow={<ListItem>No projects defined</ListItem>}
/>
</List>
</Paper>
);
}
}
export default ProjectListComponent;

View File

@ -1,7 +1,6 @@
import React from 'react';
import { NavLink } from 'react-router-dom';
import { Grid, Icon } from '@material-ui/core';
import PageContent from '../../component/common/PageContent/PageContent';
import { Paper, Icon, Tabs, Tab } from '@material-ui/core';
const navLinkStyle = {
display: 'flex',
@ -23,30 +22,36 @@ const iconStyle = {
marginRight: '5px',
};
function AdminMenu() {
function AdminMenu({history}) {
const { location } = history;
const { pathname } = location;
return (
<PageContent style={{ marginBottom: '1rem' }}>
<Grid container justify={'center'}>
<Grid item md={4}>
<Paper style={{ marginBottom: '1rem' }}>
<Tabs centered value={pathname} >
<Tab value="/admin/users" label={
<NavLink to="/admin/users" activeStyle={activeNavLinkStyle} style={navLinkStyle}>
<Icon style={iconStyle}>supervised_user_circle</Icon>
Users
</NavLink>
</Grid>
<Grid item md={4}>
<Icon style={iconStyle}>supervised_user_circle</Icon>
<span>Users</span>
</NavLink>
}
>
</Tab>
<Tab value="/admin/api" label={
<NavLink to="/admin/api" activeStyle={activeNavLinkStyle} style={navLinkStyle}>
<Icon style={iconStyle}>apps</Icon>
API Access
</NavLink>
</Grid>
<Grid item md={4}>
}>
</Tab>
<Tab value="/admin/auth" label={
<NavLink to="/admin/auth" activeStyle={activeNavLinkStyle} style={navLinkStyle}>
<Icon style={iconStyle}>lock</Icon>
Authentication
</NavLink>
</Grid>
</Grid>
</PageContent>
}>
</Tab>
</Tabs>
</Paper>
);
}

View File

@ -1,25 +0,0 @@
import React from 'react';
function ApiHowTo() {
return (
<div style={{ marginBottom: '1rem' }}>
<p
style={{
backgroundColor: '#cfe5ff',
border: '2px solid #c4e1ff',
padding: '8px',
borderRadius: '5px',
}}
>
Read the{' '}
<a href="https://www.unleash-hosted.com/docs" target="_blank" rel="noreferrer">
Getting started guide
</a>{' '}
to learn how to connect to the Unleash API form your application or programmatically. <br /> <br />
Please note it can take up to 1 minute before a new API key is activated.
</p>
</div>
);
}
export default ApiHowTo;

View File

@ -7,6 +7,7 @@ import { hasPermission } from '../../../permissions';
export default connect(
state => ({
location: state.settings.toJS().location || {},
unleashUrl: state.uiConfig.toJS().unleashUrl,
keys: state.apiAdmin.toJS(),
hasPermission: permission => hasPermission(state.user.get('profile'), permission),
}),

View File

@ -2,14 +2,14 @@
import React, { useEffect, useState } from 'react';
import PropTypes from 'prop-types';
import { Icon, Table, TableHead, TableBody, TableRow, TableCell, IconButton } from '@material-ui/core';
import { Alert } from '@material-ui/lab';
import { formatFullDateTimeWithLocale } from '../../../component/common/util';
import CreateApiKey from './api-key-create';
import Secret from './secret';
import ApiHowTo from './api-howto';
import ConditionallyRender from '../../../component/common/ConditionallyRender/ConditionallyRender';
import Dialogue from '../../../component/common/Dialogue/Dialogue';
function ApiKeyList({ location, fetchApiKeys, removeKey, addKey, keys, hasPermission }) {
function ApiKeyList({ location, fetchApiKeys, removeKey, addKey, keys, hasPermission, unleashUrl }) {
const [showDelete, setShowDelete] = useState(false);
const [delKey, setDelKey] = useState(undefined);
const deleteKey = async () => {
@ -25,7 +25,22 @@ function ApiKeyList({ location, fetchApiKeys, removeKey, addKey, keys, hasPermis
return (
<div>
<ApiHowTo />
<Alert severity="info" >
<p>
Read the{' '}
<a href="https://docs.getunleash.io/docs" target="_blank" rel="noreferrer">
Getting started guide
</a>{' '}
to learn how to connect to the Unleash API form your application or programmatically.
Please note it can take up to 1 minute before a new API key is activated.
</p>
<br />
<strong>API URL: </strong> <pre style={{display: 'inline'}}>{unleashUrl}/api/</pre>
</Alert>
<br /><br />
<br />
<Table>
<TableHead>
<TableRow>
@ -93,6 +108,7 @@ ApiKeyList.propTypes = {
removeKey: PropTypes.func.isRequired,
addKey: PropTypes.func.isRequired,
keys: PropTypes.array.isRequired,
unleashUrl: PropTypes.string,
hasPermission: PropTypes.func.isRequired,
};

View File

@ -5,9 +5,9 @@ import ApiKeyList from './api-key-list-container';
import AdminMenu from '../admin-menu';
import PageContent from '../../../component/common/PageContent/PageContent';
const render = () => (
const render = ({history}) => (
<div>
<AdminMenu />
<AdminMenu history={history} />
<PageContent headerContent="API Access">
<ApiKeyList />
</PageContent>

View File

@ -0,0 +1,56 @@
import React from 'react';
import PropTypes from 'prop-types';
import AdminMenu from '../admin-menu';
import { Alert } from '@material-ui/lab';
import GoogleAuth from './google-auth-container';
import SamlAuth from './saml-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 }) {
const tabs = [
{
label: 'SAML 2.0',
component: <SamlAuth />,
},
{
label: 'Google',
component: <GoogleAuth />,
},
];
return (
<div>
<AdminMenu history={history} />
<PageContent headerContent="Authentication">
<ConditionallyRender condition={authenticationType === 'enterprise'}
show={
<TabNav tabData={tabs} />
}
/>
<ConditionallyRender condition={authenticationType === 'open-source'}
show={
<Alert severity="warning">
You are running the open-source version of Unleash. You have to use the Enterprise edition
in order configure Single Sign-on.</Alert>
}
/>
<ConditionallyRender condition={authenticationType === 'custom'}
show={
<Alert severity="warning">You have decided to use custom authentication type. You have to use the Enterprise edition
in order configure Single Sign-on from the user interface.</Alert>
}
/>
</PageContent>
</div>
);
}
AdminAuthPage.propTypes = {
match: PropTypes.object.isRequired,
history: PropTypes.object.isRequired,
authenticationType: PropTypes.string,
};
export default AdminAuthPage;

View File

@ -5,6 +5,7 @@ import { hasPermission } from '../../../permissions';
const mapStateToProps = state => ({
config: state.authAdmin.get('google'),
unleashUrl: state.uiConfig.toJS().unleashUrl,
hasPermission: permission => hasPermission(state.user.get('profile'), permission),
});

View File

@ -1,6 +1,7 @@
import React, { useState, useEffect } from 'react';
import PropTypes from 'prop-types';
import { Button, Grid, Switch, TextField, Typography } from '@material-ui/core';
import { Button, Grid, Switch, TextField } from '@material-ui/core';
import { Alert } from '@material-ui/lab';
import PageContent from '../../../component/common/PageContent/PageContent';
const initialState = {
@ -9,7 +10,7 @@ const initialState = {
unleashHostname: location.hostname,
};
function GoogleAuth({ config, getGoogleConfig, updateGoogleConfig, hasPermission }) {
function GoogleAuth({ config, getGoogleConfig, updateGoogleConfig, hasPermission, unleashUrl }) {
const [data, setData] = useState(initialState);
const [info, setInfo] = useState();
@ -58,14 +59,14 @@ function GoogleAuth({ config, getGoogleConfig, updateGoogleConfig, hasPermission
<PageContent>
<Grid container style={{ marginBottom: '1rem' }}>
<Grid item xs={12}>
<Typography variant="subtitle1">
<Alert severity="info">
Please read the{' '}
<a href="https://www.unleash-hosted.com/docs/enterprise-authentication/google" target="_blank" rel="noreferrer">
documentation
</a>{' '}
to learn how to integrate with Google OAuth 2.0. <br />
Callback URL: <code>https://[unleash.hostname.com]/auth/google/callback</code>
</Typography>
Callback URL: <code>{unleashUrl}/auth/google/callback</code>
</Alert>
</Grid>
</Grid>
<form onSubmit={onSubmit}>
@ -189,6 +190,7 @@ function GoogleAuth({ config, getGoogleConfig, updateGoogleConfig, hasPermission
GoogleAuth.propTypes = {
config: PropTypes.object,
unleashUrl: PropTypes.string,
getGoogleConfig: PropTypes.func.isRequired,
updateGoogleConfig: PropTypes.func.isRequired,
hasPermission: PropTypes.func.isRequired,

View File

@ -1,36 +1,12 @@
import React from 'react';
import PropTypes from 'prop-types';
import AdminMenu from '../admin-menu';
import GoogleAuth from './google-auth-container';
import SamlAuth from './saml-auth-container';
import TabNav from '../../../component/common/TabNav/TabNav';
import PageContent from '../../../component/common/PageContent/PageContent';
import { connect } from 'react-redux';
import component from './authentication';
import { hasPermission } from '../../../permissions';
function AdminAuthPage() {
const tabs = [
{
label: 'SAML 2.0',
component: <SamlAuth />,
},
{
label: 'Google',
component: <GoogleAuth />,
},
];
const mapStateToProps = state => ({
authenticationType: state.uiConfig.toJS().authenticationType,
hasPermission: permission => hasPermission(state.user.get('profile'), permission),
});
return (
<div>
<AdminMenu />
<PageContent headerContent="Authentication">
<TabNav tabData={tabs} />
</PageContent>
</div>
);
}
const Container = connect(mapStateToProps, { })(component);
AdminAuthPage.propTypes = {
match: PropTypes.object.isRequired,
history: PropTypes.object.isRequired,
};
export default AdminAuthPage;
export default Container;

View File

@ -5,6 +5,7 @@ import { hasPermission } from '../../../permissions';
const mapStateToProps = state => ({
config: state.authAdmin.get('saml'),
unleashUrl: state.uiConfig.toJS().unleashUrl,
hasPermission: permission => hasPermission(state.user.get('profile'), permission),
});

View File

@ -1,6 +1,7 @@
import React, { useState, useEffect } from 'react';
import PropTypes from 'prop-types';
import { Button, Grid, Switch, TextField, Typography } from '@material-ui/core';
import { Button, Grid, Switch, TextField } from '@material-ui/core';
import { Alert } from '@material-ui/lab';
import PageContent from '../../../component/common/PageContent/PageContent';
const initialState = {
@ -9,7 +10,7 @@ const initialState = {
unleashHostname: location.hostname,
};
function SamlAuth({ config, getSamlConfig, updateSamlConfig, hasPermission }) {
function SamlAuth({ config, getSamlConfig, updateSamlConfig, hasPermission, unleashUrl }) {
const [data, setData] = useState(initialState);
const [info, setInfo] = useState();
@ -26,7 +27,7 @@ function SamlAuth({ config, getSamlConfig, updateSamlConfig, hasPermission }) {
}, [config]);
if (!hasPermission('ADMIN')) {
return <span>You need admin privileges to access this section.</span>;
return <Alert severity="error">You need to be a root admin to access this section.</Alert>;
}
const updateField = e => {
@ -59,14 +60,14 @@ function SamlAuth({ config, getSamlConfig, updateSamlConfig, hasPermission }) {
<PageContent>
<Grid container style={{ marginBottom: '1rem' }}>
<Grid item md={12}>
<Typography variant="subtitle1">
<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 SAML 2.0 providers (Okta, Keycloak, etc). <br />
Callback URL: <code>https://[unleash.hostname.com]/auth/saml/callback</code>
</Typography>
Callback URL: <code>{unleashUrl}/auth/saml/callback</code>
</Alert>
</Grid>
</Grid>
<form onSubmit={onSubmit}>
@ -184,6 +185,7 @@ function SamlAuth({ config, getSamlConfig, updateSamlConfig, hasPermission }) {
SamlAuth.propTypes = {
config: PropTypes.object,
unleash: PropTypes.string,
getSamlConfig: PropTypes.func.isRequired,
updateSamlConfig: PropTypes.func.isRequired,
hasPermission: PropTypes.func.isRequired,

View File

@ -1,8 +1,8 @@
/* eslint-disable no-alert */
import React, { useEffect, useState } from 'react';
import PropTypes from 'prop-types';
import { Button, Icon, IconButton, Table, TableBody, TableCell, TableHead, TableRow } from '@material-ui/core';
import { formatFullDateTimeWithLocale } from '../../../../component/common/util';
import { Button, Icon, IconButton, Table, TableBody, TableCell, TableHead, TableRow, Avatar } from '@material-ui/core';
import { formatDateWithLocale } from '../../../../component/common/util';
import AddUser from '../add-user-component';
import ChangePassword from '../change-password-component';
import UpdateUser from '../update-user-component';
@ -78,26 +78,26 @@ function UsersList({
<Table>
<TableHead>
<TableRow>
<TableCell>Id</TableCell>
<TableCell></TableCell>
<TableCell>Created</TableCell>
<TableCell>Username</TableCell>
<TableCell>Name</TableCell>
<TableCell>Role</TableCell>
<TableCell>{hasPermission('ADMIN') ? 'Action' : ''}</TableCell>
<TableCell>Username</TableCell>
<TableCell align="center">Role</TableCell>
<TableCell align="right">{hasPermission('ADMIN') ? 'Action' : ''}</TableCell>
</TableRow>
</TableHead>
<TableBody>
{users.map(item => (
<TableRow key={item.id}>
<TableCell>{item.id}</TableCell>
<TableCell>{formatFullDateTimeWithLocale(item.createdAt, location.locale)}</TableCell>
<TableCell style={{ textAlign: 'left' }}>{item.username || item.email}</TableCell>
<TableCell><Avatar variant="rounded" alt={item.name} src={item.imageUrl} title={`${item.name || item.email || item.username} (id: ${item.id})`} /></TableCell>
<TableCell>{formatDateWithLocale(item.createdAt, location.locale)}</TableCell>
<TableCell style={{ textAlign: 'left' }}>{item.name}</TableCell>
<TableCell>{renderRole(item.rootRole)}</TableCell>
<TableCell style={{ textAlign: 'left' }}>{item.username || item.email}</TableCell>
<TableCell align="center">{renderRole(item.rootRole)}</TableCell>
<ConditionallyRender
condition={hasPermission('ADMIN')}
show={
<TableCell>
<TableCell align="right">
<IconButton aria-label="Edit" title="Edit" onClick={openUpdateDialog(item)}>
<Icon>edit</Icon>
</IconButton>

View File

@ -4,9 +4,9 @@ import UsersList from './UsersList';
import AdminMenu from '../admin-menu';
import PageContent from '../../../component/common/PageContent/PageContent';
const render = () => (
const render = ({history}) => (
<div>
<AdminMenu />
<AdminMenu history={history} />
<PageContent headerContent="Users">
<UsersList />
</PageContent>