• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

geosolutions-it / MapStore2 / 24780227871

22 Apr 2026 01:12PM UTC coverage: 76.797% (-0.01%) from 76.811%
24780227871

Pull #12288

github

web-flow
Merge 25810c28f into 849eeef4f
Pull Request #12288: [Backport c027-2025.01.xx] - #12232: Validate catalog service credentials before saving to prevent silent failures & misleading errors (#12233)

31426 of 48898 branches covered (64.27%)

23 of 34 new or added lines in 2 files covered. (67.65%)

6 existing lines in 2 files now uncovered.

39069 of 50873 relevant lines covered (76.8%)

36.49 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

90.53
/web/client/utils/SecurityUtils.js
1
/**
2
import { keys } from 'lodash';
3
 * Copyright 2016, GeoSolutions Sas.
4
 * All rights reserved.
5
 *
6
 * This source code is licensed under the BSD-style license found in the
7
 * LICENSE file in the root directory of this source tree.
8
 */
9
import axios from '../libs/ajax';
10

11
import ConfigUtils from "./ConfigUtils";
12
import URL from "url";
13
import head from "lodash/head";
14
import isNil from "lodash/isNil";
15
import isArray from "lodash/isArray";
16
import isEmpty from "lodash/isEmpty";
17

18
import {setStore as stateSetStore, getState} from "./StateUtils";
19
import { parseUrl, WMS_GET_CAPABILITIES_VERSION } from '../api/WMS';
20

21
export const USER_GROUP_ALL = 'everyone';
1✔
22

23
export function getCredentials(id) {
24
    const securityStorage = JSON.parse(sessionStorage.getItem('credentialStorage') ?? "{}");
369✔
25
    return securityStorage[id] || {};
369✔
26
}
27
export function setCredentials(id, credentials) {
28
    const securityStorage = JSON.parse(sessionStorage.getItem('credentialStorage') ?? "{}");
27✔
29
    sessionStorage.setItem('credentialStorage', JSON.stringify(Object.assign({}, securityStorage, {[id]: credentials})));
27✔
30
}
31
/**
32
 * Validates WMS service credentials using a GetCapabilities request.
33
 * @param {string} url - The WMS service URL.
34
 * @param {Object} credentials - Credentials object.
35
 * @param {string} credentials.username - Username for Basic Auth.
36
 * @param {string} credentials.password - Password for Basic Auth.
37
 * @returns {Promise<{valid: boolean, reason?: string}>}
38
 *   - `{valid: true}` if credentials work (200 response)
39
 *   - `{valid: false, reason: '...'}` if not. Reasons:
40
 *     `'invalid_credentials'` | `'forbidden'` | `'server_error'` |
41
 *     `'timeout'` | `'cors_blocked'` | `'network_error'` | `'unknown'`
42
 */
43
export async function validateServiceCredentials(url, credentials) {
44
    try {
3✔
45
        const credentialsEncoded = btoa(credentials.username + ":" + credentials.password);
3✔
46

47
        const response = await axios.get(parseUrl(url, {
3✔
48
            service: "WMS",
49
            version: WMS_GET_CAPABILITIES_VERSION,
50
            request: "GetCapabilities"
51
        }), {
52
            headers: {
53
                Authorization: `Basic ${credentialsEncoded}`,
54
                'Accept': 'application/xml, text/xml, */*'
55
            },
56
            timeout: 10000,
57
            validateStatus: (status) => status < 500,
2✔
58
            withCredentials: false
59
        });
60

61
        if (response.status === 200) return { valid: true };
2✔
62
        if (response.status === 401) return { valid: false, reason: 'invalid_credentials' };
1!
NEW
63
        if (response.status === 403) return { valid: false, reason: 'forbidden' };
×
64

NEW
65
        return { valid: false, reason: 'server_error'};
×
66

67
    } catch (error) {
68
        if (!error.response) {
1!
69
            if (error.code === 'ECONNABORTED') {
1!
NEW
70
                return { valid: false, reason: 'timeout' };
×
71
            }
72
            if (error.code === 'ERR_NETWORK' || error.message.includes('Network Error')) {
1!
73
                // This is likely CORS blocking
74
                return { valid: false, reason: 'cors_blocked' };
1✔
75
            }
NEW
76
            return { valid: false, reason: 'network_error' };
×
77
        }
78

79
        // If we have a response but didn't catch it above
NEW
80
        if (error.response.status === 401) {
×
NEW
81
            return { valid: false, reason: 'invalid_credentials' };
×
82
        }
83

NEW
84
        return { valid: false, reason: 'unknown'};
×
85
    }
86
}
87
/**
88
 * Stores the logged user security information.
89
 */
90
export function setStore(store) {
91
    stateSetStore(store);
10✔
92
}
93

94
/**
95
 * Gets security state form the store.
96
 */
