diff --git a/docs/developer-guide/local-config.md b/docs/developer-guide/local-config.md index 78fad549bbd..fe3e4fb23e6 100644 --- a/docs/developer-guide/local-config.md +++ b/docs/developer-guide/local-config.md @@ -214,6 +214,20 @@ For configuring plugins, see the [Configuring Plugins Section](plugins-documenta } ``` + - `enabled` - Optional, a plain boolean or a plugin expression string (same syntax as a plugin's `disablePluginIf`) to + conditionally apply the rule, e.g. based on the current user's groups. Rules without `enabled` are always applied. + Example: + + ```json + { + "urlPattern": ".*geoserver.*", + "params": { + "authkey": "${securityToken}" + }, + "enabled": "{includes(state('usergroups'), 'editor')}" + } + ``` + !!! 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. diff --git a/web/client/utils/SecurityUtils.js b/web/client/utils/SecurityUtils.js index 6d4483d9147..c17a7c84c31 100644 --- a/web/client/utils/SecurityUtils.js +++ b/web/client/utils/SecurityUtils.js @@ -20,6 +20,7 @@ import castArray from "lodash/castArray"; import {setStore as stateSetStore, getState} from "./StateUtils"; import { parseUrl, WMS_GET_CAPABILITIES_VERSION } from '../api/WMS'; +import { getMonitoredState, handleExpression } from './PluginsUtils'; export const USER_GROUP_ALL = 'everyone'; @@ -248,6 +249,20 @@ export const convertAuthenticationRulesToRequestConfiguration = (authRules = []) }).filter(rule => rule !== null); }; +/** + * Checks a request configuration rule's `enabled` property. + * `enabled` can be a plain boolean or a plugin expression string (e.g. `"{state('usergroups').includes('editor')}"`), + * evaluated with the same syntax used for `cfg.disablePluginIf` (see `PluginsUtils.handleExpression`). + * Rules without an `enabled` property are always applied. + * @param {object} rule the request configuration rule + * @returns {boolean} true if the rule's `enabled` resolves to a truthy value + */ +const isRuleEnabled = (rule) => { + const enabled = rule?.enabled ?? true; + const monitoredState = getMonitoredState(getState()); + return !!handleExpression((path) => get(monitoredState, path), undefined, enabled); +}; + /** * Gets all request configuration rules from Redux state or config * Automatically converts authenticationRules to new format if requestsConfigurationRules is missing @@ -257,19 +272,19 @@ export const getRequestConfigurationRules = () => { // First try to get from Redux state (if available) const stateRules = get(getState(), 'security.rules', []); if (!isEmpty(stateRules)) { - return stateRules; + return stateRules.filter(isRuleEnabled); } // Try to get new format from config const configRules = ConfigUtils.getConfigProp('requestsConfigurationRules'); if (!isEmpty(configRules)) { - return configRules; + return configRules.filter(isRuleEnabled); } // If new format is missing, convert old authenticationRules format const authRules = ConfigUtils.getConfigProp('authenticationRules'); if (!isEmpty(authRules)) { - return convertAuthenticationRulesToRequestConfiguration(authRules); + return convertAuthenticationRulesToRequestConfiguration(authRules).filter(isRuleEnabled); } // No rules found diff --git a/web/client/utils/__tests__/SecurityUtils-test.js b/web/client/utils/__tests__/SecurityUtils-test.js index f041b06ded5..9d3de4b8880 100644 --- a/web/client/utils/__tests__/SecurityUtils-test.js +++ b/web/client/utils/__tests__/SecurityUtils-test.js @@ -509,6 +509,51 @@ describe('Test security utils methods', () => { expect(result.length).toBe(2); expect(result[0].urlPattern).toBe('.*geoserver.*'); }); + + it('should keep rules without an enabled property', () => { + const rulesInConfig = [ + { urlPattern: '.*api.*', headers: { 'Authorization': 'Bearer ${securityToken}' } } + ]; + setSecurityInfo({}); + ConfigUtils.setConfigProp('requestsConfigurationRules', rulesInConfig); + + const result = SecurityUtils.getRequestConfigurationRules(); + expect(result).toEqual(rulesInConfig); + }); + + it('should filter out rules with enabled: false', () => { + const rulesInConfig = [ + { urlPattern: '.*api.*', enabled: false, headers: { 'Authorization': 'Bearer ${securityToken}' } } + ]; + setSecurityInfo({}); + ConfigUtils.setConfigProp('requestsConfigurationRules', rulesInConfig); + + const result = SecurityUtils.getRequestConfigurationRules(); + expect(result.length).toBe(0); + }); + + it('should filter out rules whose enabled expression resolves to false', () => { + const rulesInConfig = [ + { urlPattern: '.*api.*', enabled: "{includes(state('usergroups'), 'editor')}" } + ]; + setSecurityInfo({ user: { groups: { group: [] } } }); + ConfigUtils.setConfigProp('requestsConfigurationRules', rulesInConfig); + + const result = SecurityUtils.getRequestConfigurationRules(); + expect(result.length).toBe(0); + }); + + it('should keep rules whose enabled expression resolves to true against usergroups state', () => { + const rulesInConfig = [ + { urlPattern: '.*api.*', enabled: "{includes(state('usergroups'), 'editor')}" } + ]; + setSecurityInfo({ user: { groups: { group: [{ groupName: 'editor', enabled: true }] } } }); + ConfigUtils.setConfigProp('requestsConfigurationRules', rulesInConfig); + + const result = SecurityUtils.getRequestConfigurationRules(); + expect(result.length).toBe(1); + expect(result[0].urlPattern).toBe('.*api.*'); + }); }); describe('getAuthKeyParameter', () => {