From 8418a23e4b246c276ccadafa3196761234454a3e Mon Sep 17 00:00:00 2001 From: Janos Haber Date: Thu, 16 Mar 2023 16:25:03 +0100 Subject: [PATCH] FINERACT-1724 Read only tenant password encryption migration - [x] ReadOnly tenant password encryption migration script - [x] Documentation - [x] Encryption password generator --- .../appendix/properties-database.adoc | 10 ++ .../en/chapters/architecture/persistence.adoc | 19 ++++ .../database/DatabasePasswordEncryptor.java | 14 +++ .../TenantReadOnlyPasswordEncryptionTask.java | 96 +++++++++++++++++++ .../tenant-store/changelog-tenant-store.xml | 1 + ...8_encrypt_existing_ro_tenant_passwords.xml | 28 ++++++ 6 files changed, 168 insertions(+) create mode 100644 fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/service/migration/TenantReadOnlyPasswordEncryptionTask.java create mode 100644 fineract-provider/src/main/resources/db/changelog/tenant-store/parts/0008_encrypt_existing_ro_tenant_passwords.xml diff --git a/fineract-doc/src/docs/en/chapters/appendix/properties-database.adoc b/fineract-doc/src/docs/en/chapters/appendix/properties-database.adoc index 834bec705..988a9c38f 100644 --- a/fineract-doc/src/docs/en/chapters/appendix/properties-database.adoc +++ b/fineract-doc/src/docs/en/chapters/appendix/properties-database.adoc @@ -49,6 +49,16 @@ |Default Demo Tenant |TBD +|fineract.tenant.master-password +|FINERACT_DEFAULT_TENANTDB_MASTER_PASSWORD +|fineract +|TBD + +|fineract.tenant.encrytion +|FINERACT_DEFAULT_TENANTDB_ENCRYPTION +|AES/CBC/PKCS5Padding +|TBD + |spring.liquibase.enabled |FINERACT_LIQUIBASE_ENABLED |true diff --git a/fineract-doc/src/docs/en/chapters/architecture/persistence.adoc b/fineract-doc/src/docs/en/chapters/architecture/persistence.adoc index df5f4558c..94ef531a3 100644 --- a/fineract-doc/src/docs/en/chapters/architecture/persistence.adoc +++ b/fineract-doc/src/docs/en/chapters/architecture/persistence.adoc @@ -31,6 +31,25 @@ The currently supported JDBC driver and corresponding mappings can be found belo The actual code can be found in the `DatabaseTypeResolver` class. +== Tenant database security + +The tenant database schema password is stored in the `tenant_server_connections` table in the tenant database. +The password and the read only schema password are encrypted using the `fineract.tenant.master-password` property. +By default, the database property will be encrypted in the first start from a plane text. + +When you want to generate a new encrypted password, you can use the `org.apache.fineract.infrastructure.core.service.database.DatabasePasswordEncryptor` class. + +=== Database password encryption usage +``` +java -cp fineract-provider.jar -Dloader.main=org.apache.fineract.infrastructure.core.service.database.DatabasePasswordEncryptor org.springframework.boot.loader.PropertiesLauncher +``` + +For example: +``` +java -cp fineract-provider-0.0.0-48f7e315.jar -Dloader.main=org.apache.fineract.infrastructure.core.service.database.DatabasePasswordEncryptor org.springframework.boot.loader.PropertiesLauncher fineract-master-password fineract-tenant-password +The encrypted password: VLwGl7vOP/q275ZTku+PNGWnGwW4mzzNHSNaO9Pr67WT5/NZMpBr9tGYYiYsqwL1eRew2jl7O3/N1EFbLlXhSA== +``` + == Data-access layer The data-access layer of Fineract is implemented by using JPA (Java Persistence API) with the EclipseLink provider. diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/service/database/DatabasePasswordEncryptor.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/service/database/DatabasePasswordEncryptor.java index 033a13669..7dec6c30b 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/service/database/DatabasePasswordEncryptor.java +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/service/database/DatabasePasswordEncryptor.java @@ -19,6 +19,7 @@ package org.apache.fineract.infrastructure.core.service.database; import java.nio.charset.StandardCharsets; +import java.text.MessageFormat; import java.util.Optional; import lombok.RequiredArgsConstructor; import org.apache.fineract.infrastructure.core.config.FineractProperties; @@ -37,6 +38,19 @@ public class DatabasePasswordEncryptor implements PasswordEncryptor { private final FineractProperties fineractProperties; + @SuppressWarnings("checkstyle:regexpsinglelinejava") + public static void main(String[] args) { + if (args.length < 2) { + System.out.println( + "Usage: java -cp fineract-provider.jar java -Dloader.main=org.apache.fineract.infrastructure.core.service.database.DatabasePasswordEncryptor org.springframework.boot.loader.PropertiesLauncher "); + System.exit(1); + } + String masterPassword = args[0]; + String plainPassword = args[1]; + String encryptedPassword = EncryptionUtil.encryptToBase64(DEFAULT_ENCRYPTION, masterPassword, plainPassword); + System.out.println(MessageFormat.format("The encrypted password: {0}", encryptedPassword)); + } + @Override public String encrypt(String plainPassword) { String masterPassword = Optional.ofNullable(fineractProperties.getTenant()) diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/service/migration/TenantReadOnlyPasswordEncryptionTask.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/service/migration/TenantReadOnlyPasswordEncryptionTask.java new file mode 100644 index 000000000..e019ae163 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/service/migration/TenantReadOnlyPasswordEncryptionTask.java @@ -0,0 +1,96 @@ +/** + * 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 TenantReadOnlyPasswordEncryptionTask 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, readonly_schema_password FROM tenant_server_connections WHERE readonly_schema_password IS NOT NULL")) { + while (rs.next()) { + String id = rs.getString("id"); + String readOnlySchemaPassword = rs.getString("readonly_schema_password"); + String encryptedPassword = TenantReadOnlyPasswordEncryptionTask.databasePasswordEncryptor + .encrypt(readOnlySchemaPassword); + + String updateSql = String.format( + "update tenant_server_connections set readonly_schema_password = '%s', master_password_hash = '%s' where id = %s", + encryptedPassword, TenantReadOnlyPasswordEncryptionTask.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 { + TenantReadOnlyPasswordEncryptionTask.databasePasswordEncryptor = applicationContext.getBean(DatabasePasswordEncryptor.class); + } +} diff --git a/fineract-provider/src/main/resources/db/changelog/tenant-store/changelog-tenant-store.xml b/fineract-provider/src/main/resources/db/changelog/tenant-store/changelog-tenant-store.xml index 3cc73b305..42c4aba73 100644 --- a/fineract-provider/src/main/resources/db/changelog/tenant-store/changelog-tenant-store.xml +++ b/fineract-provider/src/main/resources/db/changelog/tenant-store/changelog-tenant-store.xml @@ -27,4 +27,5 @@ + diff --git a/fineract-provider/src/main/resources/db/changelog/tenant-store/parts/0008_encrypt_existing_ro_tenant_passwords.xml b/fineract-provider/src/main/resources/db/changelog/tenant-store/parts/0008_encrypt_existing_ro_tenant_passwords.xml new file mode 100644 index 000000000..6d138f104 --- /dev/null +++ b/fineract-provider/src/main/resources/db/changelog/tenant-store/parts/0008_encrypt_existing_ro_tenant_passwords.xml @@ -0,0 +1,28 @@ + + + + + + +