Skip to content

Commit

Permalink
geosolutions-it#9974: Add permission validation to manage catalog ser…
Browse files Browse the repository at this point in the history
…vice (geosolutions-it#10066)

* geosolutions-it#9974: Add permission validation to manage catalog service

* unit test

* Dashboard editor catalog permission
  • Loading branch information
dsuren1 authored Mar 18, 2024
1 parent b24872b commit 6a16bd4
Show file tree
Hide file tree
Showing 24 changed files with 510 additions and 45 deletions.
10 changes: 9 additions & 1 deletion web/client/actions/__tests__/catalog-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,9 @@ import {
FORMAT_OPTIONS_LOADING,
formatsLoading,
SET_FORMAT_OPTIONS,
setSupportedFormats, addLayerAndDescribe, ADD_LAYER_AND_DESCRIBE
setSupportedFormats, addLayerAndDescribe, ADD_LAYER_AND_DESCRIBE,
INIT_PLUGIN,
initPlugin
} from '../catalog';

import { SHOW_NOTIFICATION } from '../notifications';
Expand Down Expand Up @@ -373,4 +375,10 @@ describe('Test correctness of the catalog actions', () => {
expect(action.formats).toEqual(formats);
expect(action.url).toEqual(url);
});
it('test initPlugin', () => {
const options = {editingAllowedRoles: ['test']};
const action = initPlugin(options);
expect(action.type).toBe(INIT_PLUGIN);
expect(action.options).toEqual(options);
});
});
8 changes: 8 additions & 0 deletions web/client/actions/catalog.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ export const FORMAT_OPTIONS_FETCH = 'CATALOG:FORMAT_OPTIONS_FETCH';
export const FORMAT_OPTIONS_LOADING = 'CATALOG:FORMAT_OPTIONS_LOADING';
export const SET_FORMAT_OPTIONS = 'CATALOG:SET_FORMAT_OPTIONS';
export const NEW_SERVICE_STATUS = 'CATALOG:NEW_SERVICE_STATUS';
export const INIT_PLUGIN = 'CATALOG:INIT_PLUGIN';

/**
* Adds a list of layers from the given catalogs to the map
Expand Down Expand Up @@ -303,3 +304,10 @@ export const setNewServiceStatus = (status) => {
status
};
};

export const initPlugin = (options) => {
return {
type: INIT_PLUGIN,
options
};
};
6 changes: 6 additions & 0 deletions web/client/actions/dashboard.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export const DASHBOARD_DELETE_SERVICE = "DASHBOARD:DELETE_SERVICE";
export const DASHBOARD_SAVE_SERVICE_LOADING = "DASHBOARD:SAVE_SERVICE_LOADING";
export const DASHBOARD_EXPORT = "DASHBOARD:EXPORT";
export const DASHBOARD_IMPORT = "DASHBOARD:IMPORT";
export const INIT_PLUGIN = "DASHBOARD:INIT_PLUGIN";

export const setEditing = (editing) => ({type: SET_EDITING, editing });

Expand Down Expand Up @@ -92,3 +93,8 @@ export const dashboardDeleteService = (service, services) => ({ type: DASHBOARD_
* @param {bolean} loading the loading state of the dashboard catalog saving
*/
export const setDashboardServiceSaveLoading = loading => ({ type: DASHBOARD_SAVE_SERVICE_LOADING, loading});

/**
* @param {options} options the options to be updated on plugin initialization
*/
export const initPlugin = (options) => ({ type: INIT_PLUGIN, options });
11 changes: 6 additions & 5 deletions web/client/components/catalog/Catalog.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,8 @@ class Catalog extends React.Component {
layerBaseConfig: PropTypes.object,
service: PropTypes.object,
isNewServiceAdded: PropTypes.bool,
setNewServiceStatus: PropTypes.func
setNewServiceStatus: PropTypes.func,
canEdit: PropTypes.func
};

