Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

02023.02.xx allow edit #1

Closed
wants to merge 8 commits into from
35 changes: 35 additions & 0 deletions docs/developer-guide/integrations/auth.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,41 @@ In this section you can see the implementation details about the login / logout

<img src="../img/standard-mapstore-login.png" class="ms-docimage" style="max-width: 400px"/>

### Configure session timeout

By default MapStore session token lives 24 hours and the refresh token last forever. On application reboot anyway all the tokens are cancelled. In order to change these default. the administrator can change these defaults by adding to `mapstore-ovr.properties` file the following properties:

```properties
# Session timeout
restSessionService.sessionTimeout=60 #in seconds
restSessionService.autorefresh=false
```

Where:

- `restSessionService.sessionTimeout` refers to session token expiration time (by default it’s 24 hours)
- `restSessionService.autorefresh` refers to flag configured to handle automatic refresh process in the backend, enabling/disabling the refresh token usage:
- when set to `false`, it avoids the use of refresh token after the session token has expired, meaning, after the timeout the user will have to reconnect
- when set to `true`, the refresh token is used and the session extends every time the session timeout is met

!!! note
`sessionTimeout` and `autorefresh` in `mapstore.properties` are valid for the default session storage. If you are using openID or keycloak, they will not be used.

Additionally, on the client side, in order to configure the interval in which is session `refresh` action is fired, one can use the `tokenRefreshInterval` property. It can be configured via `localConfig.json -> tokenRefreshInterval`, the value is in milliseconds.

```json
tokenRefreshInterval: 60000 // default 30 seconds
```

When the above configured `Session timeout` is in place, the client can exhibit two behaviors based on the `tokenRefreshInterval` configured on the client side,
Disabling the refresh token (setting `restSessionService.autorefresh` to `false`) the administrator can use `sessionTimeout` and `tokenRefreshInterval` to limit the session duration this way:

- when `tokenRefreshInterval` is **less than** `sessionTimeout` configured (e.g `tokenRefreshInterval` is 30 seconds and `sessionTimeout` is 24 hours)
- when application is in use, the client performs a refresh token call before the expiring time and session is prolonged
- when the application is closed (i.e for any reason) and reopened after `sessionTimeout` configured, the client cannot perform refresh token call within the timeout window and hence the session expires and the user is asked to reconnect
- when `tokenRefreshInterval` is **greater than** `sessionTimeout` configured
- the session expires anyway before the refresh and the client is unable to perform the refresh activity within the configured time interval. The user will have to re-authenticate. In this case the two configuration should be nearly the same value, 30 seconds of difference, for example. This helps the client to perform the refresh activity immediately after the session expires to log out the user.

## OpenID MapStore Login

<img src="../img/openid-mapstore-login.png" class="ms-docimage" style="max-width: 400px"/>
4 changes: 2 additions & 2 deletions docs/developer-guide/integrations/users/openId.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ Create/edit `mapstore-ovr.properties` file (in data-dir or class path) to config

