FINERACT-1831 - Encrypted tenant passwords
- [x] Encrypt tenant passwords - [x] Update integration tests - [x] Check application database use encrypted passwords - [x] One time migration for the existing password
This commit is contained in:
committed by
Arnold Gálovics
parent
f1c89ac914
commit
3297707817
+4
@@ -67,6 +67,9 @@ public class FineractProperties {
|
|||||||
private String identifier;
|
private String identifier;
|
||||||
private String name;
|
private String name;
|
||||||
private String description;
|
private String description;
|
||||||
|
private String masterPassword;
|
||||||
|
private String encryption;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Getter
|
@Getter
|
||||||
@@ -244,4 +247,5 @@ public class FineractProperties {
|
|||||||
private boolean regexWhitelistEnabled;
|
private boolean regexWhitelistEnabled;
|
||||||
private List<String> regexWhitelist;
|
private List<String> regexWhitelist;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
+7
-109
@@ -21,11 +21,13 @@ package org.apache.fineract.infrastructure.core.domain;
|
|||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
import java.sql.Connection;
|
import java.sql.Connection;
|
||||||
import javax.sql.DataSource;
|
import javax.sql.DataSource;
|
||||||
|
import lombok.Getter;
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Holds Tenant's DB server connection connection details.
|
* Holds Tenant's DB server connection connection details.
|
||||||
*/
|
*/
|
||||||
|
@Getter
|
||||||
public class FineractPlatformTenantConnection implements Serializable {
|
public class FineractPlatformTenantConnection implements Serializable {
|
||||||
|
|
||||||
private final Long connectionId;
|
private final Long connectionId;
|
||||||
@@ -56,6 +58,8 @@ public class FineractPlatformTenantConnection implements Serializable {
|
|||||||
private final int minEvictableIdleTimeMillis;
|
private final int minEvictableIdleTimeMillis;
|
||||||
private final boolean testOnBorrow;
|
private final boolean testOnBorrow;
|
||||||
|
|
||||||
|
private final String masterPasswordHash;
|
||||||
|
|
||||||
public FineractPlatformTenantConnection(final Long connectionId, final String schemaName, String schemaServer,
|
public FineractPlatformTenantConnection(final Long connectionId, final String schemaName, String schemaServer,
|
||||||
final String schemaServerPort, final String schemaConnectionParameters, final String schemaUsername,
|
final String schemaServerPort, final String schemaConnectionParameters, final String schemaUsername,
|
||||||
final String schemaPassword, final boolean autoUpdateEnabled, final int initialSize, final long validationInterval,
|
final String schemaPassword, final boolean autoUpdateEnabled, final int initialSize, final long validationInterval,
|
||||||
@@ -63,7 +67,8 @@ public class FineractPlatformTenantConnection implements Serializable {
|
|||||||
final int abandonWhenPercentageFull, final int maxActive, final int minIdle, final int maxIdle, final int suspectTimeout,
|
final int abandonWhenPercentageFull, final int maxActive, final int minIdle, final int maxIdle, final int suspectTimeout,
|
||||||
final int timeBetweenEvictionRunsMillis, final int minEvictableIdleTimeMillis, final boolean tesOnBorrow,
|
final int timeBetweenEvictionRunsMillis, final int minEvictableIdleTimeMillis, final boolean tesOnBorrow,
|
||||||
final String readOnlySchemaServer, final String readOnlySchemaServerPort, final String readOnlySchemaName,
|
final String readOnlySchemaServer, final String readOnlySchemaServerPort, final String readOnlySchemaName,
|
||||||
final String readOnlySchemaUsername, final String readOnlySchemaPassword, final String readOnlySchemaConnectionParameters) {
|
final String readOnlySchemaUsername, final String readOnlySchemaPassword, final String readOnlySchemaConnectionParameters,
|
||||||
|
final String masterPasswordHash) {
|
||||||
|
|
||||||
this.connectionId = connectionId;
|
this.connectionId = connectionId;
|
||||||
this.schemaName = schemaName;
|
this.schemaName = schemaName;
|
||||||
@@ -92,114 +97,7 @@ public class FineractPlatformTenantConnection implements Serializable {
|
|||||||
this.readOnlySchemaUsername = readOnlySchemaUsername;
|
this.readOnlySchemaUsername = readOnlySchemaUsername;
|
||||||
this.readOnlySchemaPassword = readOnlySchemaPassword;
|
this.readOnlySchemaPassword = readOnlySchemaPassword;
|
||||||
this.readOnlySchemaConnectionParameters = readOnlySchemaConnectionParameters;
|
this.readOnlySchemaConnectionParameters = readOnlySchemaConnectionParameters;
|
||||||
}
|
this.masterPasswordHash = masterPasswordHash;
|
||||||
|
|
||||||
public String getSchemaServer() {
|
|
||||||
return this.schemaServer;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getSchemaServerPort() {
|
|
||||||
return this.schemaServerPort;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getSchemaConnectionParameters() {
|
|
||||||
return this.schemaConnectionParameters;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getSchemaUsername() {
|
|
||||||
return this.schemaUsername;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getSchemaPassword() {
|
|
||||||
return this.schemaPassword;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isAutoUpdateEnabled() {
|
|
||||||
return this.autoUpdateEnabled;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getInitialSize() {
|
|
||||||
return this.initialSize;
|
|
||||||
}
|
|
||||||
|
|
||||||
public long getValidationInterval() {
|
|
||||||
return this.validationInterval;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isRemoveAbandoned() {
|
|
||||||
return this.removeAbandoned;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getRemoveAbandonedTimeout() {
|
|
||||||
return this.removeAbandonedTimeout;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isLogAbandoned() {
|
|
||||||
return this.logAbandoned;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getAbandonWhenPercentageFull() {
|
|
||||||
return this.abandonWhenPercentageFull;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getMaxActive() {
|
|
||||||
return this.maxActive;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getMinIdle() {
|
|
||||||
return this.minIdle;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getMaxIdle() {
|
|
||||||
return this.maxIdle;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getSuspectTimeout() {
|
|
||||||
return this.suspectTimeout;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getTimeBetweenEvictionRunsMillis() {
|
|
||||||
return this.timeBetweenEvictionRunsMillis;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getMinEvictableIdleTimeMillis() {
|
|
||||||
return this.minEvictableIdleTimeMillis;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isTestOnBorrow() {
|
|
||||||
return testOnBorrow;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Long getConnectionId() {
|
|
||||||
return connectionId;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getSchemaName() {
|
|
||||||
return schemaName;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getReadOnlySchemaServer() {
|
|
||||||
return readOnlySchemaServer;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getReadOnlySchemaServerPort() {
|
|
||||||
return readOnlySchemaServerPort;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getReadOnlySchemaName() {
|
|
||||||
return readOnlySchemaName;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getReadOnlySchemaUsername() {
|
|
||||||
return readOnlySchemaUsername;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getReadOnlySchemaPassword() {
|
|
||||||
return readOnlySchemaPassword;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getReadOnlySchemaConnectionParameters() {
|
|
||||||
return readOnlySchemaConnectionParameters;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
+18
-26
@@ -24,9 +24,9 @@ import static org.apache.fineract.infrastructure.core.domain.FineractPlatformTen
|
|||||||
import com.zaxxer.hikari.HikariConfig;
|
import com.zaxxer.hikari.HikariConfig;
|
||||||
import javax.sql.DataSource;
|
import javax.sql.DataSource;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import org.apache.fineract.infrastructure.core.config.FineractProperties;
|
import org.apache.fineract.infrastructure.core.config.FineractProperties;
|
||||||
import org.apache.fineract.infrastructure.core.domain.FineractPlatformTenantConnection;
|
import org.apache.fineract.infrastructure.core.domain.FineractPlatformTenantConnection;
|
||||||
import org.apache.fineract.infrastructure.security.constants.TenantConstants;
|
|
||||||
import org.springframework.beans.factory.annotation.Qualifier;
|
import org.springframework.beans.factory.annotation.Qualifier;
|
||||||
import org.springframework.context.ApplicationContext;
|
import org.springframework.context.ApplicationContext;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
@@ -47,16 +47,24 @@ public class DataSourcePerTenantServiceFactory {
|
|||||||
private final DataSource tenantDataSource;
|
private final DataSource tenantDataSource;
|
||||||
private final HikariDataSourceFactory hikariDataSourceFactory;
|
private final HikariDataSourceFactory hikariDataSourceFactory;
|
||||||
|
|
||||||
|
private final DatabasePasswordEncryptor databasePasswordEncryptor;
|
||||||
|
|
||||||
public DataSourcePerTenantServiceFactory(@Qualifier("hikariTenantDataSource") DataSource tenantDataSource, HikariConfig hikariConfig,
|
public DataSourcePerTenantServiceFactory(@Qualifier("hikariTenantDataSource") DataSource tenantDataSource, HikariConfig hikariConfig,
|
||||||
FineractProperties fineractProperties, ApplicationContext context, HikariDataSourceFactory hikariDataSourceFactory) {
|
FineractProperties fineractProperties, ApplicationContext context, HikariDataSourceFactory hikariDataSourceFactory,
|
||||||
|
DatabasePasswordEncryptor databasePasswordEncryptor) {
|
||||||
this.hikariConfig = hikariConfig;
|
this.hikariConfig = hikariConfig;
|
||||||
this.fineractProperties = fineractProperties;
|
this.fineractProperties = fineractProperties;
|
||||||
this.context = context;
|
this.context = context;
|
||||||
this.tenantDataSource = tenantDataSource;
|
this.tenantDataSource = tenantDataSource;
|
||||||
this.hikariDataSourceFactory = hikariDataSourceFactory;
|
this.hikariDataSourceFactory = hikariDataSourceFactory;
|
||||||
|
this.databasePasswordEncryptor = databasePasswordEncryptor;
|
||||||
}
|
}
|
||||||
|
|
||||||
public DataSource createNewDataSourceFor(final FineractPlatformTenantConnection tenantConnection) {
|
public DataSource createNewDataSourceFor(final FineractPlatformTenantConnection tenantConnection) {
|
||||||
|
if (!databasePasswordEncryptor.isMasterPasswordHashValid(tenantConnection.getMasterPasswordHash())) {
|
||||||
|
throw new IllegalArgumentException(
|
||||||
|
"Invalid master password on tenant connection %d.".formatted(tenantConnection.getConnectionId()));
|
||||||
|
}
|
||||||
String protocol = toProtocol(tenantDataSource);
|
String protocol = toProtocol(tenantDataSource);
|
||||||
// Default properties for Writing
|
// Default properties for Writing
|
||||||
String schemaServer = tenantConnection.getSchemaServer();
|
String schemaServer = tenantConnection.getSchemaServer();
|
||||||
@@ -67,18 +75,13 @@ public class DataSourcePerTenantServiceFactory {
|
|||||||
String schemaConnectionParameters = tenantConnection.getSchemaConnectionParameters();
|
String schemaConnectionParameters = tenantConnection.getSchemaConnectionParameters();
|
||||||
// Properties to ReadOnly case
|
// Properties to ReadOnly case
|
||||||
if (fineractProperties.getMode().isReadOnlyMode()) {
|
if (fineractProperties.getMode().isReadOnlyMode()) {
|
||||||
schemaServer = getPropertyValue(tenantConnection.getReadOnlySchemaServer(), TenantConstants.PROPERTY_RO_SCHEMA_SERVER_NAME,
|
schemaServer = StringUtils.defaultIfBlank(tenantConnection.getReadOnlySchemaServer(), schemaServer);
|
||||||
schemaServer);
|
schemaPort = StringUtils.defaultIfBlank(tenantConnection.getReadOnlySchemaServerPort(), schemaPort);
|
||||||
schemaPort = getPropertyValue(tenantConnection.getReadOnlySchemaServerPort(), TenantConstants.PROPERTY_RO_SCHEMA_SERVER_PORT,
|
schemaName = StringUtils.defaultIfBlank(tenantConnection.getReadOnlySchemaName(), schemaName);
|
||||||
schemaPort);
|
schemaUsername = StringUtils.defaultIfBlank(tenantConnection.getReadOnlySchemaUsername(), schemaUsername);
|
||||||
schemaName = getPropertyValue(tenantConnection.getReadOnlySchemaName(), TenantConstants.PROPERTY_RO_SCHEMA_SCHEMA_NAME,
|
schemaPassword = StringUtils.defaultIfBlank(tenantConnection.getReadOnlySchemaPassword(), schemaPassword);
|
||||||
schemaName);
|
schemaConnectionParameters = StringUtils.defaultIfBlank(tenantConnection.getReadOnlySchemaConnectionParameters(),
|
||||||
schemaUsername = getPropertyValue(tenantConnection.getReadOnlySchemaUsername(), TenantConstants.PROPERTY_RO_SCHEMA_USERNAME,
|
schemaConnectionParameters);
|
||||||
schemaUsername);
|
|
||||||
schemaPassword = getPropertyValue(tenantConnection.getReadOnlySchemaPassword(), TenantConstants.PROPERTY_RO_SCHEMA_PASSWORD,
|
|
||||||
schemaPassword);
|
|
||||||
schemaConnectionParameters = getPropertyValue(tenantConnection.getReadOnlySchemaConnectionParameters(),
|
|
||||||
TenantConstants.PROPERTY_RO_SCHEMA_CONNECTION_PARAMETERS, schemaConnectionParameters);
|
|
||||||
}
|
}
|
||||||
String jdbcUrl = toJdbcUrl(protocol, schemaServer, schemaPort, schemaName, schemaConnectionParameters);
|
String jdbcUrl = toJdbcUrl(protocol, schemaServer, schemaPort, schemaName, schemaConnectionParameters);
|
||||||
log.debug("{}", jdbcUrl);
|
log.debug("{}", jdbcUrl);
|
||||||
@@ -88,7 +91,7 @@ public class DataSourcePerTenantServiceFactory {
|
|||||||
config.setJdbcUrl(jdbcUrl);
|
config.setJdbcUrl(jdbcUrl);
|
||||||
config.setPoolName(schemaName + "_pool");
|
config.setPoolName(schemaName + "_pool");
|
||||||
config.setUsername(schemaUsername);
|
config.setUsername(schemaUsername);
|
||||||
config.setPassword(schemaPassword);
|
config.setPassword(databasePasswordEncryptor.decrypt(schemaPassword));
|
||||||
config.setMinimumIdle(tenantConnection.getInitialSize());
|
config.setMinimumIdle(tenantConnection.getInitialSize());
|
||||||
config.setMaximumPoolSize(tenantConnection.getMaxActive());
|
config.setMaximumPoolSize(tenantConnection.getMaxActive());
|
||||||
config.setValidationTimeout(tenantConnection.getValidationInterval());
|
config.setValidationTimeout(tenantConnection.getValidationInterval());
|
||||||
@@ -108,15 +111,4 @@ public class DataSourcePerTenantServiceFactory {
|
|||||||
return hikariDataSourceFactory.create(config);
|
return hikariDataSourceFactory.create(config);
|
||||||
}
|
}
|
||||||
|
|
||||||
private String getPropertyValue(final String baseValue, final String propertyName, final String defaultValue) {
|
|
||||||
// If the property already has set, return It
|
|
||||||
if (null != baseValue) {
|
|
||||||
return baseValue;
|
|
||||||
}
|
|
||||||
if (context == null) {
|
|
||||||
return defaultValue;
|
|
||||||
}
|
|
||||||
return context.getEnvironment().getProperty(propertyName, defaultValue);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
+73
@@ -0,0 +1,73 @@
|
|||||||
|
/**
|
||||||
|
* 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.database;
|
||||||
|
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.Optional;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.apache.fineract.infrastructure.core.config.FineractProperties;
|
||||||
|
import org.apache.fineract.infrastructure.security.service.PasswordEncryptor;
|
||||||
|
import org.apache.fineract.infrastructure.security.utils.EncryptionUtil;
|
||||||
|
import org.springframework.security.crypto.bcrypt.BCrypt;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
@Component
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class DatabasePasswordEncryptor implements PasswordEncryptor {
|
||||||
|
|
||||||
|
public static final String DEFAULT_ENCRYPTION = "AES/CBC/PKCS5Padding";
|
||||||
|
|
||||||
|
public static final String DEFAULT_MASTER_PASSWORD = "fineract";
|
||||||
|
|
||||||
|
private final FineractProperties fineractProperties;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String encrypt(String plainPassword) {
|
||||||
|
String masterPassword = Optional.ofNullable(fineractProperties.getTenant())
|
||||||
|
.map(FineractProperties.FineractTenantProperties::getMasterPassword).orElse(DEFAULT_MASTER_PASSWORD);
|
||||||
|
String encryption = Optional.ofNullable(fineractProperties.getTenant())
|
||||||
|
.map(FineractProperties.FineractTenantProperties::getEncryption).orElse(DEFAULT_ENCRYPTION);
|
||||||
|
return EncryptionUtil.encryptToBase64(encryption, masterPassword, plainPassword);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String decrypt(String encryptedPassword) {
|
||||||
|
String masterPassword = Optional.ofNullable(fineractProperties.getTenant())
|
||||||
|
.map(FineractProperties.FineractTenantProperties::getMasterPassword).orElse(DEFAULT_MASTER_PASSWORD);
|
||||||
|
String encryption = Optional.ofNullable(fineractProperties.getTenant())
|
||||||
|
.map(FineractProperties.FineractTenantProperties::getEncryption).orElse(DEFAULT_ENCRYPTION);
|
||||||
|
return EncryptionUtil.decryptFromBase64(encryption, masterPassword, encryptedPassword);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getMasterPasswordHash() {
|
||||||
|
String masterPassword = Optional.ofNullable(fineractProperties) //
|
||||||
|
.map(FineractProperties::getTenant) //
|
||||||
|
.map(FineractProperties.FineractTenantProperties::getMasterPassword) //
|
||||||
|
.orElse(DEFAULT_MASTER_PASSWORD);
|
||||||
|
return BCrypt.hashpw(masterPassword.getBytes(StandardCharsets.UTF_8), BCrypt.gensalt());
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isMasterPasswordHashValid(String hashed) {
|
||||||
|
String masterPassword = Optional.ofNullable(fineractProperties) //
|
||||||
|
.map(FineractProperties::getTenant) //
|
||||||
|
.map(FineractProperties.FineractTenantProperties::getMasterPassword) //
|
||||||
|
.orElse(DEFAULT_MASTER_PASSWORD);
|
||||||
|
return BCrypt.checkpw(masterPassword, hashed);
|
||||||
|
}
|
||||||
|
}
|
||||||
+10
-2
@@ -25,6 +25,7 @@ import com.zaxxer.hikari.HikariDataSource;
|
|||||||
import javax.sql.DataSource;
|
import javax.sql.DataSource;
|
||||||
import org.apache.fineract.infrastructure.core.domain.FineractPlatformTenant;
|
import org.apache.fineract.infrastructure.core.domain.FineractPlatformTenant;
|
||||||
import org.apache.fineract.infrastructure.core.domain.FineractPlatformTenantConnection;
|
import org.apache.fineract.infrastructure.core.domain.FineractPlatformTenantConnection;
|
||||||
|
import org.apache.fineract.infrastructure.core.service.database.DatabasePasswordEncryptor;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
@@ -38,9 +39,13 @@ public class TenantDataSourceFactory {
|
|||||||
|
|
||||||
private final HikariDataSource tenantDataSource;
|
private final HikariDataSource tenantDataSource;
|
||||||
|
|
||||||
|
private final DatabasePasswordEncryptor databasePasswordEncryptor;
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
public TenantDataSourceFactory(@Qualifier("hikariTenantDataSource") HikariDataSource tenantDataSource) {
|
public TenantDataSourceFactory(@Qualifier("hikariTenantDataSource") HikariDataSource tenantDataSource,
|
||||||
|
DatabasePasswordEncryptor databasePasswordEncryptor) {
|
||||||
this.tenantDataSource = tenantDataSource;
|
this.tenantDataSource = tenantDataSource;
|
||||||
|
this.databasePasswordEncryptor = databasePasswordEncryptor;
|
||||||
}
|
}
|
||||||
|
|
||||||
public DataSource create(FineractPlatformTenant tenant) {
|
public DataSource create(FineractPlatformTenant tenant) {
|
||||||
@@ -53,8 +58,11 @@ public class TenantDataSourceFactory {
|
|||||||
dataSource.setConnectionTestQuery(tenantDataSource.getConnectionTestQuery());
|
dataSource.setConnectionTestQuery(tenantDataSource.getConnectionTestQuery());
|
||||||
|
|
||||||
FineractPlatformTenantConnection tenantConnection = tenant.getConnection();
|
FineractPlatformTenantConnection tenantConnection = tenant.getConnection();
|
||||||
|
if (!databasePasswordEncryptor.isMasterPasswordHashValid(tenantConnection.getMasterPasswordHash())) {
|
||||||
|
throw new IllegalArgumentException("Invalid master password");
|
||||||
|
}
|
||||||
dataSource.setUsername(tenantConnection.getSchemaUsername());
|
dataSource.setUsername(tenantConnection.getSchemaUsername());
|
||||||
dataSource.setPassword(tenantConnection.getSchemaPassword());
|
dataSource.setPassword(databasePasswordEncryptor.decrypt(tenantConnection.getSchemaPassword()));
|
||||||
String protocol = toProtocol(tenantDataSource);
|
String protocol = toProtocol(tenantDataSource);
|
||||||
String tenantJdbcUrl = toJdbcUrl(protocol, tenantConnection.getSchemaServer(), tenantConnection.getSchemaServerPort(),
|
String tenantJdbcUrl = toJdbcUrl(protocol, tenantConnection.getSchemaServer(), tenantConnection.getSchemaServerPort(),
|
||||||
tenantConnection.getSchemaName(), tenantConnection.getSchemaConnectionParameters());
|
tenantConnection.getSchemaName(), tenantConnection.getSchemaConnectionParameters());
|
||||||
|
|||||||
+4
@@ -24,6 +24,7 @@ import java.util.Arrays;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.function.Function;
|
import java.util.function.Function;
|
||||||
import javax.sql.DataSource;
|
import javax.sql.DataSource;
|
||||||
|
import liquibase.change.custom.CustomTaskChange;
|
||||||
import liquibase.exception.LiquibaseException;
|
import liquibase.exception.LiquibaseException;
|
||||||
import liquibase.integration.spring.SpringLiquibase;
|
import liquibase.integration.spring.SpringLiquibase;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
@@ -59,6 +60,9 @@ public class TenantDatabaseUpgradeService implements InitializingBean {
|
|||||||
private final TenantDataSourceFactory tenantDataSourceFactory;
|
private final TenantDataSourceFactory tenantDataSourceFactory;
|
||||||
private final Environment environment;
|
private final Environment environment;
|
||||||
|
|
||||||
|
// DO NOT REMOVE! Required for liquibase custom task initialization
|
||||||
|
private final List<CustomTaskChange> customTaskChangesForDependencyInjection;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void afterPropertiesSet() throws Exception {
|
public void afterPropertiesSet() throws Exception {
|
||||||
if (notLiquibaseOnlyMode()) {
|
if (notLiquibaseOnlyMode()) {
|
||||||
|
|||||||
+94
@@ -0,0 +1,94 @@
|
|||||||
|
/**
|
||||||
|
* 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.migration;
|
||||||
|
|
||||||
|
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
|
||||||
|
import java.sql.ResultSet;
|
||||||
|
import java.sql.Statement;
|
||||||
|
import liquibase.change.custom.CustomTaskChange;
|
||||||
|
import liquibase.database.Database;
|
||||||
|
import liquibase.database.jvm.JdbcConnection;
|
||||||
|
import liquibase.exception.CustomChangeException;
|
||||||
|
import liquibase.exception.SetupException;
|
||||||
|
import liquibase.exception.ValidationErrors;
|
||||||
|
import liquibase.resource.ResourceAccessor;
|
||||||
|
import org.apache.fineract.infrastructure.core.service.database.DatabasePasswordEncryptor;
|
||||||
|
import org.springframework.beans.BeansException;
|
||||||
|
import org.springframework.context.ApplicationContext;
|
||||||
|
import org.springframework.context.ApplicationContextAware;
|
||||||
|
import org.springframework.core.Ordered;
|
||||||
|
import org.springframework.core.annotation.Order;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
@Component
|
||||||
|
@Order(Ordered.HIGHEST_PRECEDENCE)
|
||||||
|
public class TenantPasswordEncryptionTask implements CustomTaskChange, ApplicationContextAware {
|
||||||
|
|
||||||
|
private static DatabasePasswordEncryptor databasePasswordEncryptor;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void execute(Database database) throws CustomChangeException {
|
||||||
|
JdbcConnection dbConn = (JdbcConnection) database.getConnection(); // autocommit is false
|
||||||
|
try (Statement selectStatement = dbConn.createStatement(); Statement updateStatement = dbConn.createStatement()) {
|
||||||
|
|
||||||
|
try (ResultSet rs = selectStatement.executeQuery("SELECT id, schema_password FROM tenant_server_connections")) {
|
||||||
|
while (rs.next()) {
|
||||||
|
String id = rs.getString("id");
|
||||||
|
String schemaPassword = rs.getString("schema_password");
|
||||||
|
String encryptedPassword = TenantPasswordEncryptionTask.databasePasswordEncryptor.encrypt(schemaPassword);
|
||||||
|
|
||||||
|
String updateSql = String.format(
|
||||||
|
"update tenant_server_connections set schema_password = '%s', master_password_hash = '%s' where id = %s",
|
||||||
|
encryptedPassword, TenantPasswordEncryptionTask.databasePasswordEncryptor.getMasterPasswordHash(), id);
|
||||||
|
updateStatement.execute(updateSql);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new CustomChangeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getConfirmationMessage() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setUp() throws SetupException {
|
||||||
|
// Not required
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setFileOpener(ResourceAccessor resourceAccessor) {
|
||||||
|
// Not required
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ValidationErrors validate(Database database) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@SuppressWarnings("static-access")
|
||||||
|
@SuppressFBWarnings("ST_WRITE_TO_STATIC_FROM_INSTANCE_METHOD")
|
||||||
|
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
|
||||||
|
TenantPasswordEncryptionTask.databasePasswordEncryptor = applicationContext.getBean(DatabasePasswordEncryptor.class);
|
||||||
|
}
|
||||||
|
}
|
||||||
+4
-3
@@ -40,8 +40,8 @@ public final class TenantMapper implements RowMapper<FineractPlatformTenant> {
|
|||||||
+ " ts.readonly_schema_server as readOnlySchemaServer, " + " ts.readonly_schema_server_port as readOnlySchemaServerPort, "
|
+ " ts.readonly_schema_server as readOnlySchemaServer, " + " ts.readonly_schema_server_port as readOnlySchemaServerPort, "
|
||||||
+ " ts.readonly_schema_name as readOnlySchemaName, " + " ts.readonly_schema_username as readOnlySchemaUsername, "
|
+ " ts.readonly_schema_name as readOnlySchemaName, " + " ts.readonly_schema_username as readOnlySchemaUsername, "
|
||||||
+ " ts.readonly_schema_password as readOnlySchemaPassword, "
|
+ " ts.readonly_schema_password as readOnlySchemaPassword, "
|
||||||
+ " ts.readonly_schema_connection_parameters as readOnlySchemaConnectionParameters "
|
+ " ts.readonly_schema_connection_parameters as readOnlySchemaConnectionParameters, "
|
||||||
+ " from tenants t left join tenant_server_connections ts ";
|
+ " ts.master_password_hash as masterPasswordHash " + " from tenants t left join tenant_server_connections ts ";
|
||||||
private final StringBuilder sqlBuilder = new StringBuilder(TENANT_SERVER_CONNECTION_BUILDER);
|
private final StringBuilder sqlBuilder = new StringBuilder(TENANT_SERVER_CONNECTION_BUILDER);
|
||||||
|
|
||||||
public TenantMapper(boolean isReport) {
|
public TenantMapper(boolean isReport) {
|
||||||
@@ -98,11 +98,12 @@ public final class TenantMapper implements RowMapper<FineractPlatformTenant> {
|
|||||||
final int suspectTimeout = rs.getInt("poolSuspectTimeout");
|
final int suspectTimeout = rs.getInt("poolSuspectTimeout");
|
||||||
final int timeBetweenEvictionRunsMillis = rs.getInt("poolTimeBetweenEvictionRunsMillis");
|
final int timeBetweenEvictionRunsMillis = rs.getInt("poolTimeBetweenEvictionRunsMillis");
|
||||||
final int minEvictableIdleTimeMillis = rs.getInt("poolMinEvictableIdleTimeMillis");
|
final int minEvictableIdleTimeMillis = rs.getInt("poolMinEvictableIdleTimeMillis");
|
||||||
|
final String masterPasswordHash = rs.getString("masterPasswordHash");
|
||||||
|
|
||||||
return new FineractPlatformTenantConnection(connectionId, schemaName, schemaServer, schemaServerPort, schemaConnectionParameters,
|
return new FineractPlatformTenantConnection(connectionId, schemaName, schemaServer, schemaServerPort, schemaConnectionParameters,
|
||||||
schemaUsername, schemaPassword, autoUpdateEnabled, initialSize, validationInterval, removeAbandoned, removeAbandonedTimeout,
|
schemaUsername, schemaPassword, autoUpdateEnabled, initialSize, validationInterval, removeAbandoned, removeAbandonedTimeout,
|
||||||
logAbandoned, abandonWhenPercentageFull, maxActive, minIdle, maxIdle, suspectTimeout, timeBetweenEvictionRunsMillis,
|
logAbandoned, abandonWhenPercentageFull, maxActive, minIdle, maxIdle, suspectTimeout, timeBetweenEvictionRunsMillis,
|
||||||
minEvictableIdleTimeMillis, testOnBorrow, readOnlySchemaServer, readOnlySchemaServerPort, readOnlySchemaName,
|
minEvictableIdleTimeMillis, testOnBorrow, readOnlySchemaServer, readOnlySchemaServerPort, readOnlySchemaName,
|
||||||
readOnlySchemaUsername, readOnlySchemaPassword, readOnlySchemaConnectionParameters);
|
readOnlySchemaUsername, readOnlySchemaPassword, readOnlySchemaConnectionParameters, masterPasswordHash);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+26
@@ -0,0 +1,26 @@
|
|||||||
|
/**
|
||||||
|
* 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.security.service;
|
||||||
|
|
||||||
|
public interface PasswordEncryptor {
|
||||||
|
|
||||||
|
String encrypt(String plainPassword);
|
||||||
|
|
||||||
|
String decrypt(String encryptedPassword);
|
||||||
|
}
|
||||||
+97
@@ -0,0 +1,97 @@
|
|||||||
|
/**
|
||||||
|
* 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.security.utils;
|
||||||
|
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
import java.security.SecureRandom;
|
||||||
|
import java.security.spec.InvalidKeySpecException;
|
||||||
|
import java.security.spec.KeySpec;
|
||||||
|
import java.util.Base64;
|
||||||
|
import javax.crypto.Cipher;
|
||||||
|
import javax.crypto.SecretKeyFactory;
|
||||||
|
import javax.crypto.spec.IvParameterSpec;
|
||||||
|
import javax.crypto.spec.PBEKeySpec;
|
||||||
|
import javax.crypto.spec.SecretKeySpec;
|
||||||
|
import javax.validation.constraints.NotNull;
|
||||||
|
|
||||||
|
public final class EncryptionUtil {
|
||||||
|
|
||||||
|
private static SecureRandom random = new SecureRandom();
|
||||||
|
|
||||||
|
private EncryptionUtil() {}
|
||||||
|
|
||||||
|
public static String encryptToBase64(@NotNull String cipherType, @NotNull String masterPassword, @NotNull String data) {
|
||||||
|
try {
|
||||||
|
Cipher cipher = Cipher.getInstance(cipherType);
|
||||||
|
byte[] salt = new byte[16];
|
||||||
|
byte[] ivBytes = new byte[16];
|
||||||
|
random.nextBytes(salt);
|
||||||
|
random.nextBytes(ivBytes);
|
||||||
|
|
||||||
|
IvParameterSpec iv = new IvParameterSpec(ivBytes);
|
||||||
|
|
||||||
|
SecretKeySpec keySpec = generateKeySpec(masterPassword, salt);
|
||||||
|
|
||||||
|
cipher.init(Cipher.ENCRYPT_MODE, keySpec, iv);
|
||||||
|
byte[] encValue = cipher.doFinal(data.getBytes(StandardCharsets.UTF_8));
|
||||||
|
byte[] finalCiphertext = new byte[encValue.length + 2 * 16];
|
||||||
|
|
||||||
|
System.arraycopy(ivBytes, 0, finalCiphertext, 0, 16);
|
||||||
|
System.arraycopy(salt, 0, finalCiphertext, 16, 16);
|
||||||
|
System.arraycopy(encValue, 0, finalCiphertext, 32, encValue.length);
|
||||||
|
return Base64.getEncoder().encodeToString(finalCiphertext);
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new IllegalArgumentException("Unable to encrypt data. Please check if the master password and algorithm is correct.", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String decryptFromBase64(@NotNull String cipherType, @NotNull String masterPassword, @NotNull String encryptedData) {
|
||||||
|
try {
|
||||||
|
Cipher cipher = Cipher.getInstance(cipherType);
|
||||||
|
byte[] rawData = Base64.getDecoder().decode(encryptedData);
|
||||||
|
byte[] ivBytes = new byte[16];
|
||||||
|
byte[] salt = new byte[16];
|
||||||
|
byte[] ciphertext = new byte[rawData.length - 32];
|
||||||
|
|
||||||
|
System.arraycopy(rawData, 0, ivBytes, 0, 16);
|
||||||
|
System.arraycopy(rawData, 16, salt, 0, 16);
|
||||||
|
System.arraycopy(rawData, 32, ciphertext, 0, rawData.length - 32);
|
||||||
|
|
||||||
|
SecretKeySpec keySpec = generateKeySpec(masterPassword, salt);
|
||||||
|
|
||||||
|
IvParameterSpec iv = new IvParameterSpec(ivBytes);
|
||||||
|
|
||||||
|
cipher.init(Cipher.DECRYPT_MODE, keySpec, iv);
|
||||||
|
return new String(cipher.doFinal(ciphertext), StandardCharsets.UTF_8);
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new IllegalArgumentException("Unable to decrypt data. Please check if the master password and algorithm is correct.", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
private static SecretKeySpec generateKeySpec(String masterPassword, byte[] salt)
|
||||||
|
throws NoSuchAlgorithmException, InvalidKeySpecException {
|
||||||
|
KeySpec spec = new PBEKeySpec(masterPassword.toCharArray(), salt, 65536, 256); // AES-256
|
||||||
|
SecretKeyFactory f = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1");
|
||||||
|
byte[] key = f.generateSecret(spec).getEncoded();
|
||||||
|
return new SecretKeySpec(key, "AES");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -34,6 +34,8 @@ fineract.tenant.timezone=${FINERACT_DEFAULT_TENANTDB_TIMEZONE:Asia/Kolkata}
|
|||||||
fineract.tenant.identifier=${FINERACT_DEFAULT_TENANTDB_IDENTIFIER:default}
|
fineract.tenant.identifier=${FINERACT_DEFAULT_TENANTDB_IDENTIFIER:default}
|
||||||
fineract.tenant.name=${FINERACT_DEFAULT_TENANTDB_NAME:fineract_default}
|
fineract.tenant.name=${FINERACT_DEFAULT_TENANTDB_NAME:fineract_default}
|
||||||
fineract.tenant.description=${FINERACT_DEFAULT_TENANTDB_DESCRIPTION:Default Demo Tenant}
|
fineract.tenant.description=${FINERACT_DEFAULT_TENANTDB_DESCRIPTION:Default Demo Tenant}
|
||||||
|
fineract.tenant.master-password=${FINERACT_DEFAULT_TENANTDB_MASTER_PASSWORD:fineract}
|
||||||
|
fineract.tenant.encrytion=${FINERACT_DEFAULT_TENANTDB_ENCRYPTION:"AES/CBC/PKCS5Padding"}
|
||||||
|
|
||||||
fineract.mode.read-enabled=${FINERACT_MODE_READ_ENABLED:true}
|
fineract.mode.read-enabled=${FINERACT_MODE_READ_ENABLED:true}
|
||||||
fineract.mode.write-enabled=${FINERACT_MODE_WRITE_ENABLED:true}
|
fineract.mode.write-enabled=${FINERACT_MODE_WRITE_ENABLED:true}
|
||||||
|
|||||||
+1
@@ -26,4 +26,5 @@
|
|||||||
<include file="parts/0004_readonly_database_connection.xml" relativeToChangelogFile="true"/>
|
<include file="parts/0004_readonly_database_connection.xml" relativeToChangelogFile="true"/>
|
||||||
<include file="parts/0005_jdbc_connection_string.xml" relativeToChangelogFile="true"/>
|
<include file="parts/0005_jdbc_connection_string.xml" relativeToChangelogFile="true"/>
|
||||||
<include file="parts/0006_drop_retry_parameter_columns.xml" relativeToChangelogFile="true"/>
|
<include file="parts/0006_drop_retry_parameter_columns.xml" relativeToChangelogFile="true"/>
|
||||||
|
<include file="parts/0007_encrypt_existing_tenant_passwords.xml" relativeToChangelogFile="true"/>
|
||||||
</databaseChangeLog>
|
</databaseChangeLog>
|
||||||
|
|||||||
+34
@@ -0,0 +1,34 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!--
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
-->
|
||||||
|
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.1.xsd">
|
||||||
|
<changeSet author="fineract" id="1" context="tenant_store_db">
|
||||||
|
<modifyDataType tableName="tenant_server_connections" columnName="schema_password" newDataType="varchar(255)"/>
|
||||||
|
<addColumn tableName="tenant_server_connections">
|
||||||
|
<column name="master_password_hash" type="varchar(255)"></column>
|
||||||
|
</addColumn>
|
||||||
|
</changeSet>
|
||||||
|
<changeSet author="fineract" id="2" context="tenant_store_db">
|
||||||
|
<customChange class="org.apache.fineract.infrastructure.core.service.migration.TenantPasswordEncryptionTask"/>
|
||||||
|
</changeSet>
|
||||||
|
</databaseChangeLog>
|
||||||
@@ -22,10 +22,13 @@ import static org.mockito.Mockito.RETURNS_MOCKS;
|
|||||||
import static org.mockito.Mockito.mock;
|
import static org.mockito.Mockito.mock;
|
||||||
|
|
||||||
import com.zaxxer.hikari.HikariDataSource;
|
import com.zaxxer.hikari.HikariDataSource;
|
||||||
|
import java.util.List;
|
||||||
import javax.sql.DataSource;
|
import javax.sql.DataSource;
|
||||||
|
import liquibase.change.custom.CustomTaskChange;
|
||||||
import org.apache.fineract.infrastructure.core.config.FineractProperties;
|
import org.apache.fineract.infrastructure.core.config.FineractProperties;
|
||||||
import org.apache.fineract.infrastructure.core.domain.FineractPlatformTenant;
|
import org.apache.fineract.infrastructure.core.domain.FineractPlatformTenant;
|
||||||
import org.apache.fineract.infrastructure.core.service.database.DatabaseIndependentQueryService;
|
import org.apache.fineract.infrastructure.core.service.database.DatabaseIndependentQueryService;
|
||||||
|
import org.apache.fineract.infrastructure.core.service.database.DatabasePasswordEncryptor;
|
||||||
import org.apache.fineract.infrastructure.core.service.migration.ExtendedSpringLiquibaseFactory;
|
import org.apache.fineract.infrastructure.core.service.migration.ExtendedSpringLiquibaseFactory;
|
||||||
import org.apache.fineract.infrastructure.core.service.migration.TenantDataSourceFactory;
|
import org.apache.fineract.infrastructure.core.service.migration.TenantDataSourceFactory;
|
||||||
import org.apache.fineract.infrastructure.core.service.migration.TenantDatabaseStateVerifier;
|
import org.apache.fineract.infrastructure.core.service.migration.TenantDatabaseStateVerifier;
|
||||||
@@ -83,8 +86,8 @@ import org.springframework.transaction.annotation.EnableTransactionManagement;
|
|||||||
public class TestConfiguration {
|
public class TestConfiguration {
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
public TenantDataSourceFactory tenantDataSourceFactory() {
|
public TenantDataSourceFactory tenantDataSourceFactory(DatabasePasswordEncryptor databasePasswordEncryptor) {
|
||||||
return new TenantDataSourceFactory(null) {
|
return new TenantDataSourceFactory(null, databasePasswordEncryptor) {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public DataSource create(FineractPlatformTenant tenant) {
|
public DataSource create(FineractPlatformTenant tenant) {
|
||||||
@@ -151,9 +154,10 @@ public class TestConfiguration {
|
|||||||
public TenantDatabaseUpgradeService tenantDatabaseUpgradeService(TenantDetailsService tenantDetailsService,
|
public TenantDatabaseUpgradeService tenantDatabaseUpgradeService(TenantDetailsService tenantDetailsService,
|
||||||
HikariDataSource tenantDataSource, TenantDatabaseStateVerifier tenantDatabaseStateVerifier,
|
HikariDataSource tenantDataSource, TenantDatabaseStateVerifier tenantDatabaseStateVerifier,
|
||||||
ExtendedSpringLiquibaseFactory liquibaseFactory, TenantDataSourceFactory tenantDataSourceFactory,
|
ExtendedSpringLiquibaseFactory liquibaseFactory, TenantDataSourceFactory tenantDataSourceFactory,
|
||||||
FineractProperties fineractProperties, Environment environment) {
|
FineractProperties fineractProperties, Environment environment,
|
||||||
|
List<CustomTaskChange> customTaskChangesForDependencyInjection) {
|
||||||
return new TenantDatabaseUpgradeService(tenantDetailsService, tenantDataSource, fineractProperties, tenantDatabaseStateVerifier,
|
return new TenantDatabaseUpgradeService(tenantDetailsService, tenantDataSource, fineractProperties, tenantDatabaseStateVerifier,
|
||||||
liquibaseFactory, tenantDataSourceFactory, environment);
|
liquibaseFactory, tenantDataSourceFactory, environment, customTaskChangesForDependencyInjection);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
+27
-2
@@ -38,7 +38,9 @@ import org.apache.fineract.infrastructure.core.config.FineractProperties;
|
|||||||
import org.apache.fineract.infrastructure.core.domain.FineractPlatformTenant;
|
import org.apache.fineract.infrastructure.core.domain.FineractPlatformTenant;
|
||||||
import org.apache.fineract.infrastructure.core.domain.FineractPlatformTenantConnection;
|
import org.apache.fineract.infrastructure.core.domain.FineractPlatformTenantConnection;
|
||||||
import org.apache.fineract.infrastructure.core.service.database.DataSourcePerTenantServiceFactory;
|
import org.apache.fineract.infrastructure.core.service.database.DataSourcePerTenantServiceFactory;
|
||||||
|
import org.apache.fineract.infrastructure.core.service.database.DatabasePasswordEncryptor;
|
||||||
import org.apache.fineract.infrastructure.core.service.database.HikariDataSourceFactory;
|
import org.apache.fineract.infrastructure.core.service.database.HikariDataSourceFactory;
|
||||||
|
import org.apache.fineract.infrastructure.security.utils.EncryptionUtil;
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.junit.jupiter.api.extension.ExtendWith;
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
@@ -49,6 +51,7 @@ import org.mockito.Mock;
|
|||||||
import org.mockito.junit.jupiter.MockitoExtension;
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
import org.mockito.junit.jupiter.MockitoSettings;
|
import org.mockito.junit.jupiter.MockitoSettings;
|
||||||
import org.mockito.quality.Strictness;
|
import org.mockito.quality.Strictness;
|
||||||
|
import org.springframework.security.crypto.bcrypt.BCrypt;
|
||||||
|
|
||||||
@ExtendWith(MockitoExtension.class)
|
@ExtendWith(MockitoExtension.class)
|
||||||
@MockitoSettings(strictness = Strictness.LENIENT)
|
@MockitoSettings(strictness = Strictness.LENIENT)
|
||||||
@@ -81,6 +84,10 @@ public class DataSourcePerTenantServiceFactoryTest {
|
|||||||
public static final String MASTER_DB_CONN_TEST_QUERY = "SELECT 1";
|
public static final String MASTER_DB_CONN_TEST_QUERY = "SELECT 1";
|
||||||
public static final boolean MASTER_DB_AUTO_COMMIT_ENABLED = true;
|
public static final boolean MASTER_DB_AUTO_COMMIT_ENABLED = true;
|
||||||
|
|
||||||
|
public static final String MASTER_MASTER_PASSWORD = "fineract";
|
||||||
|
|
||||||
|
public static final String MASTER_ENCRYPTION = "AES/CBC/PKCS5Padding";
|
||||||
|
|
||||||
@Mock
|
@Mock
|
||||||
private FineractProperties fineractProperties;
|
private FineractProperties fineractProperties;
|
||||||
|
|
||||||
@@ -102,6 +109,9 @@ public class DataSourcePerTenantServiceFactoryTest {
|
|||||||
@Captor
|
@Captor
|
||||||
private ArgumentCaptor<HikariConfig> hikariConfigCaptor;
|
private ArgumentCaptor<HikariConfig> hikariConfigCaptor;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private DatabasePasswordEncryptor databasePasswordEncryptor;
|
||||||
|
|
||||||
@InjectMocks
|
@InjectMocks
|
||||||
private DataSourcePerTenantServiceFactory underTest;
|
private DataSourcePerTenantServiceFactory underTest;
|
||||||
|
|
||||||
@@ -119,15 +129,19 @@ public class DataSourcePerTenantServiceFactoryTest {
|
|||||||
given(tenantConnection.getSchemaServerPort()).willReturn(MASTER_DB_SERVER_PORT);
|
given(tenantConnection.getSchemaServerPort()).willReturn(MASTER_DB_SERVER_PORT);
|
||||||
given(tenantConnection.getSchemaName()).willReturn(MASTER_DB_SCHEMA_NAME);
|
given(tenantConnection.getSchemaName()).willReturn(MASTER_DB_SCHEMA_NAME);
|
||||||
given(tenantConnection.getSchemaUsername()).willReturn(MASTER_DB_USERNAME);
|
given(tenantConnection.getSchemaUsername()).willReturn(MASTER_DB_USERNAME);
|
||||||
given(tenantConnection.getSchemaPassword()).willReturn(MASTER_DB_PASSWORD);
|
given(tenantConnection.getSchemaPassword())
|
||||||
|
.willReturn(EncryptionUtil.encryptToBase64(MASTER_ENCRYPTION, MASTER_MASTER_PASSWORD, MASTER_DB_PASSWORD));
|
||||||
given(tenantConnection.getSchemaConnectionParameters()).willReturn(MASTER_DB_CONN_PARAMS);
|
given(tenantConnection.getSchemaConnectionParameters()).willReturn(MASTER_DB_CONN_PARAMS);
|
||||||
|
|
||||||
given(tenantConnection.getReadOnlySchemaServer()).willReturn(READONLY_DB_SERVER);
|
given(tenantConnection.getReadOnlySchemaServer()).willReturn(READONLY_DB_SERVER);
|
||||||
given(tenantConnection.getReadOnlySchemaServerPort()).willReturn(READONLY_DB_SERVER_PORT);
|
given(tenantConnection.getReadOnlySchemaServerPort()).willReturn(READONLY_DB_SERVER_PORT);
|
||||||
given(tenantConnection.getReadOnlySchemaName()).willReturn(READONLY_DB_SCHEMA_NAME);
|
given(tenantConnection.getReadOnlySchemaName()).willReturn(READONLY_DB_SCHEMA_NAME);
|
||||||
given(tenantConnection.getReadOnlySchemaUsername()).willReturn(READONLY_DB_USERNAME);
|
given(tenantConnection.getReadOnlySchemaUsername()).willReturn(READONLY_DB_USERNAME);
|
||||||
given(tenantConnection.getReadOnlySchemaPassword()).willReturn(READONLY_DB_PASSWORD);
|
given(tenantConnection.getReadOnlySchemaPassword())
|
||||||
|
.willReturn(EncryptionUtil.encryptToBase64(MASTER_ENCRYPTION, MASTER_MASTER_PASSWORD, READONLY_DB_PASSWORD));
|
||||||
given(tenantConnection.getReadOnlySchemaConnectionParameters()).willReturn(READONLY_DB_CONN_PARAMS);
|
given(tenantConnection.getReadOnlySchemaConnectionParameters()).willReturn(READONLY_DB_CONN_PARAMS);
|
||||||
|
String hashedMasterPassword = BCrypt.hashpw("master-password", BCrypt.gensalt());
|
||||||
|
given(tenantConnection.getMasterPasswordHash()).willReturn(hashedMasterPassword);
|
||||||
|
|
||||||
given(defaultTenant.getConnection()).willReturn(tenantConnection);
|
given(defaultTenant.getConnection()).willReturn(tenantConnection);
|
||||||
given(tenantConnection.getInitialSize()).willReturn(MASTER_DB_INITIAL_SIZE);
|
given(tenantConnection.getInitialSize()).willReturn(MASTER_DB_INITIAL_SIZE);
|
||||||
@@ -141,6 +155,17 @@ public class DataSourcePerTenantServiceFactoryTest {
|
|||||||
given(tenantHikariConfig.isAutoCommit()).willReturn(MASTER_DB_AUTO_COMMIT_ENABLED);
|
given(tenantHikariConfig.isAutoCommit()).willReturn(MASTER_DB_AUTO_COMMIT_ENABLED);
|
||||||
|
|
||||||
given(hikariDataSourceFactory.create(any())).willReturn(mock(HikariDataSource.class));
|
given(hikariDataSourceFactory.create(any())).willReturn(mock(HikariDataSource.class));
|
||||||
|
FineractProperties.FineractTenantProperties tenantPropertiesMock = mock(FineractProperties.FineractTenantProperties.class);
|
||||||
|
given(tenantPropertiesMock.getEncryption()).willReturn(MASTER_ENCRYPTION);
|
||||||
|
given(tenantPropertiesMock.getMasterPassword()).willReturn(MASTER_MASTER_PASSWORD);
|
||||||
|
given(fineractProperties.getTenant()).willReturn(tenantPropertiesMock);
|
||||||
|
|
||||||
|
given(databasePasswordEncryptor.isMasterPasswordHashValid(any())).willReturn(true);
|
||||||
|
given(databasePasswordEncryptor.getMasterPasswordHash()).willReturn(hashedMasterPassword);
|
||||||
|
given(databasePasswordEncryptor.decrypt(any())).will(
|
||||||
|
answer -> EncryptionUtil.decryptFromBase64(MASTER_ENCRYPTION, MASTER_MASTER_PASSWORD, answer.getArgument(0, String.class)));
|
||||||
|
given(databasePasswordEncryptor.encrypt(any())).will(
|
||||||
|
answer -> EncryptionUtil.encryptToBase64(MASTER_ENCRYPTION, MASTER_MASTER_PASSWORD, answer.getArgument(0, String.class)));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|||||||
+8
-1
@@ -29,16 +29,19 @@ import static org.mockito.Mockito.verifyNoInteractions;
|
|||||||
import static org.mockito.Mockito.verifyNoMoreInteractions;
|
import static org.mockito.Mockito.verifyNoMoreInteractions;
|
||||||
|
|
||||||
import io.cucumber.java8.En;
|
import io.cucumber.java8.En;
|
||||||
|
import java.util.Arrays;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import javax.sql.DataSource;
|
import javax.sql.DataSource;
|
||||||
import org.apache.fineract.infrastructure.core.config.FineractProperties;
|
import org.apache.fineract.infrastructure.core.config.FineractProperties;
|
||||||
import org.apache.fineract.infrastructure.core.domain.FineractPlatformTenant;
|
import org.apache.fineract.infrastructure.core.domain.FineractPlatformTenant;
|
||||||
|
import org.apache.fineract.infrastructure.core.service.database.DatabasePasswordEncryptor;
|
||||||
import org.apache.fineract.infrastructure.core.service.migration.ExtendedSpringLiquibase;
|
import org.apache.fineract.infrastructure.core.service.migration.ExtendedSpringLiquibase;
|
||||||
import org.apache.fineract.infrastructure.core.service.migration.ExtendedSpringLiquibaseFactory;
|
import org.apache.fineract.infrastructure.core.service.migration.ExtendedSpringLiquibaseFactory;
|
||||||
import org.apache.fineract.infrastructure.core.service.migration.SchemaUpgradeNeededException;
|
import org.apache.fineract.infrastructure.core.service.migration.SchemaUpgradeNeededException;
|
||||||
import org.apache.fineract.infrastructure.core.service.migration.TenantDataSourceFactory;
|
import org.apache.fineract.infrastructure.core.service.migration.TenantDataSourceFactory;
|
||||||
import org.apache.fineract.infrastructure.core.service.migration.TenantDatabaseStateVerifier;
|
import org.apache.fineract.infrastructure.core.service.migration.TenantDatabaseStateVerifier;
|
||||||
import org.apache.fineract.infrastructure.core.service.migration.TenantDatabaseUpgradeService;
|
import org.apache.fineract.infrastructure.core.service.migration.TenantDatabaseUpgradeService;
|
||||||
|
import org.apache.fineract.infrastructure.core.service.migration.TenantPasswordEncryptionTask;
|
||||||
import org.apache.fineract.infrastructure.core.service.tenant.TenantDetailsService;
|
import org.apache.fineract.infrastructure.core.service.tenant.TenantDetailsService;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.core.env.Environment;
|
import org.springframework.core.env.Environment;
|
||||||
@@ -65,6 +68,8 @@ public class LiquibaseStepDefinitions implements En {
|
|||||||
private DataSource defaultTenantDataSource;
|
private DataSource defaultTenantDataSource;
|
||||||
private Environment environment;
|
private Environment environment;
|
||||||
|
|
||||||
|
private DatabasePasswordEncryptor databasePasswordEncryptor;
|
||||||
|
|
||||||
public LiquibaseStepDefinitions() {
|
public LiquibaseStepDefinitions() {
|
||||||
Given("Liquibase is disabled with a default tenant", () -> {
|
Given("Liquibase is disabled with a default tenant", () -> {
|
||||||
initializeLiquibase(false);
|
initializeLiquibase(false);
|
||||||
@@ -169,6 +174,8 @@ public class LiquibaseStepDefinitions implements En {
|
|||||||
|
|
||||||
defaultTenantDataSource = mock(DataSource.class);
|
defaultTenantDataSource = mock(DataSource.class);
|
||||||
|
|
||||||
|
TenantPasswordEncryptionTask tenantPasswordEncryptor = mock(TenantPasswordEncryptionTask.class);
|
||||||
|
|
||||||
given(databaseStateVerifier.isLiquibaseDisabled()).willReturn(!liquibaseEnabled);
|
given(databaseStateVerifier.isLiquibaseDisabled()).willReturn(!liquibaseEnabled);
|
||||||
given(liquibaseFactory.create(tenantStoreDataSource, "tenant_store_db", "initial_switch")).willReturn(initialTenantStoreLiquibase);
|
given(liquibaseFactory.create(tenantStoreDataSource, "tenant_store_db", "initial_switch")).willReturn(initialTenantStoreLiquibase);
|
||||||
given(liquibaseFactory.create(tenantStoreDataSource, "tenant_store_db")).willReturn(tenantStoreLiquibase);
|
given(liquibaseFactory.create(tenantStoreDataSource, "tenant_store_db")).willReturn(tenantStoreLiquibase);
|
||||||
@@ -180,6 +187,6 @@ public class LiquibaseStepDefinitions implements En {
|
|||||||
given(liquibaseFactory.create(defaultTenantDataSource, "tenant_db", "custom_changelog")).willReturn(customChangeLogLiquibase);
|
given(liquibaseFactory.create(defaultTenantDataSource, "tenant_db", "custom_changelog")).willReturn(customChangeLogLiquibase);
|
||||||
|
|
||||||
tenantDatabaseUpgradeService = new TenantDatabaseUpgradeService(tenantDetailsService, tenantStoreDataSource, fineractProperties,
|
tenantDatabaseUpgradeService = new TenantDatabaseUpgradeService(tenantDetailsService, tenantStoreDataSource, fineractProperties,
|
||||||
databaseStateVerifier, liquibaseFactory, tenantDataSourceFactory, environment);
|
databaseStateVerifier, liquibaseFactory, tenantDataSourceFactory, environment, Arrays.asList(tenantPasswordEncryptor));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,6 +32,8 @@ fineract.tenant.timezone=Asia/Kolkata
|
|||||||
fineract.tenant.identifier=default
|
fineract.tenant.identifier=default
|
||||||
fineract.tenant.name=fineract_default
|
fineract.tenant.name=fineract_default
|
||||||
fineract.tenant.description=Default Demo Tenant
|
fineract.tenant.description=Default Demo Tenant
|
||||||
|
fineract.tenant.master-password=fineract
|
||||||
|
fineract.tenant.encrytion="AES/CBC/PKCS5Padding"
|
||||||
|
|
||||||
fineract.mode.read-enabled=true
|
fineract.mode.read-enabled=true
|
||||||
fineract.mode.write-enabled=true
|
fineract.mode.write-enabled=true
|
||||||
|
|||||||
Reference in New Issue
Block a user