97
export function getSecurityInfo() {
98
    return getState().security || {};
384✔
99
}
100

101
/**
102
 * Returns the current user or undefined if not available.
103
 */
104
export function getUser() {
105
    return getSecurityInfo()?.user;
12✔
106
}
107

108
/**
109
 * Returns the current user basic authentication header value.
110
 */
111
export function getBasicAuthHeader() {
112
    return getSecurityInfo()?.authHeader;
3✔
113
}
114

115
/**
116
 * Returns the current user access token value.
117
 */
118
export function getToken() {
119
    return getSecurityInfo()?.token;
351✔
120
}
121

122
/**
123
 * Returns the current user refresh token value.
124
 * The refresh token is used to get a new access token.
125
 */
126
export function getRefreshToken() {
127
    return getSecurityInfo()?.refresh_token;
18✔
128
}
129

130
/**
131
 * Return the user attributes as an array. If the user is undefined or
132
 * doesn't have any attributes an empty array is returned.
133
 */
134
export function getUserAttributes(providedUser) {
135
    const user = providedUser || getUser();
40✔
136
    if (!user || !user.attribute) {
40✔
137
        // not user defined or the user doesn't have any attributes
138
        return [];
28✔
139
    }
140
    const attributes = user.attribute;
12✔
141
    // if the user has only one attribute we need to put it in an array
142
    return isArray(attributes) ? attributes : [attributes];
12✔
143
}
144

145
/**
146
 * Search in the user attributes an attribute that matches the provided
147
 * attribute name. The search will not be case sensitive. Undefined is
148
 * returned if the attribute could not be found.
149
 */
150
export function findUserAttribute(attributeName) {
151
    // getting the user attributes
152
    const userAttributes = getUserAttributes();
8✔
153
    if (!userAttributes || !attributeName ) {
8!
154
        // the user as no attributes or the provided attribute name is undefined
155
        return null;
×
156
    }
157
    return head(userAttributes.filter(attribute => attribute.name
8✔
158
        && attribute.name.toLowerCase() === attributeName.toLowerCase()));
159
}
160

161
/**
162
 * Search in the user attributes an attribute that matches the provided
163
 * attribute name. The search will not be case sensitive. Undefined is
164
 * returned if the attribute could not be found otherwise the attribute
165
 * value is returned.
166
 */
167
export function findUserAttributeValue(attributeName) {
168
    const userAttribute = findUserAttribute(attributeName);
4✔
169
    return userAttribute?.value;
4✔
170
}
171

172
/**
173
 * Returns an array with the configured authentication rules. If no rules
174
 * were configured an empty array is returned.
175
 */
176
export function getAuthenticationRules() {
177
    return ConfigUtils.getConfigProp('authenticationRules') || [];
1,344✔
178
}
179

180
/**
181
 * Checks if authentication is activated or not.
182
 */
183
export function isAuthenticationActivated() {
184
    return ConfigUtils.getConfigProp('useAuthenticationRules') || false;
1,431✔
185
}
186

187
/**
188
 * Returns the authentication method that should be used for the provided URL.
189
 * We go through the authentication rules and find the first one that matches
190
 * the provided URL, if no rule matches the provided URL undefined is returned.
191
 */
192
export function getAuthenticationMethod(url) {
193
    const foundRule = head(getAuthenticationRules().filter(
318✔
194
        rule => rule && rule.urlPattern && url.match(new RegExp(rule.urlPattern, "i"))));
557✔
195
    return foundRule?.method;
318✔
196
}
197

198
/**
199
 * Returns the authentication rule that should be used for the provided URL.
200
 * We go through the authentication rules and find the first one that matches
201
 * the provided URL, if no rule matches the provided URL undefined is returned.
202
 */
203
export function getAuthenticationRule(url) {
204
    return head(getAuthenticationRules().filter(
1,024✔
205
        rule => rule && rule.urlPattern && url.match(new RegExp(rule.urlPattern, "i"))));
1,470✔
206
}
207

208
export function getAuthKeyParameter(url) {
209
    const foundRule = getAuthenticationRule(url);
496✔
210
    return foundRule?.authkeyParamName ?? 'authkey';
496✔
211
}
212

213
export function getAuthenticationHeaders(url, securityToken, security) {
214
    if (!url || !isAuthenticationActivated()) {
123✔
215
        return null;
35✔
216
    }
217
    const storedProtectedService = getCredentials(security?.sourceId);
88✔
218
    if (security && storedProtectedService) {
88✔
219
        return {
4✔
220
            "Authorization": `Basic ${btoa(storedProtectedService.username + ":" + storedProtectedService.password)}`
221
        };
222
    }
223
    switch (getAuthenticationMethod(url)) {
84✔
224
    case 'bearer': {
225
        const token = !isNil(securityToken) ? securityToken : getToken();
3!
226
        if (!token) {
3!
227
            return null;
×
228
        }
229
        return {
3✔
230
            "Authorization": `Bearer ${token}`
231
        };
232
    }
233
    case 'header': {
234
        const rule = getAuthenticationRule(url);
1✔
235
        return rule.headers;
1✔
236
    }
237
    default:
238
        // we cannot handle the required authentication method
239
        return null;
80✔
240
    }
241
}
242