```properties
# enables the keycloak OpenID Connect filter
keycloakOAuth2Config.enabled=false
keycloakOAuth2Config.enabled=true

# Configuration
keycloakOAuth2Config.jsonConfig=<copy-here-the-json-config-from-keycloak-removing-all-the-spaces>
Expand All @@ -144,7 +144,7 @@ keycloakOAuth2Config.autoCreateUser=true
keycloakOAuth2Config.roleMappings=admin:ADMIN,user:USER

# Comma separated list of <keycloak-role>:<geostore-group>
keycloakOAuth2Config.roleMappings=MY_KEYCLOAK_ROLE:MY_MAPSTORE_GROUP,MY_KEYCLOAK_ROLE2:MY_MAPSTORE_GROUP2
keycloakOAuth2Config.groupMappings=MY_KEYCLOAK_ROLE:MY_MAPSTORE_GROUP,MY_KEYCLOAK_ROLE2:MY_MAPSTORE_GROUP2

# Default role, when no mapping has matched
keycloakOAuth2Config.authenticatedDefaultRole=USER
Expand Down
13 changes: 6 additions & 7 deletions web/client/actions/__tests__/featuregrid-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -85,8 +85,6 @@ import {
GRID_QUERY_RESULT,
moreFeatures,
LOAD_MORE_FEATURES,
hideSyncPopover,
HIDE_SYNC_POPOVER,
toggleShowAgain,
TOGGLE_SHOW_AGAIN_FLAG,
setSyncTool,
Expand Down Expand Up @@ -153,11 +151,6 @@ describe('Test correctness of featurgrid actions', () => {
expect(retval).toExist();
expect(retval.type).toBe(CLEAR_CHANGES_CONFIRMED);
});
it('Test hideSyncPopover action creator', () => {
const retval = hideSyncPopover();
expect(retval).toExist();
expect(retval.type).toBe(HIDE_SYNC_POPOVER);
});
it('Test toggleShowAgain action creator', () => {
const retval = toggleShowAgain();
expect(retval).toExist();
Expand Down Expand Up @@ -279,6 +272,12 @@ describe('Test correctness of featurgrid actions', () => {
expect(retval).toExist();
expect(retval.type).toBe(CLOSE_FEATURE_GRID);
});
it('Test closeFeatureGrid with closer', () => {
const retval = closeFeatureGrid('closer1');
expect(retval).toExist();
expect(retval.type).toBe(CLOSE_FEATURE_GRID);
expect(retval.closer).toBe('closer1');
});
it('Test closeFeatureGridConfirm', () => {
const retval = closeFeatureGridConfirm();
expect(retval).toExist();
Expand Down
11 changes: 3 additions & 8 deletions web/client/actions/featuregrid.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,6 @@ export const ZOOM_ALL = 'FEATUREGRID:ZOOM_ALL';
export const INIT_PLUGIN = 'FEATUREGRID:INIT_PLUGIN';
export const SIZE_CHANGE = 'FEATUREGRID:SIZE_CHANGE';
export const TOGGLE_SHOW_AGAIN_FLAG = 'FEATUREGRID:TOGGLE_SHOW_AGAIN_FLAG';
export const HIDE_SYNC_POPOVER = 'FEATUREGRID:HIDE_SYNC_POPOVER';
export const UPDATE_EDITORS_OPTIONS = 'FEATUREGRID:UPDATE_EDITORS_OPTIONS';
export const LAUNCH_UPDATE_FILTER_FUNC = 'FEATUREGRID:LAUNCH_UPDATE_FILTER_FUNC';
export const SET_SYNC_TOOL = 'FEATUREGRID:SET_SYNC_TOOL';
Expand All @@ -72,11 +71,6 @@ export function toggleShowAgain() {
type: TOGGLE_SHOW_AGAIN_FLAG
};
}
export function hideSyncPopover() {
return {
type: HIDE_SYNC_POPOVER
};
}
export function featureGridQueryResult(features, pages) {
return {
type: GRID_QUERY_RESULT,
Expand Down Expand Up @@ -314,9 +308,10 @@ export function closeFeatureGridConfirm() {
type: CLOSE_FEATURE_GRID_CONFIRM
};
}
export function closeFeatureGrid() {
export function closeFeatureGrid(closer) {
return {
type: CLOSE_FEATURE_GRID
type: CLOSE_FEATURE_GRID,
closer // the closer of feature grid like: 'queryPanel'
};
}
export function openFeatureGrid() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -254,13 +254,6 @@ function VisibilityLimitsForm({
clearMessages();
}, [ dpu, resolutionString ]);

useEffect(() => {
if (isMounted.current && (!isNil(maxResolution) || !isNil(minResolution))) {
setCapabilitiesMessage(maxResolution, minResolution);
setRangeError(maxResolution, minResolution);
}
}, [isMounted]);

return (
<div className="ms-visibility-limits-form">
<div className="ms-visibility-limits-form-title" style={{ display: 'flex', alignItems: 'center' }}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,6 @@ describe('VisibilityLimitsForm', () => {
'1 : 37795',
'layerProperties.visibilityLimits.scale'
]);
const message = document.querySelector('.alert-success');
expect(message.textContent).toBe('layerProperties.visibilityLimits.serverValuesUpdate');
});
it('should render maxResolution and minResolution labels as resolution', () => {
const layer = {
Expand Down
3 changes: 2 additions & 1 deletion web/client/components/data/featuregrid/editors/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ const types = {
<Editor dataType="string" {...props}/>,
"boolean": (props) => <DropDownEditor dataType="string" {...props} value={props.value && props.value.toString()} emptyValue={null} filter={false} values={["true", "false"]}/>,
"date-time": (props) => <DateTimeEditor dataType="date-time" calendar time popupPosition="top" {...props} />,
"date": (props) => <DateTimeEditor dataType="date"time={false} calendar popupPosition="top" {...props} />,
"date": (props) => <DateTimeEditor dataType="date" time={false} calendar popupPosition="top" {...props} />,
"time": (props) => <DateTimeEditor dataType="time" calendar={false} time popupPosition="top" {...props} />
};
export default (type, props) => types[type] ? types[type](props) : types.defaultEditor(props);

84 changes: 51 additions & 33 deletions web/client/components/data/featuregrid/enhancers/editor.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree.
*/
import { isNil } from 'lodash';
import { isNil, find } from 'lodash';
import { compose, createEventHandler, defaultProps, withHandlers, withPropsOnChange } from 'recompose';

import EditorRegistry from '../../../../utils/featuregrid/EditorRegistry';
Expand Down Expand Up @@ -118,9 +118,11 @@ const featuresToGrid = compose(
),
withPropsOnChange(
["newFeatures", "changes", "focusOnEdit"],
props => ({
isFocused: props.focusOnEdit && (props.changes && Object.keys(props.changes).length > 0 || props.newFeatures && props.newFeatures.length > 0 )
})
props => {
return {
isFocused: props.focusOnEdit && (props.changes && Object.keys(props.changes).length > 0 || props.newFeatures && props.newFeatures.length > 0 )
}
}
),
withPropsOnChange(
["features", "newFeatures", "isFocused", "virtualScroll", "pagination"],
Expand All @@ -142,37 +144,53 @@ const featuresToGrid = compose(
// return empty component if no filter renderer is defined, to avoid failures
return () => null;
};
const getToolsCols = getToolColumns(props.tools, props.rowGetter, props.describeFeatureType, props.actionOpts, getFilterRendererFunc);
const colsOptions = {
editable: props.mode === "EDIT",
sortable: props.sortable && !props.isFocused,
defaultSize: props.defaultSize,
options: props.options?.propertyName
};

const result = ({
columns: getToolColumns(props.tools, props.rowGetter, props.describeFeatureType, props.actionOpts, getFilterRendererFunc)
.concat(featureTypeToGridColumns(props.describeFeatureType, props.columnSettings, props.fields, {
editable: props.mode === "EDIT",
sortable: props.sortable && !props.isFocused,
defaultSize: props.defaultSize,
options: props.options?.propertyName
}, {
getHeaderRenderer,
getEditor: (desc) => {
const generalProps = {
onTemporaryChanges: props.gridEvents && props.gridEvents.onTemporaryChanges,
autocompleteEnabled: props.autocompleteEnabled,
url: props.url,
typeName: props.typeName
};
const regexProps = {attribute: desc.name, url: props.url, typeName: props.typeName};
const rules = props.customEditorsOptions && props.customEditorsOptions.rules || [];
const editorProps = {type: desc.localType, generalProps, props};
const editor = EditorRegistry.getCustomEditor(regexProps, rules, editorProps);
const customEditorInfos = {
customEditorOptions: props.customEditorsOptions && props.customEditorsOptions.rules || [],
url: props.url,
typeName: props.typeName
};

if (!isNil(editor)) {
return editor;
}
return props.editors(desc.localType, generalProps);
},
getFilterRenderer: getFilterRendererFunc,
getFormatter: (desc) => getFormatter(desc, (props.fields ?? []).find(f => f.name === desc.name), {dateFormats: props.dateFormats})
}))
const featureTypeToCols = featureTypeToGridColumns(customEditorInfos, props.describeFeatureType, props.columnSettings, props.fields, colsOptions, {
getHeaderRenderer,
getEditor: (desc, x) => {
const generalProps = {
onTemporaryChanges: props.gridEvents && props.gridEvents.onTemporaryChanges,
autocompleteEnabled: props.autocompleteEnabled,
url: props.url,
typeName: props.typeName
};
const regexProps = {attribute: desc.name, url: props.url, typeName: props.typeName};
const rules = props.customEditorsOptions && props.customEditorsOptions.rules || [];
const editorProps = {type: desc.localType, generalProps, props};
const editor = EditorRegistry.getCustomEditor(regexProps, rules, editorProps);

if (!isNil(editor)) {
return editor;
}
return props.editors(desc.localType, generalProps);
},
getFilterRenderer: getFilterRendererFunc,
getFormatter: (desc) => getFormatter(desc, (props.fields ?? []).find(f => f.name === desc.name), { dateFormats: props.dateFormats })
});

const result = { columns: getToolsCols.concat(featureTypeToCols) };

// test gb - allow to insert event on columns
result.columns.forEach(x => x.events = {
onClick: (ev, args) => {
console.log(ev);
console.log(args);
console.log(props);
}
})
return result;
}
),
Expand All @@ -183,7 +201,7 @@ const featuresToGrid = compose(
// bind and get proper grid events from gridEvents object
let {
onRowsSelected = () => {},
onRowsDeselected = () => {},
onRowsDeselected = () => { },
onRowsToggled = () => {},
...gridEvents} = getGridEvents(props.gridEvents, props.rowGetter, props.describeFeatureType, props.actionOpts, props.columns);

Expand Down
23 changes: 16 additions & 7 deletions web/client/components/data/featuregrid/toolbars/Toolbar.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ const standardButtons = {
visible={selectedCount <= 1 && mode === "VIEW"}
onClick={events.settings}
glyph="features-grid-set"/>),
syncGridFilterToMap: ({disabled, isSyncActive = false, showSyncOnMapButton = true, events = {}, syncPopover = { showPopoverSync: true, dockSize: "32.2%" }}) => (<TButton
syncGridFilterToMap: ({disabled, isSyncActive = false, showSyncOnMapButton = true, events = {}, syncPopover = { dockSize: "32.2%" }, showPopoverSync, hideSyncPopover}) => (<TButton
id="grid-map-filter"
keyProp="grid-map-filter"
tooltipId="featuregrid.toolbar.syncOnMap"
Expand All @@ -129,7 +129,7 @@ const standardButtons = {
visible={showSyncOnMapButton}
onClick={events.sync}
glyph="map-filter"
renderPopover={syncPopover.showPopoverSync}
renderPopover={showPopoverSync}
popoverOptions={!disabled && {
placement: "top",
content: (<span>
Expand All @@ -152,7 +152,7 @@ const standardButtons = {
console.error(e);
}
}
events.hideSyncPopover();
hideSyncPopover();
}} className="close">
<Glyphicon className="pull-right" glyph="1-close"/>
</button>
Expand Down Expand Up @@ -304,12 +304,21 @@ const buttons = [
* @param {bool} showSyncOnMapButton shows / hide the show on map button (defaults to true)
* @param {bool} showTimeSyncButton shows / hide the timeSync button (defaults to false)
*/
export default (props = {}) => {
export default React.memo((props = {}) => {
const {
toolbarItems = []
toolbarItems = [],
pluginCfg = { showPopoverSync: false }
} = props;
const [showPopover, setShowPopoverSync] = React.useState(getApi().getItem("showPopoverSync") !== null && pluginCfg?.showPopoverSync ? getApi().getItem("showPopoverSync") === "true" : pluginCfg?.showPopoverSync);
React.useEffect(()=>{
if (showPopover && props.mode === 'EDIT') {
setShowPopoverSync(false);
} else if (!showPopover && props.mode !== 'EDIT') {
setShowPopoverSync(getApi().getItem("showPopoverSync") !== null && pluginCfg?.showPopoverSync ? getApi().getItem("showPopoverSync") === "true" : pluginCfg?.showPopoverSync);
}
}, [props.mode]);
return (<ButtonGroup id="featuregrid-toolbar" className="featuregrid-toolbar featuregrid-toolbar-margin">

{sortBy(buttons.concat(toolbarItems), ["position"]).map(({Component}) => <Component {...props} mode={props?.mode ?? "VIEW"} disabled={props.disableToolbar} />)}
{sortBy(buttons.concat(toolbarItems), ["position"]).map(({Component}) => <Component {...props} showPopoverSync={showPopover} hideSyncPopover={() => setShowPopoverSync(false)} mode={props?.mode ?? "VIEW"} disabled={props.disableToolbar} />)}
</ButtonGroup>);
};
});
Original file line number Diff line number Diff line change
Expand Up @@ -444,4 +444,16 @@ describe('Featuregrid toolbar component', () => {
expect(spy.calls[1].arguments[0]).toBe(true);
});
});
describe('showing/hiding PopoverSync in feature grid', () => {
it('check showPopoverSync false', () => {
ReactDOM.render(<Toolbar pluginCfg={{showPopoverSync: false}} />, document.getElementById("container"));
const el = document.getElementById("sync-popover");
expect(el).toBe(null);
});
it('check showPopoverSync true', () => {
ReactDOM.render(<Toolbar pluginCfg={{showPopoverSync: true}} />, document.getElementById("container"));
const el = document.getElementById("sync-popover");
expect(el).toExist();
});
});
});
Loading