FINERACT-2081: External service credential values masked

This commit is contained in:
Jose Alberto Hernandez
2025-04-14 16:05:21 +02:00
committed by Adam Saghy
parent 07ea81e399
commit b4b2ef3701
7 changed files with 116 additions and 8 deletions
@@ -0,0 +1,36 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.infrastructure.core.service;
public final class StringUtil {
private StringUtil() {}
public static String maskValue(String value) {
return maskValue(value, 4);
}
public static String maskValue(String value, Integer unmaskedLength) {
if (value.length() <= unmaskedLength) {
return "****";
}
return value.substring(0, 1) + "*".repeat(value.length() - 1 - unmaskedLength) + value.substring(value.length() - unmaskedLength);
}
}
@@ -233,7 +233,6 @@ public class AppUser extends AbstractPersistableCustom<Long> implements Platform
if (command.isChangeInPasswordParameterNamed(passwordParamName, this.password, platformPasswordEncoder, getId())) {
final String passwordEncodedValue = command.passwordValueOfParameterNamed(passwordParamName, platformPasswordEncoder,
getId());
actualChanges.put(passwordEncodedParamName, passwordEncodedValue);
updatePassword(passwordEncodedValue);
}
}
@@ -241,7 +240,6 @@ public class AppUser extends AbstractPersistableCustom<Long> implements Platform
if (command.hasParameter(passwordEncodedParamName)) {
if (command.isChangeInStringParameterNamed(passwordEncodedParamName, this.password)) {
final String newValue = command.stringValueOfParameterNamed(passwordEncodedParamName);
actualChanges.put(passwordEncodedParamName, newValue);
updatePassword(newValue);
}
}
@@ -18,10 +18,12 @@
*/
package org.apache.fineract.infrastructure.configuration.data;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import java.io.Serializable;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
import org.apache.fineract.infrastructure.core.jersey.serializer.MaskedValueSerializer;
@Data
@NoArgsConstructor
@@ -31,6 +33,7 @@ public class ExternalServicesPropertiesData implements Serializable {
private static final long serialVersionUID = 1L;
private String name;
@JsonSerialize(using = MaskedValueSerializer.class)
private String value;
}
@@ -20,13 +20,16 @@ package org.apache.fineract.infrastructure.configuration.service;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.infrastructure.campaigns.sms.data.MessageGatewayConfigurationData;
import org.apache.fineract.infrastructure.configuration.data.ExternalServicesPropertiesData;
import org.apache.fineract.infrastructure.configuration.data.S3CredentialsData;
import org.apache.fineract.infrastructure.configuration.data.SMTPCredentialsData;
import org.apache.fineract.infrastructure.configuration.exception.ExternalServiceConfigurationNotFoundException;
import org.apache.fineract.infrastructure.core.service.StringUtil;
import org.apache.fineract.infrastructure.gcm.domain.NotificationConfigurationData;
import org.springframework.dao.DataAccessException;
import org.springframework.jdbc.core.JdbcTemplate;
@@ -99,14 +102,21 @@ public class ExternalServicesPropertiesReadPlatformServiceImpl implements Extern
private static final class ExternalServiceMapper implements RowMapper<ExternalServicesPropertiesData> {
List<String> secretAttributes;
ExternalServiceMapper() {
secretAttributes = new ArrayList<>();
secretAttributes.add("password");
secretAttributes.add("server_key");
}
@Override
public ExternalServicesPropertiesData mapRow(ResultSet rs, @SuppressWarnings("unused") int rowNum) throws SQLException {
// TODO Auto-generated method stub
final String name = rs.getString("name");
String value = rs.getString("value");
// Masking the password as we should not send the password back
if (name != null && "password".equalsIgnoreCase(name)) {
value = "XXXX";
if (name != null && secretAttributes.contains(name)) {
value = StringUtil.maskValue(value);
}
return new ExternalServicesPropertiesData().setName(name).setValue(value);
}
@@ -0,0 +1,40 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.infrastructure.core.jersey.serializer;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import java.io.IOException;
import org.apache.fineract.infrastructure.core.service.StringUtil;
public class MaskedValueSerializer extends JsonSerializer<String> {
@Override
public void serialize(String value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
if (value == null || value.isBlank()) {
gen.writeString(value);
return;
}
String masked = StringUtil.maskValue(value);
gen.writeString(masked);
}
}
@@ -197,8 +197,6 @@ final class UsersApiResourceSwagger {
@Schema(example = "Test")
public String firstname;
@Schema(example = "abc3326b1bb376351c7baeb4175f5e0504e33aadf6a158474a6d71de1befae51")
public String passwordEncoded;
}
@Schema(example = "1")
@@ -18,6 +18,8 @@
*/
package org.apache.fineract.integrationtests;
import static org.junit.jupiter.api.Assertions.assertTrue;
import io.restassured.builder.RequestSpecBuilder;
import io.restassured.builder.ResponseSpecBuilder;
import io.restassured.http.ContentType;
@@ -83,7 +85,6 @@ public class ExternalServicesConfigurationTest {
}
// Checking for SMTP:
configName = "username";
externalServicesConfig = ExternalServicesConfigurationHelper.getExternalServicesConfigurationByServiceName(requestSpec,
responseSpec, "SMTP");
@@ -111,6 +112,28 @@ public class ExternalServicesConfigurationTest {
}
// Checking for Notifications:
configName = "server_key";
externalServicesConfig = ExternalServicesConfigurationHelper.getExternalServicesConfigurationByServiceName(requestSpec,
responseSpec, "NOTIFICATION");
Assertions.assertNotNull(externalServicesConfig);
for (Integer configIndex = 0; configIndex < externalServicesConfig.size(); configIndex++) {
String name = (String) externalServicesConfig.get(configIndex).get("name");
String value = null;
if (name.equals(configName)) {
value = (String) externalServicesConfig.get(configIndex).get("value");
if (value == null) {
value = "testnull";
}
LOG.info("{} : {}", name, value);
assertTrue(hasMoreThanThreeStars(value));
}
}
}
private boolean hasMoreThanThreeStars(String input) {
return input != null && input.matches("(.*\\*.*){4,}");
}
}