From f669f96d493dfe467734ee0c6b32da64b8f90607 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivar=20Conradi=20=C3=98sthus?= Date: Tue, 20 Apr 2021 19:13:31 +0200 Subject: [PATCH] wip: frontend should understand rbac permissions (#269) * chore: update changelog * 4.0.0-alpha.4 * wip: frontend should understand rbac permissions * move all feature components to hasAccess * fix: remove all change permissions * fix all the tests * fix all the tests x2 * fix snapshot for node 12 * fine tune perms a bit * refactor: rewrite to ts * refactor: use admin constant * fix: import Co-authored-by: Fredrik Oseberg --- frontend/CHANGELOG.md | 6 + frontend/package.json | 2 +- frontend/src/accessStoreFake.js | 12 + .../AccessProvider/AccessProvider.tsx | 38 ++ .../AccessProvider/permissions.ts} | 10 +- .../component/addons/AddonList/AddonList.jsx | 11 +- .../AvailableAddons/AvailableAddons.jsx | 8 +- .../ConfiguredAddons/ConfiguredAddons.jsx | 12 +- frontend/src/component/addons/index.jsx | 2 - .../application-edit-component-test.js | 28 +- .../application/application-edit-component.js | 15 +- .../application/application-edit-container.js | 2 - .../application/application-view.jsx | 8 +- .../src/component/archive/view-container.js | 2 - .../context/ContextList/ContextList.jsx | 13 +- .../component/context/ContextList/index.jsx | 2 - .../src/component/error/error-component.jsx | 9 +- .../src/component/error/error-container.jsx | 11 +- .../FeatureToggleList/FeatureToggleList.jsx | 72 ++-- .../FeatureToggleListItem.jsx | 12 +- .../feature-list-item-component-test.jsx.snap | 11 +- .../list-component-test.jsx.snap | 36 +- .../feature-list-item-component-test.jsx | 5 +- .../__tests__/list-component-test.jsx | 14 +- .../feature/FeatureToggleList/index.jsx | 10 +- .../feature/FeatureView/FeatureView.jsx | 41 +- .../component/feature/FeatureView/index.jsx | 2 - .../feature/create/add-feature-component.jsx | 1 + .../feature/feature-type-select-component.jsx | 4 +- .../update-variant-component-test.jsx.snap | 30 ++ .../update-variant-component-test.jsx | 7 +- .../variant/update-variant-component.jsx | 9 +- .../variant/update-variant-container.jsx | 1 - .../variant/variant-view-component.jsx | 7 +- .../view-component-test.jsx.snap | 2 + .../view/__tests__/view-component-test.jsx | 7 +- .../view/update-description-component.jsx | 9 +- .../__snapshots__/routes-test.jsx.snap | 16 +- frontend/src/component/menu/routes.js | 34 +- .../project/ProjectList/ProjectList.jsx | 15 +- .../component/project/ProjectList/index.jsx | 2 - .../StrategiesList/StrategiesList.jsx | 12 +- .../strategies/StrategiesList/index.jsx | 2 - .../list-component-test.jsx.snap | 230 +++++----- .../strategy-details-component-test.jsx.snap | 401 +++++++----------- .../__tests__/list-component-test.jsx | 26 +- .../strategy-details-component-test.jsx | 28 +- .../strategies/strategy-details-component.jsx | 11 +- .../strategies/strategy-details-container.js | 2 - .../tag-types/TagTypeList/TagTypeList.jsx | 14 +- .../tag-type-create-component-test.js | 2 - .../__tests__/tag-type-list-component-test.js | 52 ++- frontend/src/component/tag-types/index.jsx | 2 - .../src/component/tags/TagList/TagList.jsx | 17 +- frontend/src/component/tags/index.jsx | 2 - frontend/src/contexts/AccessContext.js | 5 + frontend/src/index.tsx | 23 +- .../page/admin/api/api-key-list-container.js | 3 - frontend/src/page/admin/api/api-key-list.jsx | 41 +- .../page/admin/auth/google-auth-container.js | 2 - frontend/src/page/admin/auth/google-auth.jsx | 10 +- frontend/src/page/admin/auth/index.js | 2 - .../page/admin/auth/saml-auth-container.js | 2 - frontend/src/page/admin/auth/saml-auth.jsx | 10 +- .../page/admin/users/UsersList/UsersList.jsx | 13 +- .../src/page/admin/users/UsersList/index.js | 2 - frontend/src/store/user/index.js | 5 +- 67 files changed, 742 insertions(+), 715 deletions(-) create mode 100644 frontend/src/accessStoreFake.js create mode 100644 frontend/src/component/AccessProvider/AccessProvider.tsx rename frontend/src/{permissions.js => component/AccessProvider/permissions.ts} (83%) create mode 100644 frontend/src/contexts/AccessContext.js diff --git a/frontend/CHANGELOG.md b/frontend/CHANGELOG.md index a764450170..22e5b5bece 100644 --- a/frontend/CHANGELOG.md +++ b/frontend/CHANGELOG.md @@ -8,6 +8,12 @@ and this project adheres to [Semantic Versioning](http://semver.org/). The latest version of this document is always available in [releases][releases-url]. +# 4.0.0-alpha.4 +- fix: overall bugs +- feat: user flow +- fix: small description for toggles +- fix: make admin pages fork for OSS and enterprise + # 4.0.0-alpha.3 - fix: logout redirect logic - fix: redirect from login page if authorized diff --git a/frontend/package.json b/frontend/package.json index fec0e13bd3..b51d51b065 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,7 +1,7 @@ { "name": "unleash-frontend", "description": "unleash your features", - "version": "4.0.0-alpha.3", + "version": "4.0.0-alpha.4", "keywords": [ "unleash", "feature toggle", diff --git a/frontend/src/accessStoreFake.js b/frontend/src/accessStoreFake.js new file mode 100644 index 0000000000..c0069ca1b0 --- /dev/null +++ b/frontend/src/accessStoreFake.js @@ -0,0 +1,12 @@ +import { Map as $MAp } from 'immutable'; + +export const createFakeStore = (permissions) => { + return { + getState: () => ({ + user: + new $MAp({ + permissions + }) + }), + } +} \ No newline at end of file diff --git a/frontend/src/component/AccessProvider/AccessProvider.tsx b/frontend/src/component/AccessProvider/AccessProvider.tsx new file mode 100644 index 0000000000..45c62d41e7 --- /dev/null +++ b/frontend/src/component/AccessProvider/AccessProvider.tsx @@ -0,0 +1,38 @@ +import { FC } from "react"; + +import AccessContext from '../../contexts/AccessContext' +import { ADMIN } from "./permissions"; + +// TODO: Type up redux store +interface IAccessProvider { + store: any; +} + +interface IPermission { + permission: string; + project: string | null; +} + +const AccessProvider: FC = ({store, children}) => { + const hasAccess = (permission: string, project: string) => { + const permissions = store.getState().user.get('permissions') || []; + + const result = permissions.some((p: IPermission) => { + if(p.permission === ADMIN) { + return true + } + if(p.permission === permission && p.project === project) { + return true; + } + return false; + }); + + return result; + }; + + const context = { hasAccess }; + + return {children} +} + +export default AccessProvider; \ No newline at end of file diff --git a/frontend/src/permissions.js b/frontend/src/component/AccessProvider/permissions.ts similarity index 83% rename from frontend/src/permissions.js rename to frontend/src/component/AccessProvider/permissions.ts index ac6edb8b0a..b9bd814315 100644 --- a/frontend/src/permissions.js +++ b/frontend/src/component/AccessProvider/permissions.ts @@ -20,10 +20,6 @@ export const DELETE_TAG = 'DELETE_TAG'; export const CREATE_ADDON = 'CREATE_ADDON'; export const UPDATE_ADDON = 'UPDATE_ADDON'; export const DELETE_ADDON = 'DELETE_ADDON'; - -export function hasPermission(user, permission) { - return ( - user && - (!user.permissions || user.permissions.indexOf(ADMIN) !== -1 || user.permissions.indexOf(permission) !== -1) - ); -} +export const UPDATE_API_TOKEN = 'UPDATE_API_TOKEN'; +export const CREATE_API_TOKEN = 'CREATE_API_TOKEN'; +export const DELETE_API_TOKEN = 'DELETE_API_TOKEN'; diff --git a/frontend/src/component/addons/AddonList/AddonList.jsx b/frontend/src/component/addons/AddonList/AddonList.jsx index 5237567add..87e3ad9683 100644 --- a/frontend/src/component/addons/AddonList/AddonList.jsx +++ b/frontend/src/component/addons/AddonList/AddonList.jsx @@ -1,9 +1,10 @@ -import React, { useEffect } from 'react'; +import React, { useContext, useEffect } from 'react'; import PropTypes from 'prop-types'; import ConfiguredAddons from './ConfiguredAddons'; import AvailableAddons from './AvailableAddons'; import { Avatar, Icon } from '@material-ui/core'; import ConditionallyRender from '../../common/ConditionallyRender/ConditionallyRender'; +import AccessContext from '../../../contexts/AccessContext'; const style = { width: '40px', @@ -29,7 +30,8 @@ const getIcon = name => { } }; -const AddonList = ({ addons, providers, fetchAddons, removeAddon, toggleAddon, history, hasPermission }) => { +const AddonList = ({ addons, providers, fetchAddons, removeAddon, toggleAddon, history }) => { + const { hasAccess } = useContext(AccessContext); useEffect(() => { if (addons.length === 0) { fetchAddons(); @@ -45,7 +47,7 @@ const AddonList = ({ addons, providers, fetchAddons, removeAddon, toggleAddon, h @@ -53,7 +55,7 @@ const AddonList = ({ addons, providers, fetchAddons, removeAddon, toggleAddon, h />
- + ); }; @@ -65,7 +67,6 @@ AddonList.propTypes = { removeAddon: PropTypes.func.isRequired, toggleAddon: PropTypes.func.isRequired, history: PropTypes.object.isRequired, - hasPermission: PropTypes.func.isRequired, }; export default AddonList; diff --git a/frontend/src/component/addons/AddonList/AvailableAddons/AvailableAddons.jsx b/frontend/src/component/addons/AddonList/AvailableAddons/AvailableAddons.jsx index ad75d2d9a6..10606b0438 100644 --- a/frontend/src/component/addons/AddonList/AvailableAddons/AvailableAddons.jsx +++ b/frontend/src/component/addons/AddonList/AvailableAddons/AvailableAddons.jsx @@ -2,17 +2,17 @@ import React from 'react'; import PageContent from '../../../common/PageContent/PageContent'; import { Button, List, ListItem, ListItemAvatar, ListItemSecondaryAction, ListItemText } from '@material-ui/core'; import ConditionallyRender from '../../../common/ConditionallyRender/ConditionallyRender'; -import { CREATE_ADDON } from '../../../../permissions'; +import { CREATE_ADDON } from '../../../AccessProvider/permissions'; import PropTypes from 'prop-types'; -const AvailableAddons = ({ providers, getIcon, hasPermission, history }) => { +const AvailableAddons = ({ providers, getIcon, hasAccess, history }) => { const renderProvider = provider => ( {getIcon(provider.name)} { AvailableAddons.propTypes = { providers: PropTypes.array.isRequired, getIcon: PropTypes.func.isRequired, - hasPermission: PropTypes.func.isRequired, + hasAccess: PropTypes.func.isRequired, history: PropTypes.object.isRequired, }; diff --git a/frontend/src/component/addons/AddonList/ConfiguredAddons/ConfiguredAddons.jsx b/frontend/src/component/addons/AddonList/ConfiguredAddons/ConfiguredAddons.jsx index a6d56ee09b..f132576cfd 100644 --- a/frontend/src/component/addons/AddonList/ConfiguredAddons/ConfiguredAddons.jsx +++ b/frontend/src/component/addons/AddonList/ConfiguredAddons/ConfiguredAddons.jsx @@ -9,12 +9,12 @@ import { ListItemText, } from '@material-ui/core'; import ConditionallyRender from '../../../common/ConditionallyRender/ConditionallyRender'; -import { DELETE_ADDON, UPDATE_ADDON } from '../../../../permissions'; +import { DELETE_ADDON, UPDATE_ADDON } from '../../../AccessProvider/permissions'; import { Link } from 'react-router-dom'; import PageContent from '../../../common/PageContent/PageContent'; import PropTypes from 'prop-types'; -const ConfiguredAddons = ({ addons, hasPermission, removeAddon, getIcon, toggleAddon }) => { +const ConfiguredAddons = ({ addons, hasAccess, removeAddon, getIcon, toggleAddon }) => { const onRemoveAddon = addon => () => removeAddon(addon); const renderAddon = addon => ( @@ -23,7 +23,7 @@ const ConfiguredAddons = ({ addons, hasPermission, removeAddon, getIcon, toggleA primary={ {addon.provider} @@ -38,7 +38,7 @@ const ConfiguredAddons = ({ addons, hasPermission, removeAddon, getIcon, toggleA /> delete @@ -68,7 +68,7 @@ const ConfiguredAddons = ({ addons, hasPermission, removeAddon, getIcon, toggleA }; ConfiguredAddons.propTypes = { addons: PropTypes.array.isRequired, - hasPermission: PropTypes.func.isRequired, + hasAccess: PropTypes.func.isRequired, removeAddon: PropTypes.func.isRequired, toggleAddon: PropTypes.func.isRequired, getIcon: PropTypes.func.isRequired, diff --git a/frontend/src/component/addons/index.jsx b/frontend/src/component/addons/index.jsx index 1a5c6d65b8..c04dbe4966 100644 --- a/frontend/src/component/addons/index.jsx +++ b/frontend/src/component/addons/index.jsx @@ -1,7 +1,6 @@ import { connect } from 'react-redux'; import AddonsListComponent from './AddonList'; import { fetchAddons, removeAddon, updateAddon } from '../../store/addons/actions'; -import { hasPermission } from '../../permissions'; const mapStateToProps = state => { const list = state.addons.toJS(); @@ -9,7 +8,6 @@ const mapStateToProps = state => { return { addons: list.addons, providers: list.providers, - hasPermission: hasPermission.bind(null, state.user.get('profile')), }; }; diff --git a/frontend/src/component/application/__tests__/application-edit-component-test.js b/frontend/src/component/application/__tests__/application-edit-component-test.js index c8935c57b7..65499899ed 100644 --- a/frontend/src/component/application/__tests__/application-edit-component-test.js +++ b/frontend/src/component/application/__tests__/application-edit-component-test.js @@ -4,19 +4,23 @@ import { ThemeProvider } from '@material-ui/core'; import ClientApplications from '../application-edit-component'; import renderer from 'react-test-renderer'; import { MemoryRouter } from 'react-router-dom'; -import { CREATE_FEATURE, CREATE_STRATEGY, UPDATE_APPLICATION } from '../../../permissions'; +import { ADMIN, CREATE_FEATURE, CREATE_STRATEGY, UPDATE_APPLICATION } from '../../AccessProvider/permissions'; import theme from '../../../themes/main-theme'; +import { createFakeStore } from '../../../accessStoreFake'; +import AccessProvider from '../../AccessProvider/AccessProvider'; + test('renders correctly if no application', () => { const tree = renderer .create( - Promise.resolve({})} - storeApplicationMetaData={jest.fn()} - deleteApplication={jest.fn()} - hasPermission={() => true} - history={{}} - /> + + Promise.resolve({})} + storeApplicationMetaData={jest.fn()} + deleteApplication={jest.fn()} + history={{}} + /> + ) .toJSON(); @@ -28,6 +32,7 @@ test('renders correctly without permission', () => { .create( + Promise.resolve({})} storeApplicationMetaData={jest.fn()} @@ -71,8 +76,8 @@ test('renders correctly without permission', () => { description: 'app description', }} location={{ locale: 'en-GB' }} - hasPermission={() => false} /> + ) @@ -86,6 +91,7 @@ test('renders correctly with permissions', () => { .create( + Promise.resolve({})} storeApplicationMetaData={jest.fn()} @@ -129,10 +135,8 @@ test('renders correctly with permissions', () => { description: 'app description', }} location={{ locale: 'en-GB' }} - hasPermission={permission => - [CREATE_FEATURE, CREATE_STRATEGY, UPDATE_APPLICATION].indexOf(permission) !== -1 - } /> + ) diff --git a/frontend/src/component/application/application-edit-component.js b/frontend/src/component/application/application-edit-component.js index bedd4bc1bf..bbfb73bf4d 100644 --- a/frontend/src/component/application/application-edit-component.js +++ b/frontend/src/component/application/application-edit-component.js @@ -5,15 +5,18 @@ import PropTypes from 'prop-types'; import { Avatar, Link, Icon, IconButton, Button, LinearProgress, Typography } from '@material-ui/core'; import ConditionallyRender from '../common/ConditionallyRender/ConditionallyRender'; import { formatFullDateTimeWithLocale, formatDateWithLocale } from '../common/util'; -import { UPDATE_APPLICATION } from '../../permissions'; +import { UPDATE_APPLICATION } from '../AccessProvider/permissions'; import ApplicationView from './application-view'; import ApplicationUpdate from './application-update'; import TabNav from '../common/TabNav/TabNav'; import Dialogue from '../common/Dialogue'; import PageContent from '../common/PageContent'; import HeaderTitle from '../common/HeaderTitle'; +import AccessContext from '../../contexts/AccessContext'; class ClientApplications extends PureComponent { + static contextType = AccessContext; + static propTypes = { fetchApplication: PropTypes.func.isRequired, appName: PropTypes.string, @@ -21,7 +24,6 @@ class ClientApplications extends PureComponent { location: PropTypes.object, storeApplicationMetaData: PropTypes.func.isRequired, deleteApplication: PropTypes.func.isRequired, - hasPermission: PropTypes.func.isRequired, history: PropTypes.object.isRequired, }; @@ -60,7 +62,8 @@ class ClientApplications extends PureComponent { } else if (!this.props.application) { return

Application ({this.props.appName}) not found

; } - const { application, storeApplicationMetaData, hasPermission } = this.props; + const { hasAccess } = this.context; + const { application, storeApplicationMetaData } = this.props; const { appName, instances, strategies, seenToggles, url, description, icon = 'apps', createdAt } = application; const toggleModal = () => { @@ -84,7 +87,7 @@ class ClientApplications extends PureComponent { strategies={strategies} instances={instances} seenToggles={seenToggles} - hasPermission={hasPermission} + hasAccess={hasAccess} formatFullDateTime={this.formatFullDateTime} /> ), @@ -126,7 +129,7 @@ class ClientApplications extends PureComponent { /> Delete @@ -145,7 +148,7 @@ class ClientApplications extends PureComponent { {renderModal()} diff --git a/frontend/src/component/application/application-edit-container.js b/frontend/src/component/application/application-edit-container.js index de708d4ac6..4d17ce5d88 100644 --- a/frontend/src/component/application/application-edit-container.js +++ b/frontend/src/component/application/application-edit-container.js @@ -1,7 +1,6 @@ import { connect } from 'react-redux'; import ApplicationEdit from './application-edit-component'; import { fetchApplication, storeApplicationMetaData, deleteApplication } from './../../store/application/actions'; -import { hasPermission } from '../../permissions'; const mapStateToProps = (state, props) => { let application = state.applications.getIn(['apps', props.appName]); @@ -12,7 +11,6 @@ const mapStateToProps = (state, props) => { return { application, location, - hasPermission: hasPermission.bind(null, state.user.get('profile')), }; }; diff --git a/frontend/src/component/application/application-view.jsx b/frontend/src/component/application/application-view.jsx index 5ac900932c..c32994497c 100644 --- a/frontend/src/component/application/application-view.jsx +++ b/frontend/src/component/application/application-view.jsx @@ -3,14 +3,14 @@ import { Link } from 'react-router-dom'; import PropTypes from 'prop-types'; import { Grid, List, ListItem, ListItemText, ListItemAvatar, Switch, Icon, Typography } from '@material-ui/core'; import { shorten } from '../common'; -import { CREATE_FEATURE, CREATE_STRATEGY } from '../../permissions'; +import { CREATE_FEATURE, CREATE_STRATEGY } from '../AccessProvider/permissions'; import ConditionallyRender from '../common/ConditionallyRender/ConditionallyRender'; -function ApplicationView({ seenToggles, hasPermission, strategies, instances, formatFullDateTime }) { +function ApplicationView({ seenToggles, hasAccess, strategies, instances, formatFullDateTime }) { const notFoundListItem = ({ createUrl, name, permission }) => ( @@ -149,7 +149,7 @@ ApplicationView.propTypes = { instances: PropTypes.array.isRequired, seenToggles: PropTypes.array.isRequired, strategies: PropTypes.array.isRequired, - hasPermission: PropTypes.func.isRequired, + hasAccess: PropTypes.func.isRequired, formatFullDateTime: PropTypes.func.isRequired, }; diff --git a/frontend/src/component/archive/view-container.js b/frontend/src/component/archive/view-container.js index 40d584cd5a..d572f7e919 100644 --- a/frontend/src/component/archive/view-container.js +++ b/frontend/src/component/archive/view-container.js @@ -1,7 +1,6 @@ import { connect } from 'react-redux'; import { fetchArchive, revive } from './../../store/archive/actions'; import ViewToggleComponent from '../feature/FeatureView/FeatureView'; -import { hasPermission } from '../../permissions'; import { fetchTags } from '../../store/feature-tags/actions'; export default connect( @@ -14,7 +13,6 @@ export default connect( tagTypes: state.tagTypes.toJS(), featureTags: state.featureTags.toJS(), activeTab: props.activeTab, - hasPermission: hasPermission.bind(null, state.user.get('profile')), }), { fetchArchive, diff --git a/frontend/src/component/context/ContextList/ContextList.jsx b/frontend/src/component/context/ContextList/ContextList.jsx index 7c5279ab25..7d03957dd3 100644 --- a/frontend/src/component/context/ContextList/ContextList.jsx +++ b/frontend/src/component/context/ContextList/ContextList.jsx @@ -2,14 +2,16 @@ import PropTypes from 'prop-types'; import PageContent from '../../common/PageContent/PageContent'; import HeaderTitle from '../../common/HeaderTitle'; import ConditionallyRender from '../../common/ConditionallyRender/ConditionallyRender'; -import { CREATE_CONTEXT_FIELD, DELETE_CONTEXT_FIELD } from '../../../permissions'; +import { CREATE_CONTEXT_FIELD, DELETE_CONTEXT_FIELD } from '../../AccessProvider/permissions'; import { Icon, IconButton, List, ListItem, ListItemIcon, ListItemText, Tooltip } from '@material-ui/core'; -import React, { useState } from 'react'; +import React, { useContext, useState } from 'react'; import { Link } from 'react-router-dom'; import { useStyles } from './styles'; import ConfirmDialogue from '../../common/Dialogue'; +import AccessContext from '../../../contexts/AccessContext'; -const ContextList = ({ removeContextField, hasPermission, history, contextFields }) => { +const ContextList = ({ removeContextField, history, contextFields }) => { + const { hasAccess } = useContext(AccessContext); const [showDelDialogue, setShowDelDialogue] = useState(false); const [name, setName] = useState(); @@ -29,7 +31,7 @@ const ContextList = ({ removeContextField, hasPermission, history, contextFields secondary={field.description} /> ( history.push('/context/create')}> @@ -88,7 +90,6 @@ ContextList.propTypes = { contextFields: PropTypes.array.isRequired, removeContextField: PropTypes.func.isRequired, history: PropTypes.object.isRequired, - hasPermission: PropTypes.func.isRequired, }; export default ContextList; diff --git a/frontend/src/component/context/ContextList/index.jsx b/frontend/src/component/context/ContextList/index.jsx index 56fb6afda7..34d0e1c684 100644 --- a/frontend/src/component/context/ContextList/index.jsx +++ b/frontend/src/component/context/ContextList/index.jsx @@ -1,14 +1,12 @@ import { connect } from 'react-redux'; import ContextList from './ContextList'; import { fetchContext, removeContextField } from '../../../store/context/actions'; -import { hasPermission } from '../../../permissions'; const mapStateToProps = state => { const list = state.context.toJS(); return { contextFields: list, - hasPermission: hasPermission.bind(null, state.user.get('profile')), }; }; diff --git a/frontend/src/component/error/error-component.jsx b/frontend/src/component/error/error-component.jsx index 01e2b182da..b1595b0d63 100644 --- a/frontend/src/component/error/error-component.jsx +++ b/frontend/src/component/error/error-component.jsx @@ -3,24 +3,23 @@ import PropTypes from 'prop-types'; import { Snackbar, Icon, IconButton } from '@material-ui/core'; -const ErrorComponent = ({ errors, ...props }) => { +const ErrorComponent = ({ errors, muteError }) => { const showError = errors.length > 0; const error = showError ? errors[0] : undefined; - const muteError = () => props.muteError(error); return ( - + close } open={showError} - onClose={muteError} + onClose={() => muteError(error)} autoHideDuration={10000} message={ -
+
question_answer {error}
diff --git a/frontend/src/component/error/error-container.jsx b/frontend/src/component/error/error-container.jsx index 8cda379397..6c8fca1ab8 100644 --- a/frontend/src/component/error/error-container.jsx +++ b/frontend/src/component/error/error-container.jsx @@ -6,11 +6,14 @@ const mapDispatchToProps = { muteError, }; -const mapStateToProps = state => ({ - errors: state.error +const mapStateToProps = state => { + return { + errors: state.error .get('list') .toArray() - .reverse(), -}); + .reverse() + } + +}; export default connect(mapStateToProps, mapDispatchToProps)(ErrorComponent); diff --git a/frontend/src/component/feature/FeatureToggleList/FeatureToggleList.jsx b/frontend/src/component/feature/FeatureToggleList/FeatureToggleList.jsx index d3e7d7fabf..444ed6810f 100644 --- a/frontend/src/component/feature/FeatureToggleList/FeatureToggleList.jsx +++ b/frontend/src/component/feature/FeatureToggleList/FeatureToggleList.jsx @@ -1,4 +1,4 @@ -import { useLayoutEffect } from 'react'; +import { useContext, useLayoutEffect } from 'react'; import PropTypes from 'prop-types'; import classnames from 'classnames'; import { Link } from 'react-router-dom'; @@ -21,21 +21,24 @@ import HeaderTitle from '../../common/HeaderTitle'; import loadingFeatures from './loadingFeatures'; -import { CREATE_FEATURE } from '../../../permissions'; +import { CREATE_FEATURE } from '../../AccessProvider/permissions'; + +import AccessContext from '../../../contexts/AccessContext'; import { useStyles } from './styles'; const FeatureToggleList = ({ fetcher, features, - hasPermission, settings, revive, + currentProjectId, updateSetting, featureMetrics, toggleFeature, loading, }) => { + const { hasAccess } = useContext(AccessContext); const styles = useStyles(); const smallScreen = useMediaQuery('(max-width:700px)'); @@ -66,7 +69,7 @@ const FeatureToggleList = ({ feature={feature} toggleFeature={toggleFeature} revive={revive} - hasPermission={hasPermission} + hasAccess={hasAccess} className={'skeleton'} /> )); @@ -86,7 +89,7 @@ const FeatureToggleList = ({ feature={feature} toggleFeature={toggleFeature} revive={revive} - hasPermission={hasPermission} + hasAccess={hasAccess} /> ))} elseShow={ @@ -132,39 +135,38 @@ const FeatureToggleList = ({ } /> + - - add - - - } - elseShow={ - - } - /> + + + add + + + } + elseShow={ + } /> +
} /> @@ -185,8 +187,8 @@ FeatureToggleList.propTypes = { toggleFeature: PropTypes.func, settings: PropTypes.object, history: PropTypes.object.isRequired, - hasPermission: PropTypes.func.isRequired, loading: PropTypes.bool, + currentProjectId: PropTypes.string.isRequired, }; export default FeatureToggleList; diff --git a/frontend/src/component/feature/FeatureToggleList/FeatureToggleListItem/FeatureToggleListItem.jsx b/frontend/src/component/feature/FeatureToggleList/FeatureToggleListItem/FeatureToggleListItem.jsx index 28f8ac086a..d100591dc3 100644 --- a/frontend/src/component/feature/FeatureToggleList/FeatureToggleListItem/FeatureToggleListItem.jsx +++ b/frontend/src/component/feature/FeatureToggleList/FeatureToggleListItem/FeatureToggleListItem.jsx @@ -10,7 +10,7 @@ import Status from '../../status-component'; import FeatureToggleListItemChip from './FeatureToggleListItemChip'; import ConditionallyRender from '../../../common/ConditionallyRender/ConditionallyRender'; -import { UPDATE_FEATURE } from '../../../../permissions'; +import { UPDATE_FEATURE } from '../../../AccessProvider/permissions'; import { calc, styles as commonStyles } from '../../../common'; import { useStyles } from './styles'; @@ -22,12 +22,12 @@ const FeatureToggleListItem = ({ metricsLastHour = { yes: 0, no: 0, isFallback: true }, metricsLastMinute = { yes: 0, no: 0, isFallback: true }, revive, - hasPermission, + hasAccess, ...rest }) => { const styles = useStyles(); - const { name, description, enabled, type, stale, createdAt } = feature; + const { name, description, enabled, type, stale, createdAt, project } = feature; const { showLastHour = false } = settings; const isStale = showLastHour ? metricsLastHour.isFallback @@ -64,7 +64,7 @@ const FeatureToggleListItem = ({
revive(feature.name)}> undo @@ -134,7 +134,7 @@ FeatureToggleListItem.propTypes = { metricsLastHour: PropTypes.object, metricsLastMinute: PropTypes.object, revive: PropTypes.func, - hasPermission: PropTypes.func.isRequired, + hasAccess: PropTypes.func.isRequired, }; export default memo(FeatureToggleListItem); diff --git a/frontend/src/component/feature/FeatureToggleList/__tests__/__snapshots__/feature-list-item-component-test.jsx.snap b/frontend/src/component/feature/FeatureToggleList/__tests__/__snapshots__/feature-list-item-component-test.jsx.snap index 42e0c72d6a..519e35cdf4 100644 --- a/frontend/src/component/feature/FeatureToggleList/__tests__/__snapshots__/feature-list-item-component-test.jsx.snap +++ b/frontend/src/component/feature/FeatureToggleList/__tests__/__snapshots__/feature-list-item-component-test.jsx.snap @@ -120,8 +120,8 @@ exports[`renders correctly with one feature without permission 1`] = ` className="MuiSwitch-root" > @@ -150,6 +150,9 @@ exports[`renders correctly with one feature without permission 1`] = ` className="MuiSwitch-thumb" /> + + + + Create feature toggle + + @@ -366,7 +392,7 @@ exports[`renders correctly with one feature without permissions 1`] = ` "reviveName": "Another", } } - hasPermission={[Function]} + hasAccess={[Function]} settings={ Object { "sort": "name", diff --git a/frontend/src/component/feature/FeatureToggleList/__tests__/feature-list-item-component-test.jsx b/frontend/src/component/feature/FeatureToggleList/__tests__/feature-list-item-component-test.jsx index d2f43ddc98..4a0e2345ef 100644 --- a/frontend/src/component/feature/FeatureToggleList/__tests__/feature-list-item-component-test.jsx +++ b/frontend/src/component/feature/FeatureToggleList/__tests__/feature-list-item-component-test.jsx @@ -4,7 +4,6 @@ import { ThemeProvider } from '@material-ui/core'; import FeatureToggleListItem from '../FeatureToggleListItem'; import renderer from 'react-test-renderer'; -import { UPDATE_FEATURE } from '../../../../permissions'; import theme from '../../../../themes/main-theme'; @@ -38,7 +37,7 @@ test('renders correctly with one feature', () => { metricsLastMinute={featureMetrics.lastMinute[feature.name]} feature={feature} toggleFeature={jest.fn()} - hasPermission={permission => permission === UPDATE_FEATURE} + hasAccess={() => true} /> @@ -75,7 +74,7 @@ test('renders correctly with one feature without permission', () => { metricsLastMinute={featureMetrics.lastMinute[feature.name]} feature={feature} toggleFeature={jest.fn()} - hasPermission={() => false} + hasAccess={() => true} /> diff --git a/frontend/src/component/feature/FeatureToggleList/__tests__/list-component-test.jsx b/frontend/src/component/feature/FeatureToggleList/__tests__/list-component-test.jsx index 1c3e6fa801..eda6282fe6 100644 --- a/frontend/src/component/feature/FeatureToggleList/__tests__/list-component-test.jsx +++ b/frontend/src/component/feature/FeatureToggleList/__tests__/list-component-test.jsx @@ -4,8 +4,12 @@ import { ThemeProvider } from '@material-ui/core'; import FeatureToggleList from '../FeatureToggleList'; import renderer from 'react-test-renderer'; -import { CREATE_FEATURE } from '../../../../permissions'; import theme from '../../../../themes/main-theme'; +import { createFakeStore } from '../../../../accessStoreFake'; +import { ADMIN, CREATE_FEATURE } from '../../../AccessProvider/permissions'; +import AccessProvider from '../../../AccessProvider/AccessProvider'; + + jest.mock('../FeatureToggleListItem', () => ({ __esModule: true, @@ -25,6 +29,7 @@ test('renders correctly with one feature', () => { const tree = renderer.create( + { features={features} toggleFeature={jest.fn()} fetcher={jest.fn()} - hasPermission={permission => permission === CREATE_FEATURE} + currentProjectId='default' /> + ); @@ -53,6 +59,7 @@ test('renders correctly with one feature without permissions', () => { const tree = renderer.create( + { features={features} toggleFeature={jest.fn()} fetcher={jest.fn()} - hasPermission={() => false} + currentProjectId='default' /> + ); diff --git a/frontend/src/component/feature/FeatureToggleList/index.jsx b/frontend/src/component/feature/FeatureToggleList/index.jsx index 0039ad6989..6013c6957f 100644 --- a/frontend/src/component/feature/FeatureToggleList/index.jsx +++ b/frontend/src/component/feature/FeatureToggleList/index.jsx @@ -3,8 +3,6 @@ import { toggleFeature, fetchFeatureToggles } from '../../../store/feature-toggl import { updateSettingForGroup } from '../../../store/settings/actions'; import FeatureToggleList from './FeatureToggleList'; -import { hasPermission } from '../../../permissions'; - function checkConstraints(strategy, regex) { if (!strategy.constraints) { return; @@ -12,6 +10,12 @@ function checkConstraints(strategy, regex) { return strategy.constraints.some(c => c.values.some(v => regex.test(v))); } +function resolveCurrentProjectId(settings) { + if(!settings.currentProjectId || settings.currentProjectId === '*') { + return 'default'; + } return settings.currentProjectId; +} + export const mapStateToPropsConfigurable = isFeature => state => { const featureMetrics = state.featureMetrics.toJS(); const settings = state.settings.toJS().feature || {}; @@ -96,9 +100,9 @@ export const mapStateToPropsConfigurable = isFeature => state => { return { features, + currentProjectId: resolveCurrentProjectId(settings), featureMetrics, settings, - hasPermission: hasPermission.bind(null, state.user.get('profile')), loading: state.apiCalls.fetchTogglesState.loading, }; }; diff --git a/frontend/src/component/feature/FeatureView/FeatureView.jsx b/frontend/src/component/feature/FeatureView/FeatureView.jsx index 18d0096b1a..b3c83e8312 100644 --- a/frontend/src/component/feature/FeatureView/FeatureView.jsx +++ b/frontend/src/component/feature/FeatureView/FeatureView.jsx @@ -1,4 +1,4 @@ -import React, { useEffect, useLayoutEffect, useState } from 'react'; +import React, { useContext, useEffect, useLayoutEffect, useState } from 'react'; import classnames from 'classnames'; import PropTypes from 'prop-types'; import { Link } from 'react-router-dom'; @@ -18,10 +18,9 @@ import FeatureTypeSelect from '../feature-type-select-container'; import ProjectSelect from '../project-select-container'; import UpdateDescriptionComponent from '../view/update-description-component'; import { - CREATE_FEATURE, DELETE_FEATURE, UPDATE_FEATURE, -} from '../../../permissions'; +} from '../../AccessProvider/permissions'; import StatusComponent from '../status-component'; import FeatureTagComponent from '../feature-tag-component'; import StatusUpdateComponent from '../view/status-update-component'; @@ -35,6 +34,7 @@ import styles from './FeatureView.module.scss'; import ConfirmDialogue from '../../common/Dialogue'; import { useCommonStyles } from '../../../common.styles'; +import AccessContext from '../../../contexts/AccessContext'; const FeatureView = ({ activeTab, @@ -49,7 +49,6 @@ const FeatureView = ({ editFeatureToggle, featureToggle, history, - hasPermission, untagFeature, featureTags, fetchTags, @@ -58,6 +57,8 @@ const FeatureView = ({ const isFeatureView = !!fetchFeatureToggles; const [delDialog, setDelDialog] = useState(false); const commonStyles = useCommonStyles(); + const { hasAccess } = useContext(AccessContext); + const { project } = featureToggle || { }; useEffect(() => { scrollToTop(); @@ -76,26 +77,17 @@ const FeatureView = ({ // eslint-disable-next-line react-hooks/exhaustive-deps }, []); + const editable = isFeatureView && hasAccess(UPDATE_FEATURE, project); + const getTabComponent = key => { switch (key) { case 'activation': - if (isFeatureView && hasPermission(UPDATE_FEATURE)) { - return ( - - ); - } - return ( - - ); case 'metrics': return ; case 'variants': @@ -104,7 +96,7 @@ const FeatureView = ({ featureToggle={featureToggle} features={features} history={history} - hasPermission={hasPermission} + editable={editable} /> ); case 'log': @@ -152,7 +144,7 @@ const FeatureView = ({ Could not find the toggle{' '}
 
+ `; diff --git a/frontend/src/component/feature/variant/__tests__/update-variant-component-test.jsx b/frontend/src/component/feature/variant/__tests__/update-variant-component-test.jsx index f2219efd55..78977b9d48 100644 --- a/frontend/src/component/feature/variant/__tests__/update-variant-component-test.jsx +++ b/frontend/src/component/feature/variant/__tests__/update-variant-component-test.jsx @@ -3,7 +3,6 @@ import { ThemeProvider } from '@material-ui/core'; import UpdateVariant from './../update-variant-component'; import renderer from 'react-test-renderer'; -import { UPDATE_FEATURE } from '../../../../permissions'; import { weightTypes } from '../enums'; import theme from '../../../../themes/main-theme'; @@ -24,7 +23,7 @@ test('renders correctly with without variants', () => { updateVariant={jest.fn()} stickinessOptions={['default']} updateStickiness={jest.fn()} - hasPermission={permission => permission === UPDATE_FEATURE} + editable /> @@ -45,7 +44,7 @@ test('renders correctly with without variants and no permissions', () => { updateVariant={jest.fn()} stickinessOptions={['default']} updateStickiness={jest.fn()} - hasPermission={() => false} + editable /> @@ -105,7 +104,7 @@ test('renders correctly with with variants', () => { updateVariant={jest.fn()} stickinessOptions={['default']} updateStickiness={jest.fn()} - hasPermission={permission => permission === UPDATE_FEATURE} + editable /> diff --git a/frontend/src/component/feature/variant/update-variant-component.jsx b/frontend/src/component/feature/variant/update-variant-component.jsx index 1fb2c79d13..982a82fe64 100644 --- a/frontend/src/component/feature/variant/update-variant-component.jsx +++ b/frontend/src/component/feature/variant/update-variant-component.jsx @@ -4,7 +4,6 @@ import classnames from 'classnames'; import VariantViewComponent from './variant-view-component'; import styles from './variant.module.scss'; -import { UPDATE_FEATURE } from '../../../permissions'; import { Table, TableHead, @@ -46,7 +45,7 @@ class UpdateVariantComponent extends Component { openEditVariant = (e, index, variant) => { e.preventDefault(); - if (this.props.hasPermission(UPDATE_FEATURE)) { + if (this.props.editable) { this.setState({ showDialog: true, editVariant: variant, @@ -73,7 +72,7 @@ class UpdateVariantComponent extends Component { variant={variant} editVariant={e => this.openEditVariant(e, index, variant)} removeVariant={e => this.onRemoveVariant(e, index)} - hasPermission={this.props.hasPermission} + editable={this.props.editable} /> ); @@ -162,7 +161,7 @@ class UpdateVariantComponent extends Component {
+ + + + + +`; + +exports[`renders correctly with one strategy without permissions 1`] = `
@@ -182,118 +297,3 @@ exports[`renders correctly with one strategy 1`] = `
`; - -exports[`renders correctly with one strategy without permissions 1`] = ` -
-
-
-
-

- Strategies -

-
-
-
-
-
-
    -
  • -
    - - extension - -
    -
    - - - - Another - - - -

    - another's description -

    -
    -
    - -
    -
  • -
-
-
-`; diff --git a/frontend/src/component/strategies/__tests__/__snapshots__/strategy-details-component-test.jsx.snap b/frontend/src/component/strategies/__tests__/__snapshots__/strategy-details-component-test.jsx.snap index b3a1c79fbb..93ad6d319c 100644 --- a/frontend/src/component/strategies/__tests__/__snapshots__/strategy-details-component-test.jsx.snap +++ b/frontend/src/component/strategies/__tests__/__snapshots__/strategy-details-component-test.jsx.snap @@ -35,294 +35,187 @@ exports[`renders correctly with one strategy 1`] = ` > another's description -
+
- - -
- -
-
-
-
-
- -
+
diff --git a/frontend/src/component/strategies/__tests__/list-component-test.jsx b/frontend/src/component/strategies/__tests__/list-component-test.jsx index bd6366b3b6..41f8e35010 100644 --- a/frontend/src/component/strategies/__tests__/list-component-test.jsx +++ b/frontend/src/component/strategies/__tests__/list-component-test.jsx @@ -4,8 +4,10 @@ import { ThemeProvider } from '@material-ui/core'; import StrategiesListComponent from '../StrategiesList/StrategiesList'; import renderer from 'react-test-renderer'; -import { CREATE_STRATEGY, DELETE_STRATEGY } from '../../../permissions'; import theme from '../../../themes/main-theme'; +import AccessProvider from '../../AccessProvider/AccessProvider'; +import { createFakeStore } from '../../../accessStoreFake'; +import { ADMIN } from '../../AccessProvider/permissions'; test('renders correctly with one strategy', () => { const strategy = { @@ -15,15 +17,16 @@ test('renders correctly with one strategy', () => { const tree = renderer.create( - [CREATE_STRATEGY, DELETE_STRATEGY].indexOf(permission) !== -1} - /> + + + ); @@ -39,6 +42,7 @@ test('renders correctly with one strategy without permissions', () => { const tree = renderer.create( + { deprecateStrategy={jest.fn()} reactivateStrategy={jest.fn()} history={{}} - hasPermission={() => false} /> + ); diff --git a/frontend/src/component/strategies/__tests__/strategy-details-component-test.jsx b/frontend/src/component/strategies/__tests__/strategy-details-component-test.jsx index 99cf0a5412..5d22a32a82 100644 --- a/frontend/src/component/strategies/__tests__/strategy-details-component-test.jsx +++ b/frontend/src/component/strategies/__tests__/strategy-details-component-test.jsx @@ -2,9 +2,10 @@ import React from 'react'; import { ThemeProvider } from '@material-ui/core'; import StrategyDetails from '../strategy-details-component'; import renderer from 'react-test-renderer'; -import { UPDATE_STRATEGY } from '../../../permissions'; import { MemoryRouter } from 'react-router-dom'; import theme from '../../../themes/main-theme'; +import { createFakeStore } from '../../../accessStoreFake'; +import AccessProvider from '../../AccessProvider/AccessProvider'; test('renders correctly with one strategy', () => { const strategy = { @@ -34,20 +35,21 @@ test('renders correctly with one strategy', () => { ]; const tree = renderer.create( + - [UPDATE_STRATEGY].indexOf(permission) !== -1} - /> + + ); diff --git a/frontend/src/component/strategies/strategy-details-component.jsx b/frontend/src/component/strategies/strategy-details-component.jsx index 42046c79cd..0bc2aad017 100644 --- a/frontend/src/component/strategies/strategy-details-component.jsx +++ b/frontend/src/component/strategies/strategy-details-component.jsx @@ -3,12 +3,15 @@ import PropTypes from 'prop-types'; import { Grid, Typography } from '@material-ui/core'; import ShowStrategy from './show-strategy-component'; import EditStrategy from './form-container'; -import { UPDATE_STRATEGY } from '../../permissions'; +import { UPDATE_STRATEGY } from '../AccessProvider/permissions'; import ConditionallyRender from '../common/ConditionallyRender/ConditionallyRender'; import TabNav from '../common/TabNav/TabNav'; import PageContent from '../common/PageContent/PageContent'; +import AccessContext from '../../contexts/AccessContext'; export default class StrategyDetails extends Component { + static contextType = AccessContext; + static propTypes = { strategyName: PropTypes.string.isRequired, toggles: PropTypes.array, @@ -19,7 +22,6 @@ export default class StrategyDetails extends Component { fetchApplications: PropTypes.func.isRequired, fetchFeatureToggles: PropTypes.func.isRequired, history: PropTypes.object.isRequired, - hasPermission: PropTypes.func.isRequired, }; componentDidMount() { @@ -52,13 +54,16 @@ export default class StrategyDetails extends Component { component: , }, ]; + + const { hasAccess } = this.context; + return ( {strategy.description} diff --git a/frontend/src/component/strategies/strategy-details-container.js b/frontend/src/component/strategies/strategy-details-container.js index 85a8626591..00a6b4611d 100644 --- a/frontend/src/component/strategies/strategy-details-container.js +++ b/frontend/src/component/strategies/strategy-details-container.js @@ -3,7 +3,6 @@ import ShowStrategy from './strategy-details-component'; import { fetchStrategies } from './../../store/strategy/actions'; import { fetchAll } from './../../store/application/actions'; import { fetchFeatureToggles } from './../../store/feature-toggle/actions'; -import { hasPermission } from '../../permissions'; const mapStateToProps = (state, props) => { let strategy = state.strategies.get('list').find(n => n.name === props.strategyName); @@ -22,7 +21,6 @@ const mapStateToProps = (state, props) => { applications: applications && applications.toJS(), toggles: toggles && toggles.toJS(), activeTab: props.activeTab, - hasPermission: hasPermission.bind(null, state.user.get('profile')), }; }; diff --git a/frontend/src/component/tag-types/TagTypeList/TagTypeList.jsx b/frontend/src/component/tag-types/TagTypeList/TagTypeList.jsx index 950794244e..8e294653dd 100644 --- a/frontend/src/component/tag-types/TagTypeList/TagTypeList.jsx +++ b/frontend/src/component/tag-types/TagTypeList/TagTypeList.jsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from 'react'; +import React, { useContext, useEffect, useState } from 'react'; import PropTypes from 'prop-types'; import { Link, useHistory } from 'react-router-dom'; @@ -16,16 +16,19 @@ import { import HeaderTitle from '../../common/HeaderTitle'; import PageContent from '../../common/PageContent/PageContent'; import ConditionallyRender from '../../common/ConditionallyRender/ConditionallyRender'; -import { CREATE_TAG_TYPE, DELETE_TAG_TYPE } from '../../../permissions'; +import { CREATE_TAG_TYPE, DELETE_TAG_TYPE } from '../../AccessProvider/permissions'; import Dialogue from '../../common/Dialogue/Dialogue'; import useMediaQuery from '@material-ui/core/useMediaQuery'; import styles from '../TagType.module.scss'; +import AccessContext from '../../../contexts/AccessContext'; -const TagTypeList = ({ tagTypes, fetchTagTypes, removeTagType, hasPermission }) => { +const TagTypeList = ({ tagTypes, fetchTagTypes, removeTagType }) => { + const { hasAccess } = useContext(AccessContext); const [deletion, setDeletion] = useState({ open: false }); const history = useHistory(); const smallScreen = useMediaQuery('(max-width:700px)'); + useEffect(() => { fetchTagTypes(); @@ -37,7 +40,7 @@ const TagTypeList = ({ tagTypes, fetchTagTypes, removeTagType, hasPermission }) title="Tag Types" actions={ label - +
); }; @@ -127,7 +130,6 @@ TagTypeList.propTypes = { tagTypes: PropTypes.array.isRequired, fetchTagTypes: PropTypes.func.isRequired, removeTagType: PropTypes.func.isRequired, - hasPermission: PropTypes.func.isRequired, }; export default TagTypeList; diff --git a/frontend/src/component/tag-types/__tests__/tag-type-create-component-test.js b/frontend/src/component/tag-types/__tests__/tag-type-create-component-test.js index 8a314b7249..187de7478d 100644 --- a/frontend/src/component/tag-types/__tests__/tag-type-create-component-test.js +++ b/frontend/src/component/tag-types/__tests__/tag-type-create-component-test.js @@ -15,7 +15,6 @@ test('renders correctly for creating', () => { title="Add tag type" createTagType={jest.fn()} validateName={() => Promise.resolve(true)} - hasPermission={() => true} tagType={{ name: '', description: '', icon: '' }} editMode={false} submit={jest.fn()} @@ -35,7 +34,6 @@ test('it supports editMode', () => { title="Add tag type" createTagType={jest.fn()} validateName={() => Promise.resolve(true)} - hasPermission={() => true} tagType={{ name: '', description: '', icon: '' }} editMode submit={jest.fn()} diff --git a/frontend/src/component/tag-types/__tests__/tag-type-list-component-test.js b/frontend/src/component/tag-types/__tests__/tag-type-list-component-test.js index 0de6050d93..c9be42d784 100644 --- a/frontend/src/component/tag-types/__tests__/tag-type-list-component-test.js +++ b/frontend/src/component/tag-types/__tests__/tag-type-list-component-test.js @@ -5,18 +5,25 @@ import renderer from 'react-test-renderer'; import { MemoryRouter } from 'react-router-dom'; import { ThemeProvider } from '@material-ui/styles'; import theme from '../../../themes/main-theme'; +import { createFakeStore } from '../../../accessStoreFake'; +import AccessProvider from '../../AccessProvider/AccessProvider'; + +import { ADMIN, CREATE_TAG_TYPE, UPDATE_TAG_TYPE, DELETE_TAG_TYPE } from '../../AccessProvider/permissions'; + + test('renders an empty list correctly', () => { const tree = renderer.create( - true} - /> + + + ); @@ -27,19 +34,24 @@ test('renders a list with elements correctly', () => { const tree = renderer.create( - true} - /> + + + ); diff --git a/frontend/src/component/tag-types/index.jsx b/frontend/src/component/tag-types/index.jsx index 8701bf5f71..5cd1cf92d5 100644 --- a/frontend/src/component/tag-types/index.jsx +++ b/frontend/src/component/tag-types/index.jsx @@ -1,13 +1,11 @@ import { connect } from 'react-redux'; import TagTypesListComponent from './TagTypeList'; import { fetchTagTypes, removeTagType } from '../../store/tag-type/actions'; -import { hasPermission } from '../../permissions'; const mapStateToProps = state => { const list = state.tagTypes.toJS(); return { tagTypes: list, - hasPermission: hasPermission.bind(null, state.user.get('profile')), }; }; diff --git a/frontend/src/component/tags/TagList/TagList.jsx b/frontend/src/component/tags/TagList/TagList.jsx index 7e1db5e51b..1ba550d9bf 100644 --- a/frontend/src/component/tags/TagList/TagList.jsx +++ b/frontend/src/component/tags/TagList/TagList.jsx @@ -1,20 +1,22 @@ -import React, { useEffect } from 'react'; +import React, { useContext, useEffect } from 'react'; import PropTypes from 'prop-types'; import useMediaQuery from '@material-ui/core/useMediaQuery'; import { useHistory } from 'react-router-dom'; import { Button, Icon, IconButton, List, ListItem, ListItemIcon, ListItemText, Tooltip } from '@material-ui/core'; -import { CREATE_TAG, DELETE_TAG } from '../../../permissions'; +import { CREATE_TAG, DELETE_TAG } from '../../AccessProvider/permissions'; import ConditionallyRender from '../../common/ConditionallyRender/ConditionallyRender'; import HeaderTitle from '../../common/HeaderTitle'; import PageContent from '../../common/PageContent/PageContent'; import { useStyles } from './TagList.styles'; +import AccessContext from '../../../contexts/AccessContext'; -const TagList = ({ tags, fetchTags, removeTag, hasPermission }) => { +const TagList = ({ tags, fetchTags, removeTag }) => { const history = useHistory(); const smallScreen = useMediaQuery('(max-width:700px)'); const styles = useStyles(); + const { hasAccess } = useContext(AccessContext); useEffect(() => { fetchTags(); @@ -33,7 +35,7 @@ const TagList = ({ tags, fetchTags, removeTag, hasPermission }) => { } />
@@ -52,9 +54,9 @@ const TagList = ({ tags, fetchTags, removeTag, hasPermission }) => { tagValue: PropTypes.string, }; - const AddButton = ({ hasPermission }) => ( + const AddButton = ({ hasAccess }) => ( { /> ); return ( - } />}> + } />}> 0} @@ -100,7 +102,6 @@ TagList.propTypes = { tags: PropTypes.array.isRequired, fetchTags: PropTypes.func.isRequired, removeTag: PropTypes.func.isRequired, - hasPermission: PropTypes.func.isRequired, }; export default TagList; diff --git a/frontend/src/component/tags/index.jsx b/frontend/src/component/tags/index.jsx index c6b529c88d..35483b01fa 100644 --- a/frontend/src/component/tags/index.jsx +++ b/frontend/src/component/tags/index.jsx @@ -1,13 +1,11 @@ import { connect } from 'react-redux'; import TagsListComponent from './TagList'; import { fetchTags, removeTag } from '../../store/tag/actions'; -import { hasPermission } from '../../permissions'; const mapStateToProps = state => { const list = state.tags.toJS(); return { tags: list, - hasPermission: hasPermission.bind(null, state.user.get('profile')), }; }; diff --git a/frontend/src/contexts/AccessContext.js b/frontend/src/contexts/AccessContext.js new file mode 100644 index 0000000000..18ddd1fe72 --- /dev/null +++ b/frontend/src/contexts/AccessContext.js @@ -0,0 +1,5 @@ +import React from 'react'; + +const AccessContext = React.createContext() + +export default AccessContext; diff --git a/frontend/src/index.tsx b/frontend/src/index.tsx index 9441593e9f..809024718d 100644 --- a/frontend/src/index.tsx +++ b/frontend/src/index.tsx @@ -16,6 +16,7 @@ import MetricsPoller from './metrics-poller'; import App from './component/App'; import ScrollToTop from './component/scroll-to-top'; import { writeWarning } from './security-logger'; +import AccessProvider from './component/AccessProvider/AccessProvider'; let composeEnhancers; @@ -38,16 +39,18 @@ metricsPoller.start(); ReactDOM.render( - - - - - - - - - - + + + + + + + + + + + + , document.getElementById('app') ); diff --git a/frontend/src/page/admin/api/api-key-list-container.js b/frontend/src/page/admin/api/api-key-list-container.js index ec1553ab8a..1a872a55ad 100644 --- a/frontend/src/page/admin/api/api-key-list-container.js +++ b/frontend/src/page/admin/api/api-key-list-container.js @@ -2,14 +2,11 @@ import { connect } from 'react-redux'; import Component from './api-key-list'; import { fetchApiKeys, removeKey, addKey } from './../../../store/e-api-admin/actions'; -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), }), { fetchApiKeys, removeKey, addKey } )(Component); diff --git a/frontend/src/page/admin/api/api-key-list.jsx b/frontend/src/page/admin/api/api-key-list.jsx index 94e7e00c0a..6b786e61a5 100644 --- a/frontend/src/page/admin/api/api-key-list.jsx +++ b/frontend/src/page/admin/api/api-key-list.jsx @@ -1,5 +1,5 @@ /* eslint-disable no-alert */ -import React, { useEffect, useState } from 'react'; +import React, { useContext, 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'; @@ -8,8 +8,11 @@ import CreateApiKey from './api-key-create'; import Secret from './secret'; import ConditionallyRender from '../../../component/common/ConditionallyRender/ConditionallyRender'; import Dialogue from '../../../component/common/Dialogue/Dialogue'; +import AccessContext from '../../../contexts/AccessContext'; +import { DELETE_API_TOKEN, CREATE_API_TOKEN } from '../../../component/AccessProvider/permissions'; -function ApiKeyList({ location, fetchApiKeys, removeKey, addKey, keys, hasPermission, unleashUrl }) { +function ApiKeyList({ location, fetchApiKeys, removeKey, addKey, keys, unleashUrl }) { + const { hasAccess } = useContext(AccessContext); const [showDelete, setShowDelete] = useState(false); const [delKey, setDelKey] = useState(undefined); const deleteKey = async () => { @@ -55,7 +58,7 @@ function ApiKeyList({ location, fetchApiKeys, removeKey, addKey, keys, hasPermis {keys.map(item => ( - {formatFullDateTimeWithLocale(item.created, location.locale)} + {formatFullDateTimeWithLocale(item.createdAt, location.locale)} {item.username} {item.type} @@ -63,7 +66,7 @@ function ApiKeyList({ location, fetchApiKeys, removeKey, addKey, keys, hasPermis - { - setShowDelete(false); - setDelKey(undefined); - }} - title="Really delete API key?" - > -
Are you sure you want to delete?
- - } - /> - } /> + { + setShowDelete(false); + setDelKey(undefined); + }} + title="Really delete API key?" + > +
Are you sure you want to delete?
+
+ } /> ); } @@ -109,7 +107,6 @@ ApiKeyList.propTypes = { addKey: PropTypes.func.isRequired, keys: PropTypes.array.isRequired, unleashUrl: PropTypes.string, - hasPermission: PropTypes.func.isRequired, }; export default ApiKeyList; diff --git a/frontend/src/page/admin/auth/google-auth-container.js b/frontend/src/page/admin/auth/google-auth-container.js index e5cc107d77..d1a25ed616 100644 --- a/frontend/src/page/admin/auth/google-auth-container.js +++ b/frontend/src/page/admin/auth/google-auth-container.js @@ -1,12 +1,10 @@ import { connect } from 'react-redux'; import GoogleAuth from './google-auth'; import { getGoogleConfig, updateGoogleConfig } from './../../../store/e-admin-auth/actions'; -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), }); const Container = connect(mapStateToProps, { getGoogleConfig, updateGoogleConfig })(GoogleAuth); diff --git a/frontend/src/page/admin/auth/google-auth.jsx b/frontend/src/page/admin/auth/google-auth.jsx index 8d5c3770ee..4d84dd5572 100644 --- a/frontend/src/page/admin/auth/google-auth.jsx +++ b/frontend/src/page/admin/auth/google-auth.jsx @@ -1,8 +1,10 @@ -import React, { useState, useEffect } from 'react'; +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, @@ -10,9 +12,10 @@ const initialState = { unleashHostname: location.hostname, }; -function GoogleAuth({ config, getGoogleConfig, updateGoogleConfig, hasPermission, unleashUrl }) { +function GoogleAuth({ config, getGoogleConfig, updateGoogleConfig, unleashUrl }) { const [data, setData] = useState(initialState); const [info, setInfo] = useState(); + const { hasAccess } = useContext(AccessContext); useEffect(() => { getGoogleConfig(); @@ -25,7 +28,7 @@ function GoogleAuth({ config, getGoogleConfig, updateGoogleConfig, hasPermission } }, [config]); - if (!hasPermission('ADMIN')) { + if (!hasAccess(ADMIN)) { return You need admin privileges to access this section.; } @@ -193,7 +196,6 @@ GoogleAuth.propTypes = { unleashUrl: PropTypes.string, getGoogleConfig: PropTypes.func.isRequired, updateGoogleConfig: PropTypes.func.isRequired, - hasPermission: PropTypes.func.isRequired, }; export default GoogleAuth; diff --git a/frontend/src/page/admin/auth/index.js b/frontend/src/page/admin/auth/index.js index 05a89bf256..07d89be806 100644 --- a/frontend/src/page/admin/auth/index.js +++ b/frontend/src/page/admin/auth/index.js @@ -1,10 +1,8 @@ import { connect } from 'react-redux'; import component from './authentication'; -import { hasPermission } from '../../../permissions'; const mapStateToProps = state => ({ authenticationType: state.uiConfig.toJS().authenticationType, - hasPermission: permission => hasPermission(state.user.get('profile'), permission), }); const Container = connect(mapStateToProps, { })(component); diff --git a/frontend/src/page/admin/auth/saml-auth-container.js b/frontend/src/page/admin/auth/saml-auth-container.js index f91ffab053..29e410e84d 100644 --- a/frontend/src/page/admin/auth/saml-auth-container.js +++ b/frontend/src/page/admin/auth/saml-auth-container.js @@ -1,12 +1,10 @@ import { connect } from 'react-redux'; import SamlAuth from './saml-auth'; import { getSamlConfig, updateSamlConfig } from './../../../store/e-admin-auth/actions'; -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), }); const Container = connect(mapStateToProps, { getSamlConfig, updateSamlConfig })(SamlAuth); diff --git a/frontend/src/page/admin/auth/saml-auth.jsx b/frontend/src/page/admin/auth/saml-auth.jsx index 8e819bbcce..7c635f4f91 100644 --- a/frontend/src/page/admin/auth/saml-auth.jsx +++ b/frontend/src/page/admin/auth/saml-auth.jsx @@ -1,8 +1,10 @@ -import React, { useState, useEffect } from 'react'; +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, @@ -10,9 +12,10 @@ const initialState = { unleashHostname: location.hostname, }; -function SamlAuth({ config, getSamlConfig, updateSamlConfig, hasPermission, unleashUrl }) { +function SamlAuth({ config, getSamlConfig, updateSamlConfig, unleashUrl }) { const [data, setData] = useState(initialState); const [info, setInfo] = useState(); + const { hasAccess } = useContext(AccessContext); useEffect(() => { getSamlConfig(); @@ -26,7 +29,7 @@ function SamlAuth({ config, getSamlConfig, updateSamlConfig, hasPermission, unle // eslint-disable-next-line react-hooks/exhaustive-deps }, [config]); - if (!hasPermission('ADMIN')) { + if (!hasAccess(ADMIN)) { return You need to be a root admin to access this section.; } @@ -188,7 +191,6 @@ SamlAuth.propTypes = { unleash: PropTypes.string, getSamlConfig: PropTypes.func.isRequired, updateSamlConfig: PropTypes.func.isRequired, - hasPermission: PropTypes.func.isRequired, }; export default SamlAuth; diff --git a/frontend/src/page/admin/users/UsersList/UsersList.jsx b/frontend/src/page/admin/users/UsersList/UsersList.jsx index fe42e72f93..4c8fa1a605 100644 --- a/frontend/src/page/admin/users/UsersList/UsersList.jsx +++ b/frontend/src/page/admin/users/UsersList/UsersList.jsx @@ -1,5 +1,5 @@ /* eslint-disable no-alert */ -import React, { useEffect, useState } from 'react'; +import React, { useContext, useEffect, useState } from 'react'; import PropTypes from 'prop-types'; import { Button, Icon, IconButton, Table, TableBody, TableCell, TableHead, TableRow, Avatar } from '@material-ui/core'; import { formatDateWithLocale } from '../../../../component/common/util'; @@ -8,6 +8,8 @@ import ChangePassword from '../change-password-component'; import UpdateUser from '../update-user-component'; import DelUser from '../del-user-component'; import ConditionallyRender from '../../../../component/common/ConditionallyRender/ConditionallyRender'; +import AccessContext from '../../../../contexts/AccessContext'; +import { ADMIN } from '../../../../component/AccessProvider/permissions'; function UsersList({ roles, @@ -18,9 +20,9 @@ function UsersList({ changePassword, users, location, - hasPermission, validatePassword, }) { + const { hasAccess } = useContext(AccessContext); const [showDialog, setDialog] = useState(false); const [pwDialog, setPwDialog] = useState({ open: false }); const [delDialog, setDelDialog] = useState(false); @@ -83,7 +85,7 @@ function UsersList({ Name Username Role - {hasPermission('ADMIN') ? 'Action' : ''} + {hasAccess('ADMIN') ? 'Action' : ''}
@@ -95,7 +97,7 @@ function UsersList({ {item.username || item.email} {renderRole(item.rootRole)} @@ -117,7 +119,7 @@ function UsersList({
Add new user @@ -168,7 +170,6 @@ UsersList.propTypes = { fetchUsers: PropTypes.func.isRequired, removeUser: PropTypes.func.isRequired, addUser: PropTypes.func.isRequired, - hasPermission: PropTypes.func.isRequired, validatePassword: PropTypes.func.isRequired, updateUser: PropTypes.func.isRequired, changePassword: PropTypes.func.isRequired, diff --git a/frontend/src/page/admin/users/UsersList/index.js b/frontend/src/page/admin/users/UsersList/index.js index 18516e6f1d..4b8853c049 100644 --- a/frontend/src/page/admin/users/UsersList/index.js +++ b/frontend/src/page/admin/users/UsersList/index.js @@ -8,13 +8,11 @@ import { updateUser, validatePassword, } from '../../../../store/e-user-admin/actions'; -import { hasPermission } from '../../../../permissions'; const mapStateToProps = state => ({ users: state.userAdmin.toJS(), roles: state.roles.get('root').toJS() || [], location: state.settings.toJS().location || {}, - hasPermission: permission => hasPermission(state.user.get('profile'), permission), }); const Container = connect(mapStateToProps, { diff --git a/frontend/src/store/user/index.js b/frontend/src/store/user/index.js index b1f3ceb7e5..6ccbcc9a6d 100644 --- a/frontend/src/store/user/index.js +++ b/frontend/src/store/user/index.js @@ -2,11 +2,12 @@ import { Map as $Map } from 'immutable'; import { USER_CHANGE_CURRENT, USER_LOGOUT } from './actions'; import { AUTH_REQUIRED } from '../util'; -const userStore = (state = new $Map(), action) => { +const userStore = (state = new $Map({permissions: []}), action) => { switch (action.type) { case USER_CHANGE_CURRENT: state = state - .set('profile', action.value) + .set('profile', action.value.user) + .set('permissions', action.value.permissions || []) .set('showDialog', false) .set('authDetails', undefined); return state;