static contextTypes = {
Expand Down Expand Up @@ -323,7 +324,7 @@ class Catalog extends React.Component {
<ControlLabel><Message msgId="catalog.service" /></ControlLabel>
</FormGroup>
<FormGroup controlId="service" key="service">
<InputGroup>
<InputGroup style={{width: '100%'}}>
<Select
clearValueText={getMessageById(this.context.messages, "catalog.clearValueText")}
noResultsText={getMessageById(this.context.messages, "catalog.noResultsText")}
Expand All @@ -332,13 +333,13 @@ class Catalog extends React.Component {
value={this.props.selectedService}
onChange={(val) => this.props.onChangeSelectedService(val && val.value ? val.value : "")}
placeholder={getMessageById(this.context.messages, "catalog.servicePlaceholder")} />
{this.isValidServiceSelected() && !this.props.services[this.props.selectedService].readOnly ? (<InputGroup.Addon className="btn"
{this.props.canEdit && this.isValidServiceSelected() && !this.props.services[this.props.selectedService].readOnly ? (<InputGroup.Addon className="btn"
onClick={() => this.props.onChangeCatalogMode("edit", false)}>
<Glyphicon glyph="pencil" />
</InputGroup.Addon>) : null}
<InputGroup.Addon className="btn" onClick={() => this.props.onChangeCatalogMode("edit", true)}>
{this.props.canEdit && <InputGroup.Addon className="btn" onClick={() => this.props.onChangeCatalogMode("edit", true)}>
<Glyphicon glyph="plus" />
</InputGroup.Addon>
</InputGroup.Addon>}
</InputGroup>
</FormGroup>
{this.props.services?.[this.props.selectedService]?.type !== '3dtiles' && <FormGroup controlId="searchText" key="searchText">
Expand Down
6 changes: 4 additions & 2 deletions web/client/components/catalog/CatalogForm.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,16 @@ const SearchInput = localizeProps("placeholder")(FormControl);

export default ({ onSearchTextChange = () => { }, onChangeSelectedService = () => {}, searchText, title = <Message msgId={"catalog.title"} />, services, showCatalogSelector,
selectedService,
onChangeCatalogMode}) =>
onChangeCatalogMode,
canEditService }) =>
( <Grid className="catalog-form" fluid><Row><Col xs={12}>
<h4 className="text-center">{title}</h4>
{showCatalogSelector
? (<FormGroup>
<CatalogServiceSelector onChangeCatalogMode={onChangeCatalogMode} services={services} onChangeSelectedService={onChangeSelectedService}
selectedService={{label: selectedService?.title ?? "", value: selectedService ?? {}}}
isValidServiceSelected={selectedService} />
isValidServiceSelected={selectedService}
canEdit={canEditService} />
</FormGroup>) : null}
<FormGroup controlId="catalog-form">
<SearchInput type="text" placeholder="catalog.textSearchPlaceholder" value={searchText} onChange={(e) => onSearchTextChange(e.currentTarget.value)}/>
Expand Down
11 changes: 6 additions & 5 deletions web/client/components/catalog/CatalogServiceSelector.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,9 @@ export default ({
selectedService,
onChangeSelectedService = () => {},
onChangeCatalogMode = () => {},
isValidServiceSelected
}) => (<InputGroup>
isValidServiceSelected,
canEdit
}) => (<InputGroup style={{ width: '100%' }}>
<Select
clearValueText={"catalog.clearValueText"}
noResultsText={"catalog.noResultsText"}
Expand All @@ -25,12 +26,12 @@ export default ({
value={selectedService}
onChange={(val) => onChangeSelectedService(val && val.value ? val.value : "")}
placeholder={"catalog.servicePlaceholder"} />
{isValidServiceSelected ? (<InputGroup.Addon className="btn"
{canEdit && isValidServiceSelected ? (<InputGroup.Addon className="btn"
onClick={() => onChangeCatalogMode("edit", false)}>
<Glyphicon glyph="pencil"/>
</InputGroup.Addon>) : null}
<InputGroup.Addon className="btn" onClick={() => onChangeCatalogMode("edit", true)}>
{canEdit && <InputGroup.Addon className="btn" onClick={() => onChangeCatalogMode("edit", true)}>
<Glyphicon glyph="plus"/>
</InputGroup.Addon>
</InputGroup.Addon>}
</InputGroup>
);
7 changes: 5 additions & 2 deletions web/client/components/catalog/CompactCatalog.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -127,15 +127,18 @@ export default compose(
onChangeSelectedService = () => {},
selectedService, onChangeCatalogMode = () => {},
getItems = (_items) => getCatalogItems(_items, selected),
onItemClick = ({record} = {}) => onRecordSelected(record, catalog)}) => {
onItemClick = ({record} = {}) => onRecordSelected(record, catalog),
canEditService
}) => {
return (<BorderLayout
className="compat-catalog"
header={<CatalogForm onChangeCatalogMode={onChangeCatalogMode} onChangeSelectedService={onChangeSelectedService}
services={Object.keys(services).map(key =>({ label: services[key]?.title, value: {...services[key], key}}))}
selectedService={services[selectedService]} showCatalogSelector={showCatalogSelector}
title={title}
searchText={searchText}
onSearchTextChange={setSearchText}/>}
onSearchTextChange={setSearchText}
canEditService={canEditService}/>}
footer={<div className="catalog-footer">
{loading ? <LoadingSpinner /> : null}
{!isNil(total) ? <span className="res-info"><Message msgId="catalog.pageInfoInfinite" msgParams={{loaded: items.length, total}}/></span> : null}
Expand Down
42 changes: 42 additions & 0 deletions web/client/components/catalog/__tests__/Catalog-test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -182,4 +182,46 @@ describe('Test Catalog panel', () => {
expect(spyOnSearch.calls[0].arguments[0]).toEqual({ format: 'csw', url: 'url', startPosition: 1, maxRecords: 4, text: '', options: {service: SERVICE} });
expect(catalogPagination.length).toBe(1); // Pagination is displayed
});
it('test manage service with permission', () => {
const SERVICE = {
type: "csw",
url: "url",
title: "csw"
};
ReactDOM.render(<Catalog
services={{ "csw": SERVICE}}
canEdit
selectedService="csw"
isNewServiceAdded={false}
result={{numberOfRecordsMatched: 4, numberOfRecordsReturned: 10}}
searchOptions={{startPosition: 1}}
/>, document.getElementById("container"));
const container = document.getElementById("container");
expect(container).toBeTruthy();
let editEl = document.querySelector('.glyphicon-pencil');
let addEl = document.querySelector('.glyphicon-plus');
expect(editEl).toBeTruthy();
expect(addEl).toBeTruthy();
});
it('test manage service with no permission', () => {
const SERVICE = {
type: "csw",
url: "url",
title: "csw"
};
ReactDOM.render(<Catalog
services={{ "csw": SERVICE}}
canEdit={false}
selectedService="csw"
isNewServiceAdded={false}
result={{numberOfRecordsMatched: 4, numberOfRecordsReturned: 10}}
searchOptions={{startPosition: 1}}
/>, document.getElementById("container"));
const container = document.getElementById("container");
expect(container).toBeTruthy();
let editEl = document.querySelector('.glyphicon-pencil');
let addEl = document.querySelector('.glyphicon-plus');
expect(editEl).toBeFalsy();
expect(addEl).toBeFalsy();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/**
* Copyright 2024, GeoSolutions Sas.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree.
*/
import React from 'react';
import ReactDOM from 'react-dom';
import expect from 'expect';
import CatalogServiceSelector from '../CatalogServiceSelector';

describe('Test CatalogServiceEditor', () => {
beforeEach((done) => {
document.body.innerHTML = '<div id="container"></div>';
setTimeout(done);
});
afterEach((done) => {
ReactDOM.unmountComponentAtNode(document.getElementById("container"));
document.body.innerHTML = '';
setTimeout(done);
});

it('creates the component with defaults', () => {
ReactDOM.render(<CatalogServiceSelector services={[]} />, document.getElementById("container"));
expect(document.getElementById('container')).toBeTruthy();
expect(document.querySelector('input')).toBeTruthy();
expect(document.querySelector('.glyphicon-pencil')).toBeFalsy();
expect(document.querySelector('.glyphicon-plus')).toBeFalsy();
});
it('test isValidServiceSelected', () => {
ReactDOM.render(<CatalogServiceSelector services={[]} canEdit />, document.getElementById("container"));
expect(document.getElementById('container')).toBeTruthy();
expect(document.querySelector('input')).toBeTruthy();
expect(document.querySelector('.glyphicon-pencil')).toBeFalsy();
expect(document.querySelector('.glyphicon-plus')).toBeTruthy();
});
it('test isValidServiceSelected & canEdit', () => {
ReactDOM.render(<CatalogServiceSelector services={[]} isValidServiceSelected canEdit />, document.getElementById("container"));
expect(document.getElementById('container')).toBeTruthy();
expect(document.querySelector('input')).toBeTruthy();
expect(document.querySelector('.glyphicon-pencil')).toBeTruthy();
expect(document.querySelector('.glyphicon-plus')).toBeTruthy();
});
});
20 changes: 14 additions & 6 deletions web/client/plugins/DashboardEditor.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,12 @@ import { createSelector } from 'reselect';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import { createPlugin } from '../utils/PluginsUtils';
import { isDashboardEditing, isDashboardLoading, isDashboardAvailable } from '../selectors/dashboard';
import { isDashboardEditing, isDashboardLoading, canEditServiceSelector } from '../selectors/dashboard';
import { dashboardSelector, dashboardsLocalizedSelector } from './widgetbuilder/commons';

import { toggleConnection } from '../actions/widgets';

import { setEditing, setEditorAvailable, triggerShowConnections } from '../actions/dashboard';
import { initPlugin, setEditing, setEditorAvailable, triggerShowConnections } from '../actions/dashboard';

import withDashboardExitButton from './widgetbuilder/enhancers/withDashboardExitButton';
import WidgetTypeBuilder from './widgetbuilder/WidgetTypeBuilder';
Expand All @@ -42,6 +42,7 @@ const Builder =
* @prop {object} cfg.catalog **Deprecated** in favor of `cfg.services`. Can contain a catalog configuration
* @prop {object} cfg.services Object with the catalogs available to select layers for maps, charts and tables. The format is the same of the `Catalog` plugin.
* @prop {string} cfg.selectedService the key of service selected by default from the list of `cfg.services`
* @prop {string} cfg.servicesPermission object with permission properties to manage catalog service. Configurations are `editingAllowedRoles` & `editingAllowedGroups`. By default `editingAllowedRoles: ["ADMIN"]`
* @prop {boolean} cfg.disableEmptyMap disable empty map entry from the available maps of map widget
*/
class DashboardEditorComponent extends React.Component {
Expand All @@ -61,7 +62,8 @@ class DashboardEditorComponent extends React.Component {
style: PropTypes.object,
pluginCfg: PropTypes.object,
catalog: PropTypes.object,
disableEmptyMap: PropTypes.bool
disableEmptyMap: PropTypes.bool,
servicesPermission: PropTypes.object
};
static defaultProps = {
id: "dashboard-editor",
Expand All @@ -74,9 +76,13 @@ class DashboardEditorComponent extends React.Component {
position: "left",
onMount: () => { },
onUnmount: () => { },
setEditing: () => { }
setEditing: () => { },
servicesPermission: {
editingAllowedRoles: ["ALL"]
}
};
componentDidMount() {
this.props.onInit({ servicesPermission: this.props.servicesPermission });
this.props.onMount();
}

Expand All @@ -94,6 +100,7 @@ class DashboardEditorComponent extends React.Component {
disableEmptyMap={this.props.disableEmptyMap}
defaultSelectedService={defaultSelectedService}
defaultServices={defaultServices}
canEditService={this.props.canEditService}
enabled={this.props.editing}
onClose={() => this.props.setEditing(false)}
catalog={this.props.catalog}
Expand All @@ -107,10 +114,11 @@ const Plugin = connect(
createSelector(
isDashboardEditing,
isDashboardLoading,
isDashboardAvailable,
(editing, isDashboardOpened) => ({ editing, isDashboardOpened })
canEditServiceSelector,
(editing, isDashboardOpened, canEditService) => ({ editing, isDashboardOpened, canEditService })
), {
setEditing,
onInit: initPlugin,
onMount: () => setEditorAvailable(true),
onUnmount: () => setEditorAvailable(false)
}
Expand Down
Loading

0 comments on commit 6a16bd4

Please sign in to comment.