diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/service/migration/TenantDatabaseUpgradeService.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/service/migration/TenantDatabaseUpgradeService.java index 9f4e81033..02b3c950d 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/service/migration/TenantDatabaseUpgradeService.java +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/service/migration/TenantDatabaseUpgradeService.java @@ -68,6 +68,7 @@ public class TenantDatabaseUpgradeService implements InitializingBean { private final ExtendedSpringLiquibaseFactory liquibaseFactory; private final TenantDataSourceFactory tenantDataSourceFactory; private final Environment environment; + private final TenantInitialUserProvisioner initialUserProvisioner; // DO NOT REMOVE! Required for liquibase custom task initialization private final List customTaskChangesForDependencyInjection; @@ -195,10 +196,15 @@ public class TenantDatabaseUpgradeService implements InitializingBean { // configuration // settings, and we should not use them to control the script order if (databaseStateVerifier.isFirstLiquibaseMigration(tenantDataSource)) { + // a Flyway-era database already has its real users; only a brand-new tenant gets the seeded ones + final boolean brandNewTenant = !databaseStateVerifier.isFlywayPresent(tenantDataSource); ExtendedSpringLiquibase liquibase = liquibaseFactory.create(tenantDataSource, TENANT_DB_CONTEXT, CUSTOM_CHANGELOG_CONTEXT, INITIAL_SWITCH_CONTEXT, tenant.getTenantIdentifier()); applyInitialLiquibase(tenantDataSource, liquibase, tenant.getTenantIdentifier(), (ds) -> !databaseStateVerifier.isTenantOnLatestUpgradableVersion(ds)); + if (brandNewTenant) { + initialUserProvisioner.provision(tenantDataSource, tenant.getTenantIdentifier()); + } } SpringLiquibase tenantLiquibase = liquibaseFactory.create(tenantDataSource, TENANT_DB_CONTEXT, CUSTOM_CHANGELOG_CONTEXT, tenant.getTenantIdentifier()); diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/service/migration/TenantInitialUserProvisioner.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/service/migration/TenantInitialUserProvisioner.java new file mode 100644 index 000000000..eb0b17c3d --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/service/migration/TenantInitialUserProvisioner.java @@ -0,0 +1,88 @@ +/** + * 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 java.util.Locale; +import javax.sql.DataSource; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.core.env.Environment; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.security.crypto.factory.PasswordEncoderFactories; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Component; + +/** + * Personalises the three users that the initial Liquibase data seeds into every brand-new tenant database (ids 1-3, + * which otherwise ship with the well-known {@code mifos} / {@code system} / {@code interopUser} usernames and the + * public default password). + * + * Runs only right after the very first migration of a tenant that was never on Flyway, so existing tenants are never + * touched. The seeded changeSet itself is deliberately left alone: editing it would change its Liquibase checksum and + * break validation on every tenant that already ran it. + * + * Usernames become {@code _admin}, {@code _system} and + * {@code _interop}. All three share one password: the value of + * {@code fineract.tenant.initial-user-password} (env {@code FINERACT_TENANT_INITIAL_USER_PASSWORD}) when set, + * otherwise {@link #DEFAULT_PASSWORD}. + */ +@Component +@Slf4j +@RequiredArgsConstructor +public class TenantInitialUserProvisioner { + + static final String PASSWORD_PROPERTY = "fineract.tenant.initial-user-password"; + + private static final long ADMIN_ID = 1L; + private static final long SYSTEM_ID = 2L; + private static final long INTEROP_ID = 3L; + // Meets the "strong" password policy (upper, lower, digit, special, no space, under 50 chars). + static final String DEFAULT_PASSWORD = "Cbs@Nexora#2026!Secure"; + + private final Environment environment; + + public void provision(final DataSource dataSource, final String tenantIdentifier) { + final JdbcTemplate jdbc = new JdbcTemplate(dataSource); + final String prefix = usernamePrefix(tenantIdentifier); + + final String configured = environment.getProperty(PASSWORD_PROPERTY); + final boolean overridden = configured != null && !configured.isBlank(); + final String password = overridden ? configured : DEFAULT_PASSWORD; + final PasswordEncoder encoder = PasswordEncoderFactories.createDelegatingPasswordEncoder(); + final String encoded = encoder.encode(password); + + update(jdbc, ADMIN_ID, prefix + "_admin", encoded); + update(jdbc, SYSTEM_ID, prefix + "_system", encoded); + update(jdbc, INTEROP_ID, prefix + "_interop", encoded); + + log.warn("Tenant '{}' initial users created: {}_admin, {}_system, {}_interop (password: {}). Change it after first login.", + tenantIdentifier, prefix, prefix, prefix, overridden ? "from " + PASSWORD_PROPERTY : "built-in default"); + } + + private void update(final JdbcTemplate jdbc, final long id, final String username, final String encodedPassword) { + jdbc.update("UPDATE m_appuser SET username = ?, password = ?, last_time_password_updated = CURRENT_DATE WHERE id = ?", username, + encodedPassword, id); + } + + static String usernamePrefix(final String tenantIdentifier) { + final String cleaned = tenantIdentifier.toLowerCase(Locale.ROOT).replaceAll("[^a-z0-9]+", "_").replaceAll("^_+|_+$", ""); + return cleaned.isEmpty() ? "tenant" : cleaned; + } + +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/dailyinterest/SavingsPooledInterestPostingService.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/dailyinterest/SavingsPooledInterestPostingService.java index 2082c2c46..8cac4b0b7 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/dailyinterest/SavingsPooledInterestPostingService.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/dailyinterest/SavingsPooledInterestPostingService.java @@ -424,8 +424,8 @@ public class SavingsPooledInterestPostingService { * columns are NOT NULL, so entries are stamped to the system account the scheduler uses. */ private Long systemUserId() { - final List ids = jdbcTemplate.queryForList( - "SELECT id FROM m_appuser WHERE username = 'system' ORDER BY id LIMIT 1", Long.class); + // id 2 is the seeded system user; its username is per-tenant now, so it is found by id + final List ids = jdbcTemplate.queryForList("SELECT id FROM m_appuser WHERE id = 2", Long.class); return ids.isEmpty() ? 1L : ids.get(0); } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/fdinterest/FixedDepositInterestPostingService.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/fdinterest/FixedDepositInterestPostingService.java index aef8786e5..33ba4630e 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/fdinterest/FixedDepositInterestPostingService.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/fdinterest/FixedDepositInterestPostingService.java @@ -469,8 +469,8 @@ public class FixedDepositInterestPostingService { * scheduler's own postings always were. */ private Long systemUserId() { - final List ids = this.jdbcTemplate.queryForList("SELECT id FROM m_appuser WHERE username = 'system' ORDER BY id LIMIT 1", - Long.class); + // id 2 is the seeded system user; its username is per-tenant now, so it is found by id + final List ids = this.jdbcTemplate.queryForList("SELECT id FROM m_appuser WHERE id = 2", Long.class); return ids.isEmpty() ? 1L : ids.get(0); } diff --git a/fineract-provider/src/test/java/org/apache/fineract/TestConfiguration.java b/fineract-provider/src/test/java/org/apache/fineract/TestConfiguration.java index 4f38aadbf..11ae68a4a 100644 --- a/fineract-provider/src/test/java/org/apache/fineract/TestConfiguration.java +++ b/fineract-provider/src/test/java/org/apache/fineract/TestConfiguration.java @@ -159,7 +159,9 @@ public class TestConfiguration { FineractProperties fineractProperties, Environment environment, List customTaskChangesForDependencyInjection) { return new TenantDatabaseUpgradeService(tenantDetailsService, tenantDataSource, fineractProperties, tenantDatabaseStateVerifier, - liquibaseFactory, tenantDataSourceFactory, environment, customTaskChangesForDependencyInjection); + liquibaseFactory, tenantDataSourceFactory, environment, + new org.apache.fineract.infrastructure.core.service.migration.TenantInitialUserProvisioner(environment), + customTaskChangesForDependencyInjection); } /** diff --git a/fineract-provider/src/test/java/org/apache/fineract/infrastructure/core/LiquibaseStepDefinitions.java b/fineract-provider/src/test/java/org/apache/fineract/infrastructure/core/LiquibaseStepDefinitions.java index a22499905..621bbe96e 100644 --- a/fineract-provider/src/test/java/org/apache/fineract/infrastructure/core/LiquibaseStepDefinitions.java +++ b/fineract-provider/src/test/java/org/apache/fineract/infrastructure/core/LiquibaseStepDefinitions.java @@ -189,6 +189,8 @@ public class LiquibaseStepDefinitions implements En { .willReturn(tenantLiquibase); tenantDatabaseUpgradeService = new TenantDatabaseUpgradeService(tenantDetailsService, tenantStoreDataSource, fineractProperties, - databaseStateVerifier, liquibaseFactory, tenantDataSourceFactory, environment, Arrays.asList(tenantPasswordEncryptor)); + databaseStateVerifier, liquibaseFactory, tenantDataSourceFactory, environment, + new org.apache.fineract.infrastructure.core.service.migration.TenantInitialUserProvisioner(environment), + Arrays.asList(tenantPasswordEncryptor)); } }