243
/**
244
 * This method will add query parameter based authentications to an object
245
 * containing query parameters.
246
 */
247
export function addAuthenticationParameter(url, parameters, securityToken) {
248
    if (!url || !isAuthenticationActivated()) {
280✔
249
        return parameters;
86✔
250
    }
251
    switch (getAuthenticationMethod(url)) {
194✔
252
    case 'authkey': {
253
        const token = !isNil(securityToken) ? securityToken : getToken();
160✔
254
        if (!token) {
160✔
255
            return parameters;
12✔
256
        }
257
        const authParam = getAuthKeyParameter(url);
148✔
258
        return Object.assign(parameters || {}, {[authParam]: token});
148✔
259
    }
260
    case 'test': {
261
        const rule = getAuthenticationRule(url);
1✔
262
        const token = rule ? rule.token : "";
1!
263
        const authParam = getAuthKeyParameter(url);
1✔
264
        return Object.assign(parameters || {}, { [authParam]: token });
1!
265
    }
266
    default:
267
        // we cannot handle the required authentication method
268
        return parameters;
33✔
269
    }
270
}
271

272
/**
273
 * This method will add query parameter based authentications to an url.
274
 */
275
export function addAuthenticationToUrl(url) {
276
    if (!url || !isAuthenticationActivated()) {
5✔
277
        return url;
1✔
278
    }
279
    const parsedUrl = URL.parse(url, true);
4✔
280
    parsedUrl.query = addAuthenticationParameter(url, parsedUrl.query);
4✔
281
    // we need to remove this to force the use of query
282
    delete parsedUrl.search;
4✔
283
    return URL.format(parsedUrl);
4✔
284
}
285

286
export function clearNilValuesForParams(params = {}) {
68✔
287
    return Object.keys(params).reduce((pre, cur) => {
71✔
288
        return !isNil(params[cur]) ? {...pre, [cur]: params[cur]} : pre;
5✔
289
    }, {});
290
}
291

292
export function addAuthenticationToSLD(layerParams, options) {
293
    if (layerParams.SLD) {
234✔
294
        const parsed = URL.parse(layerParams.SLD, true);
2✔
295
        const params = addAuthenticationParameter(layerParams.SLD, parsed.query, options.securityToken);
2✔
296
        return Object.assign({}, layerParams, {
2✔
297
            SLD: URL.format(Object.assign({}, parsed, {
298
                query: params,
299
                search: undefined
300
            }))
301
        });
302
    }
303
    return layerParams;
232✔
304
}
305

306
export function cleanAuthParamsFromURL(url) {
307
    return ConfigUtils.filterUrlParams(url, [getAuthKeyParameter(url)].filter(p => p));
344✔
308
}
309

310
/**
311
 * it creates the headers function for axios config, if it finds a reference in sessionStorage
312
 * @param {string} protectedId the id of the protected service to look for in sessionStorage
313
 * @returns {object} the headers Basic
314
 */
315
// herer
316
export const getAuthorizationBasic = (protectedId) => {
1✔
317
    let headers = {};
215✔
318
    const storedProtectedService = getCredentials(protectedId);
215✔
319
    if (!isEmpty(storedProtectedService)) {
215✔
320
        headers = {
1✔
321
            Authorization: `Basic ${btoa(storedProtectedService.username + ":" + storedProtectedService.password)}`
322
        };
323
    }
324
    return headers;
215✔
325
};
326

327
/**
328
 * This utility class will get information about the current logged user directly from the store.
329
 */
330
const SecurityUtils = {
1✔
331
    getAuthorizationBasic,
332
    getCredentials,
333
    setCredentials,
334
    setStore,
335
    getSecurityInfo,
336
    getUser,
337
    getBasicAuthHeader,
338
    getToken,
339
    getRefreshToken,
340
    getUserAttributes,
341
    findUserAttribute,
342
    findUserAttributeValue,
343
    getAuthenticationRules,
344
    isAuthenticationActivated,
345
    getAuthenticationMethod,
346
    getAuthenticationRule,
347
    addAuthenticationToUrl,
348
    addAuthenticationParameter,
349
    clearNilValuesForParams,
350
    addAuthenticationToSLD,
351
    getAuthKeyParameter,
352
    cleanAuthParamsFromURL,
353
    getAuthenticationHeaders,
354
    USER_GROUP_ALL,
355
    validateServiceCredentials
356
};
357

358
export default SecurityUtils;
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc