From d83982341e5f38d0310077ebc461760320a84502 Mon Sep 17 00:00:00 2001 From: Suren Date: Wed, 7 Jun 2023 14:57:44 +0530 Subject: [PATCH] #9179: Include editing support to allowed user groups (#9210) * #9179: Include editing support to allowed user groups * Code refactor * Unit tests corrected * Permission hierarchy updated --- .../actions/__tests__/featuregrid-test.js | 4 +- .../actions/__tests__/styleeditor-test.js | 9 +- web/client/actions/styleeditor.js | 6 +- .../data/featuregrid/enhancers/editor.js | 3 +- web/client/plugins/FeatureEditor.jsx | 6 +- web/client/plugins/StyleEditor.jsx | 143 ++++++++++-------- .../plugins/__tests__/StyleEditor-test.jsx | 37 +++-- .../plugins/featuregrid/FeatureEditor.jsx | 16 +- .../reducers/__tests__/featuregrid-test.js | 13 +- .../reducers/__tests__/styleeditor-test.js | 9 +- web/client/reducers/featuregrid.js | 3 + web/client/reducers/styleeditor.js | 3 +- .../selectors/__tests__/featuregrid-test.js | 129 +++++++++++++++- .../selectors/__tests__/security-test.js | 85 ++++++++++- .../selectors/__tests__/styleeditor-test.js | 134 ++++++++++++++-- web/client/selectors/featuregrid.js | 22 +-- web/client/selectors/security.js | 33 +++- web/client/selectors/styleeditor.js | 40 ++++- web/client/utils/StyleEditorUtils.js | 2 +- 19 files changed, 569 insertions(+), 128 deletions(-) diff --git a/web/client/actions/__tests__/featuregrid-test.js b/web/client/actions/__tests__/featuregrid-test.js index 110f8ed0d1..3a5b834206 100644 --- a/web/client/actions/__tests__/featuregrid-test.js +++ b/web/client/actions/__tests__/featuregrid-test.js @@ -90,7 +90,9 @@ import { toggleShowAgain, TOGGLE_SHOW_AGAIN_FLAG, setSyncTool, - SET_SYNC_TOOL, setViewportFilter, SET_VIEWPORT_FILTER + SET_SYNC_TOOL, + setViewportFilter, + SET_VIEWPORT_FILTER } from '../featuregrid'; const idFeature = "2135"; diff --git a/web/client/actions/__tests__/styleeditor-test.js b/web/client/actions/__tests__/styleeditor-test.js index 599cbb0ac9..2f97dcf750 100644 --- a/web/client/actions/__tests__/styleeditor-test.js +++ b/web/client/actions/__tests__/styleeditor-test.js @@ -163,12 +163,15 @@ describe('Test the styleeditor actions', () => { }); it('initStyleService', () => { const service = { baseUrl: '/geoserver/' }; - const canEdit = true; - const retval = initStyleService(service, canEdit); + const permissions = { + editingAllowedRoles: ["USER"], + editingAllowedGroups: ["testGroup"] + }; + const retval = initStyleService(service, permissions); expect(retval).toExist(); expect(retval.type).toBe(INIT_STYLE_SERVICE); expect(retval.service).toBe(service); - expect(retval.canEdit).toBe(canEdit); + expect(retval.permissions).toEqual(permissions); }); it('setEditPermissionStyleEditor', () => { const canEdit = true; diff --git a/web/client/actions/styleeditor.js b/web/client/actions/styleeditor.js index 7332aaac87..d806bd12af 100644 --- a/web/client/actions/styleeditor.js +++ b/web/client/actions/styleeditor.js @@ -186,14 +186,14 @@ export function deleteStyle(styleName) { * Setup the style editor service * @memberof actions.styleeditor * @param {object} service style editor service -* @param {bool} canEdit flag to enable/disable style editor in current session +* @param {object} permissions editing allowed roles and groups permission object * @return {object} of type `INIT_STYLE_SERVICE` */ -export function initStyleService(service, canEdit) { +export function initStyleService(service, permissions) { return { type: INIT_STYLE_SERVICE, service, - canEdit + permissions }; } /** diff --git a/web/client/components/data/featuregrid/enhancers/editor.js b/web/client/components/data/featuregrid/enhancers/editor.js index 8a000023a3..5bb9124bcc 100644 --- a/web/client/components/data/featuregrid/enhancers/editor.js +++ b/web/client/components/data/featuregrid/enhancers/editor.js @@ -84,9 +84,10 @@ const featuresToGrid = compose( props => ({displayFilters: props.enableColumnFilters}) ), withPropsOnChange( - ["editingAllowedRoles", "virtualScroll"], + ["editingAllowedRoles", "editingAllowedGroups", "virtualScroll"], props => ({ editingAllowedRoles: props.editingAllowedRoles, + editingAllowedGroups: props.editingAllowedGroups, initPlugin: props.initPlugin }) ), diff --git a/web/client/plugins/FeatureEditor.jsx b/web/client/plugins/FeatureEditor.jsx index 80b6cf2051..a6eade5f14 100644 --- a/web/client/plugins/FeatureEditor.jsx +++ b/web/client/plugins/FeatureEditor.jsx @@ -49,7 +49,11 @@ import {isViewportFilterActive} from "../selectors/featuregrid"; * }] *} * ``` - * @prop {object} cfg.editingAllowedRoles array of user roles allowed to enter in edit mode + * @prop {string[]} cfg.editingAllowedRoles array of user roles allowed to enter in edit mode. + * Support predefined ('ADMIN', 'USER', 'ALL') and custom roles. Default value is ['ADMIN']. + * Configuring with ["ALL"] allows all users to have access regardless of user's permission. + * @prop {string[]} cfg.editingAllowedGroups array of user groups allowed to enter in edit mode. + * When configured, gives the editing permissions to users members of one of the groups listed. * @prop {boolean} cfg.virtualScroll default true. Activates virtualScroll. When false the grid uses normal pagination * @prop {number} cfg.maxStoredPages default 5. In virtual Scroll mode determines the size of the loaded pages cache * @prop {number} cfg.vsOverScan default 20. Number of rows to load above/below the visible slice of the grid diff --git a/web/client/plugins/StyleEditor.jsx b/web/client/plugins/StyleEditor.jsx index 97d0f7624b..9fca0c85f0 100644 --- a/web/client/plugins/StyleEditor.jsx +++ b/web/client/plugins/StyleEditor.jsx @@ -6,89 +6,104 @@ * LICENSE file in the root directory of this source tree. */ -import { isArray, isString } from 'lodash'; +import React, {useEffect} from 'react'; import assign from 'object-assign'; import PropTypes from 'prop-types'; -import React from 'react'; import { connect } from 'react-redux'; import { branch, compose, lifecycle, toClass } from 'recompose'; import { createSelector } from 'reselect'; import { updateSettingsParams } from '../actions/layers'; -import { initStyleService, toggleStyleEditor } from '../actions/styleeditor'; +import { initStyleService, setEditPermissionStyleEditor, toggleStyleEditor } from '../actions/styleeditor'; import HTML from '../components/I18N/HTML'; import BorderLayout from '../components/layout/BorderLayout'; import emptyState from '../components/misc/enhancers/emptyState'; import loadingState from '../components/misc/enhancers/loadingState'; import Loader from '../components/misc/Loader'; -import { userRoleSelector } from '../selectors/security'; import { canEditStyleSelector, errorStyleSelector, - getUpdatedLayer, loadingStyleSelector, statusStyleSelector, styleServiceSelector } from '../selectors/styleeditor'; -import { isSameOrigin } from '../utils/StyleEditorUtils'; import { StyleCodeEditor, StyleSelector, StyleToolbar } from './styleeditor/index'; -class StyleEditorPanel extends React.Component { - static propTypes = { - layer: PropTypes.object, - header: PropTypes.node, - isEditing: PropTypes.bool, - showToolbar: PropTypes.node.bool, - onInit: PropTypes.func, - styleService: PropTypes.object, - userRole: PropTypes.string, - editingAllowedRoles: PropTypes.array, - enableSetDefaultStyle: PropTypes.bool, - canEdit: PropTypes.bool, - editorConfig: PropTypes.object - }; +const StyleEditorPanel = ({ + header, + isEditing, + showToolbar, + onInit, + styleService, + editingAllowedRoles, + editingAllowedGroups, + enableSetDefaultStyle, + canEdit, + editorConfig, + onSetPermission +}) => { - static defaultProps = { - layer: {}, - onInit: () => {}, - editingAllowedRoles: [ - 'ADMIN' - ], - editorConfig: {} - }; + useEffect(() => { + onInit( + styleService, + { + editingAllowedRoles, + editingAllowedGroups + } + ); + }, []); - UNSAFE_componentWillMount() { - const canEdit = !this.props.editingAllowedRoles || (isArray(this.props.editingAllowedRoles) && isString(this.props.userRole) - && this.props.editingAllowedRoles.indexOf(this.props.userRole) !== -1); - this.props.onInit(this.props.styleService, canEdit && isSameOrigin(this.props.layer, this.props.styleService)); - } + useEffect(() => { + onSetPermission(canEdit); + }, [canEdit]); + + return ( + + {header} +
+ +
+ : null + } + footer={
}> + {isEditing + ? + : } + + ); +}; +StyleEditorPanel.propTypes = { + header: PropTypes.node, + isEditing: PropTypes.bool, + showToolbar: PropTypes.bool, + onInit: PropTypes.func, + styleService: PropTypes.object, + editingAllowedRoles: PropTypes.array, + editingAllowedGroups: PropTypes.array, + enableSetDefaultStyle: PropTypes.bool, + canEdit: PropTypes.bool, + editorConfig: PropTypes.object, + onSetPermission: PropTypes.func +}; +StyleEditorPanel.defaultProps = { + layer: {}, + onInit: () => {}, + editingAllowedRoles: [ + 'ADMIN' + ], + editingAllowedGroups: [], + editorConfig: {} +}; - render() { - return ( - - {this.props.header} -
- -
-
: null - } - footer={
}> - {this.props.isEditing - ? - : } - - ); - } -} /** * StyleEditor plugin. * - Select styles from available styles of the layer @@ -101,7 +116,12 @@ class StyleEditorPanel extends React.Component { * @prop {string} cfg.styleService.baseUrl base url of service eg: '/geoserver/' * @prop {array} cfg.styleService.availableUrls a list of urls that can access directly to the style service * @prop {array} cfg.styleService.formats supported formats, could be one of [ 'sld' ] or [ 'sld', 'css' ] - * @prop {array} cfg.editingAllowedRoles all roles with edit permission eg: [ 'ADMIN' ], if null all roles have edit permission + * @prop {string[]} cfg.editingAllowedRoles array of user roles allowed to enter in edit mode. + * Support predefined ('ADMIN', 'USER', 'ALL') and custom roles. Default value is ['ADMIN']. + * Configuring with ["ALL"] allows all users to have access regardless of user's permission. + * However, the outcome can be influenced by the user's permission to access the requested style service. + * @prop {string[]} cfg.editingAllowedGroups array of user groups allowed to enter in edit mode. + * When configured, gives the editing permissions to users members of one of the groups listed. * @prop {array} cfg.enableSetDefaultStyle enable set default style functionality * @prop {object} cfg.editorConfig contains editor configurations * @prop {object} cfg.editorConfig.classification configuration of the classification symbolizer @@ -123,7 +143,7 @@ const StyleEditorPlugin = compose( // in this case 'branch' return always a functional component and PluginUtils expects a class toClass, // No rendering if not active - // eg: now only TOCItemsSettings can active following plugin + // eg: now only TOCItemsSettings can activate the following plugin branch( ({ active } = {}) => !active, () => () => null @@ -134,25 +154,22 @@ const StyleEditorPlugin = compose( [ statusStyleSelector, loadingStyleSelector, - getUpdatedLayer, errorStyleSelector, - userRoleSelector, canEditStyleSelector, styleServiceSelector ], - (status, loading, layer, error, userRole, canEdit, styleService) => ({ + (status, loading, error, canEdit, styleService) => ({ isEditing: status === 'edit', loading, - layer, error, - userRole, canEdit, styleService }) ), { onInit: initStyleService, - onUpdateParams: updateSettingsParams + onUpdateParams: updateSettingsParams, + onSetPermission: setEditPermissionStyleEditor }, (stateProps, dispatchProps, ownProps) => { // detect if the static service has been updated with new information in the global state diff --git a/web/client/plugins/__tests__/StyleEditor-test.jsx b/web/client/plugins/__tests__/StyleEditor-test.jsx index a6b6ebc542..2353e50f96 100644 --- a/web/client/plugins/__tests__/StyleEditor-test.jsx +++ b/web/client/plugins/__tests__/StyleEditor-test.jsx @@ -12,7 +12,11 @@ import ReactDOM from 'react-dom'; import StyleEditorPlugin from '../StyleEditor'; import { getPluginForTest } from './pluginsTestUtils'; import { act } from 'react-dom/test-utils'; -import { INIT_STYLE_SERVICE, TOGGLE_STYLE_EDITOR } from '../../actions/styleeditor'; +import { + INIT_STYLE_SERVICE, + TOGGLE_STYLE_EDITOR, + SET_EDIT_PERMISSION +} from '../../actions/styleeditor'; describe('StyleEditor Plugin', () => { beforeEach((done) => { @@ -41,11 +45,12 @@ describe('StyleEditor Plugin', () => { styleService={cfgStyleService} />, document.getElementById("container")); }); - expect(actions.length).toBe(2); + expect(actions.length).toBe(3); expect(actions.map(action => action.type)) - .toEqual([ INIT_STYLE_SERVICE, TOGGLE_STYLE_EDITOR ]); - expect(actions[0].service).toBeTruthy(); - expect(actions[0].service).toEqual({ ...cfgStyleService, isStatic: true }); + .toEqual([ TOGGLE_STYLE_EDITOR, INIT_STYLE_SERVICE, SET_EDIT_PERMISSION ]); + expect(actions[1].service).toBeTruthy(); + expect(actions[1].service).toEqual({ ...cfgStyleService, isStatic: true }); + expect(actions[1].permissions.editingAllowedRoles).toEqual(['ADMIN']); }); it('should use the static service from the state', () => { const cfgStyleService = { @@ -70,17 +75,23 @@ describe('StyleEditor Plugin', () => { service: stateStyleService } }); + const permissions = { + "editingAllowedRoles": ["USER"], + "editingAllowedGroups": ["temp"] + }; act(() => { ReactDOM.render(, document.getElementById("container")); }); - expect(actions.length).toBe(2); + expect(actions.length).toBe(3); expect(actions.map(action => action.type)) - .toEqual([ INIT_STYLE_SERVICE, TOGGLE_STYLE_EDITOR ]); - expect(actions[0].service).toBeTruthy(); - expect(actions[0].service).toEqual(stateStyleService); + .toEqual([ TOGGLE_STYLE_EDITOR, INIT_STYLE_SERVICE, SET_EDIT_PERMISSION ]); + expect(actions[1].service).toBeTruthy(); + expect(actions[1].service).toEqual(stateStyleService); + expect(actions[1].permissions).toEqual(permissions); }); it('should use the service from the state', () => { const styleService = { @@ -99,10 +110,10 @@ describe('StyleEditor Plugin', () => { active />, document.getElementById("container")); }); - expect(actions.length).toBe(2); + expect(actions.length).toBe(3); expect(actions.map(action => action.type)) - .toEqual([ INIT_STYLE_SERVICE, TOGGLE_STYLE_EDITOR ]); - expect(actions[0].service).toBeTruthy(); - expect(actions[0].service).toEqual(styleService); + .toEqual([ TOGGLE_STYLE_EDITOR, INIT_STYLE_SERVICE, SET_EDIT_PERMISSION ]); + expect(actions[1].service).toBeTruthy(); + expect(actions[1].service).toEqual(styleService); }); }); diff --git a/web/client/plugins/featuregrid/FeatureEditor.jsx b/web/client/plugins/featuregrid/FeatureEditor.jsx index 7e062e9fc7..059c70ed36 100644 --- a/web/client/plugins/featuregrid/FeatureEditor.jsx +++ b/web/client/plugins/featuregrid/FeatureEditor.jsx @@ -70,7 +70,11 @@ const Dock = connect(createSelector( * }] *} * ``` - * @prop {object} cfg.editingAllowedRoles array of user roles allowed to enter in edit mode + * @prop {string[]} cfg.editingAllowedRoles array of user roles allowed to enter in edit mode. + * Support predefined ('ADMIN', 'USER', 'ALL') and custom roles. Default value is ['ADMIN']. + * Configuring with ["ALL"] allows all users to have access regardless of user's permission. + * @prop {string[]} cfg.editingAllowedGroups array of user groups allowed to enter in edit mode. + * When configured, gives the editing permissions to users members of one of the groups listed. * @prop {boolean} cfg.virtualScroll default true. Activates virtualScroll. When false the grid uses normal pagination * @prop {number} cfg.maxStoredPages default 5. In virtual Scroll mode determines the size of the loaded pages cache * @prop {number} cfg.vsOverScan default 20. Number of rows to load above/below the visible slice of the grid @@ -94,7 +98,7 @@ const Dock = connect(createSelector( * * @classdesc * `FeatureEditor` Plugin, also called *FeatureGrid*, provides functionalities to browse/edit data via WFS. The grid can be configured to use paging or - *
virtual scroll mechanisms. By default virtual scroll is enabled. When on virtual scroll mode, the maxStoredPages param + *
virtual scroll mechanisms. By default, virtual scroll is enabled. When on virtual scroll mode, the maxStoredPages param * sets the size of loaded pages cache, while vsOverscan and scrollDebounce params determine the behavior of grid scrolling * and of row loading. *
Furthermore it can be configured to use custom editor cells for certain layers/columns, specifying the rules to recognize them. If no rule matches, then it will be used the default editor based on the dataType of that column. @@ -187,10 +191,16 @@ const FeatureDock = (props = { // const editors = items.filter(({target}) => target === 'editors'); useEffect(() => { - props.initPlugin({virtualScroll, editingAllowedRoles: props.editingAllowedRoles, maxStoredPages: props.maxStoredPages}); + props.initPlugin({ + virtualScroll, + editingAllowedRoles: props.editingAllowedRoles, + editingAllowedGroups: props.editingAllowedGroups, + maxStoredPages: props.maxStoredPages + }); }, [ virtualScroll, (props.editingAllowedRoles ?? []).join(","), // this avoids multiple calls when the array remains the equal + (props.editingAllowedGroups ?? []).join(","), props.maxStoredPages ]); diff --git a/web/client/reducers/__tests__/featuregrid-test.js b/web/client/reducers/__tests__/featuregrid-test.js index bd1af232ad..44bc0ba25b 100644 --- a/web/client/reducers/__tests__/featuregrid-test.js +++ b/web/client/reducers/__tests__/featuregrid-test.js @@ -114,13 +114,22 @@ describe('Test the featuregrid reducer', () => { let state2 = featuregrid({showAgain: true}, toggleShowAgain()); expect(state2.showAgain).toBe(false); }); - it('initPlugin', () => { + it('initPlugin with default roles and groups', () => { + let state = featuregrid({}, initPlugin({})); + expect(state).toExist(); + expect(state.editingAllowedRoles.length).toBe(1); + expect(state.editingAllowedRoles).toEqual(["ADMIN"]); + expect(state.editingAllowedGroups).toEqual([]); + }); + it('initPlugin with roles and groups allowed', () => { const someValue = "someValue"; const editingAllowedRoles = [someValue]; - let state = featuregrid({}, initPlugin({editingAllowedRoles})); + const editingAllowedGroups = [someValue]; + let state = featuregrid({}, initPlugin({editingAllowedRoles, editingAllowedGroups})); expect(state).toExist(); expect(state.editingAllowedRoles.length).toBe(1); expect(state.editingAllowedRoles[0]).toBe(someValue); + expect(state.editingAllowedGroups[0]).toBe(someValue); }); it('openFeatureGrid', () => { let state = featuregrid(undefined, openFeatureGrid()); diff --git a/web/client/reducers/__tests__/styleeditor-test.js b/web/client/reducers/__tests__/styleeditor-test.js index 7ef85d5003..5809e7952a 100644 --- a/web/client/reducers/__tests__/styleeditor-test.js +++ b/web/client/reducers/__tests__/styleeditor-test.js @@ -28,11 +28,14 @@ describe('Test styleeditor reducer', () => { const service = { baseUrl: '/geoserver' }; - const canEdit = true; - const state = styleeditor({}, initStyleService(service, canEdit)); + const permissions = { + editingAllowedRoles: ['ADMIN'], + editingAllowedGroups: ['test'] + }; + const state = styleeditor({}, initStyleService(service, permissions)); expect(state).toEqual({ service, - canEdit: true + ...permissions }); }); it('test setEditPermissionStyleEditor', () => { diff --git a/web/client/reducers/featuregrid.js b/web/client/reducers/featuregrid.js index 3fcd17417f..515f9a3ea2 100644 --- a/web/client/reducers/featuregrid.js +++ b/web/client/reducers/featuregrid.js @@ -60,6 +60,7 @@ const emptyResultsState = { advancedFilters: {}, filters: {}, editingAllowedRoles: ["ADMIN"], + editingAllowedGroups: [], enableColumnFilters: true, showFilteredObject: false, timeSync: false, @@ -111,6 +112,7 @@ const applyNewChanges = (features, changedFeatures, updates, updatesGeom) => * Manages the state of the featuregrid * The properties represent the shape of the state * @prop {string[]} editingAllowedRoles array of user roles allowed to enter in edit mode + * @prop {string[]} editingAllowedGroups array of user roles allowed to enter in edit mode, when logged-in user role is not ADMIN * @prop {boolean} canEdit flag used to enable editing on the feature grid * @prop {object} filters filters for quick search. `{attribute: "name", value: "filter_value", opeartor: "=", rawValue: "the fitler raw value"}` * @prop {boolean} enableColumnFilters enables column filter. [configurable] @@ -155,6 +157,7 @@ function featuregrid(state = emptyResultsState, action) { return assign({}, state, { showPopoverSync: getApi().getItem("showPopoverSync") !== null ? getApi().getItem("showPopoverSync") === "true" : true, editingAllowedRoles: action.options.editingAllowedRoles || state.editingAllowedRoles || ["ADMIN"], + editingAllowedGroups: action.options.editingAllowedGroups || state.editingAllowedGroups || [], virtualScroll: !!action.options.virtualScroll, maxStoredPages: action.options.maxStoredPages || 5 }); diff --git a/web/client/reducers/styleeditor.js b/web/client/reducers/styleeditor.js index ff951a52d9..da5b244ef4 100644 --- a/web/client/reducers/styleeditor.js +++ b/web/client/reducers/styleeditor.js @@ -27,7 +27,8 @@ function styleeditor(state = {}, action) { return { ...state, service: action.service, - canEdit: action.canEdit + editingAllowedRoles: action?.permissions?.editingAllowedRoles || state.editingAllowedRoles, + editingAllowedGroups: action?.permissions?.editingAllowedGroups || state.editingAllowedGroups }; } case SET_EDIT_PERMISSION: { diff --git a/web/client/selectors/__tests__/featuregrid-test.js b/web/client/selectors/__tests__/featuregrid-test.js index 81db96898a..471e6e25cb 100644 --- a/web/client/selectors/__tests__/featuregrid-test.js +++ b/web/client/selectors/__tests__/featuregrid-test.js @@ -37,8 +37,13 @@ import { queryOptionsSelector, showTimeSync, timeSyncActive, - multiSelect, isViewportFilterActive, viewportFilter, isFilterByViewportSupported, - selectedLayerFieldsSelector + multiSelect, + isViewportFilterActive, + viewportFilter, + isFilterByViewportSupported, + selectedLayerFieldsSelector, + editingAllowedGroupsSelector, + isEditingAllowedSelector } from '../featuregrid'; const idFt1 = "idFt1"; @@ -667,4 +672,124 @@ describe('Test featuregrid selectors', () => { }; expect(selectedLayerFieldsSelector(state)).toEqual([FIELD]); }); + it('editingAllowedGroupsSelector', () => { + const editingAllowedGroups = ['test']; + expect(editingAllowedGroupsSelector({ + featuregrid: { + editingAllowedGroups + } + })).toEqual(editingAllowedGroups); + }); + describe('isEditingAllowedSelector', () => { + const state = { + featuregrid: { + canEdit: false, + editingAllowedRoles: ['USER'], + editingAllowedGroups: ['test'] + }, + security: { + user: { + role: 'USER', + groups: { + group: { + enabled: true, + groupName: 'test' + } + } + } + } + }; + it('test isEditingAllowedSelector with canEdit', () => { + expect(isEditingAllowedSelector({ + ...state, + featuregrid: { + canEdit: true + } + })).toBeTruthy(); + }); + it('test isEditingAllowedSelector with ALL role', () => { + expect(isEditingAllowedSelector({ + ...state, + featuregrid: { + editingAllowedRoles: ["ALL"] + } + })).toBeTruthy(); + }); + it('test isEditingAllowedSelector with defaults', () => { + expect(isEditingAllowedSelector({ + featuregrid: { + editingAllowedRoles: ["ADMIN"], + editingAllowedGroups: [] + }, + security: { + user: { + role: 'ADMIN', + groups: { + group: { + enabled: true, + groupName: 'test' + } + } + } + } + })).toBeTruthy(); + }); + it('test isEditingAllowedSelector with ADMIN user matching allowedGroups', () => { + expect(isEditingAllowedSelector({ + featuregrid: { + editingAllowedGroups: ['test'] + }, + security: { + user: { + role: 'ADMIN', + groups: { + group: { + enabled: true, + groupName: 'test' + } + } + } + } + })).toBeTruthy(); + }); + it('test isEditingAllowedSelector with non-admin user matching allowed roles', () => { + expect(isEditingAllowedSelector({ + ...state, + featuregrid: { + editingAllowedRoles: ['USER'] + } + })).toBeTruthy(); + }); + it('test isEditingAllowedSelector with non-admin user with non-allowed groups', () => { + expect(isEditingAllowedSelector({ + ...state, + featuregrid: { + editingAllowedRoles: ['USER1'], + editingAllowedGroups: ['some'] + } + })).toBeFalsy(); + }); + it('test isEditingAllowedSelector with non-admin user and with default editingAllowedRoles', () => { + expect(isEditingAllowedSelector({ + ...state, + featuregrid: {} + })).toBeFalsy(); + }); + it('test isEditingAllowedSelector with ADMIN user and with default editingAllowedRoles', () => { + expect(isEditingAllowedSelector({ + featuregrid: {}, + security: { + user: { + role: 'ADMIN', + groups: { + group: { + enabled: true, + groupName: 'test' + } + } + } + } + })).toBeTruthy(); + }); + }); }); diff --git a/web/client/selectors/__tests__/security-test.js b/web/client/selectors/__tests__/security-test.js index 07919b910e..ec3809ad79 100644 --- a/web/client/selectors/__tests__/security-test.js +++ b/web/client/selectors/__tests__/security-test.js @@ -15,7 +15,9 @@ import { rulesSelector, securityTokenSelector, userGroupSecuritySelector, - userParamsSelector + userParamsSelector, + userGroupsEnabledSelector, + isUserAllowedSelectorCreator } from '../security'; const id = 1833; @@ -95,4 +97,85 @@ describe('Test security selectors', () => { expect(userParams.id).toBe(id); expect(userParams.name).toBe(name); }); + it('test userGroupsEnabledSelector ', () => { + const userGroups = userGroupsEnabledSelector(initialState); + expect(userGroups).toBeTruthy(); + expect(userGroups).toEqual(['everyone']); + }); + describe('isUserAllowedForEditingSelector', () => { + const state = { + security: { + user: { + role: 'USER', + groups: { + group: { + enabled: true, + groupName: 'test' + } + } + } + } + }; + it('test with allowedRole ALL', () => { + expect(isUserAllowedSelectorCreator({ + allowedRoles: ["ALL"] + })(state)).toBeTruthy(); + }); + it('test with both role and group matching both allowedRoles and allowedGroups', () => { + expect(isUserAllowedSelectorCreator({ + allowedRoles: ["USER"], + allowedGroups: ["test"] + })(state)).toBeTruthy(); + }); + it('test with role ADMIN and allowedRoles', () => { + expect(isUserAllowedSelectorCreator({ + allowedRoles: ['ADMIN'] + })({ + security: { + user: { + role: 'ADMIN', + groups: { + group: { + enabled: true, + groupName: 'test' + } + } + } + } + })).toBeTruthy(); + }); + it('test with role ADMIN and allowedGroups', () => { + expect(isUserAllowedSelectorCreator({ + allowedGroups: ['test'] + })({ + security: { + user: { + role: 'ADMIN', + groups: { + group: { + enabled: true, + groupName: 'test' + } + } + } + } + })).toBeTruthy(); + }); + it('test with role non-admin and allowedgroups', () => { + expect(isUserAllowedSelectorCreator({ + allowedGroups: ['test'] + })(state)).toBeTruthy(); + }); + it('test with role non-admin and allowedroles', () => { + expect(isUserAllowedSelectorCreator({ + allowedRoles: ['USER'] + })(state)).toBeTruthy(); + }); + it('test not allowed for edit', () => { + expect(isUserAllowedSelectorCreator({ + allowedRoles: ['USER1'], + allowedGroups: ['some'] + })(state)).toBeFalsy(); + }); + }); }); diff --git a/web/client/selectors/__tests__/styleeditor-test.js b/web/client/selectors/__tests__/styleeditor-test.js index c07b62ca07..842b7d4b82 100644 --- a/web/client/selectors/__tests__/styleeditor-test.js +++ b/web/client/selectors/__tests__/styleeditor-test.js @@ -29,8 +29,14 @@ import { getAllStyles, selectedStyleFormatSelector, editorMetadataSelector, - selectedStyleMetadataSelector + selectedStyleMetadataSelector, + editingAllowedRolesSelector, + editingAllowedGroupsSelector } from '../styleeditor'; +import { + setCustomUtils, + StyleEditorCustomUtils +} from "../../utils/StyleEditorUtils"; describe('Test styleeditor selector', () => { it('test temporaryIdSelector', () => { @@ -390,17 +396,6 @@ describe('Test styleeditor selector', () => { } ); }); - it('test canEditStyleSelector', () => { - const state = { - styleeditor: { - canEdit: true - } - }; - const retval = canEditStyleSelector(state); - - expect(retval).toExist(); - expect(retval).toBe(true); - }); it('test getUpdatedLayer', () => { const state = { layers: { @@ -683,4 +678,119 @@ describe('Test styleeditor selector', () => { styleJSON: 'null' }); }); + it('test editingAllowedRolesSelector', () => { + expect(editingAllowedRolesSelector({ + styleeditor: { + editingAllowedRoles: ['ADMIN'] + } + })).toEqual(['ADMIN']); + }); + it('test editingAllowedGroupsSelector', () => { + expect(editingAllowedGroupsSelector({ + styleeditor: { + editingAllowedGroups: ['test'] + } + })).toEqual(['test']); + }); + describe('canEditStyleSelector', () => { + const isSameOrigin = StyleEditorCustomUtils.isSameOrigin; + before(() => { + setCustomUtils('isSameOrigin', () => true); + }); + after(()=> { + setCustomUtils('isSameOrigin', isSameOrigin); + }); + it('test with role ADMIN', () => { + expect(canEditStyleSelector({ + styleeditor: { + editingAllowedRoles: ['ADMIN'] + }, + security: { + user: { + role: 'ADMIN', + groups: { + group: { + enabled: true, + groupName: 'test' + } + } + } + } + })).toBeTruthy(); + }); + it('test with user matching allowedRoles', () => { + expect(canEditStyleSelector({ + styleeditor: { + editingAllowedRoles: ['USER'] + }, + security: { + user: { + role: 'USER', + groups: { + group: { + enabled: true, + groupName: 'test' + } + } + } + } + })).toBeTruthy(); + }); + it('test with user matching allowedGroups', () => { + expect(canEditStyleSelector({ + styleeditor: { + editingAllowedGroups: ['test'] + }, + security: { + user: { + role: 'USER', + groups: { + group: { + enabled: true, + groupName: 'test' + } + } + } + } + })).toBeTruthy(); + }); + it('test with user matching both allowedRoles and allowedGroups', () => { + expect(canEditStyleSelector({ + styleeditor: { + editingAllowedRoles: ['USER'], + editingAllowedGroups: ['test'] + }, + security: { + user: { + role: 'USER', + groups: { + group: { + enabled: true, + groupName: 'test' + } + } + } + } + })).toBeTruthy(); + }); + it('test not allowed for editing', () => { + expect(canEditStyleSelector({ + styleeditor: { + editingAllowedRoles: ['USER1'], + editingAllowedGroups: ['some'] + }, + security: { + user: { + role: 'USER', + groups: { + group: { + enabled: true, + groupName: 'test' + } + } + } + } + })).toBeFalsy(); + }); + }); }); diff --git a/web/client/selectors/featuregrid.js b/web/client/selectors/featuregrid.js index 7a4125028a..b9856e6de8 100644 --- a/web/client/selectors/featuregrid.js +++ b/web/client/selectors/featuregrid.js @@ -14,13 +14,13 @@ import { currentLocaleSelector } from './locale'; import { isSimpleGeomType } from '../utils/MapUtils'; import { toChangesMap } from '../utils/FeatureGridUtils'; import { layerDimensionSelectorCreator } from './dimension'; -import { userRoleSelector } from './security'; +import { isUserAllowedSelectorCreator } from './security'; import {isCesium, mapTypeSelector} from './maptype'; import { attributesSelector, describeSelector } from './query'; -import {createShallowSelectorCreator} from "../utils/ReselectUtils"; +import { createShallowSelectorCreator } from "../utils/ReselectUtils"; import isEqual from "lodash/isEqual"; -import {mapBboxSelector, projectionSelector} from "./map"; -import {bboxToFeatureGeometry} from "../utils/CoordinatesUtils"; +import { mapBboxSelector, projectionSelector } from "./map"; +import { bboxToFeatureGeometry } from "../utils/CoordinatesUtils"; import { MapLibraries } from '../utils/MapTypeUtils'; export const getLayerById = getLayerFromId; @@ -76,6 +76,7 @@ export const getAttributeFilters = state => state && state.featuregrid && state. export const selectedLayerParamsSelector = state => get(getLayerById(state, selectedLayerIdSelector(state)), "params"); export const selectedLayerSelector = state => getLayerById(state, selectedLayerIdSelector(state)); export const editingAllowedRolesSelector = state => get(state, "featuregrid.editingAllowedRoles", ["ADMIN"]); +export const editingAllowedGroupsSelector = state => get(state, "featuregrid.editingAllowedGroups", []); export const canEditSelector = state => state && state.featuregrid && state.featuregrid.canEdit; /** * selects featuregrid state @@ -192,12 +193,15 @@ export const queryOptionsSelector = state => { cqlFilter }; }; -export const isEditingAllowedSelector = state => { - const role = userRoleSelector(state); - const editingAllowedRoles = editingAllowedRolesSelector(state) || ['ADMIN']; +export const isEditingAllowedSelector = (state) => { + const allowedRoles = editingAllowedRolesSelector(state); + const allowedGroups = editingAllowedGroupsSelector(state); const canEdit = canEditSelector(state); - - return (editingAllowedRoles.indexOf(role) !== -1 || canEdit) && !isCesium(state); + const isAllowed = isUserAllowedSelectorCreator({ + allowedRoles, + allowedGroups + })(state); + return (canEdit || isAllowed) && !isCesium(state); }; export const paginationSelector = state => get(state, "featuregrid.pagination"); export const useLayerFilterSelector = state => get(state, "featuregrid.useLayerFilter", true); diff --git a/web/client/selectors/security.js b/web/client/selectors/security.js index 786286a970..0f268a8e50 100644 --- a/web/client/selectors/security.js +++ b/web/client/selectors/security.js @@ -8,7 +8,8 @@ import assign from 'object-assign'; -import { get } from 'lodash'; +import get from 'lodash/get'; +import castArray from "lodash/castArray"; export const rulesSelector = (state) => { if (!state.security || !state.security.rules) { @@ -32,6 +33,14 @@ export const rulesSelector = (state) => { export const userSelector = (state) => state && state.security && state.security.user; export const userGroupSecuritySelector = (state) => get(state, "security.user.groups.group"); +export const userGroupsEnabledSelector = (state) => { + const securityGroup = userGroupSecuritySelector(state); + return securityGroup + ? castArray(securityGroup) + ?.filter(group => group.enabled) + ?.map(group => group.groupName) + : []; +}; export const userRoleSelector = (state) => userSelector(state) && userSelector(state).role; export const userParamsSelector = (state) => { const user = userSelector(state); @@ -46,3 +55,25 @@ export const securityTokenSelector = state => state.security && state.security.t export const isAdminUserSelector = (state) => userRoleSelector(state) === "ADMIN"; export const isUserSelector = (state) => userRoleSelector(state) === "USER"; export const authProviderSelector = state => state.security && state.security.authProvider; + +/** + * Creates a selector that checks if user is allowed to edit + * something based on the user's role and groups + * by passing the authorized roles and groups as parameter for selector creation. + * @param {string[]} allowedRoles array of roles allowed. Supports predefined ("ADMIN", "USER", "ALL") and custom roles + * @param {string[]} allowedGroups array of user group names allowed + * @returns {function(*): boolean} + */ +export const isUserAllowedSelectorCreator = ({ + allowedRoles, + allowedGroups +})=> (state) => { + const role = userRoleSelector(state); + const groups = userGroupsEnabledSelector(state); + return ( + castArray(allowedRoles).includes('ALL') + || castArray(allowedRoles).includes(role) + || castArray(allowedGroups) + .some((group) => groups.includes(group)) + ); +}; diff --git a/web/client/selectors/styleeditor.js b/web/client/selectors/styleeditor.js index 04d8422a6e..3031ce1d08 100644 --- a/web/client/selectors/styleeditor.js +++ b/web/client/selectors/styleeditor.js @@ -9,7 +9,8 @@ import { get, head, uniqBy, find, isString } from 'lodash'; import { layerSettingSelector, getSelectedLayer } from './layers'; -import { STYLE_ID_SEPARATOR, extractFeatureProperties } from '../utils/StyleEditorUtils'; +import { STYLE_ID_SEPARATOR, extractFeatureProperties, isSameOrigin } from '../utils/StyleEditorUtils'; +import { isUserAllowedSelectorCreator } from "./security"; /** * selects styleeditor state @@ -102,13 +103,6 @@ export const enabledStyleEditorSelector = state => get(state, 'styleeditor.enabl * @return {object} eg: styleService: {baseUrl: '/geoserver/', formats: ['css', 'sld'], availableUrls: ['http://localhost:8081/geoserver/']} */ export const styleServiceSelector = state => get(state, 'styleeditor.service') || {}; -/** - * selects canEdit status of styleeditor service from state - * @memberof selectors.styleeditor - * @param {object} state the state - * @return {bool} - */ -export const canEditStyleSelector = state => get(state, 'styleeditor.canEdit'); /** * selects layer with current changes applied in settings session from state * @memberof selectors.styleeditor @@ -120,6 +114,36 @@ export const getUpdatedLayer = state => { const selectedLayer = getSelectedLayer(state) || {}; return {...selectedLayer, ...(settings && settings.options || {})}; }; +/** + * Selects configured editing roles allowed + * @memberof selectors.styleeditor + * @param {object} state the state + * @returns {object} + */ +export const editingAllowedRolesSelector = (state) => get(state, 'styleeditor.editingAllowedRoles', []); +/** + * Selects configured editing groups allowed + * @memberof selectors.styleeditor + * @param {object} state the state + * @returns {object} + */ +export const editingAllowedGroupsSelector = (state) => get(state, 'styleeditor.editingAllowedGroups', []); +/** + * selects canEdit status of styleeditor service from state + * @memberof selectors.styleeditor + * @param {object} state the state + * @return {bool} + */ +export const canEditStyleSelector = (state) => { + const allowedRoles = editingAllowedRolesSelector(state); + const allowedGroups = editingAllowedGroupsSelector(state); + const _isSameOrigin = isSameOrigin(getUpdatedLayer(state), styleServiceSelector(state)); + const isAllowed = isUserAllowedSelectorCreator({ + allowedRoles, + allowedGroups + })(state); + return isAllowed && _isSameOrigin; +}; /** * selects geometry type of selected layer from state * @memberof selectors.styleeditor diff --git a/web/client/utils/StyleEditorUtils.js b/web/client/utils/StyleEditorUtils.js index 97804382a7..749a3e4190 100644 --- a/web/client/utils/StyleEditorUtils.js +++ b/web/client/utils/StyleEditorUtils.js @@ -36,7 +36,7 @@ const xmlBuilder = new xml2js.Builder(); export const STYLE_ID_SEPARATOR = '___'; export const STYLE_OWNER_NAME = 'styleeditor'; -const StyleEditorCustomUtils = {}; +export const StyleEditorCustomUtils = {}; const EDITOR_MODES = { 'css': 'geocss',