From 1eb8fc04645e46b0bdd262951c92d44618cd0677 Mon Sep 17 00:00:00 2001 From: Benjamin Ludewig Date: Wed, 19 Dec 2018 14:54:52 +0100 Subject: [PATCH] feature: Add support for permission system in unleash frontend --- .../application/application-edit-component.js | 77 +++++++++++----- .../component/common/permission-component.jsx | 47 ++++++++++ .../component/common/permission-container.js | 8 ++ .../feature-list-item-component-test.jsx.snap | 2 +- .../feature/feature-list-item-component.jsx | 6 +- .../src/component/feature/list-component.jsx | 17 ++-- .../src/component/feature/view-component.jsx | 88 ++++++++++++++----- .../component/strategies/list-component.jsx | 24 +++-- .../strategies/strategy-details-component.jsx | 15 +++- frontend/src/data/helper.js | 13 +++ frontend/src/permissions.js | 8 ++ 11 files changed, 244 insertions(+), 61 deletions(-) create mode 100644 frontend/src/component/common/permission-component.jsx create mode 100644 frontend/src/component/common/permission-container.js create mode 100644 frontend/src/permissions.js diff --git a/frontend/src/component/application/application-edit-component.js b/frontend/src/component/application/application-edit-component.js index 5dcc164875..65a034af42 100644 --- a/frontend/src/component/application/application-edit-component.js +++ b/frontend/src/component/application/application-edit-component.js @@ -22,6 +22,8 @@ import { } from 'react-mdl'; import { IconLink, shorten, styles as commonStyles } from '../common'; import { formatFullDateTimeWithLocale } from '../common/util'; +import { CREATE_FEATURE, CREATE_STRATEGY, UPDATE_APPLICATION } from '../../permissions'; +import PermissionComponent from '../common/permission-container'; class StatefulTextfield extends Component { static propTypes = { @@ -91,11 +93,26 @@ class ClientApplications extends PureComponent { {seenToggles.map( ({ name, description, enabled, notFound }, i) => notFound ? ( - - - {name} - - + + + {name} + + + } + otherwise={ + + + {name} + + + } + /> ) : ( notFound ? ( - - - {name} - - + + + {name} + + + } + otherwise={ + + + {name} + + + } + /> ) : ( @@ -203,16 +235,21 @@ class ClientApplications extends PureComponent { )}
- this.setState({ activeTab: tabId })} - ripple - tabBarProps={{ style: { width: '100%' } }} - className="mdl-color--grey-100" - > - Details - Edit - + this.setState({ activeTab: tabId })} + ripple + tabBarProps={{ style: { width: '100%' } }} + className="mdl-color--grey-100" + > + Details + Edit + + } + /> {content} diff --git a/frontend/src/component/common/permission-component.jsx b/frontend/src/component/common/permission-component.jsx new file mode 100644 index 0000000000..5b6278fb00 --- /dev/null +++ b/frontend/src/component/common/permission-component.jsx @@ -0,0 +1,47 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import { ADMIN } from '../../permissions'; + +class PermissionComponent extends Component { + static propTypes = { + user: PropTypes.object, + component: PropTypes.oneOfType([PropTypes.node, PropTypes.func]), + others: PropTypes.object, + denied: PropTypes.object, + granted: PropTypes.object, + otherwise: PropTypes.node, + permission: PropTypes.string, + children: PropTypes.node, + }; + + render() { + const { user, otherwise, component: Component, permission, granted, denied, children, ...others } = this.props; + let grantedComponent = Component; + let deniedCompoinent = otherwise || ''; + + if (granted || denied) { + grantedComponent = ( + + {children} + + ); + deniedCompoinent = ( + + {children} + + ); + } + + if (!user) return deniedCompoinent; + if ( + !user.permissions || + user.permissions.indexOf(ADMIN) !== -1 || + user.permissions.indexOf(permission) !== -1 + ) { + return grantedComponent; + } + return deniedCompoinent; + } +} + +export default PermissionComponent; diff --git a/frontend/src/component/common/permission-container.js b/frontend/src/component/common/permission-container.js new file mode 100644 index 0000000000..f4cf8d9d1e --- /dev/null +++ b/frontend/src/component/common/permission-container.js @@ -0,0 +1,8 @@ +import { connect } from 'react-redux'; +import PermissionComponent from './permission-component'; + +const mapStateToProps = state => ({ user: state.user.get('profile') }); + +const Container = connect(mapStateToProps)(PermissionComponent); + +export default Container; diff --git a/frontend/src/component/feature/__tests__/__snapshots__/feature-list-item-component-test.jsx.snap b/frontend/src/component/feature/__tests__/__snapshots__/feature-list-item-component-test.jsx.snap index 07294b161d..e4f9576656 100644 --- a/frontend/src/component/feature/__tests__/__snapshots__/feature-list-item-component-test.jsx.snap +++ b/frontend/src/component/feature/__tests__/__snapshots__/feature-list-item-component-test.jsx.snap @@ -22,7 +22,7 @@ exports[`renders correctly with one feature 1`] = ` > diff --git a/frontend/src/component/feature/feature-list-item-component.jsx b/frontend/src/component/feature/feature-list-item-component.jsx index 9ec3d019b5..9e3bb14e17 100644 --- a/frontend/src/component/feature/feature-list-item-component.jsx +++ b/frontend/src/component/feature/feature-list-item-component.jsx @@ -14,6 +14,7 @@ const Feature = ({ metricsLastHour = { yes: 0, no: 0, isFallback: true }, metricsLastMinute = { yes: 0, no: 0, isFallback: true }, revive, + updateable, }) => { const { name, description, enabled, strategies } = feature; const { showLastHour = false } = settings; @@ -42,7 +43,7 @@ const Feature = ({ toggleFeature(name)} @@ -59,7 +60,7 @@ const Feature = ({ {strategyChips} {summaryChip} - {revive ? ( + {updateable && revive ? ( revive(feature.name)}> @@ -77,6 +78,7 @@ Feature.propTypes = { metricsLastHour: PropTypes.object, metricsLastMinute: PropTypes.object, revive: PropTypes.func, + updateable: PropTypes.bool, }; export default Feature; diff --git a/frontend/src/component/feature/list-component.jsx b/frontend/src/component/feature/list-component.jsx index 849542ed51..294941f8e5 100644 --- a/frontend/src/component/feature/list-component.jsx +++ b/frontend/src/component/feature/list-component.jsx @@ -5,6 +5,8 @@ import { Link } from 'react-router-dom'; import { Icon, FABButton, Textfield, Menu, MenuItem, Card, CardActions, List } from 'react-mdl'; import { MenuItemWithIcon, DropdownButton, styles as commonStyles } from '../common'; import styles from './feature.scss'; +import { CREATE_FEATURE } from '../../permissions'; +import PermissionComponent from '../common/permission-container'; export default class FeatureListComponent extends React.Component { static propTypes = { @@ -62,11 +64,16 @@ export default class FeatureListComponent extends React.Component { label="Search" style={{ width: '100%' }} /> - - - - - + + + + + + } + /> diff --git a/frontend/src/component/feature/view-component.jsx b/frontend/src/component/feature/view-component.jsx index 16c2cbf3db..7fb48d9b38 100644 --- a/frontend/src/component/feature/view-component.jsx +++ b/frontend/src/component/feature/view-component.jsx @@ -8,6 +8,8 @@ import MetricComponent from './metric-container'; import EditFeatureToggle from './form/form-update-feature-container'; import ViewFeatureToggle from './form/form-view-feature-container'; import { styles as commonStyles } from '../common'; +import { CREATE_FEATURE, DELETE_FEATURE, UPDATE_FEATURE } from '../../permissions'; +import PermissionComponent from '../common/permission-container'; const TABS = { strategies: 0, @@ -54,7 +56,17 @@ export default class ViewFeatureToggleComponent extends React.Component { } else if (TABS[activeTab] === TABS.strategies) { if (this.isFeatureView) { return ( - + + } + otherwise={} + /> ); } return ; @@ -87,14 +99,20 @@ export default class ViewFeatureToggleComponent extends React.Component { return ( Could not find the toggle{' '} - - {featureToggleName} - + + {featureToggleName} + + } + otherwise={featureToggleName} + /> ); } @@ -115,8 +133,8 @@ export default class ViewFeatureToggleComponent extends React.Component { revive(featureToggle.name); this.props.history.push('/features'); }; - const updateFeatureToggle = () => { - let feature = { ...featureToggle }; + const updateFeatureToggle = e => { + let feature = { ...featureToggle, description: e.target.value }; if (Array.isArray(feature.strategies)) { feature.strategies.forEach(s => { delete s.id; @@ -135,15 +153,22 @@ export default class ViewFeatureToggleComponent extends React.Component { {featureToggle.name} {this.isFeatureView ? ( - setValue('description', v), + onBlur: updateFeatureToggle, + }} + denied={{ + disabled: true, + }} floatingLabel style={{ width: '100%' }} rows={1} label="Description" required value={featureToggle.description} - onChange={v => setValue('description', v)} - onBlur={updateFeatureToggle} /> ) : ( - toggleFeature(featureToggle.name)} > {featureToggle.enabled ? 'Enabled' : 'Disabled'} - + {this.isFeatureView ? ( - + + Archive + + } + /> ) : ( - + + Revive + + } + /> )}
diff --git a/frontend/src/component/strategies/list-component.jsx b/frontend/src/component/strategies/list-component.jsx index a1fbbc0c85..bf9749ffc5 100644 --- a/frontend/src/component/strategies/list-component.jsx +++ b/frontend/src/component/strategies/list-component.jsx @@ -4,6 +4,8 @@ import { Link } from 'react-router-dom'; import { List, ListItem, ListItemContent, IconButton, Grid, Cell } from 'react-mdl'; import { HeaderTitle } from '../common'; +import { CREATE_STRATEGY, DELETE_STRATEGY } from '../../permissions'; +import PermissionComponent from '../common/permission-container'; class StrategiesListComponent extends Component { static propTypes = { @@ -26,11 +28,16 @@ class StrategiesListComponent extends Component { this.props.history.push('/strategies/create')} - title="Add new strategy" + this.props.history.push('/strategies/create')} + title="Add new strategy" + /> + } /> } /> @@ -46,7 +53,12 @@ class StrategiesListComponent extends Component { {strategy.editable === false ? ( '' ) : ( - removeStrategy(strategy)} /> + removeStrategy(strategy)} /> + } + /> )}
)) diff --git a/frontend/src/component/strategies/strategy-details-component.jsx b/frontend/src/component/strategies/strategy-details-component.jsx index 49a7b4a4f5..fb01bec8d1 100644 --- a/frontend/src/component/strategies/strategy-details-component.jsx +++ b/frontend/src/component/strategies/strategy-details-component.jsx @@ -4,6 +4,8 @@ import { Tabs, Tab, ProgressBar, Grid, Cell } from 'react-mdl'; import ShowStrategy from './show-strategy-component'; import EditStrategy from './edit-container'; import { HeaderTitle } from '../common'; +import { UPDATE_STRATEGY } from '../../permissions'; +import PermissionComponent from '../common/permission-container'; const TABS = { view: 0, @@ -69,10 +71,15 @@ export default class StrategyDetails extends Component { {strategy.editable === false ? ( '' ) : ( - - this.goToTab('view')}>Details - this.goToTab('edit')}>Edit - + + this.goToTab('view')}>Details + this.goToTab('edit')}>Edit + + } + /> )}
diff --git a/frontend/src/data/helper.js b/frontend/src/data/helper.js index f1132843fa..00d18e8fde 100644 --- a/frontend/src/data/helper.js +++ b/frontend/src/data/helper.js @@ -23,12 +23,25 @@ export class AuthenticationError extends Error { } } +export class ForbiddenError extends Error { + constructor(statusCode, body) { + super('You cannot perform this action'); + this.name = 'ForbiddenError'; + this.statusCode = statusCode; + this.body = body; + } +} + export function throwIfNotSuccess(response) { if (!response.ok) { if (response.status === 401) { return new Promise((resolve, reject) => { response.json().then(body => reject(new AuthenticationError(response.status, body))); }); + } else if (response.status === 403) { + return new Promise((resolve, reject) => { + response.json().then(body => reject(new ForbiddenError(response.status, body))); + }); } else if (response.status > 399 && response.status < 404) { return new Promise((resolve, reject) => { response.json().then(body => { diff --git a/frontend/src/permissions.js b/frontend/src/permissions.js new file mode 100644 index 0000000000..58daa194e3 --- /dev/null +++ b/frontend/src/permissions.js @@ -0,0 +1,8 @@ +export const ADMIN = 'ADMIN'; +export const CREATE_FEATURE = 'CREATE_FEATURE'; +export const UPDATE_FEATURE = 'UPDATE_FEATURE'; +export const DELETE_FEATURE = 'DELETE_FEATURE'; +export const CREATE_STRATEGY = 'CREATE_STRATEGY'; +export const UPDATE_STRATEGY = 'UPDATE_STRATEGY'; +export const DELETE_STRATEGY = 'DELETE_STRATEGY'; +export const UPDATE_APPLICATION = 'UPDATE_APPLICATION';