Skip to content

Commit 84f038f

Browse files
committed
fix[backend,frontend,plugins](integrations): mask sensitive config values, improve validation error messages per provider, and prevent double-encryption
1 parent 03d4e86 commit 84f038f

18 files changed

Lines changed: 501 additions & 178 deletions

File tree

backend/src/main/java/com/park/utmstack/config/Constants.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,7 @@ public final class Constants {
161161

162162
public static final String CONF_TYPE_PASSWORD = "password";
163163
public static final String CONF_TYPE_FILE = "file";
164+
public static final String MASKED_VALUE = "*****";
164165

165166
public static final String API_KEY_HEADER = "Utm-Api-Key";
166167
public static final List<String> API_ENDPOINT_IGNORE = Collections.emptyList();

backend/src/main/java/com/park/utmstack/domain/application_modules/validators/UtmModuleConfigValidator.java

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -35,12 +35,15 @@ public boolean validate(UtmModule module, List<UtmModuleGroupConfiguration> keys
3535
List<UtmModuleGroupConfDTO> configDTOs = dbConfigs.stream()
3636
.map(dbConf -> {
3737
UtmModuleGroupConfiguration override = findInKeys(keys, dbConf.getConfKey());
38-
UtmModuleGroupConfiguration source = override != null ? override : dbConf;
39-
40-
return new UtmModuleGroupConfDTO(
41-
source.getConfKey(),
42-
override != null ? source.getConfValue() : decryptIfNeeded(source.getConfDataType(), source.getConfValue())
43-
);
38+
String value;
39+
if (override != null && !Constants.MASKED_VALUE.equals(override.getConfValue())) {
40+
// User provided a new value — use it as plaintext
41+
value = override.getConfValue();
42+
} else {
43+
// No override or masked — decrypt from DB
44+
value = decryptIfNeeded(dbConf.getConfDataType(), dbConf.getConfValue());
45+
}
46+
return new UtmModuleGroupConfDTO(dbConf.getConfKey(), value);
4447
})
4548
.toList();
4649

backend/src/main/java/com/park/utmstack/service/UtmIntegrationConfService.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,8 @@ public UtmIntegrationConf save(UtmIntegrationConf utmIntegrationConf) throws Exc
4040
final String ctx = CLASSNAME + ".save";
4141
try {
4242
String dataType = utmIntegrationConf.getConfDatatype();
43-
if (StringUtils.hasText(dataType) && dataType.equals("password"))
43+
if (StringUtils.hasText(dataType) && dataType.equals("password")
44+
&& !Constants.MASKED_VALUE.equals(utmIntegrationConf.getConfValue()))
4445
utmIntegrationConf.setConfValue(CipherUtil.encrypt(utmIntegrationConf.getConfValue(), System.getenv(Constants.ENV_ENCRYPTION_KEY)));
4546
return utmIntegrationConfRepository.save(utmIntegrationConf);
4647
} catch (Exception e) {

backend/src/main/java/com/park/utmstack/service/application_modules/UtmModuleGroupConfigurationService.java

Lines changed: 46 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@
2222
import java.util.Collections;
2323
import java.util.List;
2424
import java.util.Map;
25-
import java.util.Objects;
2625
import java.util.stream.Collectors;
2726

2827
/**
@@ -51,38 +50,41 @@ public void createConfigurationKeys(List<UtmModuleGroupConfiguration> keys) thro
5150
}
5251

5352
/**
54-
* Update configuration of the application modules
55-
*
56-
* @param keys List of configuration keys to save
53+
* Update configuration of the application modules.
54+
* Password/file fields with the masked value are skipped (not changed).
55+
* Password/file fields with a new value are encrypted before saving.
5756
*/
5857
public UtmModule updateConfigurationKeys(Long moduleId, List<UtmModuleGroupConfiguration> keys) {
5958
final String ctx = CLASSNAME + ".updateConfigurationKeys";
6059
try {
6160
if (CollectionUtils.isEmpty(keys))
6261
throw new ApiException("No configuration keys were provided to update", HttpStatus.BAD_REQUEST);
6362

64-
// Load existing values from DB to detect unchanged encrypted fields
65-
Map<String, String> existingValues = keys.stream()
66-
.filter(k -> k.getId() != null)
67-
.map(k -> moduleConfigurationRepository.findById(k.getId()).orElse(null))
68-
.filter(java.util.Objects::nonNull)
69-
.collect(Collectors.toMap(UtmModuleGroupConfiguration::getConfKey,
70-
c -> c.getConfValue() != null ? c.getConfValue() : "", (a, b) -> a));
71-
7263
for (UtmModuleGroupConfiguration key : keys) {
64+
boolean isSensitive = isSensitiveType(key.getConfDataType());
65+
66+
// Skip masked values — the user did not change this field
67+
if (isSensitive && Constants.MASKED_VALUE.equals(key.getConfValue())) {
68+
continue;
69+
}
70+
7371
if (key.getConfRequired() && !StringUtils.hasText(key.getConfValue()))
7472
throw new Exception(String.format("No value was found for required configuration: %1$s (%2$s)", key.getConfName(), key.getConfKey()));
75-
if (key.getConfDataType().equals("password") || key.getConfDataType().equals("file")) {
76-
String existingEncrypted = existingValues.get(key.getConfKey());
77-
// Only encrypt if the value actually changed (is not the same encrypted value from DB)
78-
if (existingEncrypted != null && existingEncrypted.equals(key.getConfValue())) {
79-
// Value unchanged - already encrypted in DB, don't re-encrypt
80-
continue;
81-
}
73+
74+
// Encrypt new sensitive values
75+
if (isSensitive) {
8276
key.setConfValue(CipherUtil.encrypt(key.getConfValue(), System.getenv(Constants.ENV_ENCRYPTION_KEY)));
8377
}
8478
}
85-
moduleConfigurationRepository.saveAll(keys);
79+
80+
// Remove masked entries so they don't overwrite DB values
81+
List<UtmModuleGroupConfiguration> toSave = keys.stream()
82+
.filter(k -> !(isSensitiveType(k.getConfDataType()) && Constants.MASKED_VALUE.equals(k.getConfValue())))
83+
.collect(Collectors.toList());
84+
85+
if (!toSave.isEmpty()) {
86+
moduleConfigurationRepository.saveAll(toSave);
87+
}
8688

8789
List<ModuleName> needRestartModules = Arrays.asList(ModuleName.AWS_IAM_USER, ModuleName.AZURE,
8890
ModuleName.GCP, ModuleName.SOPHOS);
@@ -100,32 +102,27 @@ public UtmModule updateConfigurationKeys(Long moduleId, List<UtmModuleGroupConfi
100102
}
101103

102104
/**
103-
* Find all configurations of a module group
104-
*
105-
* @param groupId Identifier of the group to get the configurations
106-
* @return A list of configuration of a group
107-
* @throws Exception In case of any error
105+
* Find all configurations of a module group.
106+
* Sensitive values (password, file) are masked before returning.
108107
*/
109108
public List<UtmModuleGroupConfiguration> findAllByGroupId(Long groupId) throws Exception {
110109
final String ctx = CLASSNAME + ".findAllByGroupId";
111110
try {
112-
return moduleConfigurationRepository.findAllByGroupId(groupId);
111+
List<UtmModuleGroupConfiguration> configs = moduleConfigurationRepository.findAllByGroupId(groupId);
112+
maskSensitiveValues(configs);
113+
return configs;
113114
} catch (Exception e) {
114115
throw new Exception(ctx + ": " + e.getMessage());
115116
}
116117
}
117118

118119
/**
119120
* Gets all configuration parameter for a group and convert it to a map
120-
*
121-
* @param groupId Identifier of a group
122-
* @return A map with the module group configuration
123-
* @throws Exception In case of any error
124121
*/
125122
public Map<String, String> getGroupConfigurationAsMap(Long groupId) throws Exception {
126123
final String ctx = CLASSNAME + ".getGroupConfigurationAsMap";
127124
try {
128-
List<UtmModuleGroupConfiguration> configurations = findAllByGroupId(groupId);
125+
List<UtmModuleGroupConfiguration> configurations = moduleConfigurationRepository.findAllByGroupId(groupId);
129126

130127
if (CollectionUtils.isEmpty(configurations))
131128
return Collections.emptyMap();
@@ -138,18 +135,30 @@ public Map<String, String> getGroupConfigurationAsMap(Long groupId) throws Excep
138135

139136
/**
140137
* Find a configuration parameter by his group and key
141-
*
142-
* @param groupId Identifier of the group to the param belongs
143-
* @param confKey Key word of the configuration parameter
144-
* @return A ${@link UtmModuleGroupConfiguration} object with the configuration parameter information
145-
* @throws Exception In case of any error
146138
*/
147139
public UtmModuleGroupConfiguration findByGroupIdAndConfKey(Long groupId, String confKey) throws Exception {
148140
final String ctx = CLASSNAME + ".findByGroupIdAndConfKey";
149141
try {
150-
return moduleConfigurationRepository.findByGroupIdAndConfKey(groupId, confKey);
142+
UtmModuleGroupConfiguration config = moduleConfigurationRepository.findByGroupIdAndConfKey(groupId, confKey);
143+
if (config != null && isSensitiveType(config.getConfDataType())) {
144+
config.setConfValue(Constants.MASKED_VALUE);
145+
}
146+
return config;
151147
} catch (Exception e) {
152148
throw new Exception(ctx + ": " + e.getMessage());
153149
}
154150
}
151+
152+
private boolean isSensitiveType(String dataType) {
153+
return Constants.CONF_TYPE_PASSWORD.equals(dataType) || Constants.CONF_TYPE_FILE.equals(dataType);
154+
}
155+
156+
private void maskSensitiveValues(List<UtmModuleGroupConfiguration> configs) {
157+
if (configs == null) return;
158+
for (UtmModuleGroupConfiguration config : configs) {
159+
if (isSensitiveType(config.getConfDataType()) && StringUtils.hasText(config.getConfValue())) {
160+
config.setConfValue(Constants.MASKED_VALUE);
161+
}
162+
}
163+
}
155164
}

backend/src/main/java/com/park/utmstack/service/application_modules/UtmModuleGroupService.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,9 @@ public void updateCollectorConfigurationKeys(CollectorConfigDTO collectorConfig)
250250
} else {
251251
for (UtmModuleGroupConfiguration key : keys) {
252252
if (key.getConfDataType().equals("password")) {
253+
if (Constants.MASKED_VALUE.equals(key.getConfValue())) {
254+
continue;
255+
}
253256
key.setConfValue(CipherUtil.encrypt(key.getConfValue(), System.getenv(Constants.ENV_ENCRYPTION_KEY)));
254257
}
255258
}

backend/src/main/java/com/park/utmstack/service/collectors/CollectorOpsService.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -436,6 +436,9 @@ public void updateCollectorConfigurationKeys(CollectorConfigDTO collectorConfig)
436436
utmModuleGroupRepository.deleteAll(configs);
437437
} else {
438438
for (UtmModuleGroupConfiguration key : keys) {
439+
if (key.getConfDataType().equals("password") && Constants.MASKED_VALUE.equals(key.getConfValue())) {
440+
continue;
441+
}
439442
if (key.getConfRequired() && !StringUtils.hasText(key.getConfValue()))
440443
throw new Exception(String.format("No value was found for required configuration: %1$s (%2$s)", key.getConfName(), key.getConfKey()));
441444
if (key.getConfDataType().equals("password"))

backend/src/main/java/com/park/utmstack/web/rest/application_modules/UtmModuleGroupResource.java

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,19 @@ public ResponseEntity<UtmModuleGroup> updateUtmConfigurationGroup(@Valid @Reques
131131
public ResponseEntity<List<UtmModuleGroup>> getModuleGroups(@RequestParam Long moduleId) {
132132
final String ctx = CLASSNAME + ".getModuleGroups";
133133
try {
134-
return ResponseEntity.ok(moduleGroupService.findAllByModuleId(moduleId));
134+
List<UtmModuleGroup> groups = moduleGroupService.findAllByModuleId(moduleId);
135+
for (UtmModuleGroup group : groups) {
136+
if (group.getModuleGroupConfigurations() != null) {
137+
for (UtmModuleGroupConfiguration conf : group.getModuleGroupConfigurations()) {
138+
if ((Constants.CONF_TYPE_PASSWORD.equals(conf.getConfDataType())
139+
|| Constants.CONF_TYPE_FILE.equals(conf.getConfDataType()))
140+
&& conf.getConfValue() != null) {
141+
conf.setConfValue(Constants.MASKED_VALUE);
142+
}
143+
}
144+
}
145+
}
146+
return ResponseEntity.ok(groups);
135147
} catch (Exception e) {
136148
String msg = ctx + ": " + e.getMessage();
137149
log.error(msg);

frontend/src/app/app-module/conf/int-generic-group-config/int-generic-group-config.component.html

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,11 +119,14 @@
119119
&& integrationConfig.confDataType !== 'select'
120120
&& integrationConfig.confDataType !== 'file'"
121121
(input)="addChange(integrationConfig)"
122+
(focus)="onPasswordFocus(integrationConfig)"
123+
(blur)="onPasswordBlur(integrationConfig)"
122124
[(ngModel)]="integrationConfig.confValue"
123125
[attr.autocomplete]="'disabled'"
124126
[id]="'sectionParam'+integrationConfig.id"
125127
[name]="integrationConfig.confName"
126128
[type]="integrationConfig.confDataType"
129+
[placeholder]="integrationConfig.confDataType === 'password' ? 'Enter new value to change' : ''"
127130
(ngModelChange)="integrationConfig.confOptions === 'unique' ? inputChange$.next({id: integrationConfig.groupId, value: $event}) : null"
128131
class="form-control" [required]="integrationConfig.confRequired">
129132

frontend/src/app/app-module/conf/int-generic-group-config/int-generic-group-config.component.ts

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -205,8 +205,8 @@ export class IntGenericGroupConfigComponent implements OnInit, OnChanges, OnDest
205205
},
206206
error: err => {
207207
if (err.status === 400) {
208-
this.toast.showError('Invalid Configuration',
209-
'The configuration data is invalid. Please check your inputs and try again.');
208+
const message = this.extractValidationError(err);
209+
this.toast.showError('Invalid Configuration', message);
210210
} else {
211211
this.toast.showError('Error saving configuration',
212212
'Error while trying to save tenant configuration, please try again.');
@@ -452,6 +452,43 @@ export class IntGenericGroupConfigComponent implements OnInit, OnChanges, OnDest
452452
}
453453

454454

455+
extractValidationError(err: any): string {
456+
const defaultMsg = 'The configuration data is invalid. Please check your inputs and try again.';
457+
try {
458+
const body = err.error;
459+
// Spring fieldErrors format
460+
if (body && body.fieldErrors && body.fieldErrors.length > 0) {
461+
return body.fieldErrors.map(e => e.message).join('. ');
462+
}
463+
// Spring message format
464+
if (body && body.message) {
465+
return body.message;
466+
}
467+
// X-UtmStack-error header
468+
const headerError = err.headers ? err.headers.get('X-UtmStack-error') : null;
469+
if (headerError) {
470+
return headerError;
471+
}
472+
// Plain string body
473+
if (typeof body === 'string' && body.length > 0) {
474+
return body;
475+
}
476+
} catch (e) {}
477+
return defaultMsg;
478+
}
479+
480+
onPasswordFocus(config: UtmModuleGroupConfType) {
481+
if (config.confDataType === 'password' && config.confValue === '*****') {
482+
config.confValue = '';
483+
}
484+
}
485+
486+
onPasswordBlur(config: UtmModuleGroupConfType) {
487+
if (config.confDataType === 'password' && (config.confValue === '' || config.confValue === null)) {
488+
config.confValue = '*****';
489+
}
490+
}
491+
455492
ngOnDestroy() {
456493
this.destroy$.next();
457494
this.destroy$.complete();

frontend/src/app/app-module/guides/guide-soc-ai/guide-soc-ai.component.ts

Lines changed: 30 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -370,17 +370,15 @@ export class GuideSocAiComponent implements OnInit {
370370
this.formValues['url'] = urlConf.confValue || '';
371371
}
372372

373-
// Extract API key from custom headers
373+
// Check if API key exists in custom headers — show masked if so
374374
const customHeaders = this.getConf('utmstack.socai.customHeaders');
375-
if (customHeaders && customHeaders.confValue) {
375+
if (customHeaders && customHeaders.confValue && customHeaders.confValue !== '{}') {
376376
try {
377377
const headers = JSON.parse(customHeaders.confValue);
378378
const authConfig = this.providerAuthHeaders[this.activeProvider];
379379
if (authConfig && headers[authConfig.headerName]) {
380-
const rawValue = headers[authConfig.headerName];
381-
this.formValues['apiKey'] = authConfig.headerValuePrefix
382-
? rawValue.replace(authConfig.headerValuePrefix, '')
383-
: rawValue;
380+
// API key exists — show masked, don't expose the real value
381+
this.formValues['apiKey'] = '*****';
384382
}
385383
} catch (e) {}
386384
this.formValues['customHeaders'] = customHeaders.confValue;
@@ -462,12 +460,14 @@ export class GuideSocAiComponent implements OnInit {
462460
} else {
463461
// Known providers: build auth header from API key
464462
const authConfig = this.providerAuthHeaders[this.activeProvider];
465-
if (authConfig && this.formValues['apiKey']) {
463+
if (authConfig && this.formValues['apiKey'] && this.formValues['apiKey'] !== '*****') {
464+
// User entered a new API key — build auth headers
466465
const headers: {[k: string]: string} = {};
467466
headers[authConfig.headerName] = authConfig.headerValuePrefix + this.formValues['apiKey'];
468467
this.pushChange(changes, 'utmstack.socai.authType', 'custom-headers');
469468
this.pushChange(changes, 'utmstack.socai.customHeaders', JSON.stringify(headers));
470469
}
470+
// If apiKey is '*****', don't touch customHeaders — keep existing value in DB
471471
}
472472

473473
this.moduleGroupConfService.update({
@@ -481,7 +481,8 @@ export class GuideSocAiComponent implements OnInit {
481481
(err) => {
482482
this.saving = false;
483483
if (err.status === 400) {
484-
this.toast.showError('Invalid Configuration', 'Please check your inputs and try again.');
484+
const message = this.extractValidationError(err);
485+
this.toast.showError('Invalid Configuration', message);
485486
} else {
486487
this.toast.showError('Error', 'Failed to save configuration. Please try again.');
487488
}
@@ -501,6 +502,27 @@ export class GuideSocAiComponent implements OnInit {
501502
}
502503
}
503504

505+
private extractValidationError(err: any): string {
506+
const defaultMsg = 'The configuration data is invalid. Please check your inputs and try again.';
507+
try {
508+
const body = err.error;
509+
if (body && body.fieldErrors && body.fieldErrors.length > 0) {
510+
return body.fieldErrors.map((e: any) => e.message).join('. ');
511+
}
512+
if (body && body.message) {
513+
return body.message;
514+
}
515+
const headerError = err.headers ? err.headers.get('X-UtmStack-error') : null;
516+
if (headerError) {
517+
return headerError;
518+
}
519+
if (typeof body === 'string' && body.length > 0) {
520+
return body;
521+
}
522+
} catch (e) {}
523+
return defaultMsg;
524+
}
525+
504526
onToggle(key: string, value: boolean) {
505527
this.formValues[key] = value.toString();
506528
}

0 commit comments

Comments
 (0)