From 87a7ce239f7e03f6c425050fb87b57a2c29c6d04 Mon Sep 17 00:00:00 2001 From: Suren Date: Wed, 29 Oct 2025 17:27:48 +0530 Subject: [PATCH 1/8] Implement dynamic request configurations --- .../developer-guide/integrations/geoserver.md | 29 +- docs/developer-guide/local-config.md | 100 +++- web/client/actions/security.js | 39 +- web/client/api/ArcGIS.js | 4 +- web/client/api/CSW.js | 8 +- web/client/api/TMS.js | 4 +- web/client/api/ThreeDTiles.js | 4 +- web/client/api/WFS.js | 4 +- web/client/api/WMS.js | 11 +- web/client/api/WMTS.js | 7 +- web/client/api/catalog/TMS_1_0_0.js | 5 +- .../map/cesium/plugins/ArcGISLayer.js | 3 +- .../map/cesium/plugins/GraticuleLayer.js | 3 +- .../map/cesium/plugins/MarkerLayer.js | 4 +- .../map/cesium/plugins/ModelLayer.js | 2 +- .../map/cesium/plugins/OverlayLayer.js | 2 +- .../map/cesium/plugins/TerrainLayer.js | 4 +- .../map/cesium/plugins/ThreeDTilesLayer.js | 2 +- .../map/cesium/plugins/VectorLayer.js | 2 +- .../map/cesium/plugins/WMTSLayer.js | 16 +- .../map/leaflet/plugins/ElevationLayer.js | 4 +- .../map/leaflet/plugins/VectorLayer.jsx | 4 +- .../map/leaflet/plugins/WFSLayer.jsx | 2 +- .../map/leaflet/plugins/WMSLayer.js | 18 +- .../map/leaflet/plugins/WMTSLayer.js | 10 +- .../map/openlayers/plugins/ArcGISLayer.js | 6 +- .../map/openlayers/plugins/COGLayer.js | 15 +- .../map/openlayers/plugins/ElevationLayer.js | 4 +- .../map/openlayers/plugins/TMSLayer.js | 7 +- .../openlayers/plugins/TileProviderLayer.js | 6 +- .../map/openlayers/plugins/WMSLayer.js | 5 +- .../map/openlayers/plugins/WMTSLayer.js | 14 +- web/client/components/misc/SecureImage.jsx | 53 +- web/client/configs/localConfig.json | 17 +- web/client/epics/security.js | 95 +++- web/client/epics/wfsquery.js | 4 +- web/client/libs/ajax.js | 79 ++- web/client/observables/wfs.js | 17 +- web/client/observables/wms.js | 10 +- web/client/observables/wps/execute.js | 9 +- .../components/StyleBasedWMSJsonLegend.jsx | 5 +- web/client/reducers/security.js | 34 +- web/client/selectors/catalog.js | 12 +- web/client/selectors/security.js | 3 +- web/client/utils/LayersUtils.js | 7 +- web/client/utils/SecurityUtils.js | 314 +++++++---- .../utils/__tests__/SecurityUtils-test.js | 508 +++++++++++++++++- web/client/utils/cesium/WMSUtils.js | 6 +- web/client/utils/mapinfo/wfs.js | 13 +- web/client/utils/mapinfo/wms.js | 5 +- web/client/utils/mapinfo/wmts.js | 5 +- 51 files changed, 1148 insertions(+), 396 deletions(-) diff --git a/docs/developer-guide/integrations/geoserver.md b/docs/developer-guide/integrations/geoserver.md index 68cf8a9215a..2e9cfb5f053 100644 --- a/docs/developer-guide/integrations/geoserver.md +++ b/docs/developer-guide/integrations/geoserver.md @@ -158,25 +158,30 @@ The last step is to configure MapStore to use the authkey with the configured in ```javascript //... -"useAuthenticationRules": true, - "authenticationRules": [{ +"requestsConfigurationRules": [ + { "urlPattern": ".*geostore.*", - "method": "bearer" - }, { + "headers": { + "Authorization": "Bearer ${securityToken}" + } + }, + { "urlPattern": "\\/geoserver/.*", - "authkeyParamName": "authkey", - "method": "authkey" - }], + "params": { + "authkey": "${securityToken}" + } + } +], //... ``` -- Verify that "useAuthenticationRules" is set to `true` -- `authenticationRules` array should contain 2 rules: - - The first rule should already be present, and defines the authentication method used internally in mapstore +- Note: The new `requestsConfigurationRules` system is always active when rules are present, no flag needed +- `requestsConfigurationRules` array should contain 2 rules: + - The first rule should already be present, and defines the authentication method used internally in mapstore (Bearer token) - The second rule (the one you need to add) should be added and defines how to authenticate to GeoServer: - `urlPattern`: is a regular expression that identifies the request url where to apply the rule - - `method`: set it to `authkey` to use the authentication filter you just created in Geoserver. - - `authkeyParamName`: is the name of the authkey parameter defined in GeoServer (set to `authkey` by default) + - `params`: use query parameters for authkey authentication + - `authkey`: the name of the parameter (must match the one in GeoServer configuration, default is `authkey`) ### Advantages of user integration diff --git a/docs/developer-guide/local-config.md b/docs/developer-guide/local-config.md index 46a7f4a7df3..8a95038d673 100644 --- a/docs/developer-guide/local-config.md +++ b/docs/developer-guide/local-config.md @@ -44,17 +44,30 @@ This is the main structure: // path to the translation files directory (if different from default) "translationsPath", // if true, every ajax and mapping request will be authenticated with the configurations if match a rule (default: true) - "useAuthenticationRules": true - // the authentication rules to match - "authenticationRules": [ - { // every rule has a `urlPattern` regex to match - "urlPattern": ".*geostore.*", - // and a authentication `method` to use (basic, authkey, browserWithCredentials, header) - "method": "basic" - }, { - "urlPattern": "\\/geoserver.*", - "method": "authkey" - }], + // the request configuration rules to match + "requestsConfigurationRules": [ + { // every rule has a `urlPattern` regex to match + "urlPattern": ".*geostore.*", + // headers to add to matching requests + "headers": { + "Authorization": "Bearer ${securityToken}" + } + }, { + "urlPattern": "\\/geoserver/.*", + // parameters to add to matching requests + "params": { + "authkey": "${securityToken}" + } + }, { + "urlPattern": ".*azure-blob.*", + // expiration timestamp (optional, Unix timestamp in seconds) + "expires": 1735689600, + // parameters can be used for SAS tokens + "params": { + "sv": "2024-11-04", + "sig": "${sasToken}" + } + }], // flag for postponing mapstore 2 load time after theme "loadAfterTheme": false, // if defined, WMS layer styles localization will be added @@ -149,23 +162,60 @@ For configuring plugins, see the [Configuring Plugins Section](plugins-documenta - `initialState`: is an object that will initialize the state with some default values and this WILL OVERRIDE the initialState imposed by plugins & reducers. - `projectionDefs`: is an array of objects that contain definitions for Coordinate Reference Systems - `gridFiles`: is an object that contains definitions for grid files used in coordinate transformations -- `useAuthenticationRules`: if this flag is set to true, the `authenticationRules` will be used to authenticate every ajax and mapping request. If the flag is set to false, the `authenticationRules` will be ignored. -- `authenticationRules`: is an array of objects that contain rules to match for authentication. Each rule has a `urlPattern` regex to match and a `method` to use (`basic`, `authkey`, `header`, `browserWithCredentials`). If the URL of a request matches the `urlPattern` of a rule, the `method` will be used to authenticate the request. The `method` can be: - - `basic` will use the basic authentication method getting the credentials from the user that logged in (adding the header `Authorization` `Basic ` to the request). ***Note**: this method is not implemented for image tile requests (e.g. layers) but only for ajax requests.* - - `authkey` will use the authkey method getting the credentials from the user that logged in. The token of the current MapStore session will be used as the authkey value, so this works only with the geoserver integration. - - `bearer` will use the header `Authorization` `Bearer ` getting the credentials from the user that logged in. The token of the current MapStore session will be used as the bearer value, so this works only with the geoserver integration. - - `header` will use the header method getting the credentials from the user that logged in. You can add an `headers` object containing the static headers to this rule to specify witch headers to use. e.g. - - `browserWithCredentials` will add the `withCredentials` parameter to ajax requests, so the browser will send the cookies and the authentication headers to the server. This method is useful when you have a proxy that needs to authenticate the user. ***Note**: this method is not implemented for image tile requests (e.g. layers) but only for ajax requests.* - - ```json +- `useAuthenticationRules` (deprecated): if this flag is set to true, legacy `authenticationRules` will be used. The new `requestsConfigurationRules` system does not require this flag and is always active when rules are present. +- `requestsConfigurationRules`: is an array of objects that contain rules to match for request configuration. Each rule has a `urlPattern` regex to match and either `headers`, `params`, or `withCredentials` configuration. If the URL of a request matches the `urlPattern` of a rule, the configuration will be applied to the request. + + **Available variables for template substitution (ES6 template syntax `${variable}`):** + - `${securityToken}` - The current MapStore session token (automatically replaced) + - `${authHeader}` - The basic authentication header (automatically replaced) + + **Configuration options:** + - `headers` - Object containing HTTP headers to add to matching requests. Example: + + ```json { - "urlPattern": ".*geostore.*", - "method": "header", - "headers": { - "X-Auth-Token": "mytoken" - } + "urlPattern": ".*geostore.*", + "headers": { + "Authorization": "Bearer ${securityToken}" + } } ``` + + - `params` - Object containing query parameters to add to matching requests. Example: + + ```json + { + "urlPattern": "\\/geoserver/.*", + "params": { + "authkey": "${securityToken}" + } + } + ``` + + - `withCredentials` - Boolean to enable sending credentials with requests (useful with proxies): + + ```json + { + "urlPattern": ".*internal-api.*", + "withCredentials": true + } + ``` + + - `expires` - Optional Unix timestamp (in seconds) for automatic rule expiration. Example: + + ```json + { + "urlPattern": ".*azure-blob.*", + "expires": 1735689600, + "params": { + "sv": "2024-11-04", + "sig": "token" + } + } + ``` + +!!! note "Backward Compatibility" + The old `useAuthenticationRules` and `authenticationRules` configuration still works and will be automatically converted to the new format. However, the new format is recommended for better flexibility and features like expiration support. ### initialState configuration diff --git a/web/client/actions/security.js b/web/client/actions/security.js index 25522cef465..1c696350d70 100644 --- a/web/client/actions/security.js +++ b/web/client/actions/security.js @@ -14,7 +14,6 @@ import AuthenticationAPI from '../api/GeoStoreDAO'; import {setCredentials, getToken, getRefreshToken} from '../utils/SecurityUtils'; import {encodeUTF8} from '../utils/EncodeUtils'; - export const CHECK_LOGGED_USER = 'CHECK_LOGGED_USER'; export const LOGIN_SUBMIT = 'LOGIN_SUBMIT'; export const LOGIN_PROMPT_CLOSED = "LOGIN:LOGIN_PROMPT_CLOSED"; @@ -34,6 +33,11 @@ export const SET_CREDENTIALS = 'SECURITY:SET_CREDENTIALS'; export const CLEAR_SECURITY = 'SECURITY:CLEAR_SECURITY'; export const SET_PROTECTED_SERVICES = 'SECURITY:SET_PROTECTED_SERVICES'; export const REFRESH_SECURITY_LAYERS = 'SECURITY:REFRESH_SECURITY_LAYERS'; + +export const UPDATE_REQUESTS_RULES = 'SECURITY:UPDATE_REQUESTS_RULES'; +export const LOAD_REQUESTS_RULES = 'SECURITY:LOAD_REQUESTS_RULES'; +export const LOAD_REQUESTS_RULES_ERROR = 'SECURITY:LOAD_REQUESTS_RULES_ERROR'; + export function loginSuccess(userDetails, username, password, authProvider) { return { type: LOGIN_SUCCESS, @@ -229,3 +233,36 @@ export function refreshSecurityLayers() { type: REFRESH_SECURITY_LAYERS }; } + +/** + * Updates the request configuration rules + * @param {Array} rules - Array of request configuration rules + * @param {boolean} enabled - Whether request configuration is enabled + */ +export const updateRequestsRules = (rules) => { + return { + type: UPDATE_REQUESTS_RULES, + rules + }; +}; + +/** + * Starts loading request configuration rules + */ +export const loadRequestsRules = (rules) => { + return { + type: LOAD_REQUESTS_RULES, + rules + }; +}; + +/** + * Error loading request configuration rules + * @param {Error} error - The error that occurred + */ +export const loadRequestsRulesError = (error) => { + return { + type: LOAD_REQUESTS_RULES_ERROR, + error + }; +}; diff --git a/web/client/api/ArcGIS.js b/web/client/api/ArcGIS.js index 5f0a5b042a4..ff3e994863a 100644 --- a/web/client/api/ArcGIS.js +++ b/web/client/api/ArcGIS.js @@ -6,7 +6,6 @@ * LICENSE file in the root directory of this source tree. */ -import { getAuthorizationBasic } from '../utils/SecurityUtils'; import axios from '../libs/ajax'; import { reprojectBbox } from '../utils/CoordinatesUtils'; import trimEnd from 'lodash/trimEnd'; @@ -89,14 +88,13 @@ export const searchAndPaginate = (records, params) => { }; const getData = (url, params = {}) => { const protectedId = params?.info?.options?.service?.protectedId; - let headers = getAuthorizationBasic(protectedId); const request = _cache[url] ? () => Promise.resolve(_cache[url]) : () => axios.get(url, { params: { f: 'json' }, - headers + _msAuthSourceId: protectedId }).then(({ data }) => { _cache[url] = data; return data; diff --git a/web/client/api/CSW.js b/web/client/api/CSW.js index 9613fe29cf2..bc4b3859a4d 100644 --- a/web/client/api/CSW.js +++ b/web/client/api/CSW.js @@ -16,7 +16,6 @@ import { extractCrsFromURN, makeBboxFromOWS, makeNumericEPSG, getExtentFromNorma import WMS from "../api/WMS"; import { THREE_D_TILES, getCapabilities } from './ThreeDTiles'; import { getDefaultUrl } from '../utils/URLUtils'; -import { getAuthorizationBasic } from '../utils/SecurityUtils'; export const parseUrl = (url) => { const parsed = urlUtil.parse(getDefaultUrl(url), true); @@ -513,12 +512,11 @@ const Api = { getRecords: function(url, startPosition, maxRecords, text, options) { const body = constructXMLBody(startPosition, maxRecords, text, options); const protectedId = options?.options?.service?.protectedId; - let headers = getAuthorizationBasic(protectedId); return axios.post(parseUrl(url), body, { headers: { - 'Content-Type': 'application/xml', - ...headers - } + 'Content-Type': 'application/xml' + }, + _msAuthSourceId: protectedId }).then((response) => { const { error, _dcRef, result } = parseCSWResponse(response) || {}; if (result) { diff --git a/web/client/api/TMS.js b/web/client/api/TMS.js index f80c5948038..9d71c4ee9c3 100644 --- a/web/client/api/TMS.js +++ b/web/client/api/TMS.js @@ -7,7 +7,6 @@ */ import xml2js from 'xml2js'; import axios from '../libs/ajax'; -import { getAuthorizationBasic } from '../utils/SecurityUtils'; /** * Common requests to TMS services. @@ -21,8 +20,7 @@ import { getAuthorizationBasic } from '../utils/SecurityUtils'; */ export const getTileMap = (url, options) => { const protectedId = options?.service?.protectedId; - let headers = getAuthorizationBasic(protectedId); - return axios.get(url, {headers}) + return axios.get(url, {_msAuthSourceId: protectedId}) .then(response => { return new Promise((resolve) => { xml2js.parseString(response.data, { explicitArray: false }, (ignore, result) => resolve(result)); diff --git a/web/client/api/ThreeDTiles.js b/web/client/api/ThreeDTiles.js index a0b5ffc1830..467d5f26169 100644 --- a/web/client/api/ThreeDTiles.js +++ b/web/client/api/ThreeDTiles.js @@ -10,7 +10,6 @@ import axios from '../libs/ajax'; import { convertRadianToDegrees } from '../utils/CoordinatesUtils'; import { METERS_PER_UNIT } from '../utils/MapUtils'; import { logError } from '../utils/DebugUtils'; -import { getAuthorizationBasic } from '../utils/SecurityUtils'; // converts the boundingVolume of the root tileset to a valid layer bbox function tilesetToBoundingBox(Cesium, tileset) { @@ -140,8 +139,7 @@ function extractCapabilities(tileset) { */ export const getCapabilities = (url, info) => { const protectedId = info?.options?.service?.protectedId; - let headers = getAuthorizationBasic(protectedId); - return axios.get(url, {headers}) + return axios.get(url, {_msAuthSourceId: protectedId}) .then(({ data }) => { return extractCapabilities(data).then((properties) => ({ tileset: data, ...properties })); }).catch((e) => { diff --git a/web/client/api/WFS.js b/web/client/api/WFS.js index 250d2a1aebc..6370be114cf 100644 --- a/web/client/api/WFS.js +++ b/web/client/api/WFS.js @@ -14,7 +14,6 @@ import {toOGCFilterParts} from '../utils/FilterUtils'; import { getDefaultUrl } from '../utils/URLUtils'; import { castArray } from 'lodash'; import { isValidGetFeatureInfoFormat } from '../utils/WMSUtils'; -import { getAuthorizationBasic } from '../utils/SecurityUtils'; const capabilitiesCache = {}; @@ -140,8 +139,7 @@ export const getCapabilities = function(url, info) { return Promise.resolve(cached.data); } const protectedId = info?.options?.service?.protectedId; - let headers = getAuthorizationBasic(protectedId); - return axios.get(getCapabilitiesURL(url, {headers})) + return axios.get(getCapabilitiesURL(url), {_msAuthSourceId: protectedId}) .then((response) => { let json; xml2js.parseString(response.data, { explicitArray: false, stripPrefix: true }, (ignore, result) => { diff --git a/web/client/api/WMS.js b/web/client/api/WMS.js index b6b9def38a1..faf4998ac89 100644 --- a/web/client/api/WMS.js +++ b/web/client/api/WMS.js @@ -13,7 +13,6 @@ import axios from '../libs/ajax'; import { getConfigProp } from '../utils/ConfigUtils'; import { getWMSBoundingBox } from '../utils/CoordinatesUtils'; import { isValidGetMapFormat, isValidGetFeatureInfoFormat } from '../utils/WMSUtils'; -import { getAuthorizationBasic } from '../utils/SecurityUtils'; const capabilitiesCache = {}; export const WMS_GET_CAPABILITIES_VERSION = '1.3.0'; @@ -160,12 +159,12 @@ export const getDimensions = (layer) => { * - `Capability`: capability object that contains layers and requests formats * - `Service`: service information object */ -export const getCapabilities = (url, headers = {}) => { +export const getCapabilities = (url, {headers, params, _msAuthSourceId} = {}) => { return axios.get(parseUrl(url, { service: "WMS", version: WMS_GET_CAPABILITIES_VERSION, request: "GetCapabilities" - }), {headers}).then((response) => { + }), {headers, params, _msAuthSourceId}).then((response) => { let json; xml2js.parseString(response.data, {explicitArray: false}, (ignore, result) => { json = result; @@ -204,8 +203,7 @@ export const getRecords = (url, startPosition, maxRecords, text, options) => { }); } const protectedId = options?.options?.service?.protectedId; - let headers = getAuthorizationBasic(protectedId); - return getCapabilities(url, headers) + return getCapabilities(url, {_msAuthSourceId: protectedId}) .then((json) => { capabilitiesCache[url] = { timestamp: new Date().getTime(), @@ -215,13 +213,12 @@ export const getRecords = (url, startPosition, maxRecords, text, options) => { }); }; export const describeLayers = (url, layers, security) => { - const headers = getAuthorizationBasic(security?.sourceId); return axios.get(parseUrl(url, { service: "WMS", version: WMS_DESCRIBE_LAYER_VERSION, layers: layers, request: "DescribeLayer" - }), {headers}).then((response) => { + }), {_msAuthSourceId: security?.sourceId}).then((response) => { let descriptions; xml2js.parseString(response.data, {explicitArray: false}, (ignore, result) => { descriptions = result && result.WMS_DescribeLayerResponse && result.WMS_DescribeLayerResponse.LayerDescription; diff --git a/web/client/api/WMTS.js b/web/client/api/WMTS.js index a59c3d74f05..ce05b95ccd1 100644 --- a/web/client/api/WMTS.js +++ b/web/client/api/WMTS.js @@ -24,7 +24,6 @@ import { getDefaultStyleIdentifier, getDefaultFormat } from '../utils/WMTSUtils'; -import { getAuthorizationBasic } from '../utils/SecurityUtils'; export const parseUrl = (url) => { const parsed = urlUtil.parse(getDefaultUrl(url), true); @@ -82,8 +81,7 @@ const Api = { }); } const protectedId = options?.options?.service?.protectedId; - let headers = getAuthorizationBasic(protectedId); - return axios.get(parseUrl(url), {headers}).then((response) => { + return axios.get(parseUrl(url), {_msAuthSourceId: protectedId}).then((response) => { let json; xml2js.parseString(response.data, {explicitArray: false}, (ignore, result) => { json = result; @@ -106,8 +104,7 @@ const Api = { }); } const protectedId = options?.options?.service?.protectedId; - let headers = getAuthorizationBasic(protectedId); - return axios.get(parseUrl(url), {headers}).then((response) => { + return axios.get(parseUrl(url), {_msAuthSourceId: protectedId}).then((response) => { let json; xml2js.parseString(response.data, {explicitArray: false}, (ignore, result) => { json = result; diff --git a/web/client/api/catalog/TMS_1_0_0.js b/web/client/api/catalog/TMS_1_0_0.js index adb57e95ad2..e16fecf0d40 100644 --- a/web/client/api/catalog/TMS_1_0_0.js +++ b/web/client/api/catalog/TMS_1_0_0.js @@ -9,7 +9,7 @@ import ConfigUtils from '../../utils/ConfigUtils'; import xml2js from 'xml2js'; import axios from '../../libs/ajax'; import { get, castArray } from 'lodash'; -import { cleanAuthParamsFromURL, getAuthorizationBasic } from '../../utils/SecurityUtils'; +import { cleanAuthParamsFromURL } from '../../utils/SecurityUtils'; import { guessFormat } from '../../utils/TMSUtils'; const capabilitiesCache = {}; @@ -55,8 +55,7 @@ export const getRecords = (url, startPosition, maxRecords, text, info) => { }); } const protectedId = info?.options?.service?.protectedId; - let headers = getAuthorizationBasic(protectedId); - return axios.get(url, {headers} ).then((response) => { + return axios.get(url, {_msAuthSourceId: protectedId}).then((response) => { let json; xml2js.parseString(response.data, { explicitArray: false }, (ignore, result) => { json = { ...result, url }; diff --git a/web/client/components/map/cesium/plugins/ArcGISLayer.js b/web/client/components/map/cesium/plugins/ArcGISLayer.js index ef1d4f8e8c7..5d4b78f3629 100644 --- a/web/client/components/map/cesium/plugins/ArcGISLayer.js +++ b/web/client/components/map/cesium/plugins/ArcGISLayer.js @@ -10,6 +10,7 @@ import Layers from '../../../../utils/cesium/Layers'; import * as Cesium from 'cesium'; import { isImageServerUrl } from '../../../../utils/ArcGISUtils'; import { getProxiedUrl } from '../../../../utils/ConfigUtils'; +import isEqual from 'lodash/isEqual'; // this override is needed to apply the selected format @@ -85,7 +86,7 @@ const create = (options) => { }; const update = (layer, newOptions, oldOptions) => { - if (newOptions.forceProxy !== oldOptions.forceProxy) { + if (newOptions.forceProxy !== oldOptions.forceProxy || !isEqual(oldOptions.security, newOptions.security)) { return create(newOptions); } return null; diff --git a/web/client/components/map/cesium/plugins/GraticuleLayer.js b/web/client/components/map/cesium/plugins/GraticuleLayer.js index b38a3025bcc..f05ab08b391 100644 --- a/web/client/components/map/cesium/plugins/GraticuleLayer.js +++ b/web/client/components/map/cesium/plugins/GraticuleLayer.js @@ -8,6 +8,7 @@ import Layers from '../../../../utils/cesium/Layers'; import * as Cesium from 'cesium'; +import isEqual from 'lodash/isEqual'; /** * Created by thomas on 27/01/14. @@ -341,7 +342,7 @@ const createLayer = (options, map) => { Layers.registerType('graticule', { create: createLayer, update: (layer, newOptions, oldOptions, map) => { - if (newOptions.visibility !== oldOptions.visibility) { + if (newOptions.visibility !== oldOptions.visibility || !isEqual(oldOptions.security, newOptions.security)) { layer.setVisible(false); // clear all previous labels and primitive if (newOptions.visibility) { return createLayer(newOptions, map); diff --git a/web/client/components/map/cesium/plugins/MarkerLayer.js b/web/client/components/map/cesium/plugins/MarkerLayer.js index 1847d39152d..831e36a80c6 100644 --- a/web/client/components/map/cesium/plugins/MarkerLayer.js +++ b/web/client/components/map/cesium/plugins/MarkerLayer.js @@ -9,7 +9,7 @@ import Layers from '../../../../utils/cesium/Layers'; import * as Cesium from 'cesium'; -import { isEqual } from 'lodash'; +import isEqual from 'lodash/isEqual'; /** * @deprecated @@ -45,7 +45,7 @@ Layers.registerType('marker', { }; }, update: function(layer, newOptions, oldOptions, map) { - if (!isEqual(newOptions.point, oldOptions.point)) { + if (!isEqual(newOptions.point, oldOptions.point) || !isEqual(oldOptions.security, newOptions.security)) { return this.create(newOptions, map); } return null; diff --git a/web/client/components/map/cesium/plugins/ModelLayer.js b/web/client/components/map/cesium/plugins/ModelLayer.js index d9d578b2518..cd0ee004509 100644 --- a/web/client/components/map/cesium/plugins/ModelLayer.js +++ b/web/client/components/map/cesium/plugins/ModelLayer.js @@ -194,7 +194,7 @@ Layers.registerType('model', { if (primitives && !isEqual(newOptions?.features?.[0], oldOptions?.features?.[0])) { updatePrimitivesMatrix(primitives, newOptions?.features?.[0]); } - if (newOptions?.forceProxy !== oldOptions?.forceProxy) { + if (newOptions?.forceProxy !== oldOptions?.forceProxy || !isEqual(oldOptions.security, newOptions.security)) { return createLayer(newOptions, map); } return null; diff --git a/web/client/components/map/cesium/plugins/OverlayLayer.js b/web/client/components/map/cesium/plugins/OverlayLayer.js index 5fc793b88d9..bc2410a3e6d 100644 --- a/web/client/components/map/cesium/plugins/OverlayLayer.js +++ b/web/client/components/map/cesium/plugins/OverlayLayer.js @@ -198,7 +198,7 @@ Layers.registerType('overlay', { }; }, update: function(layer, newOptions, oldOptions, map) { - if (!isEqual(newOptions.position, oldOptions.position)) { + if (!isEqual(newOptions.position, oldOptions.position) || !isEqual(oldOptions.security, newOptions.security)) { return this.create(newOptions, map); } return null; diff --git a/web/client/components/map/cesium/plugins/TerrainLayer.js b/web/client/components/map/cesium/plugins/TerrainLayer.js index b70d9928355..a62e9dd9e4f 100644 --- a/web/client/components/map/cesium/plugins/TerrainLayer.js +++ b/web/client/components/map/cesium/plugins/TerrainLayer.js @@ -11,6 +11,7 @@ import * as Cesium from 'cesium'; import GeoServerBILTerrainProvider from '../../../../utils/cesium/GeoServerBILTerrainProvider'; import WMSUtils from '../../../../utils/cesium/WMSUtils'; import { getProxyUrl } from "../../../../utils/ProxyUtils"; +import isEqual from 'lodash/isEqual'; function cesiumOptionsMapping(config) { return { @@ -115,7 +116,8 @@ const updateLayer = (layer, newOptions, oldOptions, map) => { || newOptions?.options?.crs !== oldOptions?.options?.crs || newOptions?.version !== oldOptions?.version || newOptions?.name !== oldOptions?.name - || oldOptions.forceProxy !== newOptions.forceProxy) { + || oldOptions.forceProxy !== newOptions.forceProxy + || !isEqual(oldOptions.security, newOptions.security)) { return createLayer(newOptions, map); } return null; diff --git a/web/client/components/map/cesium/plugins/ThreeDTilesLayer.js b/web/client/components/map/cesium/plugins/ThreeDTilesLayer.js index 86b25787828..f727d5d919f 100644 --- a/web/client/components/map/cesium/plugins/ThreeDTilesLayer.js +++ b/web/client/components/map/cesium/plugins/ThreeDTilesLayer.js @@ -194,7 +194,7 @@ const createLayer = (options, map) => { Layers.registerType('3dtiles', { create: createLayer, update: function(layer, newOptions, oldOptions, map) { - if (newOptions.forceProxy !== oldOptions.forceProxy) { + if (newOptions.forceProxy !== oldOptions.forceProxy || !isEqual(oldOptions.security, newOptions.security)) { return createLayer(newOptions, map); } if ( diff --git a/web/client/components/map/cesium/plugins/VectorLayer.js b/web/client/components/map/cesium/plugins/VectorLayer.js index 86c2aba45f0..a2021714a76 100644 --- a/web/client/components/map/cesium/plugins/VectorLayer.js +++ b/web/client/components/map/cesium/plugins/VectorLayer.js @@ -60,7 +60,7 @@ const createLayer = (options, map) => { Layers.registerType('vector', { create: createLayer, update: (layer, newOptions, oldOptions, map) => { - if (!isEqual(newOptions.features, oldOptions.features)) { + if (!isEqual(newOptions.features, oldOptions.features) || !isEqual(oldOptions.security, newOptions.security)) { return createLayer(newOptions, map); } if (layer?.styledFeatures && !isEqual(newOptions?.layerFilter, oldOptions?.layerFilter)) { diff --git a/web/client/components/map/cesium/plugins/WMTSLayer.js b/web/client/components/map/cesium/plugins/WMTSLayer.js index 01dbcc4060d..c69da8ff7d9 100644 --- a/web/client/components/map/cesium/plugins/WMTSLayer.js +++ b/web/client/components/map/cesium/plugins/WMTSLayer.js @@ -18,7 +18,7 @@ import { isEqual, isObject, isArray, slice, get, head} from 'lodash'; import urlParser from 'url'; import { isVectorFormat } from '../../../../utils/VectorTileUtils'; -import { getCredentials } from '../../../../utils/SecurityUtils'; +import { getRequestConfigurationByUrl } from '../../../../utils/SecurityUtils'; function splitUrl(originalUrl) { let url = originalUrl; @@ -118,13 +118,13 @@ function wmtsToCesiumOptions(_options) { const credit = cr ? new Cesium.Credit(creditsToAttribution(cr)) : ''; let headersOpts; - if (options.security) { - const storedProtectedService = getCredentials(options.security?.sourceId) || {}; - headersOpts = { - headers: { - "Authorization": `Basic ${btoa(storedProtectedService.username + ":" + storedProtectedService.password)}` - } - }; + if (options.security && options.url) { + const urlToCheck = isArray(options.url) ? options.url[0] : options.url; + const requestConfig = getRequestConfigurationByUrl(urlToCheck, null, options.security?.sourceId); + + if (requestConfig.headers) { + headersOpts = { headers: requestConfig.headers }; + } } return Object.assign({ // TODO: multi-domain support, if use {s} switches to RESTFul mode diff --git a/web/client/components/map/leaflet/plugins/ElevationLayer.js b/web/client/components/map/leaflet/plugins/ElevationLayer.js index ff5d6dfd3b2..d6d1772c482 100644 --- a/web/client/components/map/leaflet/plugins/ElevationLayer.js +++ b/web/client/components/map/leaflet/plugins/ElevationLayer.js @@ -73,8 +73,8 @@ L.tileLayer.elevationWMS = function(urls, options, nodata, littleEndian, id) { const createWMSElevationLayer = (options) => { const urls = getWMSURLs(isArray(options.url) ? options.url : [options.url]); - const queryParameters = removeNulls(wmsToLeafletOptions(options) || {}); - urls.forEach(url => addAuthenticationParameter(url, queryParameters, options.securityToken)); + let queryParameters = removeNulls(wmsToLeafletOptions(options) || {}); + queryParameters = addAuthenticationParameter(urls[0] || '', queryParameters, options.securityToken, options.security?.sourceId); const layer = L.tileLayer.elevationWMS( urls, { diff --git a/web/client/components/map/leaflet/plugins/VectorLayer.jsx b/web/client/components/map/leaflet/plugins/VectorLayer.jsx index 0ab86451add..4ecdfc640f5 100644 --- a/web/client/components/map/leaflet/plugins/VectorLayer.jsx +++ b/web/client/components/map/leaflet/plugins/VectorLayer.jsx @@ -84,7 +84,7 @@ const updateLayerLegacy = (layer, newOptions, oldOptions) => { if (newOptions.opacity !== oldOptions.opacity) { layer.opacity = newOptions.opacity; } - if (!isEqual(newOptions.style, oldOptions.style)) { + if (!isEqual(newOptions.style, oldOptions.style) || !isEqual(oldOptions.security, newOptions.security)) { return isNewStyle(newOptions) ? createLayer(newOptions) : createLayerLegacy(newOptions); @@ -93,7 +93,7 @@ const updateLayerLegacy = (layer, newOptions, oldOptions) => { }; const updateLayer = (layer, newOptions, oldOptions) => { - if (!isEqual(oldOptions.layerFilter, newOptions.layerFilter)) { + if (!isEqual(oldOptions.layerFilter, newOptions.layerFilter) || !isEqual(oldOptions.security, newOptions.security)) { layer.remove(); return createLayer(newOptions); } diff --git a/web/client/components/map/leaflet/plugins/WFSLayer.jsx b/web/client/components/map/leaflet/plugins/WFSLayer.jsx index cb013240610..15d4491e157 100644 --- a/web/client/components/map/leaflet/plugins/WFSLayer.jsx +++ b/web/client/components/map/leaflet/plugins/WFSLayer.jsx @@ -88,7 +88,7 @@ Layers.registerType('wfs', { return layer; }, update: (layer, newOptions, oldOptions) => { - if (needsReload(oldOptions, newOptions)) { + if (needsReload(oldOptions, newOptions) || !isEqual(oldOptions.security, newOptions.security)) { loadFeatures(layer, newOptions); } if (!isEqual(newOptions.style, oldOptions.style) diff --git a/web/client/components/map/leaflet/plugins/WMSLayer.js b/web/client/components/map/leaflet/plugins/WMSLayer.js index 1cb724eccfc..a4b2a39f760 100644 --- a/web/client/components/map/leaflet/plugins/WMSLayer.js +++ b/web/client/components/map/leaflet/plugins/WMSLayer.js @@ -10,7 +10,8 @@ import Layers from '../../../../utils/leaflet/Layers'; import { filterWMSParamOptions, getWMSURLs, wmsToLeafletOptions, removeNulls } from '../../../../utils/leaflet/WMSUtils'; import L from 'leaflet'; -import { isArray } from 'lodash'; +import isEqual from 'lodash/isEqual'; +import isArray from 'lodash/isArray'; import {addAuthenticationToSLD, addAuthenticationParameter} from '../../../../utils/SecurityUtils'; import 'leaflet.nontiledlayer'; @@ -56,19 +57,24 @@ Layers.registerType('wms', { }, map, mapId); } const urls = getWMSURLs(isArray(options.url) ? options.url : [options.url]); - const queryParameters = removeNulls(wmsToLeafletOptions(options) || {}); - urls.forEach(url => addAuthenticationParameter(url, queryParameters, options.securityToken)); + let queryParameters = removeNulls(wmsToLeafletOptions(options) || {}); + queryParameters = addAuthenticationParameter(urls[0] || '', queryParameters, options.securityToken, options.security?.sourceId); if (options.singleTile) { return L.nonTiledLayer.wmsCustom(urls[0], queryParameters); } return L.tileLayer.multipleUrlWMS(urls, queryParameters); }, update: function(layer, newOptions, oldOptions) { - if (oldOptions.singleTile !== newOptions.singleTile || oldOptions.tileSize !== newOptions.tileSize || oldOptions.securityToken !== newOptions.securityToken && newOptions.visibility) { + if ( + (oldOptions.singleTile !== newOptions.singleTile + || oldOptions.tileSize !== newOptions.tileSize + || oldOptions.securityToken !== newOptions.securityToken + || !isEqual(oldOptions.security, newOptions.security)) + && newOptions.visibility) { let newLayer; const urls = getWMSURLs(isArray(newOptions.url) ? newOptions.url : [newOptions.url]); - const queryParameters = wmsToLeafletOptions(newOptions) || {}; - urls.forEach(url => addAuthenticationParameter(url, queryParameters, newOptions.securityToken)); + let queryParameters = wmsToLeafletOptions(newOptions) || {}; + queryParameters = addAuthenticationParameter(urls[0] || '', queryParameters, newOptions.securityToken, newOptions.security?.sourceId); if (newOptions.singleTile) { // return the nonTiledLayer newLayer = L.nonTiledLayer.wmsCustom(urls[0], queryParameters); diff --git a/web/client/components/map/leaflet/plugins/WMTSLayer.js b/web/client/components/map/leaflet/plugins/WMTSLayer.js index 9d59d7f0a32..f3250cedc3b 100644 --- a/web/client/components/map/leaflet/plugins/WMTSLayer.js +++ b/web/client/components/map/leaflet/plugins/WMTSLayer.js @@ -13,7 +13,8 @@ import {addAuthenticationParameter} from '../../../../utils/SecurityUtils'; import { creditsToAttribution } from '../../../../utils/LayersUtils'; import * as WMTSUtils from '../../../../utils/WMTSUtils'; import WMTS from '../../../../utils/leaflet/WMTS'; -import { isArray } from 'lodash'; +import isArray from 'lodash/isArray'; +import isEqual from 'lodash/isEqual'; import { isVectorFormat } from '../../../../utils/VectorTileUtils'; L.tileLayer.wmts = function(urls, options, matrixOptions) { @@ -47,8 +48,8 @@ function getWMSURLs(urls) { const createLayer = _options => { const options = WMTSUtils.parseTileMatrixSetOption(_options); const urls = getWMSURLs(isArray(options.url) ? options.url : [options.url]); - const queryParameters = wmtsToLeafletOptions(options) || {}; - urls.forEach(url => addAuthenticationParameter(url, queryParameters, options.securityToken)); + let queryParameters = wmtsToLeafletOptions(options) || {}; + queryParameters = addAuthenticationParameter(urls[0] || '', queryParameters, options.securityToken, options.security?.sourceId); const srs = normalizeSRS(options.srs || 'EPSG:3857', options.allowedSRS); const { tileMatrixSet, matrixIds } = WMTSUtils.getTileMatrix(options, srs); return L.tileLayer.wmts(urls, queryParameters, { @@ -65,7 +66,8 @@ const createLayer = _options => { const updateLayer = (layer, newOptions, oldOptions) => { if (oldOptions.securityToken !== newOptions.securityToken || oldOptions.format !== newOptions.format - || oldOptions.credits !== newOptions.credits) { + || oldOptions.credits !== newOptions.credits + || !isEqual(oldOptions.security, newOptions.security)) { return createLayer(newOptions); } return null; diff --git a/web/client/components/map/openlayers/plugins/ArcGISLayer.js b/web/client/components/map/openlayers/plugins/ArcGISLayer.js index 5bef918b143..714a6f63cf0 100644 --- a/web/client/components/map/openlayers/plugins/ArcGISLayer.js +++ b/web/client/components/map/openlayers/plugins/ArcGISLayer.js @@ -11,15 +11,11 @@ import { registerType } from '../../../../utils/openlayers/Layers'; import TileLayer from 'ol/layer/Tile'; import TileArcGISRest from 'ol/source/TileArcGISRest'; import axios from 'axios'; -import { getCredentials } from '../../../../utils/SecurityUtils'; import { isEqual } from 'lodash'; const tileLoadFunction = options => (image, src) => { - const storedProtectedService = getCredentials(options.security?.sourceId) || {}; axios.get(src, { - headers: { - "Authorization": `Basic ${btoa(storedProtectedService.username + ":" + storedProtectedService.password)}` - }, + _msAuthSourceId: options.security?.sourceId, responseType: 'blob' }).then(response => { image.getImage().src = URL.createObjectURL(response.data); diff --git a/web/client/components/map/openlayers/plugins/COGLayer.js b/web/client/components/map/openlayers/plugins/COGLayer.js index 103e622a687..cc54ab276e6 100644 --- a/web/client/components/map/openlayers/plugins/COGLayer.js +++ b/web/client/components/map/openlayers/plugins/COGLayer.js @@ -13,15 +13,16 @@ import get from 'lodash/get'; import GeoTIFF from 'ol/source/GeoTIFF.js'; import TileLayer from 'ol/layer/WebGLTile.js'; import { isProjectionAvailable } from '../../../../utils/ProjectionUtils'; -import { getCredentials } from '../../../../utils/SecurityUtils'; +import { getRequestConfigurationByUrl } from '../../../../utils/SecurityUtils'; function create(options) { - let sourceOptions; - if (options.security) { - const storedProtectedService = getCredentials(options.security?.sourceId) || {}; - sourceOptions.headers = { - "Authorization": `Basic ${btoa(storedProtectedService.username + ":" + storedProtectedService.password)}` - }; + let sourceOptions = {}; + if (options.security && options.sources && options.sources.length > 0) { + const firstSource = options.sources[0]; + const requestConfig = getRequestConfigurationByUrl(firstSource.url, null, options.security?.sourceId); + if (requestConfig.headers) { + sourceOptions.headers = requestConfig.headers; + } } return new TileLayer({ msId: options.id, diff --git a/web/client/components/map/openlayers/plugins/ElevationLayer.js b/web/client/components/map/openlayers/plugins/ElevationLayer.js index bb1f0167333..139dbaa873f 100644 --- a/web/client/components/map/openlayers/plugins/ElevationLayer.js +++ b/web/client/components/map/openlayers/plugins/ElevationLayer.js @@ -57,8 +57,8 @@ function getElevation(pos) { const createWMSElevationLayer = (options, map) => { const urls = getWMSURLs(isArray(options.url) ? options.url : [options.url]); - const queryParameters = wmsToOpenlayersOptions(options) || {}; - urls.forEach(url => addAuthenticationParameter(url, queryParameters, options.securityToken)); + let queryParameters = wmsToOpenlayersOptions(options) || {}; + queryParameters = addAuthenticationParameter(urls[0] || '', queryParameters, options.securityToken, options.security?.sourceId); const layer = new TileLayer({ msId: options.id, opacity: options.opacity !== undefined ? options.opacity : 1, diff --git a/web/client/components/map/openlayers/plugins/TMSLayer.js b/web/client/components/map/openlayers/plugins/TMSLayer.js index efe24b5e1ef..e745cc7d2d8 100644 --- a/web/client/components/map/openlayers/plugins/TMSLayer.js +++ b/web/client/components/map/openlayers/plugins/TMSLayer.js @@ -12,14 +12,9 @@ import TileGrid from 'ol/tilegrid/TileGrid'; import TileLayer from 'ol/layer/Tile'; import Layers from '../../../../utils/openlayers/Layers'; -import { getCredentials } from '../../../../utils/SecurityUtils'; - const tileLoadFunction = options => (image, src) => { - const storedProtectedService = getCredentials(options.security?.sourceId) || {}; axios.get(src, { - headers: { - "Authorization": `Basic ${btoa(storedProtectedService.username + ":" + storedProtectedService.password)}` - }, + _msAuthSourceId: options.security?.sourceId, responseType: 'blob' }).then(response => { image.getImage().src = URL.createObjectURL(response.data); diff --git a/web/client/components/map/openlayers/plugins/TileProviderLayer.js b/web/client/components/map/openlayers/plugins/TileProviderLayer.js index 34d6c4bda64..c6df813737a 100644 --- a/web/client/components/map/openlayers/plugins/TileProviderLayer.js +++ b/web/client/components/map/openlayers/plugins/TileProviderLayer.js @@ -12,7 +12,6 @@ import { getUrls, template } from '../../../../utils/TileProviderUtils'; import XYZ from 'ol/source/XYZ'; import TileLayer from 'ol/layer/Tile'; import axios from 'axios'; -import { getCredentials } from '../../../../utils/SecurityUtils'; import { isEqual } from 'lodash'; function lBoundsToOlExtent(bounds, destPrj) { var [ [ miny, minx], [ maxy, maxx ] ] = bounds; @@ -20,11 +19,8 @@ function lBoundsToOlExtent(bounds, destPrj) { } const tileLoadFunction = options => (image, src) => { - const storedProtectedService = getCredentials(options.security?.sourceId) || {}; axios.get(src, { - headers: { - "Authorization": `Basic ${btoa(storedProtectedService.username + ":" + storedProtectedService.password)}` - }, + _msAuthSourceId: options.security?.sourceId, responseType: 'blob' }).then(response => { image.getImage().src = URL.createObjectURL(response.data); diff --git a/web/client/components/map/openlayers/plugins/WMSLayer.js b/web/client/components/map/openlayers/plugins/WMSLayer.js index 554d314dc46..ddd1851280a 100644 --- a/web/client/components/map/openlayers/plugins/WMSLayer.js +++ b/web/client/components/map/openlayers/plugins/WMSLayer.js @@ -104,8 +104,8 @@ const createLayer = (options, map, mapId) => { }, map, mapId); } const urls = getWMSURLs(isArray(options.url) ? options.url : [options.url]); - const queryParameters = wmsToOpenlayersOptions(options) || {}; - urls.forEach(url => addAuthenticationParameter(url, queryParameters, options.securityToken)); + let queryParameters = wmsToOpenlayersOptions(options) || {}; + queryParameters = addAuthenticationParameter(urls[0] || '', queryParameters, options.securityToken, options.security?.sourceId); const headers = getAuthenticationHeaders(urls[0], options.securityToken, options.security); const vectorFormat = isVectorFormat(options.format); @@ -187,6 +187,7 @@ const mustCreateNewLayer = (oldOptions, newOptions) => { || oldOptions.forceProxy !== newOptions.forceProxy || oldOptions.tileGridStrategy !== newOptions.tileGridStrategy || !isEqual(oldOptions.tileGrids, newOptions.tileGrids) + || !isEqual(oldOptions.security, newOptions.security) ); }; diff --git a/web/client/components/map/openlayers/plugins/WMTSLayer.js b/web/client/components/map/openlayers/plugins/WMTSLayer.js index a2e28542896..1c48dda7ae6 100644 --- a/web/client/components/map/openlayers/plugins/WMTSLayer.js +++ b/web/client/components/map/openlayers/plugins/WMTSLayer.js @@ -13,7 +13,7 @@ import head from 'lodash/head'; import last from 'lodash/last'; import axios from '../../../../libs/ajax'; import { proxySource } from '../../../../utils/openlayers/WMSUtils'; -import {getCredentials, addAuthenticationParameter} from '../../../../utils/SecurityUtils'; +import {addAuthenticationParameter} from '../../../../utils/SecurityUtils'; import * as WMTSUtils from '../../../../utils/WMTSUtils'; import CoordinatesUtils from '../../../../utils/CoordinatesUtils'; import MapUtils from '../../../../utils/MapUtils'; @@ -46,11 +46,8 @@ function getWMSURLs(urls, requestEncoding) { } const tileLoadFunction = (options) => (image, src) => { - const storedProtectedService = options.security ? getCredentials(options.security?.sourceId) : {}; axios.get(src, { - headers: { - "Authorization": `Basic ${btoa(storedProtectedService.username + ":" + storedProtectedService.password)}` - }, + _msAuthSourceId: options.security?.sourceId, responseType: 'blob' }).then(response => { if (isValidResponse(response)) { @@ -119,8 +116,8 @@ const createLayer = options => { // the extent has effect to the tile ranges // we should skip the extent if the layer does not provide bounding box let extent = layerExtent && getIntersection(layerExtent, projection.getExtent()); - const queryParameters = {}; - urls.forEach(url => addAuthenticationParameter(url, queryParameters, options.securityToken)); + let queryParameters = {}; + queryParameters = addAuthenticationParameter(urls[0] || '', queryParameters, options.securityToken, options.security?.sourceId); const queryParametersString = urlParser.format({ query: { ...queryParameters } }); // TODO: support tileSizes from matrix @@ -189,7 +186,8 @@ const updateLayer = (layer, newOptions, oldOptions) => { || oldOptions.srs !== newOptions.srs || oldOptions.format !== newOptions.format || oldOptions.style !== newOptions.style - || oldOptions.credits !== newOptions.credits) { + || oldOptions.credits !== newOptions.credits + || !isEqual(oldOptions.security, newOptions.security)) { return createLayer(newOptions); } if (oldOptions.minResolution !== newOptions.minResolution) { diff --git a/web/client/components/misc/SecureImage.jsx b/web/client/components/misc/SecureImage.jsx index a87ac6c964b..858f0c8fa08 100644 --- a/web/client/components/misc/SecureImage.jsx +++ b/web/client/components/misc/SecureImage.jsx @@ -7,10 +7,7 @@ */ import React, { useEffect, useState } from 'react'; -import axios from 'axios'; - -import { getAuthKeyParameter, getAuthenticationMethod, getAuthorizationBasic, getToken } from '../../utils/SecurityUtils'; -import { updateUrlParams } from '../../utils/URLUtils'; +import axios from '../../libs/ajax'; const SecureImage = ({ @@ -29,45 +26,17 @@ const SecureImage = ({ } }; useEffect(() => { - const authMethod = getAuthenticationMethod(src); - - if (authMethod === "bearer") { - axios.get(src, { - responseType: 'blob' - }) - .then((response) => { - const imageUrl = URL.createObjectURL(response.data); - setImageSrc(imageUrl); - }) - .catch((error) => { - console.error('Error fetching image:', error); - }); - } else if (authMethod === "authkey") { - const authParam = getAuthKeyParameter(src); - const token = getToken(); - if (authParam && token) { - const newSrc = updateUrlParams(src, {[authParam]: token}); - setImageSrc(newSrc); - } else { - setImageSrc(src); - } - - } else if (props?.layer?.security?.sourceId) { - const headers = getAuthorizationBasic(props?.layer?.security?.sourceId); - axios.get(src, { - responseType: 'blob', - headers + // The axios interceptor will handle authentication based on URL rules + axios.get(src, { + responseType: 'blob' + }) + .then((response) => { + const imageUrl = URL.createObjectURL(response.data); + setImageSrc(imageUrl); }) - .then((response) => { - const imageUrl = URL.createObjectURL(response.data); - setImageSrc(imageUrl); - }) - .catch((error) => { - console.error('Error fetching image:', error); - }); - } else { - setImageSrc(src); - } + .catch((error) => { + console.error('Error fetching image:', error); + }); // Clean up the URL object when the component unmounts return () => { diff --git a/web/client/configs/localConfig.json b/web/client/configs/localConfig.json index 7dcc0277c80..7c0ed90c227 100644 --- a/web/client/configs/localConfig.json +++ b/web/client/configs/localConfig.json @@ -30,7 +30,6 @@ "mapboxAccessToken": "__ACCESS_TOKEN_MAPBOX__", "initialMapFilter": "", "ignoreMobileCss": false, - "useAuthenticationRules": true, "loadAfterTheme": true, "defaultMapOptions": { "cesium": { @@ -48,14 +47,24 @@ "localizedLayerStyles": { "name": "mapstore_language" }, - "authenticationRules": [ + "requestsConfigurationRules": [ { "urlPattern": ".*geostore.*", - "method": "bearer" + "headers": { + "Authorization": "Bearer ${securityToken}" + } }, { "urlPattern": ".*rest/config.*", - "method": "bearer" + "headers": { + "Authorization": "Bearer ${securityToken}" + } + }, + { + "urlPattern": "http(s)?\\:\\/\\/gs-stable\\.(geo-solutions\\.it|geosolutionsgroup\\.com)(\\:443|\\:80)?\\/geoserver/.*", + "params": { + "authkey": "${securityToken}" + } } ], "monitorState": [ diff --git a/web/client/epics/security.js b/web/client/epics/security.js index c5413b551cd..fcd50e7d07f 100644 --- a/web/client/epics/security.js +++ b/web/client/epics/security.js @@ -8,14 +8,32 @@ import Rx from 'rxjs'; import uniqBy from 'lodash/uniqBy'; import isArray from 'lodash/isArray'; +import get from 'lodash/get'; +import isEqual from 'lodash/isEqual'; +import head from 'lodash/head'; +import castArray from 'lodash/castArray'; +import isEmpty from 'lodash/isEmpty'; +import { v4 as uuidv4 } from 'uuid'; import { DASHBOARD_LOADED } from '../actions/dashboard'; import { SET_CURRENT_STORY } from '../actions/geostory'; import { EDITOR_CHANGE } from '../actions/widgets'; import { UPDATE_ITEM } from '../actions/mediaEditor'; import { currentMediaTypeSelector, selectedItemSelector } from '../selectors/mediaEditor'; import { MAP_CONFIG_LOADED } from '../actions/config'; -import { setShowModalStatus, setProtectedServices } from '../actions/security'; -import { getCredentials } from '../utils/SecurityUtils'; +import { + setShowModalStatus, + setProtectedServices, + loadRequestsRules, + LOAD_REQUESTS_RULES, + UPDATE_REQUESTS_RULES +} from '../actions/security'; +import { + getCredentials, + convertAuthenticationRulesToRequestConfiguration +} from '../utils/SecurityUtils'; +import { LOCAL_CONFIG_LOADED } from '../actions/localConfig'; +import { layersSelector } from '../selectors/layers'; +import { changeLayerProperties } from '../actions/layers'; /** * checks if a content is protected in a map @@ -196,3 +214,76 @@ export const checkProtectedContentGeostoryEpic = (action$) => return Rx.Observable.of(setShowModalStatus(false)); }); +/** + * Epic to handle loading request configuration rules from config + */ +export const loadRequestsRulesFromConfigEpic = (action$) => + action$.ofType(LOCAL_CONFIG_LOADED) + .switchMap((action) => { + const config = action.config; + let rules = config?.requestsConfigurationRules ?? []; + const legacyRules = config?.authenticationRules ?? []; + const shouldUseLegacyRules = config?.useAuthenticationRules ?? false; + if (!isEmpty(legacyRules) && (isEmpty(rules) || shouldUseLegacyRules)) { + rules = convertAuthenticationRulesToRequestConfiguration(legacyRules); + } + return Rx.Observable.of(loadRequestsRules(rules)); + }); + +/** + * Helper function to determine which rules have changed + * Returns an array of URL patterns from rules that have changed + */ +const getChangedRuleUrlPatterns = (oldRules, newRules) => { + const makeMap = (rules) => new Map(rules.filter(r => r?.urlPattern).map(r => [r.urlPattern, r])); + const [oldMap, newMap] = [makeMap(oldRules), makeMap(newRules)]; + const changed = new Set([ + ...[...newMap].filter(([p, n]) => !oldMap.has(p) || !isEqual(oldMap.get(p), n)).map(([p]) => p), + ...[...oldMap].filter(([p]) => !newMap.has(p)).map(([p]) => p) + ]); + return [...changed]; +}; + +/** + * Epic to refresh layers when request configuration rules are updated + * This ensures that layers re-fetch tiles with the new authentication parameters + * Only refreshes layers whose URLs match changed rules + */ +export const refreshLayersOnRulesUpdateEpic = (action$, store) => + action$.ofType(LOAD_REQUESTS_RULES, UPDATE_REQUESTS_RULES) + .switchMap((action) => { + const state = store.getState(); + const newRules = get(action, 'rules', []); + const oldRules = state.security?.previousRules || []; + + // Get URL patterns of rules that have changed + const changedPatterns = getChangedRuleUrlPatterns(oldRules, newRules); + + if (isEmpty(changedPatterns)) { + // No rules changed, no need to refresh + return Rx.Observable.empty(); + } + + const layers = layersSelector(state) || []; + + // Find layers that should be refreshed based on matching changed rules + const layersToUpdate = []; + layers.forEach(layer => { + const url = head(castArray(layer.url)); + + // Check if any layer URL matches any changed rule pattern + const shouldRefresh = changedPatterns.some(pattern => url?.match(new RegExp(pattern, "i"))); + if (shouldRefresh) layersToUpdate.push(layer); + }); + + // Dispatch changeLayerProperties for each matching layer + const actions = layersToUpdate.map(layer => { + const newSecurity = layer.security + ? { ...layer.security, refreshHash: uuidv4() } + : { refreshHash: uuidv4() }; + return changeLayerProperties(layer.id, { security: newSecurity, visibility: false }); + }); + + return actions.length > 0 ? Rx.Observable.from(actions) : Rx.Observable.empty(); + }); + diff --git a/web/client/epics/wfsquery.js b/web/client/epics/wfsquery.js index 43019031866..b0aea92cdb2 100644 --- a/web/client/epics/wfsquery.js +++ b/web/client/epics/wfsquery.js @@ -55,7 +55,6 @@ import {selectedLayerSelector, useLayerFilterSelector} from '../selectors/featur import {layerLoad} from '../actions/layers'; import { mergeFiltersToOGC } from '../utils/FilterUtils'; -import { getAuthorizationBasic } from '../utils/SecurityUtils'; const extractInfo = (data, fields = []) => { return { @@ -136,8 +135,7 @@ export const featureTypeSelectedEpic = (action$, store) => .mergeAll(); } - const headers = getAuthorizationBasic(selectedLayer?.security?.sourceId); - return Rx.Observable.defer( () => axios.get(ConfigUtils.filterUrlParams(action.url, authkeyParamNameSelector(store.getState())) + '?service=WFS&version=1.1.0&request=DescribeFeatureType&typeName=' + action.typeName + '&outputFormat=application/json', {headers})) + return Rx.Observable.defer( () => axios.get(ConfigUtils.filterUrlParams(action.url, authkeyParamNameSelector(store.getState())) + '?service=WFS&version=1.1.0&request=DescribeFeatureType&typeName=' + action.typeName + '&outputFormat=application/json', {_msAuthSourceId: selectedLayer?.security?.sourceId})) .map((response) => { if (typeof response.data === 'object' && response.data.featureTypes && response.data.featureTypes[0]) { const info = extractInfo(response.data, action.fields); diff --git a/web/client/libs/ajax.js b/web/client/libs/ajax.js index 42e0d423456..6383a289e9a 100644 --- a/web/client/libs/ajax.js +++ b/web/client/libs/ajax.js @@ -10,10 +10,9 @@ import axios from 'axios'; import combineURLs from 'axios/lib/helpers/combineURLs'; import ConfigUtils from '../utils/ConfigUtils'; import { - isAuthenticationActivated, - getAuthenticationRule, - getToken, - getBasicAuthHeader + getRequestConfigurationByUrl, + getRequestConfigurationRule, + isRequestConfigurationActivated } from '../utils/SecurityUtils'; import isObject from 'lodash/isObject'; @@ -44,59 +43,45 @@ function addHeaderToAxiosConfig(axiosConfig, headerName, headerValue) { * authentication method based on the request URL. */ function addAuthenticationToAxios(axiosConfig) { - if (!axiosConfig || !axiosConfig.url || !isAuthenticationActivated()) { + if (!axiosConfig || !axiosConfig.url) { return axiosConfig; } const axiosUrl = combineURLs(axiosConfig.baseURL || '', axiosConfig.url); - const rule = getAuthenticationRule(axiosUrl); - switch (rule && rule.method) { - case 'browserWithCredentials': - { - axiosConfig.withCredentials = true; + // Extract custom sourceId from axios config if provided + const sourceId = axiosConfig._msAuthSourceId; + + // Only process authentication if sourceId is provided or request configuration is activated + if (!sourceId && !isRequestConfigurationActivated()) { return axiosConfig; } - case 'authkey': - { - const token = getToken(); - if (!token) { - return axiosConfig; - } - addParameterToAxiosConfig(axiosConfig, rule.authkeyParamName || 'authkey', token); - return axiosConfig; + + // If request configuration is not activated but sourceId is provided, still need to handle basic auth + const { headers, params } = getRequestConfigurationByUrl(axiosUrl, undefined, sourceId); + + if (headers) { + Object.entries(headers).forEach(([headerName, headerValue]) => { + addHeaderToAxiosConfig(axiosConfig, headerName, headerValue); + }); } - case 'test': { - const token = rule ? rule.token : ""; - if (!token) { - return axiosConfig; - } - addParameterToAxiosConfig(axiosConfig, rule.authkeyParamName || 'authkey', token); - return axiosConfig; + if (params) { + Object.entries(params).forEach(([paramName, paramValue]) => { + addParameterToAxiosConfig(axiosConfig, paramName, paramValue); + }); } - case 'basic': - const basicAuthHeader = getBasicAuthHeader(); - if (!basicAuthHeader) { - return axiosConfig; - } - addHeaderToAxiosConfig(axiosConfig, 'Authorization', basicAuthHeader); - return axiosConfig; - case 'bearer': - { - const token = getToken(); - if (!token) { - return axiosConfig; + + // Check for withCredentials + if (isRequestConfigurationActivated()) { + const rule = getRequestConfigurationRule(axiosUrl); + if (rule?.withCredentials) { + axiosConfig.withCredentials = true; } - addHeaderToAxiosConfig(axiosConfig, 'Authorization', "Bearer " + token); - return axiosConfig; - } - case 'header': { - Object.entries(rule.headers).map(([headerName, headerValue]) => addHeaderToAxiosConfig(axiosConfig, headerName, headerValue)); - return axiosConfig; - } - default: - // we cannot handle the required authentication method - return axiosConfig; } + + // Remove the custom prop from config to avoid it being sent as a regular param + delete axiosConfig._msAuthSourceId; + + return axiosConfig; } const checkSameOrigin = (uri) => { diff --git a/web/client/observables/wfs.js b/web/client/observables/wfs.js index 53cae4cb928..f542b67ab63 100644 --- a/web/client/observables/wfs.js +++ b/web/client/observables/wfs.js @@ -19,7 +19,6 @@ import { getCapabilitiesUrl } from '../utils/LayersUtils'; import { interceptOGCError } from '../utils/ObservableUtils'; import requestBuilder from '../utils/ogc/WFS/RequestBuilder'; import { getDefaultUrl } from '../utils/URLUtils'; -import { getAuthorizationBasic } from '../utils/SecurityUtils'; const {getFeature, query, sortBy, propertyName} = requestBuilder({ wfsVersion: "1.1.0" }); @@ -170,7 +169,6 @@ export const getXMLFeature = (searchUrl, filterObj, options = {}, downloadOption } const { data, queryString } = getFeatureUtilities(searchUrl, filterObj, options, downloadOption); - const headers = getAuthorizationBasic(options.layer?.security?.sourceId || options.security?.sourceId); return Rx.Observable.defer(() => axios.post(queryString, data, { @@ -178,9 +176,9 @@ export const getXMLFeature = (searchUrl, filterObj, options = {}, downloadOption responseType: 'arraybuffer', headers: { 'Accept': `application/xml`, - 'Content-Type': `application/xml`, - ...headers - } + 'Content-Type': `application/xml` + }, + _msAuthSourceId: options.layer?.security?.sourceId || options.security?.sourceId })); }; @@ -278,14 +276,13 @@ export const getLayerJSONFeature = ({ search = {}, url, name, security } = {}, f }); export const describeFeatureType = ({layer}) => { - const headers = getAuthorizationBasic(layer?.security?.sourceId); + const url = toDescribeURL(layer); return Rx.Observable.defer(() => - axios.get(toDescribeURL(layer), {headers})).let(interceptOGCError); + axios.get(url, {_msAuthSourceId: layer?.security?.sourceId})).let(interceptOGCError); }; export const getLayerWFSCapabilities = ({layer}) => { - const headers = getAuthorizationBasic(layer?.security?.sourceId); - - return Rx.Observable.defer( () => axios.get(toLayerCapabilitiesURL(layer), {headers})) + const url = toLayerCapabilitiesURL(layer); + return Rx.Observable.defer( () => axios.get(url, {_msAuthSourceId: layer?.security?.sourceId})) .let(interceptOGCError) .switchMap( response => Rx.Observable.bindNodeCallback( (data, callback) => parseString(data, { tagNameProcessors: [stripPrefix], diff --git a/web/client/observables/wms.js b/web/client/observables/wms.js index e71f00bde43..ad0f8695937 100644 --- a/web/client/observables/wms.js +++ b/web/client/observables/wms.js @@ -17,7 +17,7 @@ import axios from '../libs/ajax'; import { determineCrs, fetchProjRemotely, getProjUrl } from '../utils/CoordinatesUtils'; import { getCapabilitiesUrl } from '../utils/LayersUtils'; import { interceptOGCError } from '../utils/ObservableUtils'; -import { cleanAuthParamsFromURL, getAuthorizationBasic } from '../utils/SecurityUtils'; +import { cleanAuthParamsFromURL } from '../utils/SecurityUtils'; import { getDefaultUrl } from '../utils/URLUtils'; const proj4 = Proj4js; @@ -40,12 +40,12 @@ export const toDescribeLayerURL = ({name, search = {}, url} = {}) => { }); }; export const describeLayer = l => { - const headers = getAuthorizationBasic(l?.security?.sourceId); - return Observable.defer( () => axios.get(toDescribeLayerURL(l), {headers})).let(interceptOGCError); + const url = toDescribeLayerURL(l); + return Observable.defer( () => axios.get(url, {_msAuthSourceId: l?.security?.sourceId})).let(interceptOGCError); }; export const getLayerCapabilities = l => { - const headers = getAuthorizationBasic(l?.security?.sourceId); - return Observable.defer(() => WMS.getCapabilities(getCapabilitiesUrl(l), headers)) + const url = getCapabilitiesUrl(l); + return Observable.defer(() => WMS.getCapabilities(url, {_msAuthSourceId: l?.security?.sourceId})) .let(interceptOGCError) .map(c => WMS.parseLayerCapabilities(c, l)); }; diff --git a/web/client/observables/wps/execute.js b/web/client/observables/wps/execute.js index d75c265485b..678f07824f8 100644 --- a/web/client/observables/wps/execute.js +++ b/web/client/observables/wps/execute.js @@ -13,7 +13,6 @@ import { stripPrefix } from 'xml2js/lib/processors'; import axios from '../../libs/ajax'; import { getWPSURL } from './common'; -import { getAuthorizationBasic } from '../../utils/SecurityUtils'; /** * Contains routines pertaining to Execute WPS operation. @@ -194,13 +193,13 @@ export const makeOutputsExtractor = (...extractors) => * @returns {Observable} observable that emits result from axios.post */ export const executeProcessRequest = (url, payload, requestOptions = {}, layer) => { - const headers = getAuthorizationBasic(layer?.security?.sourceId); + const wpsUrl = getWPSURL(url, {"version": "1.0.0", "REQUEST": "Execute"}); return Observable.defer(() => - axios.post(getWPSURL(url, {"version": "1.0.0", "REQUEST": "Execute"}), payload, { + axios.post(wpsUrl, payload, { headers: { - 'Content-Type': 'application/xml', - ...headers + 'Content-Type': 'application/xml' }, + _msAuthSourceId: layer?.security?.sourceId, ...requestOptions }) ); diff --git a/web/client/plugins/TOC/components/StyleBasedWMSJsonLegend.jsx b/web/client/plugins/TOC/components/StyleBasedWMSJsonLegend.jsx index a99f542437b..4fc141d6096 100644 --- a/web/client/plugins/TOC/components/StyleBasedWMSJsonLegend.jsx +++ b/web/client/plugins/TOC/components/StyleBasedWMSJsonLegend.jsx @@ -133,7 +133,7 @@ class StyleBasedWMSJsonLegend extends React.Component { const cleanParams = clearNilValuesForParams(layer.params); const scale = this.getScale(props); const projection = normalizeSRS(props.projection || 'EPSG:3857', layer.allowedSRS); - const query = { + let query = { ...getWMSLegendConfig({ layer, format: LEGEND_FORMAT.JSON, @@ -146,8 +146,7 @@ class StyleBasedWMSJsonLegend extends React.Component { ...(cleanParams && cleanParams.SLD_BODY ? { SLD_BODY: cleanParams.SLD_BODY } : {}), ...(scale !== null ? { SCALE: scale } : {}) }; - addAuthenticationParameter(url, query); - + query = addAuthenticationParameter(url, query); return urlUtil.format({ host: urlObj.host, protocol: urlObj.protocol, diff --git a/web/client/reducers/security.js b/web/client/reducers/security.js index 6462daaba66..2329300428f 100644 --- a/web/client/reducers/security.js +++ b/web/client/reducers/security.js @@ -17,14 +17,23 @@ import { SESSION_VALID, CHANGE_PASSWORD, SET_SHOW_MODAL_STATUS, - SET_PROTECTED_SERVICES + SET_PROTECTED_SERVICES, + UPDATE_REQUESTS_RULES, + LOAD_REQUESTS_RULES_ERROR, + LOAD_REQUESTS_RULES } from '../actions/security'; import { RESET_CONTROLS, SET_CONTROL_PROPERTY } from '../actions/controls'; import { USERMANAGER_UPDATE_USER } from '../actions/users'; import {getUserAttributes} from '../utils/SecurityUtils'; import { cloneDeep, head } from 'lodash'; -const initialState = {user: null, errorCause: null}; +const initialState = { + user: null, + rules: [], + loading: false, + error: null, + lastRefresh: null +}; function security(state = initialState, action) { switch (action.type) { case USERMANAGER_UPDATE_USER: @@ -135,6 +144,27 @@ function security(state = initialState, action) { }; } + case UPDATE_REQUESTS_RULES: + return { + ...state, + previousRules: state.rules ?? [], + rules: action.rules ?? [], + error: null + }; + case LOAD_REQUESTS_RULES: + return { + ...state, + loading: false, + previousRules: state.rules ?? [], + rules: action.rules ?? [], + error: null + }; + case LOAD_REQUESTS_RULES_ERROR: + return { + ...state, + loading: false, + error: action.error + }; default: return state; } diff --git a/web/client/selectors/catalog.js b/web/client/selectors/catalog.js index b3656eba9db..ac805a7a9e0 100644 --- a/web/client/selectors/catalog.js +++ b/web/client/selectors/catalog.js @@ -48,7 +48,17 @@ export const layerErrorSelector = (state) => get(state, "catalog.layerError"); export const searchTextSelector = (state) => get(state, "catalog.searchOptions.text", ""); export const isActiveSelector = (state) => get(state, "controls.toolbar.active") === "metadataexplorer" || get(state, "controls.metadataexplorer.enabled"); export const authkeyParamNameSelector = (state) => { - return (get(state, "localConfig.authenticationRules") || []).filter(a => a.method === "authkey").map(r => r.authkeyParamName) || []; + const rules = state?.security?.rules || []; + const authKeyParams = rules + .filter(rule => rule.params) + .map(rule => { + const authKeyParam = Object.keys(rule.params).find(key => + rule.params[key] && (rule.params[key].includes('${securityToken}')) + ); + return authKeyParam; + }) + .filter(param => param); + return authKeyParams; }; export const pageSizeSelector = (state) => get(state, "catalog.pageSize", 4); export const delayAutoSearchSelector = (state) => get(state, "catalog.delayAutoSearch", 1000); diff --git a/web/client/selectors/security.js b/web/client/selectors/security.js index 30dd31fe0f3..8da873b8638 100644 --- a/web/client/selectors/security.js +++ b/web/client/selectors/security.js @@ -53,7 +53,8 @@ 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; - +export const requestsRulesSelector = state => get(state, 'security.rules', []); +export const requestsRulesEnabledSelector = state => get(state, 'security.rulesEnabled', false); /** * Creates a selector that checks if user is allowed to edit * something based on the user's role and groups diff --git a/web/client/utils/LayersUtils.js b/web/client/utils/LayersUtils.js index e1f465eaa2d..50c1a05da22 100644 --- a/web/client/utils/LayersUtils.js +++ b/web/client/utils/LayersUtils.js @@ -817,11 +817,8 @@ export const setCustomUtils = (type, fun) => { export const getAuthenticationParam = options => { const urls = getURLs(isArray(options.url) ? options.url : [options.url]); - let authenticationParam = {}; - urls.forEach(url => { - addAuthenticationParameter(url, authenticationParam, options.securityToken); - }); - return authenticationParam; + // Use first URL since all URLs in array should have same auth config + return addAuthenticationParameter(urls[0] || '', {}, options.securityToken, options.security?.sourceId); }; /** * Removes google backgrounds and select an alternative one as visible diff --git a/web/client/utils/SecurityUtils.js b/web/client/utils/SecurityUtils.js index 0d0b7e0dec5..6bac2600764 100644 --- a/web/client/utils/SecurityUtils.js +++ b/web/client/utils/SecurityUtils.js @@ -13,6 +13,8 @@ import head from "lodash/head"; import isNil from "lodash/isNil"; import isArray from "lodash/isArray"; import isEmpty from "lodash/isEmpty"; +import template from "lodash/template"; +import get from "lodash/get"; import {setStore as stateSetStore, getState} from "./StateUtils"; @@ -112,110 +114,253 @@ export function findUserAttributeValue(attributeName) { } /** - * Returns an array with the configured authentication rules. If no rules - * were configured an empty array is returned. + * Parses request configuration by replacing variables with actual values using lodash template + * @param {Object} config - Configuration object with headers/params + * @param {Object} securityProperties - Security properties to replace variables + * @returns {Object} Parsed configuration with replaced variables */ -export function getAuthenticationRules() { - return ConfigUtils.getConfigProp('authenticationRules') || []; -} +const parseRequestConfiguration = (config = {}, securityProperties) => { + return Object.fromEntries( + Object.entries(config) + .map((entry) => { + const [name, value] = entry; + if (typeof value === 'string' && value.includes('${')) { + try { + // Use lodash template for variable substitution + const compiled = template(value); + const result = compiled(securityProperties); + return [name, result]; + } catch (error) { + console.warn(`Template parsing error for ${name}:`, error); + return entry; // Return original if template fails + } + } + return entry; + }) + .filter(entry => entry) + ); +}; /** - * Checks if authentication is activated or not. + * Legacy compatibility: Converts old authenticationRules to new format + * @param {Array} authRules - Old authentication rules + * @returns {Array} New request configuration rules */ -export function isAuthenticationActivated() { - return ConfigUtils.getConfigProp('useAuthenticationRules') || false; -} +export const convertAuthenticationRulesToRequestConfiguration = (authRules = []) => { + return authRules.map(rule => { + const newRule = { + urlPattern: rule.urlPattern + }; + + switch (rule.method) { + case 'bearer': + newRule.headers = { + 'Authorization': 'Bearer ${securityToken}' + }; + break; + case 'authkey': + newRule.params = { + [rule.authkeyParamName || 'authkey']: '${securityToken}' + }; + break; + case 'basic': + newRule.headers = { + 'Authorization': '${authHeader}' + }; + break; + case 'header': + newRule.headers = rule.headers || {}; + break; + case 'browserWithCredentials': + newRule.withCredentials = true; + break; + default: + // Unknown method, skip this rule + return null; + } + + return newRule; + }).filter(rule => rule !== null); +}; /** - * Returns the authentication method that should be used for the provided URL. - * We go through the authentication rules and find the first one that matches - * the provided URL, if no rule matches the provided URL undefined is returned. + * Gets all request configuration rules from Redux state or config + * Automatically converts authenticationRules to new format if requestsConfigurationRules is missing + * @returns {Array} Array of request configuration rules */ -export function getAuthenticationMethod(url) { - const foundRule = head(getAuthenticationRules().filter( - rule => rule && rule.urlPattern && url.match(new RegExp(rule.urlPattern, "i")))); - return foundRule?.method; -} +export const getRequestConfigurationRules = () => { + // First try to get from Redux state (if available) + const stateRules = get(getState(), 'security.rules', []); + if (!isEmpty(stateRules)) { + return stateRules; + } + + // Try to get new format from config + const configRules = ConfigUtils.getConfigProp('requestsConfigurationRules'); + if (!isEmpty(configRules)) { + return configRules; + } + + // If new format is missing, convert old authenticationRules format + const authRules = ConfigUtils.getConfigProp('authenticationRules'); + if (!isEmpty(authRules)) { + return convertAuthenticationRulesToRequestConfiguration(authRules); + } + + // No rules found + return []; +}; /** - * Returns the authentication rule that should be used for the provided URL. - * We go through the authentication rules and find the first one that matches - * the provided URL, if no rule matches the provided URL undefined is returned. + * Gets the request configuration rule that matches the provided URL + * @param {string} url - The URL to match against rules + * @returns {Object|null} Matching rule or null */ -export function getAuthenticationRule(url) { - return head(getAuthenticationRules().filter( - rule => rule && rule.urlPattern && url.match(new RegExp(rule.urlPattern, "i")))); -} +export const getRequestConfigurationRule = (url) => { + const rules = getRequestConfigurationRules(); + return head(rules.filter( + rule => rule && rule.urlPattern && url.match(new RegExp(rule.urlPattern, "i")) + )); +}; -export function getAuthKeyParameter(url) { - const foundRule = getAuthenticationRule(url); - return foundRule?.authkeyParamName ?? 'authkey'; -} +/** + * Checks if request configuration is activated + * Returns true only when user is authenticated and rules are present + * @returns {boolean} True if request configuration is activated + */ +export const isRequestConfigurationActivated = () => { + // Check if user is authenticated (has a token) + const token = getToken(); + if (!token) { + return false; // Not authenticated + } -export function getAuthenticationHeaders(url, securityToken, security) { - if (!url || !isAuthenticationActivated()) { - return null; + // Check if redux state exist + const state = getState(); + if (!isEmpty(state?.security?.rules)) { + return true; + } + + const newRules = ConfigUtils.getConfigProp('requestsConfigurationRules'); + if (!isEmpty(newRules)) { + return true; } - const storedProtectedService = getCredentials(security?.sourceId); - if (security && storedProtectedService) { - return { - "Authorization": `Basic ${btoa(storedProtectedService.username + ":" + storedProtectedService.password)}` + + // Legacy support + const useLegacyRules = ConfigUtils.getConfigProp('useAuthenticationRules'); + const oldRules = ConfigUtils.getConfigProp('authenticationRules'); + if (isNil(useLegacyRules)) { + return !isEmpty(oldRules); + } + return useLegacyRules; +}; + +/** + * it creates the headers function for axios config, if it finds a reference in sessionStorage + * @param {string} protectedId the id of the protected service to look for in sessionStorage + * @returns {object} the headers Basic + */ +export const getAuthorizationBasic = (protectedId) => { + let headers = {}; + const storedProtectedService = getCredentials(protectedId); + if (!isEmpty(storedProtectedService)) { + headers = { + Authorization: `Basic ${btoa(storedProtectedService.username + ":" + storedProtectedService.password)}` }; } - switch (getAuthenticationMethod(url)) { - case 'bearer': { - const token = !isNil(securityToken) ? securityToken : getToken(); - if (!token) { - return null; + return headers; +}; + +/** + * Gets request configuration (headers and params) for a given URL + * This is the main function that centralizes all request configuration logic + * @param {string} url - The URL to get configuration for + * @param {string} securityToken - Optional security token override + * @param {string} [sourceId] - Optional source ID for sessionStorage-based credentials + * @returns {Object} Object containing headers and/or params + */ +export const getRequestConfigurationByUrl = (url, securityToken, sourceId) => { + if (!url || !isRequestConfigurationActivated()) { + if (!isNil(sourceId)) { + return { headers: getAuthorizationBasic(sourceId) }; } - return { - "Authorization": `Bearer ${token}` - }; + return {}; } - case 'header': { - const rule = getAuthenticationRule(url); - return rule.headers; + + const rule = getRequestConfigurationRule(url); + if (!rule) return {}; + + const token = !isNil(securityToken) ? securityToken : getToken(); + let authHeader = getBasicAuthHeader(); + + // Fallback to sessionStorage credentials sourceId and credentials is stored + let basicAuthHeader; + if (sourceId) { + basicAuthHeader = getAuthorizationBasic(sourceId); + authHeader = basicAuthHeader?.Authorization; } - default: - // we cannot handle the required authentication method - return null; + + const securityProperties = { + ...(!isNil(token) && { securityToken: token }), + ...(!isNil(authHeader) && { authHeader }) + }; + + const parsedHeaders = parseRequestConfiguration(rule.headers, securityProperties); + const parsedParams = parseRequestConfiguration(rule.params, securityProperties); + + let finalHeaders; + if (!isEmpty(parsedHeaders)) { + finalHeaders = parsedHeaders; + } else if (sourceId && !isEmpty(basicAuthHeader)) { + finalHeaders = basicAuthHeader; } + + return { + ...(!isEmpty(finalHeaders) && { headers: finalHeaders }), + ...(!isEmpty(parsedParams) && { params: parsedParams }) + }; +}; + +export function getAuthKeyParameter(url) { + // Use the new request configuration system + const rule = getRequestConfigurationRule(url); + if (rule && rule.params) { + // Find the parameter that contains the securityToken placeholder + const authKeyParam = Object.keys(rule.params).find(key => + rule.params[key] && (rule.params[key].includes('${securityToken}')) + ); + if (authKeyParam) { + return authKeyParam; + } + } + return 'authkey'; +} + +export function getAuthenticationHeaders(url, securityToken, security) { + const requestConfig = getRequestConfigurationByUrl(url, securityToken, security?.sourceId); + if (!isEmpty(requestConfig.headers)) { + return requestConfig.headers; + } + return null; } /** * This method will add query parameter based authentications to an object * containing query parameters. */ -export function addAuthenticationParameter(url, parameters, securityToken) { - if (!url || !isAuthenticationActivated()) { - return parameters; - } - switch (getAuthenticationMethod(url)) { - case 'authkey': { - const token = !isNil(securityToken) ? securityToken : getToken(); - if (!token) { - return parameters; - } - const authParam = getAuthKeyParameter(url); - return Object.assign(parameters || {}, {[authParam]: token}); - } - case 'test': { - const rule = getAuthenticationRule(url); - const token = rule ? rule.token : ""; - const authParam = getAuthKeyParameter(url); - return Object.assign(parameters || {}, { [authParam]: token }); - } - default: - // we cannot handle the required authentication method - return parameters; +export function addAuthenticationParameter(url, parameters, securityToken, sourceId) { + const requestConfig = getRequestConfigurationByUrl(url, securityToken, sourceId); + if (!isEmpty(requestConfig.params)) { + return {...parameters, ...requestConfig.params}; } + return parameters; } /** * This method will add query parameter based authentications to an url. */ export function addAuthenticationToUrl(url) { - if (!url || !isAuthenticationActivated()) { + if (!url || !isRequestConfigurationActivated()) { return url; } const parsedUrl = URL.parse(url, true); @@ -249,22 +394,6 @@ export function cleanAuthParamsFromURL(url) { return ConfigUtils.filterUrlParams(url, [getAuthKeyParameter(url)].filter(p => p)); } -/** - * it creates the headers function for axios config, if it finds a reference in sessionStorage - * @param {string} protectedId the id of the protected service to look for in sessionStorage - * @returns {object} the headers Basic - */ -export const getAuthorizationBasic = (protectedId) => { - let headers = {}; - const storedProtectedService = getCredentials(protectedId); - if (!isEmpty(storedProtectedService)) { - headers = { - Authorization: `Basic ${btoa(storedProtectedService.username + ":" + storedProtectedService.password)}` - }; - } - return headers; -}; - /** * This utility class will get information about the current logged user directly from the store. */ @@ -281,10 +410,6 @@ const SecurityUtils = { getUserAttributes, findUserAttribute, findUserAttributeValue, - getAuthenticationRules, - isAuthenticationActivated, - getAuthenticationMethod, - getAuthenticationRule, addAuthenticationToUrl, addAuthenticationParameter, clearNilValuesForParams, @@ -292,6 +417,11 @@ const SecurityUtils = { getAuthKeyParameter, cleanAuthParamsFromURL, getAuthenticationHeaders, + getRequestConfigurationByUrl, + getRequestConfigurationRules, + getRequestConfigurationRule, + isRequestConfigurationActivated, + convertAuthenticationRulesToRequestConfiguration, USER_GROUP_ALL }; diff --git a/web/client/utils/__tests__/SecurityUtils-test.js b/web/client/utils/__tests__/SecurityUtils-test.js index 979782070da..35776b7ed66 100644 --- a/web/client/utils/__tests__/SecurityUtils-test.js +++ b/web/client/utils/__tests__/SecurityUtils-test.js @@ -183,29 +183,35 @@ describe('Test security utils methods', () => { expect(attributeValue).toBe("263c6917-543f-43e3-8e1a-6a0d29952f72"); }); - it('test get authentication method for an url', () => { - // mocking the authentication rules - ConfigUtils.setConfigProp('authenticationRules', authenticationRules); - expect(SecurityUtils.getAuthenticationRules().length).toBe(3); - // basic authentication should be found - let authenticationMethod = SecurityUtils.getAuthenticationMethod('http://www.some-site.com/index?parameter1=value1¶meter2=value2'); - expect(authenticationMethod).toBe('basic'); - // authkey authentication should be found - authenticationMethod = SecurityUtils.getAuthenticationMethod('http://www.some-site.com/geoserver?parameter1=value1¶meter2=value2'); - expect(authenticationMethod).toBe('authkey'); - // not-supported authentication should be found - authenticationMethod = SecurityUtils.getAuthenticationMethod('http://www.not-supported.com/?parameter1=value1¶meter2=value2'); - expect(authenticationMethod).toBe('not-supported'); - // no authentication method found - authenticationMethod = SecurityUtils.getAuthenticationMethod('http://www.no-authentication.com/?parameter1=value1¶meter2=value2'); - expect(authenticationMethod).toNotExist(); + it('test get request configuration rule for an url', () => { + // Set up request configuration rules (converted from old authenticationRules format) + // Note: unsupported methods are filtered out, so we expect 2 rules (geoserver and some-site) + const requestConfigRules = SecurityUtils.convertAuthenticationRulesToRequestConfiguration(authenticationRules); + expect(requestConfigRules.length).toBe(2); + + // Set the rules in config + ConfigUtils.setConfigProp('requestsConfigurationRules', requestConfigRules); + setSecurityInfo(securityInfoToken); + + // Test basic authentication rule should be found and converted + let rule = SecurityUtils.getRequestConfigurationRule('http://www.some-site.com/index?parameter1=value1¶meter2=value2'); + expect(rule).toExist(); + expect(rule.urlPattern).toBe('.*some-site.*'); + + // Test authkey authentication rule should be found + rule = SecurityUtils.getRequestConfigurationRule('http://www.some-site.com/geoserver?parameter1=value1¶meter2=value2'); + expect(rule).toExist(); + + // Test that no rule matches + rule = SecurityUtils.getRequestConfigurationRule('http://www.no-matching.com/?parameter1=value1¶meter2=value2'); + expect(rule).toNotExist(); }); it('test add authkey authentication to url', () => { - // mocking the authentication rules + // Convert authentication rules to new format and set them + const requestConfigRules = SecurityUtils.convertAuthenticationRulesToRequestConfiguration(authenticationRules); ConfigUtils.setConfigProp("useAuthenticationRules", true); - ConfigUtils.setConfigProp('authenticationRules', authenticationRules); - expect(SecurityUtils.getAuthenticationRules().length).toBe(3); + ConfigUtils.setConfigProp('requestsConfigurationRules', requestConfigRules); // authkey authentication with no user let urlWithAuthentication = SecurityUtils.addAuthenticationToUrl('http://www.some-site.com/geoserver?parameter1=value1¶meter2=value2'); expect(urlWithAuthentication).toBe('http://www.some-site.com/geoserver?parameter1=value1¶meter2=value2'); @@ -223,6 +229,7 @@ describe('Test security utils methods', () => { expect(urlWithAuthentication).toBe('http://www.some-site.com/index?parameter1=value1¶meter2=value2'); // authkey authentication with a user providing a uuid but authentication deactivated ConfigUtils.setConfigProp("useAuthenticationRules", false); + ConfigUtils.setConfigProp('requestsConfigurationRules', []); setSecurityInfo(securityInfoC); urlWithAuthentication = SecurityUtils.addAuthenticationToUrl('http://www.some-site.com/geoserver?parameter1=value1¶meter2=value2'); expect(urlWithAuthentication).toBe('http://www.some-site.com/geoserver?parameter1=value1¶meter2=value2'); @@ -240,10 +247,17 @@ describe('Test security utils methods', () => { expect(SecurityUtils.getAuthenticationHeaders("http://header-site.com/something", null)).toEqual({'X-Auth-Token': 'goodtoken'}); }); it('test getAuthenticationHeaders using basic auth', () => { + const creds = {username: "testuser", password: "testpass"}; + SecurityUtils.setCredentials("id2", creds); setSecurityInfo(securityInfoToken); - ConfigUtils.setConfigProp("useAuthenticationRules", true); - ConfigUtils.setConfigProp('authenticationRules', headerAuthenticationRules); - expect(SecurityUtils.getAuthenticationHeaders("http://header-site.com/something", null, {sourceId: "id2"})).toEqual({Authorization: "Basic dW5kZWZpbmVkOnVuZGVmaW5lZA=="}); + ConfigUtils.setConfigProp("useAuthenticationRules", false); + // Use a rule that doesn't match, so it falls back to sourceId basic auth + ConfigUtils.setConfigProp('requestsConfigurationRules', []); + + const result = SecurityUtils.getAuthenticationHeaders("http://other-site.com/something", null, {sourceId: "id2"}); + expect(result).toExist(); + expect(result.Authorization).toExist(); + expect(result.Authorization).toInclude('Basic'); }); it('cleanAuthParamsFromURL', () => { // mocking the authentication rules @@ -282,4 +296,454 @@ describe('Test security utils methods', () => { headers = SecurityUtils.getAuthorizationBasic(); expect(headers).toEqual({}); }); + + describe('getRequestConfigurationByUrl', () => { + it('should return empty object when not activated', () => { + ConfigUtils.setConfigProp('requestsConfigurationRules', null); + const result = SecurityUtils.getRequestConfigurationByUrl('https://example.com/api'); + expect(result).toEqual({}); + }); + + it('should return headers configuration with Bearer token', () => { + const rules = [ + { + urlPattern: '.*api.*', + headers: { 'Authorization': 'Bearer ${securityToken}' } + } + ]; + ConfigUtils.setConfigProp('requestsConfigurationRules', rules); + ConfigUtils.setConfigProp('useAuthenticationRules', true); + setSecurityInfo(securityInfoToken); + + const result = SecurityUtils.getRequestConfigurationByUrl('https://example.com/api'); + expect(result.headers).toExist(); + expect(result.headers.Authorization).toBe('Bearer goodtoken'); + }); + + it('should return params configuration with authkey', () => { + const rules = [ + { + urlPattern: '.*geoserver.*', + params: { 'authkey': '${securityToken}' } + } + ]; + ConfigUtils.setConfigProp('requestsConfigurationRules', rules); + ConfigUtils.setConfigProp('useAuthenticationRules', true); + setSecurityInfo(securityInfoToken); + + const result = SecurityUtils.getRequestConfigurationByUrl('https://example.com/geoserver/wms'); + expect(result.params).toExist(); + expect(result.params.authkey).toBe('goodtoken'); + }); + + it('should use sourceId for basic auth when provided', () => { + const creds = {username: "testuser", password: "testpass"}; + SecurityUtils.setCredentials("source123", creds); + + const result = SecurityUtils.getRequestConfigurationByUrl('https://example.com/api', null, "source123"); + expect(result.headers).toExist(); + expect(result.headers.Authorization).toExist(); + }); + }); + + describe('isRequestConfigurationActivated', () => { + it('should return false when user has no token', () => { + setSecurityInfo(securityInfoA); // No token + const result = SecurityUtils.isRequestConfigurationActivated(); + expect(result).toBe(false); + }); + + it('should return true when user has token and rules exist in state', () => { + const stateWithRules = { + user: securityInfoC.user, + token: 'test-token', + rules: [ + { + urlPattern: '.*api.*', + headers: { 'Authorization': 'Bearer ${securityToken}' } + } + ] + }; + setSecurityInfo(stateWithRules); + const result = SecurityUtils.isRequestConfigurationActivated(); + expect(result).toBe(true); + }); + + it('should return true when user has token and rules exist in config', () => { + setSecurityInfo(securityInfoToken); + ConfigUtils.setConfigProp('requestsConfigurationRules', [ + { + urlPattern: '.*api.*', + headers: { 'Authorization': 'Bearer ${securityToken}' } + } + ]); + const result = SecurityUtils.isRequestConfigurationActivated(); + expect(result).toBe(true); + }); + + it('should return true for legacy authenticationRules when enabled', () => { + setSecurityInfo(securityInfoToken); + ConfigUtils.setConfigProp('authenticationRules', authenticationRules); + ConfigUtils.setConfigProp('useAuthenticationRules', true); + const result = SecurityUtils.isRequestConfigurationActivated(); + expect(result).toBe(true); + }); + }); + + describe('convertAuthenticationRulesToRequestConfiguration', () => { + it('should convert bearer method', () => { + const authRules = [ + { urlPattern: '.*api.*', method: 'bearer' } + ]; + const result = SecurityUtils.convertAuthenticationRulesToRequestConfiguration(authRules); + expect(result.length).toBe(1); + expect(result[0].urlPattern).toBe('.*api.*'); + expect(result[0].headers.Authorization).toBe('Bearer ${securityToken}'); + }); + + it('should convert authkey method', () => { + const authRules = [ + { urlPattern: '.*geoserver.*', method: 'authkey', authkeyParamName: 'token' } + ]; + const result = SecurityUtils.convertAuthenticationRulesToRequestConfiguration(authRules); + expect(result.length).toBe(1); + expect(result[0].urlPattern).toBe('.*geoserver.*'); + expect(result[0].params.token).toBe('${securityToken}'); + }); + + it('should convert basic method', () => { + const authRules = [ + { urlPattern: '.*api.*', method: 'basic' } + ]; + const result = SecurityUtils.convertAuthenticationRulesToRequestConfiguration(authRules); + expect(result.length).toBe(1); + expect(result[0].headers.Authorization).toBe('${authHeader}'); + }); + + it('should convert header method', () => { + const authRules = [ + { + urlPattern: '.*api.*', + method: 'header', + headers: { 'X-API-Key': 'test123' } + } + ]; + const result = SecurityUtils.convertAuthenticationRulesToRequestConfiguration(authRules); + expect(result.length).toBe(1); + expect(result[0].headers['X-API-Key']).toBe('test123'); + }); + + it('should convert browserWithCredentials method', () => { + const authRules = [ + { urlPattern: '.*api.*', method: 'browserWithCredentials' } + ]; + const result = SecurityUtils.convertAuthenticationRulesToRequestConfiguration(authRules); + expect(result.length).toBe(1); + expect(result[0].withCredentials).toBe(true); + }); + + it('should filter out unsupported methods', () => { + const authRules = [ + { urlPattern: '.*api.*', method: 'bearer' }, + { urlPattern: '.*unsupported.*', method: 'unsupported' } + ]; + const result = SecurityUtils.convertAuthenticationRulesToRequestConfiguration(authRules); + expect(result.length).toBe(1); + expect(result[0].urlPattern).toBe('.*api.*'); + }); + }); + + describe('getRequestConfigurationRules', () => { + it('should return rules from Redux state first', () => { + const rulesInState = [ + { urlPattern: '.*api.*', headers: { 'Authorization': 'Bearer ${securityToken}' } } + ]; + setSecurityInfo({ user: securityInfoToken.user, token: 'test', rules: rulesInState }); + + const result = SecurityUtils.getRequestConfigurationRules(); + expect(result).toEqual(rulesInState); + }); + + it('should return rules from config when state is empty', () => { + const rulesInConfig = [ + { urlPattern: '.*api.*', headers: { 'Authorization': 'Bearer ${securityToken}' } } + ]; + ConfigUtils.setConfigProp('requestsConfigurationRules', rulesInConfig); + + const result = SecurityUtils.getRequestConfigurationRules(); + expect(result).toEqual(rulesInConfig); + }); + + it('should convert legacy authenticationRules when new format missing', () => { + ConfigUtils.setConfigProp('requestsConfigurationRules', null); + ConfigUtils.setConfigProp('authenticationRules', authenticationRules); + + const result = SecurityUtils.getRequestConfigurationRules(); + // Unsupported methods are filtered out, so we expect 2 rules + expect(result.length).toBe(2); + expect(result[0].urlPattern).toBe('.*geoserver.*'); + }); + }); + + describe('getAuthKeyParameter', () => { + it('should return authkey parameter from rule', () => { + const rules = [ + { + urlPattern: '.*api.*', + params: { 'customAuthKey': '${securityToken}' } + } + ]; + ConfigUtils.setConfigProp('requestsConfigurationRules', rules); + + const result = SecurityUtils.getAuthKeyParameter('https://example.com/api'); + expect(result).toBe('customAuthKey'); + }); + + it('should return default authkey when no rule found', () => { + ConfigUtils.setConfigProp('requestsConfigurationRules', null); + + const result = SecurityUtils.getAuthKeyParameter('https://example.com/api'); + expect(result).toBe('authkey'); + }); + }); + + describe('addAuthenticationParameter', () => { + it('should add authentication params to existing parameters object', () => { + const rules = [ + { + urlPattern: '.*geoserver.*', + params: { 'authkey': '${securityToken}' } + } + ]; + ConfigUtils.setConfigProp('requestsConfigurationRules', rules); + ConfigUtils.setConfigProp('useAuthenticationRules', true); + setSecurityInfo(securityInfoToken); + + const existingParams = { param1: 'value1' }; + const result = SecurityUtils.addAuthenticationParameter('https://geoserver.example.com/wms', existingParams); + + expect(result).toExist(); + expect(result.param1).toBeTruthy(); + expect(result.authkey).toBeTruthy(); + expect(result.authkey).toBe('goodtoken'); + }); + + it('should not mutate original parameters object', () => { + const rules = [ + { + urlPattern: '.*geoserver.*', + params: { 'authkey': '${securityToken}' } + } + ]; + ConfigUtils.setConfigProp('requestsConfigurationRules', rules); + ConfigUtils.setConfigProp('useAuthenticationRules', true); + setSecurityInfo(securityInfoToken); + + const originalParams = { param1: 'value1' }; + const result = SecurityUtils.addAuthenticationParameter('https://geoserver.example.com/wms', originalParams); + + expect(result).toExist(); + expect(originalParams).toEqual({ param1: 'value1' }); // Original unchanged + expect(result).toNotEqual(originalParams); // New object + }); + + it('should return original params when no auth params available', () => { + ConfigUtils.setConfigProp('requestsConfigurationRules', null); + const params = { param1: 'value1' }; + + const result = SecurityUtils.addAuthenticationParameter('https://example.com/api', params); + expect(result).toEqual(params); + }); + + it('should pass sourceId to getRequestConfigurationByUrl', () => { + const creds = { username: "testuser", password: "testpass" }; + SecurityUtils.setCredentials("testSource", creds); + + const params = { param1: 'value1' }; + const result = SecurityUtils.addAuthenticationParameter('https://example.com/api', params, null, "testSource"); + + expect(result).toExist(); + expect(result.param1).toBe('value1'); + }); + }); + + describe('addAuthenticationToSLD', () => { + it('should add authentication to SLD URL', () => { + const rules = [ + { + urlPattern: '.*geoserver.*', + params: { 'authkey': '${securityToken}' } + } + ]; + ConfigUtils.setConfigProp('requestsConfigurationRules', rules); + ConfigUtils.setConfigProp('useAuthenticationRules', true); + setSecurityInfo(securityInfoToken); + + const layerParams = { + SLD: 'http://geoserver.example.com/sld?LAYER=layer1' + }; + const options = { securityToken: 'testtoken' }; + + const result = SecurityUtils.addAuthenticationToSLD(layerParams, options); + expect(result.SLD).toInclude('authkey=testtoken'); + }); + + it('should return original layerParams when no SLD', () => { + const layerParams = { LAYERS: 'layer1' }; + const options = { securityToken: 'testtoken' }; + + const result = SecurityUtils.addAuthenticationToSLD(layerParams, options); + expect(result).toEqual(layerParams); + }); + }); + + describe('getAuthenticationHeaders', () => { + it('should return headers from request config', () => { + const rules = [ + { + urlPattern: '.*api.*', + headers: { 'Authorization': 'Bearer ${securityToken}' } + } + ]; + ConfigUtils.setConfigProp('requestsConfigurationRules', rules); + ConfigUtils.setConfigProp('useAuthenticationRules', true); + setSecurityInfo(securityInfoToken); + + const result = SecurityUtils.getAuthenticationHeaders('https://api.example.com', null); + expect(result).toExist(); + expect(result.Authorization).toBe('Bearer goodtoken'); + }); + + it('should return null when no headers available', () => { + ConfigUtils.setConfigProp('requestsConfigurationRules', null); + + const result = SecurityUtils.getAuthenticationHeaders('https://api.example.com', null); + expect(result).toBe(null); + }); + + it('should use sourceId for basic auth', () => { + const creds = { username: "testuser", password: "testpass" }; + SecurityUtils.setCredentials("testSource", creds); + + const result = SecurityUtils.getAuthenticationHeaders('https://api.example.com', null, { sourceId: "testSource" }); + expect(result).toExist(); + expect(result.Authorization).toExist(); + }); + }); + + describe('getToken', () => { + it('should return token from security info', () => { + setSecurityInfo(securityInfoToken); + const token = SecurityUtils.getToken(); + expect(token).toBe('goodtoken'); + }); + + it('should return null when no token', () => { + setSecurityInfo(securityInfoA); + const token = SecurityUtils.getToken(); + expect(token).toBe(undefined); + }); + }); + + describe('getBasicAuthHeader', () => { + it('should return basic auth header', () => { + const securityInfoWithAuth = { ...securityInfoToken, authHeader: 'Basic dGVzdDp0ZXN0' }; + setSecurityInfo(securityInfoWithAuth); + const authHeader = SecurityUtils.getBasicAuthHeader(); + expect(authHeader).toBe('Basic dGVzdDp0ZXN0'); + }); + }); + + describe('getRefreshToken', () => { + it('should return refresh token', () => { + const securityInfoWithRefresh = { ...securityInfoToken, refresh_token: 'refresh-token-123' }; + setSecurityInfo(securityInfoWithRefresh); + const refreshToken = SecurityUtils.getRefreshToken(); + expect(refreshToken).toBe('refresh-token-123'); + }); + }); + + describe('getUser', () => { + it('should return user from security info', () => { + setSecurityInfo(securityInfoToken); + const user = SecurityUtils.getUser(); + expect(user).toExist(); + expect(user.name).toBe(securityInfoC.user.name); + }); + + it('should return undefined when no user', () => { + setSecurityInfo({}); + const user = SecurityUtils.getUser(); + expect(user).toBe(undefined); + }); + }); + + describe('getSecurityInfo', () => { + it('should return security info object', () => { + setSecurityInfo(securityInfoToken); + const info = SecurityUtils.getSecurityInfo(); + expect(info).toExist(); + expect(info.user).toExist(); + expect(info.token).toBe('goodtoken'); + }); + + it('should return empty object when no security info', () => { + setSecurityInfo({}); + const info = SecurityUtils.getSecurityInfo(); + expect(info).toEqual({}); + }); + }); + + describe('addAuthenticationToUrl', () => { + it('should add authkey parameter to URL when activated', () => { + const rules = [ + { + urlPattern: '.*geoserver.*', + params: { 'authkey': '${securityToken}' } + } + ]; + ConfigUtils.setConfigProp('requestsConfigurationRules', rules); + ConfigUtils.setConfigProp('useAuthenticationRules', true); + setSecurityInfo(securityInfoToken); + + const url = 'http://geoserver.example.com/wms?LAYERS=layer1'; + const result = SecurityUtils.addAuthenticationToUrl(url); + expect(result).toInclude('authkey=goodtoken'); + }); + + it('should return original URL when not activated', () => { + ConfigUtils.setConfigProp('requestsConfigurationRules/E', null); + + const url = 'http://geoserver.example.com/wms?LAYERS=layer1'; + const result = SecurityUtils.addAuthenticationToUrl(url); + expect(result).toBe(url); + }); + + it('should return original URL when null', () => { + const result = SecurityUtils.addAuthenticationToUrl(null); + expect(result).toBe(null); + }); + }); + + describe('cleanAuthParamsFromURL', () => { + it('should remove authkey parameter from URL', () => { + const rules = [ + { + urlPattern: '.*geoserver.*', + params: { 'authkey': '${securityToken}' } + } + ]; + ConfigUtils.setConfigProp('requestsConfigurationRules', rules); + + const url = 'http://geoserver.example.com/wms?LAYERS=layer1&authkey=test123'; + const result = SecurityUtils.cleanAuthParamsFromURL(url); + expect(result).toNotInclude('authkey'); + }); + + it('should handle URLs without auth parameters', () => { + const url = 'http://example.com/api?param1=value1'; + const result = SecurityUtils.cleanAuthParamsFromURL(url); + expect(result).toExist(); + }); + }); }); diff --git a/web/client/utils/cesium/WMSUtils.js b/web/client/utils/cesium/WMSUtils.js index f2463aafcda..9dd08877bf8 100644 --- a/web/client/utils/cesium/WMSUtils.js +++ b/web/client/utils/cesium/WMSUtils.js @@ -8,7 +8,7 @@ import * as Cesium from 'cesium'; import { isArray } from 'lodash'; -import { addAuthenticationToSLD, getAuthenticationHeaders } from "../SecurityUtils"; +import { addAuthenticationParameter, addAuthenticationToSLD, getAuthenticationHeaders } from "../SecurityUtils"; import { getProxyUrl } from "../ProxyUtils"; import ConfigUtils from "../ConfigUtils"; import { creditsToAttribution, getAuthenticationParam, getURLs, getWMSVendorParams } from "../LayersUtils"; @@ -96,13 +96,13 @@ export const wmsToCesiumOptionsBIL = (layer) => { export function wmsToCesiumOptions(options) { var opacity = options.opacity !== undefined ? options.opacity : 1; - const params = optionsToVendorParams(options); + let params = optionsToVendorParams(options); const cr = options.credits; const credit = cr ? new Cesium.Credit(creditsToAttribution(cr)) : options.attribution; // NOTE: can we use opacity to manage visibility? const urls = getURLs(isArray(options.url) ? options.url : [options.url]); const headers = getAuthenticationHeaders(urls[0], options.securityToken, options.security); - + params = addAuthenticationParameter(urls[0], params, options.securityToken); return { url: new Cesium.Resource({ url: "{s}", diff --git a/web/client/utils/mapinfo/wfs.js b/web/client/utils/mapinfo/wfs.js index e81a9e0d1ee..a864a7084d5 100644 --- a/web/client/utils/mapinfo/wfs.js +++ b/web/client/utils/mapinfo/wfs.js @@ -16,7 +16,7 @@ import { describeFeatureType, getFeature } from '../../api/WFS'; import { extractGeometryAttributeName } from '../WFSLayerUtils'; -import {addAuthenticationToSLD, getAuthorizationBasic} from '../SecurityUtils'; +import {addAuthenticationToSLD} from '../SecurityUtils'; // if the url uses following constant means the whole workflow is managed client side // and prevent request to a service @@ -99,7 +99,6 @@ const getIdentifyGeometry = point => { export default { buildRequest, getIdentifyFlow: (layer = {}, baseURL, defaultParams) => { - const headers = getAuthorizationBasic(layer?.security?.sourceId); const { point, features, ...baseParams } = defaultParams || {}; if (features) { if (baseURL && baseURL !== CLIENT_WORKFLOW) { @@ -112,7 +111,7 @@ export default { ...baseParams } }, filterIdsCQL); - return Observable.defer(() => getFeature(baseURL, layer.name, params, {headers})); + return Observable.defer(() => getFeature(baseURL, layer.name, params, {_msAuthSourceId: layer?.security?.sourceId})); } return Observable.of({ data: { @@ -134,8 +133,12 @@ export default { } }, - params: Object.assign({}, layer.baseParams, layer.params, baseParams) + params: { + ...layer.baseParams, + ...layer.params, + ...baseParams + } }); - return getFeature(baseURL, layer.name, params, {headers}); + return getFeature(baseURL, layer.name, params, {_msAuthSourceId: layer?.security?.sourceId}); })); }}; diff --git a/web/client/utils/mapinfo/wms.js b/web/client/utils/mapinfo/wms.js index f488ef62b2f..414791bc3d7 100644 --- a/web/client/utils/mapinfo/wms.js +++ b/web/client/utils/mapinfo/wms.js @@ -16,7 +16,7 @@ import { generateEnvString } from '../LayerLocalizationUtils'; import axios from "../../libs/ajax"; // import {parseString} from "xml2js"; // import {stripPrefix} from "xml2js/lib/processors"; -import {addAuthenticationToSLD, getAuthorizationBasic} from '../SecurityUtils'; +import {addAuthenticationToSLD} from '../SecurityUtils'; import { interceptOGCError } from '../ObservableUtils'; export default { /** @@ -97,8 +97,7 @@ export default { * @param {object} params for the request */ getIdentifyFlow: (layer, basePath, params) => { - const headers = getAuthorizationBasic(layer?.security?.sourceId); - return Observable.defer(() => axios.get(basePath, { params, headers })) + return Observable.defer(() => axios.get(basePath, { params, _msAuthSourceId: layer?.security?.sourceId })) .let(interceptOGCError); } }; diff --git a/web/client/utils/mapinfo/wmts.js b/web/client/utils/mapinfo/wmts.js index ee002dbe518..1025c2f246d 100644 --- a/web/client/utils/mapinfo/wmts.js +++ b/web/client/utils/mapinfo/wmts.js @@ -21,8 +21,6 @@ import { } from '../WMTSUtils'; import {getLayerUrl} from '../LayersUtils'; import {optionsToVendorParams} from '../VendorParamsUtils'; -import { getAuthorizationBasic } from '../SecurityUtils'; - import {isObject, isNil, get} from 'lodash'; import Rx, {Observable} from "rxjs"; @@ -113,8 +111,7 @@ export default { }; }, getIdentifyFlow: (layer, basePath, params) => { - const headers = getAuthorizationBasic(layer?.security?.sourceId); - return Observable.defer(() => axios.get(basePath, { params, headers })) + return Observable.defer(() => axios.get(basePath, { params, _msAuthSourceId: layer?.security?.sourceId })) .catch((e) => { if (e.data.indexOf("ExceptionReport") > 0) { return Rx.Observable.bindNodeCallback( (data, callback) => parseString(data, { From 631e25a8d683c588f5fc20af56fea04670bfceda Mon Sep 17 00:00:00 2001 From: Suren Date: Thu, 30 Oct 2025 00:36:38 +0530 Subject: [PATCH 2/8] updated unit tests --- docs/developer-guide/local-config.md | 3 +- web/client/components/misc/SecureImage.jsx | 49 +++++++++++--- web/client/epics/security.js | 4 +- web/client/libs/ajax.js | 12 ++-- web/client/observables/wms.js | 6 +- .../selectors/__tests__/catalog-test.js | 9 ++- web/client/utils/SecurityUtils.js | 64 ++++++++++++++----- .../utils/__tests__/SecurityUtils-test.js | 2 +- 8 files changed, 108 insertions(+), 41 deletions(-) diff --git a/docs/developer-guide/local-config.md b/docs/developer-guide/local-config.md index 8a95038d673..c2539bac570 100644 --- a/docs/developer-guide/local-config.md +++ b/docs/developer-guide/local-config.md @@ -165,9 +165,8 @@ For configuring plugins, see the [Configuring Plugins Section](plugins-documenta - `useAuthenticationRules` (deprecated): if this flag is set to true, legacy `authenticationRules` will be used. The new `requestsConfigurationRules` system does not require this flag and is always active when rules are present. - `requestsConfigurationRules`: is an array of objects that contain rules to match for request configuration. Each rule has a `urlPattern` regex to match and either `headers`, `params`, or `withCredentials` configuration. If the URL of a request matches the `urlPattern` of a rule, the configuration will be applied to the request. - **Available variables for template substitution (ES6 template syntax `${variable}`):** + **Available variable for template substitution (ES6 template syntax `${variable}`):** - `${securityToken}` - The current MapStore session token (automatically replaced) - - `${authHeader}` - The basic authentication header (automatically replaced) **Configuration options:** - `headers` - Object containing HTTP headers to add to matching requests. Example: diff --git a/web/client/components/misc/SecureImage.jsx b/web/client/components/misc/SecureImage.jsx index 858f0c8fa08..038c1b40c87 100644 --- a/web/client/components/misc/SecureImage.jsx +++ b/web/client/components/misc/SecureImage.jsx @@ -8,6 +8,8 @@ import React, { useEffect, useState } from 'react'; import axios from '../../libs/ajax'; +import { getAuthenticationMethod, getAuthKeyParameter, getToken } from '../../utils/SecurityUtils'; +import { updateUrlParams } from '../../utils/URLUtils'; const SecureImage = ({ @@ -26,17 +28,44 @@ const SecureImage = ({ } }; useEffect(() => { - // The axios interceptor will handle authentication based on URL rules - axios.get(src, { - responseType: 'blob' - }) - .then((response) => { - const imageUrl = URL.createObjectURL(response.data); - setImageSrc(imageUrl); + const authMethod = getAuthenticationMethod(src); + + if (authMethod === "bearer") { + axios.get(src, { + responseType: 'blob' }) - .catch((error) => { - console.error('Error fetching image:', error); - }); + .then((response) => { + const imageUrl = URL.createObjectURL(response.data); + setImageSrc(imageUrl); + }) + .catch((error) => { + console.error('Error fetching image:', error); + }); + } else if (authMethod === "authkey") { + const authParam = getAuthKeyParameter(src); + const token = getToken(); + if (authParam && token) { + const newSrc = updateUrlParams(src, {[authParam]: token}); + setImageSrc(newSrc); + } else { + setImageSrc(src); + } + + } else if (props?.layer?.security?.sourceId) { + axios.get(src, { + responseType: 'blob', + _msAuthSourceId: props?.layer?.security?.sourceId + }) + .then((response) => { + const imageUrl = URL.createObjectURL(response.data); + setImageSrc(imageUrl); + }) + .catch((error) => { + console.error('Error fetching image:', error); + }); + } else { + setImageSrc(src); + } // Clean up the URL object when the component unmounts return () => { diff --git a/web/client/epics/security.js b/web/client/epics/security.js index fcd50e7d07f..d1210512ede 100644 --- a/web/client/epics/security.js +++ b/web/client/epics/security.js @@ -223,8 +223,8 @@ export const loadRequestsRulesFromConfigEpic = (action$) => const config = action.config; let rules = config?.requestsConfigurationRules ?? []; const legacyRules = config?.authenticationRules ?? []; - const shouldUseLegacyRules = config?.useAuthenticationRules ?? false; - if (!isEmpty(legacyRules) && (isEmpty(rules) || shouldUseLegacyRules)) { + const useLegacyRules = config?.useAuthenticationRules ?? false; + if (isEmpty(rules) && !isEmpty(legacyRules) && useLegacyRules) { rules = convertAuthenticationRulesToRequestConfiguration(legacyRules); } return Rx.Observable.of(loadRequestsRules(rules)); diff --git a/web/client/libs/ajax.js b/web/client/libs/ajax.js index 6383a289e9a..8b991db6879 100644 --- a/web/client/libs/ajax.js +++ b/web/client/libs/ajax.js @@ -10,8 +10,11 @@ import axios from 'axios'; import combineURLs from 'axios/lib/helpers/combineURLs'; import ConfigUtils from '../utils/ConfigUtils'; import { + getAuthenticationMethod, + getAuthorizationBasic, getRequestConfigurationByUrl, getRequestConfigurationRule, + getToken, isRequestConfigurationActivated } from '../utils/SecurityUtils'; @@ -20,6 +23,7 @@ import omitBy from 'lodash/omitBy'; import isNil from 'lodash/isNil'; import urlUtil from 'url'; import { getProxyCacheByUrl, setProxyCacheByUrl } from '../utils/ProxyUtils'; +import { isEmpty } from 'lodash'; /** * Internal helper that adds an extra paramater to an axios configuration. @@ -51,10 +55,10 @@ function addAuthenticationToAxios(axiosConfig) { // Extract custom sourceId from axios config if provided const sourceId = axiosConfig._msAuthSourceId; - // Only process authentication if sourceId is provided or request configuration is activated - if (!sourceId && !isRequestConfigurationActivated()) { - return axiosConfig; - } + const method = getAuthenticationMethod(axiosUrl); + if (method === "bearer" && !getToken()) return axiosConfig; + if (method === "authkey" && !getToken()) return axiosConfig; + if (method === "basic" && isEmpty(getAuthorizationBasic(sourceId))) return axiosConfig; // If request configuration is not activated but sourceId is provided, still need to handle basic auth const { headers, params } = getRequestConfigurationByUrl(axiosUrl, undefined, sourceId); diff --git a/web/client/observables/wms.js b/web/client/observables/wms.js index ad0f8695937..a5c8952593d 100644 --- a/web/client/observables/wms.js +++ b/web/client/observables/wms.js @@ -40,12 +40,10 @@ export const toDescribeLayerURL = ({name, search = {}, url} = {}) => { }); }; export const describeLayer = l => { - const url = toDescribeLayerURL(l); - return Observable.defer( () => axios.get(url, {_msAuthSourceId: l?.security?.sourceId})).let(interceptOGCError); + return Observable.defer( () => axios.get(toDescribeLayerURL(l), {_msAuthSourceId: l?.security?.sourceId})).let(interceptOGCError); }; export const getLayerCapabilities = l => { - const url = getCapabilitiesUrl(l); - return Observable.defer(() => WMS.getCapabilities(url, {_msAuthSourceId: l?.security?.sourceId})) + return Observable.defer(() => WMS.getCapabilities(getCapabilitiesUrl(l), {_msAuthSourceId: l?.security?.sourceId})) .let(interceptOGCError) .map(c => WMS.parseLayerCapabilities(c, l)); }; diff --git a/web/client/selectors/__tests__/catalog-test.js b/web/client/selectors/__tests__/catalog-test.js index 7ed69533e96..380925ea842 100644 --- a/web/client/selectors/__tests__/catalog-test.js +++ b/web/client/selectors/__tests__/catalog-test.js @@ -232,7 +232,14 @@ describe('Test catalog selectors', () => { expect(retVal).toBe("someval"); }); it('test authkeyParamNameSelector with authkey params set', () => { - const authkeyParamNames = authkeyParamNameSelector(state); + const authkeyParamNames = authkeyParamNameSelector({security: {rules: [ + { + urlPattern: ".*geoserver.*", + params: { + "ms2-authkey": "${securityToken}" + } + } + ]}}); expect(authkeyParamNames).toExist(); expect(authkeyParamNames.length).toBe(1); expect(authkeyParamNames[0]).toBe("ms2-authkey"); diff --git a/web/client/utils/SecurityUtils.js b/web/client/utils/SecurityUtils.js index 6bac2600764..b84c7783164 100644 --- a/web/client/utils/SecurityUtils.js +++ b/web/client/utils/SecurityUtils.js @@ -128,7 +128,8 @@ const parseRequestConfiguration = (config = {}, securityProperties) => { try { // Use lodash template for variable substitution const compiled = template(value); - const result = compiled(securityProperties); + let result = compiled(securityProperties); + result = result === "" ? undefined : result; return [name, result]; } catch (error) { console.warn(`Template parsing error for ${name}:`, error); @@ -165,7 +166,12 @@ export const convertAuthenticationRulesToRequestConfiguration = (authRules = []) break; case 'basic': newRule.headers = { - 'Authorization': '${authHeader}' + 'Authorization': '${securityToken}' + }; + break; + case 'test': + newRule.params = { + [rule.authkeyParamName || 'authkey']: rule.token ?? "" }; break; case 'header': @@ -223,18 +229,39 @@ export const getRequestConfigurationRule = (url) => { )); }; +/** + * Gets the authentication method for a given URL based on request configuration rules. + * Infers the method type from the rule structure for backward compatibility. + * @param {string} url - The URL to check. + * @returns {string} The inferred authentication method. + */ +export function getAuthenticationMethod(url) { + const rule = getRequestConfigurationRule(url); + if (!rule) return null; + + if (rule.params && Object.keys(rule.params).length > 0) { + const hasTokenParam = Object.values(rule.params) + .some(val => typeof val === 'string' && val.includes('${securityToken}')); + if (hasTokenParam) return 'authkey'; + } + + if (rule.headers) { + const authHeader = rule.headers.Authorization || rule.headers.authorization; + if (typeof authHeader === 'string') { + if (authHeader.includes('Bearer')) return 'bearer'; + if (authHeader.includes('Basic')) return 'basic'; + } + } + + return null; +} + /** * Checks if request configuration is activated * Returns true only when user is authenticated and rules are present * @returns {boolean} True if request configuration is activated */ export const isRequestConfigurationActivated = () => { - // Check if user is authenticated (has a token) - const token = getToken(); - if (!token) { - return false; // Not authenticated - } - // Check if redux state exist const state = getState(); if (!isEmpty(state?.security?.rules)) { @@ -302,7 +329,7 @@ export const getRequestConfigurationByUrl = (url, securityToken, sourceId) => { const securityProperties = { ...(!isNil(token) && { securityToken: token }), - ...(!isNil(authHeader) && { authHeader }) + ...(!isNil(authHeader) && { authHeader: authHeader }) }; const parsedHeaders = parseRequestConfiguration(rule.headers, securityProperties); @@ -344,16 +371,24 @@ export function getAuthenticationHeaders(url, securityToken, security) { return null; } +export function clearNilValuesForParams(params = {}) { + return Object.keys(params).reduce((pre, cur) => { + const value = params[cur]; + return !isNil(value) && value !== '' ? {...pre, [cur]: value} : pre; + }, {}); +} + /** * This method will add query parameter based authentications to an object * containing query parameters. */ export function addAuthenticationParameter(url, parameters, securityToken, sourceId) { + let params = {...(parameters ?? {})}; const requestConfig = getRequestConfigurationByUrl(url, securityToken, sourceId); if (!isEmpty(requestConfig.params)) { - return {...parameters, ...requestConfig.params}; + params = {...params, ...requestConfig.params}; } - return parameters; + return clearNilValuesForParams(params); } /** @@ -370,12 +405,6 @@ export function addAuthenticationToUrl(url) { return URL.format(parsedUrl); } -export function clearNilValuesForParams(params = {}) { - return Object.keys(params).reduce((pre, cur) => { - return !isNil(params[cur]) ? {...pre, [cur]: params[cur]} : pre; - }, {}); -} - export function addAuthenticationToSLD(layerParams, options) { if (layerParams.SLD) { const parsed = URL.parse(layerParams.SLD, true); @@ -420,6 +449,7 @@ const SecurityUtils = { getRequestConfigurationByUrl, getRequestConfigurationRules, getRequestConfigurationRule, + getAuthenticationMethod, isRequestConfigurationActivated, convertAuthenticationRulesToRequestConfiguration, USER_GROUP_ALL diff --git a/web/client/utils/__tests__/SecurityUtils-test.js b/web/client/utils/__tests__/SecurityUtils-test.js index 35776b7ed66..dacffde3e60 100644 --- a/web/client/utils/__tests__/SecurityUtils-test.js +++ b/web/client/utils/__tests__/SecurityUtils-test.js @@ -417,7 +417,7 @@ describe('Test security utils methods', () => { ]; const result = SecurityUtils.convertAuthenticationRulesToRequestConfiguration(authRules); expect(result.length).toBe(1); - expect(result[0].headers.Authorization).toBe('${authHeader}'); + expect(result[0].headers.Authorization).toBe('${securityToken}'); }); it('should convert header method', () => { From 883951cbcd31405b28ad75ccc1d746ec50d96398 Mon Sep 17 00:00:00 2001 From: Suren Date: Thu, 30 Oct 2025 01:56:40 +0530 Subject: [PATCH 3/8] code refactor --- web/client/libs/ajax.js | 2 +- web/client/utils/SecurityUtils.js | 41 +++++++++++++------ .../utils/__tests__/SecurityUtils-test.js | 8 +--- web/client/utils/cesium/WMSUtils.js | 6 +-- 4 files changed, 33 insertions(+), 24 deletions(-) diff --git a/web/client/libs/ajax.js b/web/client/libs/ajax.js index 8b991db6879..41810d0f1a2 100644 --- a/web/client/libs/ajax.js +++ b/web/client/libs/ajax.js @@ -58,7 +58,7 @@ function addAuthenticationToAxios(axiosConfig) { const method = getAuthenticationMethod(axiosUrl); if (method === "bearer" && !getToken()) return axiosConfig; if (method === "authkey" && !getToken()) return axiosConfig; - if (method === "basic" && isEmpty(getAuthorizationBasic(sourceId))) return axiosConfig; + if (method === "basic" && sourceId && isEmpty(getAuthorizationBasic(sourceId))) return axiosConfig; // If request configuration is not activated but sourceId is provided, still need to handle basic auth const { headers, params } = getRequestConfigurationByUrl(axiosUrl, undefined, sourceId); diff --git a/web/client/utils/SecurityUtils.js b/web/client/utils/SecurityUtils.js index b84c7783164..e58f7d6c176 100644 --- a/web/client/utils/SecurityUtils.js +++ b/web/client/utils/SecurityUtils.js @@ -166,7 +166,7 @@ export const convertAuthenticationRulesToRequestConfiguration = (authRules = []) break; case 'basic': newRule.headers = { - 'Authorization': '${securityToken}' + 'Authorization': 'Basic ${securityToken}' }; break; case 'test': @@ -298,6 +298,22 @@ export const getAuthorizationBasic = (protectedId) => { return headers; }; +/** + * Filter out headers/params that still contain unresolved template variables + * @param {object} obj - The object to filter + * @returns {object} The filtered object + */ +const filterUnresolvedTemplates = (obj) => { + if (typeof obj !== 'object' || !obj) return obj; + return Object.fromEntries( + Object.entries(obj).filter(([, v]) => !String(v).includes('${securityToken}')) + ); +}; + +const basicAuthorizationHeader = (sourceId) => { + return !isNil(sourceId) ? { headers: getAuthorizationBasic(sourceId) } : {}; +}; + /** * Gets request configuration (headers and params) for a given URL * This is the main function that centralizes all request configuration logic @@ -308,14 +324,12 @@ export const getAuthorizationBasic = (protectedId) => { */ export const getRequestConfigurationByUrl = (url, securityToken, sourceId) => { if (!url || !isRequestConfigurationActivated()) { - if (!isNil(sourceId)) { - return { headers: getAuthorizationBasic(sourceId) }; - } - return {}; + return basicAuthorizationHeader(sourceId); } - const rule = getRequestConfigurationRule(url); - if (!rule) return {}; + if (!rule) { + return basicAuthorizationHeader(sourceId); + } const token = !isNil(securityToken) ? securityToken : getToken(); let authHeader = getBasicAuthHeader(); @@ -326,25 +340,25 @@ export const getRequestConfigurationByUrl = (url, securityToken, sourceId) => { basicAuthHeader = getAuthorizationBasic(sourceId); authHeader = basicAuthHeader?.Authorization; } - const securityProperties = { ...(!isNil(token) && { securityToken: token }), ...(!isNil(authHeader) && { authHeader: authHeader }) }; - const parsedHeaders = parseRequestConfiguration(rule.headers, securityProperties); const parsedParams = parseRequestConfiguration(rule.params, securityProperties); let finalHeaders; - if (!isEmpty(parsedHeaders)) { - finalHeaders = parsedHeaders; + const filteredHeaders = filterUnresolvedTemplates(parsedHeaders); + if (!isEmpty(filteredHeaders)) { + finalHeaders = filteredHeaders; } else if (sourceId && !isEmpty(basicAuthHeader)) { finalHeaders = basicAuthHeader; } + const filteredParams = filterUnresolvedTemplates(parsedParams); return { ...(!isEmpty(finalHeaders) && { headers: finalHeaders }), - ...(!isEmpty(parsedParams) && { params: parsedParams }) + ...(!isEmpty(filteredParams) && { params: filteredParams }) }; }; @@ -374,7 +388,8 @@ export function getAuthenticationHeaders(url, securityToken, security) { export function clearNilValuesForParams(params = {}) { return Object.keys(params).reduce((pre, cur) => { const value = params[cur]; - return !isNil(value) && value !== '' ? {...pre, [cur]: value} : pre; + // return !isNil(value) && value !== '' ? {...pre, [cur]: value} : pre; + return !isNil(value) ? {...pre, [cur]: value} : pre; }, {}); } diff --git a/web/client/utils/__tests__/SecurityUtils-test.js b/web/client/utils/__tests__/SecurityUtils-test.js index dacffde3e60..fffecc4d34e 100644 --- a/web/client/utils/__tests__/SecurityUtils-test.js +++ b/web/client/utils/__tests__/SecurityUtils-test.js @@ -347,12 +347,6 @@ describe('Test security utils methods', () => { }); describe('isRequestConfigurationActivated', () => { - it('should return false when user has no token', () => { - setSecurityInfo(securityInfoA); // No token - const result = SecurityUtils.isRequestConfigurationActivated(); - expect(result).toBe(false); - }); - it('should return true when user has token and rules exist in state', () => { const stateWithRules = { user: securityInfoC.user, @@ -417,7 +411,7 @@ describe('Test security utils methods', () => { ]; const result = SecurityUtils.convertAuthenticationRulesToRequestConfiguration(authRules); expect(result.length).toBe(1); - expect(result[0].headers.Authorization).toBe('${securityToken}'); + expect(result[0].headers.Authorization).toBe('Basic ${securityToken}'); }); it('should convert header method', () => { diff --git a/web/client/utils/cesium/WMSUtils.js b/web/client/utils/cesium/WMSUtils.js index 9dd08877bf8..1293f80a65a 100644 --- a/web/client/utils/cesium/WMSUtils.js +++ b/web/client/utils/cesium/WMSUtils.js @@ -7,7 +7,7 @@ */ import * as Cesium from 'cesium'; -import { isArray } from 'lodash'; +import { isArray, castArray } from 'lodash'; import { addAuthenticationParameter, addAuthenticationToSLD, getAuthenticationHeaders } from "../SecurityUtils"; import { getProxyUrl } from "../ProxyUtils"; import ConfigUtils from "../ConfigUtils"; @@ -68,8 +68,8 @@ export const getProxy = (options) => { * @returns {object} converted BIL options */ export const wmsToCesiumOptionsBIL = (layer) => { - let url = layer.url; - const headers = getAuthenticationHeaders(url, layer.securityToken, layer.security); + const url = layer.url; + const headers = getAuthenticationHeaders(castArray(url)[0], layer.securityToken, layer.security); const params = getAuthenticationParam(layer); // specific options for terrain provider now are inside the options parameter // we still use layer object for retrocompatibility From 57b14362bfd37514a40821b3ee8f82ac32ddd420 Mon Sep 17 00:00:00 2001 From: Suren Date: Thu, 30 Oct 2025 12:19:47 +0530 Subject: [PATCH 4/8] remove test --- web/client/epics/security.js | 3 +-- web/client/utils/SecurityUtils.js | 2 -- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/web/client/epics/security.js b/web/client/epics/security.js index d1210512ede..50443b06cdd 100644 --- a/web/client/epics/security.js +++ b/web/client/epics/security.js @@ -281,9 +281,8 @@ export const refreshLayersOnRulesUpdateEpic = (action$, store) => const newSecurity = layer.security ? { ...layer.security, refreshHash: uuidv4() } : { refreshHash: uuidv4() }; - return changeLayerProperties(layer.id, { security: newSecurity, visibility: false }); + return changeLayerProperties(layer.id, { security: newSecurity }); }); return actions.length > 0 ? Rx.Observable.from(actions) : Rx.Observable.empty(); }); - diff --git a/web/client/utils/SecurityUtils.js b/web/client/utils/SecurityUtils.js index e58f7d6c176..bbf67216211 100644 --- a/web/client/utils/SecurityUtils.js +++ b/web/client/utils/SecurityUtils.js @@ -334,7 +334,6 @@ export const getRequestConfigurationByUrl = (url, securityToken, sourceId) => { const token = !isNil(securityToken) ? securityToken : getToken(); let authHeader = getBasicAuthHeader(); - // Fallback to sessionStorage credentials sourceId and credentials is stored let basicAuthHeader; if (sourceId) { basicAuthHeader = getAuthorizationBasic(sourceId); @@ -388,7 +387,6 @@ export function getAuthenticationHeaders(url, securityToken, security) { export function clearNilValuesForParams(params = {}) { return Object.keys(params).reduce((pre, cur) => { const value = params[cur]; - // return !isNil(value) && value !== '' ? {...pre, [cur]: value} : pre; return !isNil(value) ? {...pre, [cur]: value} : pre; }, {}); } From 763e0cf1e64f2207463197eb6cabebfc2de0beb3 Mon Sep 17 00:00:00 2001 From: Suren Date: Thu, 30 Oct 2025 15:57:24 +0530 Subject: [PATCH 5/8] refactor config rule function --- web/client/utils/SecurityUtils.js | 41 +++++++++++++------------------ 1 file changed, 17 insertions(+), 24 deletions(-) diff --git a/web/client/utils/SecurityUtils.js b/web/client/utils/SecurityUtils.js index bbf67216211..d1c47e30632 100644 --- a/web/client/utils/SecurityUtils.js +++ b/web/client/utils/SecurityUtils.js @@ -326,38 +326,31 @@ export const getRequestConfigurationByUrl = (url, securityToken, sourceId) => { if (!url || !isRequestConfigurationActivated()) { return basicAuthorizationHeader(sourceId); } + const rule = getRequestConfigurationRule(url); - if (!rule) { - return basicAuthorizationHeader(sourceId); - } + if (!rule) return basicAuthorizationHeader(sourceId); - const token = !isNil(securityToken) ? securityToken : getToken(); - let authHeader = getBasicAuthHeader(); + const token = securityToken ?? getToken(); + const basicAuth = sourceId ? getAuthorizationBasic(sourceId) : null; + const authHeader = basicAuth?.Authorization ?? getBasicAuthHeader(); - let basicAuthHeader; - if (sourceId) { - basicAuthHeader = getAuthorizationBasic(sourceId); - authHeader = basicAuthHeader?.Authorization; - } - const securityProperties = { + const securityProps = { ...(!isNil(token) && { securityToken: token }), ...(!isNil(authHeader) && { authHeader: authHeader }) }; - const parsedHeaders = parseRequestConfiguration(rule.headers, securityProperties); - const parsedParams = parseRequestConfiguration(rule.params, securityProperties); - - let finalHeaders; - const filteredHeaders = filterUnresolvedTemplates(parsedHeaders); - if (!isEmpty(filteredHeaders)) { - finalHeaders = filteredHeaders; - } else if (sourceId && !isEmpty(basicAuthHeader)) { - finalHeaders = basicAuthHeader; - } - const filteredParams = filterUnresolvedTemplates(parsedParams); + const parsedHeaders = filterUnresolvedTemplates( + parseRequestConfiguration(rule.headers, securityProps) + ); + const params = filterUnresolvedTemplates( + parseRequestConfiguration(rule.params, securityProps) + ); + const headers = !isEmpty(parsedHeaders) + ? parsedHeaders : (!isEmpty(basicAuth) && sourceId ? basicAuth : undefined); + return { - ...(!isEmpty(finalHeaders) && { headers: finalHeaders }), - ...(!isEmpty(filteredParams) && { params: filteredParams }) + ...(!isEmpty(headers) && { headers: headers }), + ...(!isEmpty(params) && { params: params }) }; }; From c41c790006d435e4928e5590922488e85bcc57db Mon Sep 17 00:00:00 2001 From: allyoucanmap Date: Tue, 2 Dec 2025 10:24:28 +0100 Subject: [PATCH 6/8] Add missing logic to support headers and params on layers --- .../map/cesium/plugins/ArcGISLayer.js | 17 +++++++++++------ .../map/cesium/plugins/ElevationLayer.js | 16 ++++++++++++---- .../map/cesium/plugins/GraticuleLayer.js | 4 +--- .../map/cesium/plugins/MarkerLayer.js | 2 +- .../map/cesium/plugins/OverlayLayer.js | 2 +- .../map/cesium/plugins/TerrainLayer.js | 4 ++++ .../map/cesium/plugins/ThreeDTilesLayer.js | 4 ++++ .../map/cesium/plugins/TileProviderLayer.js | 13 ++++++++++--- .../map/cesium/plugins/VectorLayer.js | 2 +- .../map/openlayers/plugins/ArcGISLayer.js | 6 ++++-- .../map/openlayers/plugins/TMSLayer.js | 6 ++++-- .../map/openlayers/plugins/TileProviderLayer.js | 6 ++++-- .../map/openlayers/plugins/WMSLayer.js | 6 ------ .../map/openlayers/plugins/WMTSLayer.js | 10 +++++----- web/client/selectors/catalog.js | 2 +- web/client/utils/SecurityUtils.js | 5 +++++ 16 files changed, 68 insertions(+), 37 deletions(-) diff --git a/web/client/components/map/cesium/plugins/ArcGISLayer.js b/web/client/components/map/cesium/plugins/ArcGISLayer.js index 5d4b78f3629..ee984b04b40 100644 --- a/web/client/components/map/cesium/plugins/ArcGISLayer.js +++ b/web/client/components/map/cesium/plugins/ArcGISLayer.js @@ -9,9 +9,9 @@ import Layers from '../../../../utils/cesium/Layers'; import * as Cesium from 'cesium'; import { isImageServerUrl } from '../../../../utils/ArcGISUtils'; -import { getProxiedUrl } from '../../../../utils/ConfigUtils'; import isEqual from 'lodash/isEqual'; - +import { getRequestConfigurationByUrl } from '../../../../utils/SecurityUtils'; +import { getProxyUrl } from "../../../../utils/ProxyUtils"; // this override is needed to apply the selected format // and to detect an ImageServer and to apply the correct exportImage path @@ -55,9 +55,7 @@ class ArcGisMapAndImageServerImageryProvider extends Cesium.ArcGisMapServerImage constructor(options) { super(options); this._format = options.format; - this._resource = new Cesium.Resource({ - url: options.url - }); + this._resource = options.url; this._resource.appendForwardSlash(); } requestImage = function( @@ -74,8 +72,15 @@ class ArcGisMapAndImageServerImageryProvider extends Cesium.ArcGisMapServerImage } const create = (options) => { + const { headers, params } = getRequestConfigurationByUrl(options.url, null, options.security?.sourceId); + const resource = new Cesium.Resource({ + url: options.url, + queryParameters: params, + headers, + proxy: options.forceProxy ? new Cesium.DefaultProxy(getProxyUrl()) : undefined + }); return new ArcGisMapAndImageServerImageryProvider({ - url: options?.forceProxy ? getProxiedUrl() + encodeURIComponent(options.url) : options.url, + url: resource, ...(options.name !== undefined && { layers: `${options.name}` }), format: options.format, // we need to disable this when using layers ids diff --git a/web/client/components/map/cesium/plugins/ElevationLayer.js b/web/client/components/map/cesium/plugins/ElevationLayer.js index 8a7e8bcf675..2bcf2040dd4 100644 --- a/web/client/components/map/cesium/plugins/ElevationLayer.js +++ b/web/client/components/map/cesium/plugins/ElevationLayer.js @@ -8,7 +8,7 @@ import Layers from '../../../../utils/cesium/Layers'; import * as Cesium from 'cesium'; - +import isEqual from 'lodash/isEqual'; import { wmsToCesiumOptions } from '../../../../utils/cesium/WMSUtils'; import { addElevationTile, getElevation, getElevationKey, getTileRelativePixel } from '../../../../utils/ElevationUtils'; @@ -144,10 +144,18 @@ const createWMSElevationLayer = (options, map) => { return layer; }; +const create = (options, map) => { + if (options.provider === 'wms') { + return createWMSElevationLayer(options, map); + } + return null; +}; + Layers.registerType('elevation', { - create: (options, map) => { - if (options.provider === 'wms') { - return createWMSElevationLayer(options, map); + create, + update: (layer, newOptions, oldOptions, map) => { + if (!isEqual(oldOptions.security, newOptions.security)) { + return create(newOptions, map); } return null; } diff --git a/web/client/components/map/cesium/plugins/GraticuleLayer.js b/web/client/components/map/cesium/plugins/GraticuleLayer.js index f05ab08b391..b9d9070e7a9 100644 --- a/web/client/components/map/cesium/plugins/GraticuleLayer.js +++ b/web/client/components/map/cesium/plugins/GraticuleLayer.js @@ -8,8 +8,6 @@ import Layers from '../../../../utils/cesium/Layers'; import * as Cesium from 'cesium'; -import isEqual from 'lodash/isEqual'; - /** * Created by thomas on 27/01/14. // [source 07APR2015: http://pad.geocento.com/AddOns/Graticule.js] @@ -342,7 +340,7 @@ const createLayer = (options, map) => { Layers.registerType('graticule', { create: createLayer, update: (layer, newOptions, oldOptions, map) => { - if (newOptions.visibility !== oldOptions.visibility || !isEqual(oldOptions.security, newOptions.security)) { + if (newOptions.visibility !== oldOptions.visibility) { layer.setVisible(false); // clear all previous labels and primitive if (newOptions.visibility) { return createLayer(newOptions, map); diff --git a/web/client/components/map/cesium/plugins/MarkerLayer.js b/web/client/components/map/cesium/plugins/MarkerLayer.js index 831e36a80c6..85a94d851c6 100644 --- a/web/client/components/map/cesium/plugins/MarkerLayer.js +++ b/web/client/components/map/cesium/plugins/MarkerLayer.js @@ -45,7 +45,7 @@ Layers.registerType('marker', { }; }, update: function(layer, newOptions, oldOptions, map) { - if (!isEqual(newOptions.point, oldOptions.point) || !isEqual(oldOptions.security, newOptions.security)) { + if (!isEqual(newOptions.point, oldOptions.point)) { return this.create(newOptions, map); } return null; diff --git a/web/client/components/map/cesium/plugins/OverlayLayer.js b/web/client/components/map/cesium/plugins/OverlayLayer.js index bc2410a3e6d..5fc793b88d9 100644 --- a/web/client/components/map/cesium/plugins/OverlayLayer.js +++ b/web/client/components/map/cesium/plugins/OverlayLayer.js @@ -198,7 +198,7 @@ Layers.registerType('overlay', { }; }, update: function(layer, newOptions, oldOptions, map) { - if (!isEqual(newOptions.position, oldOptions.position) || !isEqual(oldOptions.security, newOptions.security)) { + if (!isEqual(newOptions.position, oldOptions.position)) { return this.create(newOptions, map); } return null; diff --git a/web/client/components/map/cesium/plugins/TerrainLayer.js b/web/client/components/map/cesium/plugins/TerrainLayer.js index a62e9dd9e4f..33dd61bfc4e 100644 --- a/web/client/components/map/cesium/plugins/TerrainLayer.js +++ b/web/client/components/map/cesium/plugins/TerrainLayer.js @@ -12,11 +12,15 @@ import GeoServerBILTerrainProvider from '../../../../utils/cesium/GeoServerBILTe import WMSUtils from '../../../../utils/cesium/WMSUtils'; import { getProxyUrl } from "../../../../utils/ProxyUtils"; import isEqual from 'lodash/isEqual'; +import { getRequestConfigurationByUrl } from '../../../../utils/SecurityUtils'; function cesiumOptionsMapping(config) { + const { headers, params } = getRequestConfigurationByUrl(config.url, null, config.security?.sourceId); return { url: new Cesium.Resource({ url: config.url, + headers, + queryParameters: params, proxy: config.forceProxy ? new Cesium.DefaultProxy(getProxyUrl()) : undefined }), options: { diff --git a/web/client/components/map/cesium/plugins/ThreeDTilesLayer.js b/web/client/components/map/cesium/plugins/ThreeDTilesLayer.js index 1414746a52c..17f828ed9e6 100644 --- a/web/client/components/map/cesium/plugins/ThreeDTilesLayer.js +++ b/web/client/components/map/cesium/plugins/ThreeDTilesLayer.js @@ -17,6 +17,7 @@ import tinycolor from 'tinycolor2'; import googleOnWhiteLogo from '../img/google_on_white_hdpi.png'; import googleOnNonWhiteLogo from '../img/google_on_non_white_hdpi.png'; import { createClippingPolygonsFromGeoJSON, applyClippingPolygons } from '../../../../utils/cesium/PrimitivesUtils'; +import { getRequestConfigurationByUrl } from '../../../../utils/SecurityUtils'; const cleanStyle = (style, options) => { if (style && options?.pointCloudShading?.attenuation) { @@ -167,11 +168,14 @@ const createLayer = (options, map) => { detached: true, ...layer, add: () => { + const { headers, params } = getRequestConfigurationByUrl(options.url, null, options.security?.sourceId); // delay creation of tileset when frequents recreation are requested timeout = setTimeout(() => { timeout = undefined; resource = new Cesium.Resource({ url: options.url, + queryParameters: params, + headers, proxy: options.forceProxy ? new Cesium.DefaultProxy(getProxyUrl()) : undefined // TODO: axios supports also adding access tokens or credentials (e.g. authkey, Authentication header ...). // if we want to use internal cesium functionality to retrieve data diff --git a/web/client/components/map/cesium/plugins/TileProviderLayer.js b/web/client/components/map/cesium/plugins/TileProviderLayer.js index c0457b842f1..5e40b6e7f60 100644 --- a/web/client/components/map/cesium/plugins/TileProviderLayer.js +++ b/web/client/components/map/cesium/plugins/TileProviderLayer.js @@ -13,6 +13,7 @@ import ConfigUtils from '../../../../utils/ConfigUtils'; import {creditsToAttribution} from '../../../../utils/LayersUtils'; import {getProxyUrl} from '../../../../utils/ProxyUtils'; import isEqual from 'lodash/isEqual'; +import { getRequestConfigurationByUrl } from '../../../../utils/SecurityUtils'; function splitUrl(originalUrl) { let url = originalUrl; @@ -81,14 +82,20 @@ const create = (options) => { const cr = opt.credits; const credit = cr ? new Cesium.Credit(creditsToAttribution(cr)) : opt.attribution; - return new Cesium.UrlTemplateImageryProvider({ + const { headers, params } = getRequestConfigurationByUrl(options.url, null, options.security?.sourceId); + const resource = new Cesium.Resource({ url: template(url, opt), + queryParameters: params, + headers, + proxy: options?.forceProxy ? new TileProviderProxy(proxyUrl) : new NoProxy() + }); + return new Cesium.UrlTemplateImageryProvider({ + url: resource, enablePickFeatures: false, subdomains: opt.subdomains, maximumLevel: opt.maxZoom, minimumLevel: opt.minZoom, - credit, - proxy: options?.forceProxy ? new TileProviderProxy(proxyUrl) : new NoProxy() + credit }); }; diff --git a/web/client/components/map/cesium/plugins/VectorLayer.js b/web/client/components/map/cesium/plugins/VectorLayer.js index a2021714a76..86c2aba45f0 100644 --- a/web/client/components/map/cesium/plugins/VectorLayer.js +++ b/web/client/components/map/cesium/plugins/VectorLayer.js @@ -60,7 +60,7 @@ const createLayer = (options, map) => { Layers.registerType('vector', { create: createLayer, update: (layer, newOptions, oldOptions, map) => { - if (!isEqual(newOptions.features, oldOptions.features) || !isEqual(oldOptions.security, newOptions.security)) { + if (!isEqual(newOptions.features, oldOptions.features)) { return createLayer(newOptions, map); } if (layer?.styledFeatures && !isEqual(newOptions?.layerFilter, oldOptions?.layerFilter)) { diff --git a/web/client/components/map/openlayers/plugins/ArcGISLayer.js b/web/client/components/map/openlayers/plugins/ArcGISLayer.js index 714a6f63cf0..0394c901a85 100644 --- a/web/client/components/map/openlayers/plugins/ArcGISLayer.js +++ b/web/client/components/map/openlayers/plugins/ArcGISLayer.js @@ -12,6 +12,7 @@ import TileLayer from 'ol/layer/Tile'; import TileArcGISRest from 'ol/source/TileArcGISRest'; import axios from 'axios'; import { isEqual } from 'lodash'; +import { hasRequestConfigurationByUrl } from '../../../../utils/SecurityUtils'; const tileLoadFunction = options => (image, src) => { axios.get(src, { @@ -27,7 +28,7 @@ const tileLoadFunction = options => (image, src) => { registerType('arcgis', { create: (options) => { const sourceOpt = {}; - if (options.security) { + if (hasRequestConfigurationByUrl(options.url, null, options.security?.sourceId)) { sourceOpt.tileLoadFunction = tileLoadFunction(options); } return new TileLayer({ @@ -54,7 +55,8 @@ registerType('arcgis', { if (oldOptions.maxResolution !== newOptions.maxResolution) { layer.setMaxResolution(newOptions.maxResolution === undefined ? Infinity : newOptions.maxResolution); } - if (!isEqual(oldOptions.security, newOptions.security)) { + if (!isEqual(oldOptions.security, newOptions.security) + && hasRequestConfigurationByUrl(newOptions.url, null, newOptions.security?.sourceId)) { layer.getSource().setTileLoadFunction(tileLoadFunction(newOptions)); } }, diff --git a/web/client/components/map/openlayers/plugins/TMSLayer.js b/web/client/components/map/openlayers/plugins/TMSLayer.js index e745cc7d2d8..48f0d2571d9 100644 --- a/web/client/components/map/openlayers/plugins/TMSLayer.js +++ b/web/client/components/map/openlayers/plugins/TMSLayer.js @@ -12,6 +12,7 @@ import TileGrid from 'ol/tilegrid/TileGrid'; import TileLayer from 'ol/layer/Tile'; import Layers from '../../../../utils/openlayers/Layers'; +import { hasRequestConfigurationByUrl } from '../../../../utils/SecurityUtils'; const tileLoadFunction = options => (image, src) => { axios.get(src, { _msAuthSourceId: options.security?.sourceId, @@ -31,7 +32,7 @@ function tileXYZToOpenlayersOptions(options = {}) { url: `${options.tileMapUrl}/{z}/{x}/{-y}.${options.extension}`, // TODO use resolutions attributions: options.attribution ? [options.attribution] : [] }; - if (options.security) { + if (hasRequestConfigurationByUrl(options.url, null, options.security?.sourceId)) { sourceOpt.tileLoadFunction = tileLoadFunction(options); } @@ -84,7 +85,8 @@ Layers.registerType('tms', { if (oldOptions.maxResolution !== newOptions.maxResolution) { layer.setMaxResolution(newOptions.maxResolution === undefined ? Infinity : newOptions.maxResolution); } - if (!isEqual(oldOptions.security, newOptions.security)) { + if (!isEqual(oldOptions.security, newOptions.security) + && hasRequestConfigurationByUrl(newOptions.url, null, newOptions.security?.sourceId)) { layer.getSource().setTileLoadFunction(tileLoadFunction(newOptions)); } } diff --git a/web/client/components/map/openlayers/plugins/TileProviderLayer.js b/web/client/components/map/openlayers/plugins/TileProviderLayer.js index c6df813737a..351bce6c373 100644 --- a/web/client/components/map/openlayers/plugins/TileProviderLayer.js +++ b/web/client/components/map/openlayers/plugins/TileProviderLayer.js @@ -13,6 +13,7 @@ import XYZ from 'ol/source/XYZ'; import TileLayer from 'ol/layer/Tile'; import axios from 'axios'; import { isEqual } from 'lodash'; +import { hasRequestConfigurationByUrl } from '../../../../utils/SecurityUtils'; function lBoundsToOlExtent(bounds, destPrj) { var [ [ miny, minx], [ maxy, maxx ] ] = bounds; return CoordinatesUtils.reprojectBbox([minx, miny, maxx, maxy], 'EPSG:4326', CoordinatesUtils.normalizeSRS(destPrj)); @@ -37,7 +38,7 @@ function tileXYZToOpenlayersOptions(options) { maxZoom: options.maxZoom ? options.maxZoom : 18, minZoom: options.minZoom ? options.minZoom : 0 // dosen't affect ol layer rendering UNSUPPORTED }); - if (options.security) { + if (hasRequestConfigurationByUrl(options.url, null, options.security?.sourceId)) { sourceOpt.tileLoadFunction = tileLoadFunction(options); } let source = new XYZ(sourceOpt); @@ -66,7 +67,8 @@ Layers.registerType('tileprovider', { if (oldOptions.maxResolution !== newOptions.maxResolution) { layer.setMaxResolution(newOptions.maxResolution === undefined ? Infinity : newOptions.maxResolution); } - if (!isEqual(oldOptions.security, newOptions.security)) { + if (!isEqual(oldOptions.security, newOptions.security) + && hasRequestConfigurationByUrl(newOptions.url, null, newOptions.security?.sourceId)) { layer.getSource().setTileLoadFunction(tileLoadFunction(newOptions)); } } diff --git a/web/client/components/map/openlayers/plugins/WMSLayer.js b/web/client/components/map/openlayers/plugins/WMSLayer.js index ddd1851280a..441d245ec10 100644 --- a/web/client/components/map/openlayers/plugins/WMSLayer.js +++ b/web/client/components/map/openlayers/plugins/WMSLayer.js @@ -264,12 +264,6 @@ Layers.registerType('wms', { if (oldOptions.maxResolution !== newOptions.maxResolution) { layer.setMaxResolution(newOptions.maxResolution === undefined ? Infinity : newOptions.maxResolution); } - if (!isEqual(oldOptions.security, newOptions.security)) { - const urls = getWMSURLs(isArray(newOptions.url) ? newOptions.url : [newOptions.url]); - const headers = getAuthenticationHeaders(urls[0], newOptions.securityToken, newOptions.security); - wmsSource.setTileLoadFunction(loadFunction(newOptions, headers)); - wmsSource.refresh(); - } if (needsRefresh) { // forces tile cache drop // this prevents old cached tiles at lower zoom levels to be diff --git a/web/client/components/map/openlayers/plugins/WMTSLayer.js b/web/client/components/map/openlayers/plugins/WMTSLayer.js index 1c48dda7ae6..c1b3d3c73d6 100644 --- a/web/client/components/map/openlayers/plugins/WMTSLayer.js +++ b/web/client/components/map/openlayers/plugins/WMTSLayer.js @@ -13,7 +13,7 @@ import head from 'lodash/head'; import last from 'lodash/last'; import axios from '../../../../libs/ajax'; import { proxySource } from '../../../../utils/openlayers/WMSUtils'; -import {addAuthenticationParameter} from '../../../../utils/SecurityUtils'; +import {addAuthenticationParameter, hasRequestConfigurationByUrl} from '../../../../utils/SecurityUtils'; import * as WMTSUtils from '../../../../utils/WMTSUtils'; import CoordinatesUtils from '../../../../utils/CoordinatesUtils'; import MapUtils from '../../../../utils/MapUtils'; @@ -151,7 +151,7 @@ const createLayer = options => { }), wrapX: true }; - if (options.security?.sourceId) { + if (hasRequestConfigurationByUrl(options.url, null, options.security?.sourceId)) { wmtsOptions.urls = urls.map(url => proxySource(options.forceProxy, url)); wmtsOptions.tileLoadFunction = tileLoadFunction(options); } @@ -186,8 +186,7 @@ const updateLayer = (layer, newOptions, oldOptions) => { || oldOptions.srs !== newOptions.srs || oldOptions.format !== newOptions.format || oldOptions.style !== newOptions.style - || oldOptions.credits !== newOptions.credits - || !isEqual(oldOptions.security, newOptions.security)) { + || oldOptions.credits !== newOptions.credits) { return createLayer(newOptions); } if (oldOptions.minResolution !== newOptions.minResolution) { @@ -196,7 +195,8 @@ const updateLayer = (layer, newOptions, oldOptions) => { if (oldOptions.maxResolution !== newOptions.maxResolution) { layer.setMaxResolution(newOptions.maxResolution === undefined ? Infinity : newOptions.maxResolution); } - if (!isEqual(oldOptions.security, newOptions.security)) { + if (!isEqual(oldOptions.security, newOptions.security) + && hasRequestConfigurationByUrl(newOptions.url, null, newOptions.security?.sourceId)) { layer.getSource().setTileLoadFunction(tileLoadFunction(newOptions)); } return null; diff --git a/web/client/selectors/catalog.js b/web/client/selectors/catalog.js index ac805a7a9e0..4108438970c 100644 --- a/web/client/selectors/catalog.js +++ b/web/client/selectors/catalog.js @@ -53,7 +53,7 @@ export const authkeyParamNameSelector = (state) => { .filter(rule => rule.params) .map(rule => { const authKeyParam = Object.keys(rule.params).find(key => - rule.params[key] && (rule.params[key].includes('${securityToken}')) + rule.params[key] && (`${rule.params[key]}`.includes('${securityToken}')) ); return authKeyParam; }) diff --git a/web/client/utils/SecurityUtils.js b/web/client/utils/SecurityUtils.js index d1c47e30632..0d41d70de76 100644 --- a/web/client/utils/SecurityUtils.js +++ b/web/client/utils/SecurityUtils.js @@ -354,6 +354,11 @@ export const getRequestConfigurationByUrl = (url, securityToken, sourceId) => { }; }; +export const hasRequestConfigurationByUrl = (url, securityToken, sourceId) => { + const { headers, params } = getRequestConfigurationByUrl(url, securityToken, sourceId); + return !!(headers || params); +}; + export function getAuthKeyParameter(url) { // Use the new request configuration system const rule = getRequestConfigurationRule(url); From 2a9e70914ba514068f4c450e63f130cb3ff3d861 Mon Sep 17 00:00:00 2001 From: Suren Date: Thu, 4 Dec 2025 20:43:23 +0530 Subject: [PATCH 7/8] updated refresh hash and add migration guideline --- .../developer-guide/integrations/geoserver.md | 2 +- .../mapstore-migration-guide.md | 59 +++++++++++++++++++ .../map/openlayers/plugins/ArcGISLayer.js | 2 +- .../map/openlayers/plugins/COGLayer.js | 1 + .../map/openlayers/plugins/TMSLayer.js | 2 +- .../openlayers/plugins/TileProviderLayer.js | 2 +- .../map/openlayers/plugins/WFSLayer.js | 5 +- .../map/openlayers/plugins/WMSLayer.js | 1 + .../map/openlayers/plugins/WMTSLayer.js | 8 +-- web/client/configs/localConfig.json | 6 -- web/client/epics/security.js | 5 +- 11 files changed, 74 insertions(+), 19 deletions(-) diff --git a/docs/developer-guide/integrations/geoserver.md b/docs/developer-guide/integrations/geoserver.md index 2e9cfb5f053..7898d941b6a 100644 --- a/docs/developer-guide/integrations/geoserver.md +++ b/docs/developer-guide/integrations/geoserver.md @@ -160,7 +160,7 @@ The last step is to configure MapStore to use the authkey with the configured in //... "requestsConfigurationRules": [ { - "urlPattern": ".*geostore.*", + "urlPattern": ".*rest/geostore.*", "headers": { "Authorization": "Bearer ${securityToken}" } diff --git a/docs/developer-guide/mapstore-migration-guide.md b/docs/developer-guide/mapstore-migration-guide.md index c0d83b1fdad..93708fa1e1f 100644 --- a/docs/developer-guide/mapstore-migration-guide.md +++ b/docs/developer-guide/mapstore-migration-guide.md @@ -20,6 +20,65 @@ This is a list of things to check if you want to update from a previous version - Optionally check also accessory files like `.eslinrc`, if you want to keep aligned with lint standards. - Follow the instructions below, in order, from your version to the one you want to update to. +## Migration from 2025.02.02 to 2026.01.00 + +### Replace authenticationRules with requestsConfigurationRules + +As part of improving the authentication rules to make dynamic request configurations, we have deprecated the use of `authenticationRules` in favor of the new request rule configuration `requestsConfigurationRules`. The new system provides a more flexible way to configure request authentication and parameters. + +### Configuration Changes + +#### Old Configuration (authenticationRules) + +```json +{ + "useAuthenticationRules": true, + "authenticationRules": [ + { + "urlPattern": ".*rest/geostore.*", + "method": "bearer" + }, + { + "urlPattern": ".*rest/config.*", + "method": "bearer" + } + ] +} +``` + +#### New Configuration (requestsConfigurationRules) + +```json +{ + "requestsConfigurationRules": [ + { + "urlPattern": ".*rest/geostore.*", + "headers": { + "Authorization": "Bearer ${securityToken}" + } + }, + { + "urlPattern": ".*rest/config.*", + "headers": { + "Authorization": "Bearer ${securityToken}" + } + } + ] +} +``` + +**Note**: The `${securityToken}` placeholder is automatically replaced at runtime with the actual security token from the security context + +#### Method Mapping + +| Old Method | New Configuration | +|------------|------------------| +| `bearer` | `headers: { "Authorization": "Bearer ${securityToken}" }` | +| `authkey` | `params: { "authkey": "${securityToken}" }` | +| `basic` | `headers: { "Authorization": "${authHeader}" }` | +| `header` | `headers: { ... }` | +| `browserWithCredentials` | `withCredentials: true` | + ## Migration from 2025.01.01 to 2025.02.00 ### Update authenticationRules in localConfig.json diff --git a/web/client/components/map/openlayers/plugins/ArcGISLayer.js b/web/client/components/map/openlayers/plugins/ArcGISLayer.js index 0394c901a85..0fcb35f2644 100644 --- a/web/client/components/map/openlayers/plugins/ArcGISLayer.js +++ b/web/client/components/map/openlayers/plugins/ArcGISLayer.js @@ -56,7 +56,7 @@ registerType('arcgis', { layer.setMaxResolution(newOptions.maxResolution === undefined ? Infinity : newOptions.maxResolution); } if (!isEqual(oldOptions.security, newOptions.security) - && hasRequestConfigurationByUrl(newOptions.url, null, newOptions.security?.sourceId)) { + || !isEqual(oldOptions.requestRuleRefreshHash, newOptions.requestRuleRefreshHash)) { layer.getSource().setTileLoadFunction(tileLoadFunction(newOptions)); } }, diff --git a/web/client/components/map/openlayers/plugins/COGLayer.js b/web/client/components/map/openlayers/plugins/COGLayer.js index cc54ab276e6..2b12f4e21ed 100644 --- a/web/client/components/map/openlayers/plugins/COGLayer.js +++ b/web/client/components/map/openlayers/plugins/COGLayer.js @@ -48,6 +48,7 @@ Layers.registerType('cog', { || !isEqual(newOptions.style, oldOptions.style) || !isEqual(newOptions.security, oldOptions.security) || !isEqual(newOptions.sources, oldOptions.sources) // min/max source data value can change + || !isEqual(oldOptions.requestRuleRefreshHash, newOptions.requestRuleRefreshHash) ) { return create(newOptions, map); } diff --git a/web/client/components/map/openlayers/plugins/TMSLayer.js b/web/client/components/map/openlayers/plugins/TMSLayer.js index 48f0d2571d9..5dcfe493b73 100644 --- a/web/client/components/map/openlayers/plugins/TMSLayer.js +++ b/web/client/components/map/openlayers/plugins/TMSLayer.js @@ -86,7 +86,7 @@ Layers.registerType('tms', { layer.setMaxResolution(newOptions.maxResolution === undefined ? Infinity : newOptions.maxResolution); } if (!isEqual(oldOptions.security, newOptions.security) - && hasRequestConfigurationByUrl(newOptions.url, null, newOptions.security?.sourceId)) { + || !isEqual(oldOptions.requestRuleRefreshHash, newOptions.requestRuleRefreshHash)) { layer.getSource().setTileLoadFunction(tileLoadFunction(newOptions)); } } diff --git a/web/client/components/map/openlayers/plugins/TileProviderLayer.js b/web/client/components/map/openlayers/plugins/TileProviderLayer.js index 351bce6c373..dc490072aa7 100644 --- a/web/client/components/map/openlayers/plugins/TileProviderLayer.js +++ b/web/client/components/map/openlayers/plugins/TileProviderLayer.js @@ -68,7 +68,7 @@ Layers.registerType('tileprovider', { layer.setMaxResolution(newOptions.maxResolution === undefined ? Infinity : newOptions.maxResolution); } if (!isEqual(oldOptions.security, newOptions.security) - && hasRequestConfigurationByUrl(newOptions.url, null, newOptions.security?.sourceId)) { + || !isEqual(oldOptions.requestRuleRefreshHash, newOptions.requestRuleRefreshHash)) { layer.getSource().setTileLoadFunction(tileLoadFunction(newOptions)); } } diff --git a/web/client/components/map/openlayers/plugins/WFSLayer.js b/web/client/components/map/openlayers/plugins/WFSLayer.js index 41b52eec62d..0465eb8ef01 100644 --- a/web/client/components/map/openlayers/plugins/WFSLayer.js +++ b/web/client/components/map/openlayers/plugins/WFSLayer.js @@ -170,7 +170,10 @@ Layers.registerType('wfs', { f.getGeometry().transform(oldCrs, newCrs); }); } - if (needsReload(oldOptions, options) || !isEqual(oldOptions.security, options.security)) { + if (needsReload(oldOptions, options) + || !isEqual(oldOptions.security, options.security) + || !isEqual(oldOptions.requestRuleRefreshHash, options.requestRuleRefreshHash) + ) { source.setLoader(createLoader(source, options)); source.clear(); source.refresh(); diff --git a/web/client/components/map/openlayers/plugins/WMSLayer.js b/web/client/components/map/openlayers/plugins/WMSLayer.js index 441d245ec10..52ace782e55 100644 --- a/web/client/components/map/openlayers/plugins/WMSLayer.js +++ b/web/client/components/map/openlayers/plugins/WMSLayer.js @@ -188,6 +188,7 @@ const mustCreateNewLayer = (oldOptions, newOptions) => { || oldOptions.tileGridStrategy !== newOptions.tileGridStrategy || !isEqual(oldOptions.tileGrids, newOptions.tileGrids) || !isEqual(oldOptions.security, newOptions.security) + || !isEqual(oldOptions.requestRuleRefreshHash, newOptions.requestRuleRefreshHash) ); }; diff --git a/web/client/components/map/openlayers/plugins/WMTSLayer.js b/web/client/components/map/openlayers/plugins/WMTSLayer.js index c1b3d3c73d6..1f408aad892 100644 --- a/web/client/components/map/openlayers/plugins/WMTSLayer.js +++ b/web/client/components/map/openlayers/plugins/WMTSLayer.js @@ -152,7 +152,7 @@ const createLayer = options => { wrapX: true }; if (hasRequestConfigurationByUrl(options.url, null, options.security?.sourceId)) { - wmtsOptions.urls = urls.map(url => proxySource(options.forceProxy, url)); + wmtsOptions.urls = wmtsOptions.urls.map(url => proxySource(options.forceProxy, url)); wmtsOptions.tileLoadFunction = tileLoadFunction(options); } @@ -186,7 +186,8 @@ const updateLayer = (layer, newOptions, oldOptions) => { || oldOptions.srs !== newOptions.srs || oldOptions.format !== newOptions.format || oldOptions.style !== newOptions.style - || oldOptions.credits !== newOptions.credits) { + || oldOptions.credits !== newOptions.credits + || !isEqual(oldOptions.requestRuleRefreshHash, newOptions.requestRuleRefreshHash)) { return createLayer(newOptions); } if (oldOptions.minResolution !== newOptions.minResolution) { @@ -195,8 +196,7 @@ const updateLayer = (layer, newOptions, oldOptions) => { if (oldOptions.maxResolution !== newOptions.maxResolution) { layer.setMaxResolution(newOptions.maxResolution === undefined ? Infinity : newOptions.maxResolution); } - if (!isEqual(oldOptions.security, newOptions.security) - && hasRequestConfigurationByUrl(newOptions.url, null, newOptions.security?.sourceId)) { + if (!isEqual(oldOptions.security, newOptions.security)) { layer.getSource().setTileLoadFunction(tileLoadFunction(newOptions)); } return null; diff --git a/web/client/configs/localConfig.json b/web/client/configs/localConfig.json index 09853d782b0..512d415294a 100644 --- a/web/client/configs/localConfig.json +++ b/web/client/configs/localConfig.json @@ -59,12 +59,6 @@ "headers": { "Authorization": "Bearer ${securityToken}" } - }, - { - "urlPattern": "http(s)?\\:\\/\\/gs-stable\\.(geo-solutions\\.it|geosolutionsgroup\\.com)(\\:443|\\:80)?\\/geoserver/.*", - "params": { - "authkey": "${securityToken}" - } } ], "monitorState": [ diff --git a/web/client/epics/security.js b/web/client/epics/security.js index 50443b06cdd..0fc77e42958 100644 --- a/web/client/epics/security.js +++ b/web/client/epics/security.js @@ -278,10 +278,7 @@ export const refreshLayersOnRulesUpdateEpic = (action$, store) => // Dispatch changeLayerProperties for each matching layer const actions = layersToUpdate.map(layer => { - const newSecurity = layer.security - ? { ...layer.security, refreshHash: uuidv4() } - : { refreshHash: uuidv4() }; - return changeLayerProperties(layer.id, { security: newSecurity }); + return changeLayerProperties(layer.id, { requestRuleRefreshHash: uuidv4() }); }); return actions.length > 0 ? Rx.Observable.from(actions) : Rx.Observable.empty(); From f9ef0e5cafba5597deb505ef7700ad1e8bd66d1d Mon Sep 17 00:00:00 2001 From: Suren Date: Thu, 4 Dec 2025 21:49:35 +0530 Subject: [PATCH 8/8] update unit test --- web/client/utils/SecurityUtils.js | 6 ++-- .../utils/__tests__/SecurityUtils-test.js | 30 +++++++++++++++++++ 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/web/client/utils/SecurityUtils.js b/web/client/utils/SecurityUtils.js index 0d41d70de76..b20385f54c4 100644 --- a/web/client/utils/SecurityUtils.js +++ b/web/client/utils/SecurityUtils.js @@ -15,6 +15,7 @@ import isArray from "lodash/isArray"; import isEmpty from "lodash/isEmpty"; import template from "lodash/template"; import get from "lodash/get"; +import castArray from "lodash/castArray"; import {setStore as stateSetStore, getState} from "./StateUtils"; @@ -219,13 +220,14 @@ export const getRequestConfigurationRules = () => { /** * Gets the request configuration rule that matches the provided URL - * @param {string} url - The URL to match against rules + * @param {string|string[]} url - The URL to match against rules * @returns {Object|null} Matching rule or null */ export const getRequestConfigurationRule = (url) => { + const _url = head(castArray(url ?? [])) ?? ""; const rules = getRequestConfigurationRules(); return head(rules.filter( - rule => rule && rule.urlPattern && url.match(new RegExp(rule.urlPattern, "i")) + rule => rule && rule.urlPattern && _url.match(new RegExp(rule.urlPattern, "i")) )); }; diff --git a/web/client/utils/__tests__/SecurityUtils-test.js b/web/client/utils/__tests__/SecurityUtils-test.js index fffecc4d34e..c5afc30b587 100644 --- a/web/client/utils/__tests__/SecurityUtils-test.js +++ b/web/client/utils/__tests__/SecurityUtils-test.js @@ -205,6 +205,36 @@ describe('Test security utils methods', () => { // Test that no rule matches rule = SecurityUtils.getRequestConfigurationRule('http://www.no-matching.com/?parameter1=value1¶meter2=value2'); expect(rule).toNotExist(); + + // Test with array URL (single element) - should use first URL + rule = SecurityUtils.getRequestConfigurationRule(['http://www.some-site.com/index?parameter1=value1¶meter2=value2']); + expect(rule).toExist(); + expect(rule.urlPattern).toBe('.*some-site.*'); + + // Test with array URL (multiple elements) - should use first URL only, basepath never changes + rule = SecurityUtils.getRequestConfigurationRule([ + 'http://www.some-site.com/geoserver?parameter1=value1¶meter2=value2', + 'http://www.some-site.com/geoserver/api' + ]); + expect(rule).toExist(); + expect(rule.urlPattern).toBe('.*geoserver.*'); + + // Test that changing the order of URLs in array changes the result (only first URL matters) + rule = SecurityUtils.getRequestConfigurationRule([ + 'http://www.some-site.com/index', + 'http://www.some-site.com/api' + ]); + expect(rule).toExist(); + expect(rule.urlPattern).toBe('.*some-site.*'); + + rule = SecurityUtils.getRequestConfigurationRule([]); + expect(rule).toNotExist(); + + rule = SecurityUtils.getRequestConfigurationRule(null); + expect(rule).toNotExist(); + + rule = SecurityUtils.getRequestConfigurationRule(undefined); + expect(rule).toNotExist(); }); it('test add authkey authentication to url', () => {