From 5adb79aa3eacf2d5c376adb3873792b959161935 Mon Sep 17 00:00:00 2001 From: Maverick Studer Date: Wed, 23 Oct 2024 17:25:23 +0200 Subject: [PATCH] RED-9394: Global default SMTP configuration --- .../external/SMTPConfigurationController.java | 4 +- .../entity/GlobalSMTPConfigurationEntity.java | 60 +++++ .../GlobalSMTPConfigurationRepository.java | 15 ++ .../EnvironmentSMTPConfigurationProvider.java | 52 ++++ ...alSMTPConfigurationPersistenceService.java | 53 ++++ .../service/SMTPService.java | 118 ++++++--- .../service/TenantManagementService.java | 4 +- .../utils/SMTPConfigurationMapper.java | 60 +++++ .../db/changelog/db.changelog-master.yaml | 2 + .../master/11-add-global-smtp-config.yaml | 51 ++++ .../tests/SMTPConfigurationTest.java | 11 +- .../tests/SMTPServiceTest.java | 242 ++++++++++++++++++ 12 files changed, 618 insertions(+), 54 deletions(-) create mode 100644 src/main/java/com/knecon/fforesight/tenantusermanagement/entity/GlobalSMTPConfigurationEntity.java create mode 100644 src/main/java/com/knecon/fforesight/tenantusermanagement/repository/GlobalSMTPConfigurationRepository.java create mode 100644 src/main/java/com/knecon/fforesight/tenantusermanagement/service/EnvironmentSMTPConfigurationProvider.java create mode 100644 src/main/java/com/knecon/fforesight/tenantusermanagement/service/GlobalSMTPConfigurationPersistenceService.java create mode 100644 src/main/java/com/knecon/fforesight/tenantusermanagement/utils/SMTPConfigurationMapper.java create mode 100644 src/main/resources/db/changelog/master/11-add-global-smtp-config.yaml create mode 100644 src/test/java/com/knecon/fforesight/tenantusermanagement/tests/SMTPServiceTest.java diff --git a/src/main/java/com/knecon/fforesight/tenantusermanagement/controller/external/SMTPConfigurationController.java b/src/main/java/com/knecon/fforesight/tenantusermanagement/controller/external/SMTPConfigurationController.java index 7b0c7a4..140565d 100644 --- a/src/main/java/com/knecon/fforesight/tenantusermanagement/controller/external/SMTPConfigurationController.java +++ b/src/main/java/com/knecon/fforesight/tenantusermanagement/controller/external/SMTPConfigurationController.java @@ -53,7 +53,7 @@ public class SMTPConfigurationController implements SMTPConfigurationResource, P throw new BadRequestException("Current license does not allow updating the SMTP configuration!"); } - smtpService.updateSMTPConfiguration(smtpConfigurationModel, TenantContext.getTenantId()); + smtpService.updateSMTPConfiguration(smtpConfigurationModel); } @@ -84,7 +84,7 @@ public class SMTPConfigurationController implements SMTPConfigurationResource, P @PreAuthorize("hasAuthority('" + WRITE_SMTP_CONFIGURATION + "')") public void clearSMTPConfiguration() { - smtpService.clearSMTPConfiguration(); + smtpService.createDefaultSMTPConfiguration(); } diff --git a/src/main/java/com/knecon/fforesight/tenantusermanagement/entity/GlobalSMTPConfigurationEntity.java b/src/main/java/com/knecon/fforesight/tenantusermanagement/entity/GlobalSMTPConfigurationEntity.java new file mode 100644 index 0000000..bbb40bd --- /dev/null +++ b/src/main/java/com/knecon/fforesight/tenantusermanagement/entity/GlobalSMTPConfigurationEntity.java @@ -0,0 +1,60 @@ +package com.knecon.fforesight.tenantusermanagement.entity; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Entity +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Table(name = "global_smtp_configuration") +public class GlobalSMTPConfigurationEntity { + + @Id + @Column(nullable = false, updatable = false) + private String id = "singleton"; + + @Column + private Boolean auth; + + @Column + private String envelopeFrom; + + @Column + private String fromEmail; + + @Column + private String fromDisplayName; + + @Column + private String host; + + @Column + private String password; + + @Column + private Integer port; + + @Column + private String replyTo; + + @Column + private String replyToDisplayName; + + @Column + private Boolean ssl; + + @Column + private Boolean starttls; + + @Column + private String userName; +} + diff --git a/src/main/java/com/knecon/fforesight/tenantusermanagement/repository/GlobalSMTPConfigurationRepository.java b/src/main/java/com/knecon/fforesight/tenantusermanagement/repository/GlobalSMTPConfigurationRepository.java new file mode 100644 index 0000000..88f7c88 --- /dev/null +++ b/src/main/java/com/knecon/fforesight/tenantusermanagement/repository/GlobalSMTPConfigurationRepository.java @@ -0,0 +1,15 @@ +package com.knecon.fforesight.tenantusermanagement.repository; + +import java.util.Optional; + +import org.springframework.data.jpa.repository.JpaRepository; + +import com.knecon.fforesight.tenantusermanagement.entity.GlobalSMTPConfigurationEntity; + +public interface GlobalSMTPConfigurationRepository extends JpaRepository { + + default Optional findSingleton() { + + return findById("singleton"); + } +} diff --git a/src/main/java/com/knecon/fforesight/tenantusermanagement/service/EnvironmentSMTPConfigurationProvider.java b/src/main/java/com/knecon/fforesight/tenantusermanagement/service/EnvironmentSMTPConfigurationProvider.java new file mode 100644 index 0000000..43d2eae --- /dev/null +++ b/src/main/java/com/knecon/fforesight/tenantusermanagement/service/EnvironmentSMTPConfigurationProvider.java @@ -0,0 +1,52 @@ +package com.knecon.fforesight.tenantusermanagement.service; + +import org.apache.commons.lang3.StringUtils; +import org.springframework.core.env.Environment; +import org.springframework.stereotype.Service; + +import com.knecon.fforesight.tenantusermanagement.model.SMTPConfiguration; + +import lombok.RequiredArgsConstructor; + +@Service +@RequiredArgsConstructor +public class EnvironmentSMTPConfigurationProvider { + + private final Environment environment; + + public static final Integer KEYCLOAK_DEFAULT_PORT = 25; + public static final String SMTP_DEFAULT_HOST = "SMTP_DEFAULT_HOST"; + public static final String SMTP_DEFAULT_PORT = "SMTP_DEFAULT_PORT"; + public static final String SMTP_DEFAULT_SENDER_EMAIL = "SMTP_DEFAULT_SENDER_EMAIL"; + public static final String SMTP_DEFAULT_SENDER_NAME = "SMTP_DEFAULT_SENDER_NAME"; + public static final String SMTP_DEFAULT_REPLY_EMAIL = "SMTP_DEFAULT_REPLY_EMAIL"; + public static final String SMTP_DEFAULT_REPLY_NAME = "SMTP_DEFAULT_REPLY_NAME"; + public static final String SMTP_DEFAULT_SENDER_ENVELOPE = "SMTP_DEFAULT_SENDER_ENVELOPE"; + public static final String SMTP_DEFAULT_SSL = "SMTP_DEFAULT_SSL"; + public static final String SMTP_DEFAULT_STARTTLS = "SMTP_DEFAULT_STARTTLS"; + public static final String SMTP_DEFAULT_AUTH = "SMTP_DEFAULT_AUTH"; + public static final String SMTP_DEFAULT_AUTH_USER = "SMTP_DEFAULT_AUTH_USER"; + public static final String SMTP_DEFAULT_AUTH_PASSWORD = "SMTP_DEFAULT_AUTH_PASSWORD"; + + + public SMTPConfiguration get() { + + String port = environment.getProperty(SMTP_DEFAULT_PORT, ""); + return SMTPConfiguration.builder() + .id("singleton") + .host(environment.getProperty(SMTP_DEFAULT_HOST, "")) + .port(StringUtils.isEmpty(port) || !StringUtils.isNumeric(port) ? KEYCLOAK_DEFAULT_PORT : Integer.parseInt(port)) + .from(environment.getProperty(SMTP_DEFAULT_SENDER_EMAIL, "")) + .fromDisplayName(environment.getProperty(SMTP_DEFAULT_SENDER_NAME, "")) + .replyTo(environment.getProperty(SMTP_DEFAULT_REPLY_EMAIL, "")) + .replyToDisplayName(environment.getProperty(SMTP_DEFAULT_REPLY_NAME, "")) + .envelopeFrom(environment.getProperty(SMTP_DEFAULT_SENDER_ENVELOPE, "")) + .ssl(Boolean.parseBoolean(environment.getProperty(SMTP_DEFAULT_SSL, "false"))) + .starttls(Boolean.parseBoolean(environment.getProperty(SMTP_DEFAULT_STARTTLS, "false"))) + .auth(Boolean.parseBoolean(environment.getProperty(SMTP_DEFAULT_AUTH, "false"))) + .user(environment.getProperty(SMTP_DEFAULT_AUTH_USER, "")) + .password(environment.getProperty(SMTP_DEFAULT_AUTH_PASSWORD, "")) + .build(); + } + +} diff --git a/src/main/java/com/knecon/fforesight/tenantusermanagement/service/GlobalSMTPConfigurationPersistenceService.java b/src/main/java/com/knecon/fforesight/tenantusermanagement/service/GlobalSMTPConfigurationPersistenceService.java new file mode 100644 index 0000000..006b694 --- /dev/null +++ b/src/main/java/com/knecon/fforesight/tenantusermanagement/service/GlobalSMTPConfigurationPersistenceService.java @@ -0,0 +1,53 @@ +package com.knecon.fforesight.tenantusermanagement.service; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import com.knecon.fforesight.tenantcommons.EncryptionDecryptionService; +import com.knecon.fforesight.tenantusermanagement.entity.GlobalSMTPConfigurationEntity; +import com.knecon.fforesight.tenantusermanagement.repository.GlobalSMTPConfigurationRepository; +import com.knecon.fforesight.tenantusermanagement.utils.SMTPConfigurationMapper; + +import jakarta.annotation.PostConstruct; +import lombok.AccessLevel; +import lombok.RequiredArgsConstructor; +import lombok.experimental.FieldDefaults; + +@Service +@RequiredArgsConstructor +@FieldDefaults(level = AccessLevel.PRIVATE, makeFinal = true) +public class GlobalSMTPConfigurationPersistenceService { + + GlobalSMTPConfigurationRepository smtpConfigurationRepository; + EnvironmentSMTPConfigurationProvider environmentSMTPConfigurationProvider; + EncryptionDecryptionService encryptionDecryptionService; + + + @PostConstruct + public void init() { + + smtpConfigurationRepository.findSingleton() + .orElseGet(() -> { + GlobalSMTPConfigurationEntity defaultConfig = SMTPConfigurationMapper.toEntity(environmentSMTPConfigurationProvider.get()); + defaultConfig.setPassword(encryptionDecryptionService.encrypt(defaultConfig.getPassword())); + return smtpConfigurationRepository.save(defaultConfig); + }); + } + + + @Transactional(readOnly = true) + public GlobalSMTPConfigurationEntity getConfiguration() { + + return smtpConfigurationRepository.findSingleton() + .orElseThrow(() -> new IllegalStateException("Global SMTP Configuration not found.")); + } + + + @Transactional + public GlobalSMTPConfigurationEntity updateConfiguration(GlobalSMTPConfigurationEntity smtpConfiguration) { + + smtpConfiguration.setId("singleton"); + return smtpConfigurationRepository.save(smtpConfiguration); + } + +} diff --git a/src/main/java/com/knecon/fforesight/tenantusermanagement/service/SMTPService.java b/src/main/java/com/knecon/fforesight/tenantusermanagement/service/SMTPService.java index 765a55a..fbf65dc 100644 --- a/src/main/java/com/knecon/fforesight/tenantusermanagement/service/SMTPService.java +++ b/src/main/java/com/knecon/fforesight/tenantusermanagement/service/SMTPService.java @@ -1,11 +1,10 @@ package com.knecon.fforesight.tenantusermanagement.service; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.Optional; -import org.apache.commons.lang3.StringUtils; -import org.springframework.core.env.Environment; import org.springframework.stereotype.Service; import com.fasterxml.jackson.core.type.TypeReference; @@ -16,8 +15,13 @@ import com.iqser.red.service.persistence.service.v1.api.shared.model.license.Red import com.knecon.fforesight.tenantcommons.EncryptionDecryptionService; import com.knecon.fforesight.tenantcommons.TenantContext; import com.knecon.fforesight.tenantusermanagement.client.LicenseClient; +import com.knecon.fforesight.tenantusermanagement.entity.GlobalSMTPConfigurationEntity; +import com.knecon.fforesight.tenantusermanagement.entity.TenantEntity; import com.knecon.fforesight.tenantusermanagement.model.SMTPConfiguration; +import com.knecon.fforesight.tenantusermanagement.repository.TenantRepository; +import com.knecon.fforesight.tenantusermanagement.utils.SMTPConfigurationMapper; +import jakarta.annotation.PostConstruct; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -26,9 +30,11 @@ import lombok.extern.slf4j.Slf4j; @Slf4j public class SMTPService { - private final Environment environment; + private final GlobalSMTPConfigurationPersistenceService smtpConfigurationPersistenceService; + private final EnvironmentSMTPConfigurationProvider environmentSMTPConfigurationProvider; private final LicenseClient licenseClient; private final RealmService realmService; + private final TenantRepository tenantRepository; private final EncryptionDecryptionService encryptionDecryptionService; @@ -36,22 +42,72 @@ public class SMTPService { private final static String SMTP_PASSWORD_KEY = "FFORESIGHT_SMTP_PASSWORD"; private final static String DEFAULT_PASSWORD = "**********"; - - public static final String SMTP_DEFAULT_HOST = "SMTP_DEFAULT_HOST"; - public static final String SMTP_DEFAULT_PORT = "SMTP_DEFAULT_PORT"; - public static final String SMTP_DEFAULT_SENDER_EMAIL = "SMTP_DEFAULT_SENDER_EMAIL"; - public static final String SMTP_DEFAULT_SENDER_NAME = "SMTP_DEFAULT_SENDER_NAME"; - public static final String SMTP_DEFAULT_REPLY_EMAIL = "SMTP_DEFAULT_REPLY_EMAIL"; - public static final String SMTP_DEFAULT_REPLY_NAME = "SMTP_DEFAULT_REPLY_NAME"; - public static final String SMTP_DEFAULT_SENDER_ENVELOPE = "SMTP_DEFAULT_SENDER_ENVELOPE"; - public static final String SMTP_DEFAULT_SSL = "SMTP_DEFAULT_SSL"; - public static final String SMTP_DEFAULT_STARTTLS = "SMTP_DEFAULT_STARTTLS"; - public static final String SMTP_DEFAULT_AUTH = "SMTP_DEFAULT_AUTH"; - public static final String SMTP_DEFAULT_AUTH_USER = "SMTP_DEFAULT_AUTH_USER"; - public static final String SMTP_DEFAULT_AUTH_PASSWORD = "SMTP_DEFAULT_AUTH_PASSWORD"; public static final String CONFIGURABLE_SMTP_SERVER_FEATURE = "configurableSMTPServer"; + @PostConstruct + public void synchronizeSMTPConfigurations() { + + log.info("Starting SMTP configuration synchronization..."); + + try { + // Step 1: Retrieve all tenants + List tenants = tenantRepository.findAll(); + log.info("Retrieved {} tenants for SMTP synchronization.", tenants.size()); + + // Step 2: Retrieve the global SMTP configuration + GlobalSMTPConfigurationEntity latestGlobalConfigEntity = smtpConfigurationPersistenceService.getConfiguration(); + SMTPConfiguration latestGlobalConfig = SMTPConfigurationMapper.toModel(latestGlobalConfigEntity); + latestGlobalConfig.setPassword(encryptionDecryptionService.decrypt(latestGlobalConfig.getPassword())); + log.info("Loaded existing global SMTP configuration."); + + // Step 3: Generate the latest SMTP config from environment variables and compare it to the saved global config + SMTPConfiguration currentGlobalConfig = environmentSMTPConfigurationProvider.get(); + if (!currentGlobalConfig.equals(latestGlobalConfig)) { + log.info("Environment SMTP configuration has changed. Updating global SMTP configuration."); + + // Step 4: Iterate through each tenant to synchronize SMTP configurations + tenants + .forEach(tenant -> { + String tenantId = tenant.getTenantId(); + log.info("Processing SMTP configuration for tenant: {}", tenantId); + + try { + TenantContext.setTenantId(tenantId); + SMTPConfiguration tenantSMTPConfig = getSMTPConfiguration(); + tenantSMTPConfig.setId("singleton"); + updatePassword(tenantSMTPConfig); + if(!tenantSMTPConfig.getPassword().isBlank()) { + tenantSMTPConfig.setPassword(encryptionDecryptionService.decrypt(tenantSMTPConfig.getPassword())); + } + if (tenantSMTPConfig.equals(latestGlobalConfig)) { + log.info("Tenant '{}' SMTP configuration matches global. Updating to latest environment configuration.", tenantId); + updateSMTPConfiguration(currentGlobalConfig); + log.info("Tenant '{}' SMTP configuration updated successfully.", tenantId); + } else { + log.info("Tenant '{}' SMTP configuration differs from global. No action taken.", tenantId); + } + + } catch (Exception e) { + log.error("Error processing SMTP configuration for tenant '{}': {}", tenant.getTenantId(), e.getMessage()); + } finally { + TenantContext.clear(); + } + }); + + currentGlobalConfig.setPassword(encryptionDecryptionService.encrypt(currentGlobalConfig.getPassword())); + GlobalSMTPConfigurationEntity updatedGlobalConfig = SMTPConfigurationMapper.toEntity(currentGlobalConfig); + smtpConfigurationPersistenceService.updateConfiguration(updatedGlobalConfig); + } else { + log.info("No changes detected in environment SMTP configuration."); + } + + log.info("SMTP configuration synchronization completed successfully."); + + } catch (Exception e) { + log.error("Failed to synchronize SMTP configurations: {}", e.getMessage(), e); + } + } public SMTPConfiguration getSMTPConfiguration() { @@ -93,34 +149,16 @@ public class SMTPService { } - public void createDefaultSMTPConfiguration(String tenantId) { + public void createDefaultSMTPConfiguration() { - SMTPConfiguration defaultConfig = generateDefaultSMTPConfiguration(); - updateSMTPConfiguration(defaultConfig, tenantId); - } - - private SMTPConfiguration generateDefaultSMTPConfiguration() { - - String port = environment.getProperty(SMTP_DEFAULT_PORT, "1234"); - return SMTPConfiguration.builder() - .host(environment.getProperty(SMTP_DEFAULT_HOST, "")) - .port(StringUtils.isEmpty(port) ? null : Integer.parseInt(port)) - .from(environment.getProperty(SMTP_DEFAULT_SENDER_EMAIL, "")) - .fromDisplayName(environment.getProperty(SMTP_DEFAULT_SENDER_NAME, "")) - .replyTo(environment.getProperty(SMTP_DEFAULT_REPLY_EMAIL, "")) - .replyToDisplayName(environment.getProperty(SMTP_DEFAULT_REPLY_NAME, "")) - .envelopeFrom(environment.getProperty(SMTP_DEFAULT_SENDER_ENVELOPE, "")) - .ssl(Boolean.parseBoolean(environment.getProperty(SMTP_DEFAULT_SSL, "false"))) - .starttls(Boolean.parseBoolean(environment.getProperty(SMTP_DEFAULT_STARTTLS, "false"))) - .auth(Boolean.parseBoolean(environment.getProperty(SMTP_DEFAULT_AUTH, "false"))) - .user(environment.getProperty(SMTP_DEFAULT_AUTH_USER, "")) - .password(environment.getProperty(SMTP_DEFAULT_AUTH_PASSWORD, "")) - .build(); + SMTPConfiguration defaultConfig = environmentSMTPConfigurationProvider.get(); + updateSMTPConfiguration(defaultConfig); } - public void updateSMTPConfiguration(SMTPConfiguration smtpConfigurationModel, String tenantId) { + public void updateSMTPConfiguration(SMTPConfiguration smtpConfigurationModel) { + String tenantId = TenantContext.getTenantId(); var realmRepresentation = realmService.realm(tenantId).toRepresentation(); var propertiesMap = convertSMTPConfigurationModelToMap(smtpConfigurationModel); realmRepresentation.setSmtpServer(propertiesMap); @@ -163,7 +201,7 @@ public class SMTPService { if (DEFAULT_PASSWORD.equals(smtpConfiguration.getPassword())) { try { var password = realmService.realm(TenantContext.getTenantId()).toRepresentation().getAttributesOrEmpty() - .get(SMTP_PASSWORD_KEY); + .getOrDefault(SMTP_PASSWORD_KEY, ""); smtpConfiguration.setPassword(password); } catch (Exception e) { log.info("No current SMTP Config exists", e); diff --git a/src/main/java/com/knecon/fforesight/tenantusermanagement/service/TenantManagementService.java b/src/main/java/com/knecon/fforesight/tenantusermanagement/service/TenantManagementService.java index a483c11..902e9f5 100644 --- a/src/main/java/com/knecon/fforesight/tenantusermanagement/service/TenantManagementService.java +++ b/src/main/java/com/knecon/fforesight/tenantusermanagement/service/TenantManagementService.java @@ -214,7 +214,8 @@ public class TenantManagementService implements TenantProvider { log.info("Updated roles for tenant: {}", tenantRequest.getTenantId()); - smtpService.createDefaultSMTPConfiguration(tenantRequest.getTenantId()); + TenantContext.setTenantId(tenantEntity.getTenantId()); + smtpService.createDefaultSMTPConfiguration(); log.info("Created default SMTP configuration."); @@ -223,7 +224,6 @@ public class TenantManagementService implements TenantProvider { log.info("Persisted tenant: {}", tenantRequest.getTenantId()); - TenantContext.setTenantId(tenantEntity.getTenantId()); rabbitTemplate.convertAndSend(tenantExchangeName, "tenant.created", new TenantCreatedEvent(tenantEntity.getTenantId())); TenantContext.clear(); diff --git a/src/main/java/com/knecon/fforesight/tenantusermanagement/utils/SMTPConfigurationMapper.java b/src/main/java/com/knecon/fforesight/tenantusermanagement/utils/SMTPConfigurationMapper.java new file mode 100644 index 0000000..30b58f4 --- /dev/null +++ b/src/main/java/com/knecon/fforesight/tenantusermanagement/utils/SMTPConfigurationMapper.java @@ -0,0 +1,60 @@ +package com.knecon.fforesight.tenantusermanagement.utils; + +import com.knecon.fforesight.tenantusermanagement.entity.GlobalSMTPConfigurationEntity; +import com.knecon.fforesight.tenantusermanagement.model.SMTPConfiguration; + +import lombok.experimental.UtilityClass; + +@UtilityClass +public final class SMTPConfigurationMapper { + + public static GlobalSMTPConfigurationEntity toEntity(SMTPConfiguration smtpConfig) { + + if (smtpConfig == null) { + return null; + } + + return GlobalSMTPConfigurationEntity.builder() + .id("singleton") + .auth(smtpConfig.isAuth()) + .envelopeFrom(smtpConfig.getEnvelopeFrom()) + .fromEmail(smtpConfig.getFrom()) + .fromDisplayName(smtpConfig.getFromDisplayName()) + .host(smtpConfig.getHost()) + .password(smtpConfig.getPassword()) + .port(smtpConfig.getPort()) + .replyTo(smtpConfig.getReplyTo()) + .replyToDisplayName(smtpConfig.getReplyToDisplayName()) + .ssl(smtpConfig.isSsl()) + .starttls(smtpConfig.isStarttls()) + .userName(smtpConfig.getUser()) + .build(); + } + + + public static SMTPConfiguration toModel(GlobalSMTPConfigurationEntity entity) { + + if (entity == null) { + return null; + } + + SMTPConfiguration smtpConfig = new SMTPConfiguration(); + smtpConfig.setId(entity.getId()); + smtpConfig.setAuth(entity.getAuth() != null && entity.getAuth()); + smtpConfig.setEnvelopeFrom(entity.getEnvelopeFrom()); + smtpConfig.setFrom(entity.getFromEmail()); + smtpConfig.setFromDisplayName(entity.getFromDisplayName()); + smtpConfig.setHost(entity.getHost()); + smtpConfig.setPassword(entity.getPassword()); + smtpConfig.setPort(entity.getPort()); + smtpConfig.setReplyTo(entity.getReplyTo()); + smtpConfig.setReplyToDisplayName(entity.getReplyToDisplayName()); + smtpConfig.setSsl(entity.getSsl() != null && entity.getSsl()); + smtpConfig.setStarttls(entity.getStarttls() != null && entity.getStarttls()); + smtpConfig.setUser(entity.getUserName()); + + return smtpConfig; + } + +} + diff --git a/src/main/resources/db/changelog/db.changelog-master.yaml b/src/main/resources/db/changelog/db.changelog-master.yaml index 8bed7d1..fe55027 100644 --- a/src/main/resources/db/changelog/db.changelog-master.yaml +++ b/src/main/resources/db/changelog/db.changelog-master.yaml @@ -17,3 +17,5 @@ databaseChangeLog: file: db/changelog/master/9-add-mongodb-connection-columns.yaml - include: file: db/changelog/master/10-add-application-type-to-tenant.yaml + - include: + file: db/changelog/master/11-add-global-smtp-config.yaml diff --git a/src/main/resources/db/changelog/master/11-add-global-smtp-config.yaml b/src/main/resources/db/changelog/master/11-add-global-smtp-config.yaml new file mode 100644 index 0000000..61a161f --- /dev/null +++ b/src/main/resources/db/changelog/master/11-add-global-smtp-config.yaml @@ -0,0 +1,51 @@ +databaseChangeLog: + - changeSet: + id: add-global-smtp-config-table + author: dom + changes: + - createTable: + tableName: global_smtp_configuration + columns: + - column: + name: id + type: VARCHAR(255) + constraints: + nullable: false + primaryKey: true + primaryKeyName: global_smtp_configuration_pkey + - column: + name: auth + type: BOOLEAN + - column: + name: envelope_from + type: VARCHAR(255) + - column: + name: from_email + type: VARCHAR(255) + - column: + name: from_display_name + type: VARCHAR(255) + - column: + name: host + type: VARCHAR(255) + - column: + name: password + type: VARCHAR(255) + - column: + name: port + type: INTEGER + - column: + name: reply_to + type: VARCHAR(255) + - column: + name: reply_to_display_name + type: VARCHAR(255) + - column: + name: ssl + type: BOOLEAN + - column: + name: starttls + type: BOOLEAN + - column: + name: user_name + type: VARCHAR(255) \ No newline at end of file diff --git a/src/test/java/com/knecon/fforesight/tenantusermanagement/tests/SMTPConfigurationTest.java b/src/test/java/com/knecon/fforesight/tenantusermanagement/tests/SMTPConfigurationTest.java index a6acc9d..64852d2 100644 --- a/src/test/java/com/knecon/fforesight/tenantusermanagement/tests/SMTPConfigurationTest.java +++ b/src/test/java/com/knecon/fforesight/tenantusermanagement/tests/SMTPConfigurationTest.java @@ -1,15 +1,6 @@ package com.knecon.fforesight.tenantusermanagement.tests; -import static com.knecon.fforesight.tenantusermanagement.service.SMTPService.SMTP_DEFAULT_AUTH; -import static com.knecon.fforesight.tenantusermanagement.service.SMTPService.SMTP_DEFAULT_HOST; -import static com.knecon.fforesight.tenantusermanagement.service.SMTPService.SMTP_DEFAULT_PORT; -import static com.knecon.fforesight.tenantusermanagement.service.SMTPService.SMTP_DEFAULT_REPLY_EMAIL; -import static com.knecon.fforesight.tenantusermanagement.service.SMTPService.SMTP_DEFAULT_REPLY_NAME; -import static com.knecon.fforesight.tenantusermanagement.service.SMTPService.SMTP_DEFAULT_SENDER_EMAIL; -import static com.knecon.fforesight.tenantusermanagement.service.SMTPService.SMTP_DEFAULT_SENDER_ENVELOPE; -import static com.knecon.fforesight.tenantusermanagement.service.SMTPService.SMTP_DEFAULT_SENDER_NAME; -import static com.knecon.fforesight.tenantusermanagement.service.SMTPService.SMTP_DEFAULT_SSL; -import static com.knecon.fforesight.tenantusermanagement.service.SMTPService.SMTP_DEFAULT_STARTTLS; +import static com.knecon.fforesight.tenantusermanagement.service.EnvironmentSMTPConfigurationProvider.*; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.junit.jupiter.api.Assertions.assertThrows; diff --git a/src/test/java/com/knecon/fforesight/tenantusermanagement/tests/SMTPServiceTest.java b/src/test/java/com/knecon/fforesight/tenantusermanagement/tests/SMTPServiceTest.java new file mode 100644 index 0000000..334bed9 --- /dev/null +++ b/src/test/java/com/knecon/fforesight/tenantusermanagement/tests/SMTPServiceTest.java @@ -0,0 +1,242 @@ +package com.knecon.fforesight.tenantusermanagement.tests; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.jetbrains.annotations.NotNull; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.keycloak.admin.client.resource.RealmResource; +import org.keycloak.representations.idm.RealmRepresentation; +import org.mockito.ArgumentCaptor; +import org.mockito.InjectMocks; +import org.mockito.Mock; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.knecon.fforesight.tenantcommons.EncryptionDecryptionService; +import com.knecon.fforesight.tenantusermanagement.entity.GlobalSMTPConfigurationEntity; +import com.knecon.fforesight.tenantusermanagement.entity.TenantEntity; +import com.knecon.fforesight.tenantusermanagement.model.SMTPConfiguration; +import com.knecon.fforesight.tenantusermanagement.repository.TenantRepository; +import com.knecon.fforesight.tenantusermanagement.service.EnvironmentSMTPConfigurationProvider; +import com.knecon.fforesight.tenantusermanagement.service.GlobalSMTPConfigurationPersistenceService; +import com.knecon.fforesight.tenantusermanagement.service.RealmService; +import com.knecon.fforesight.tenantusermanagement.service.SMTPService; + +@ExtendWith(org.mockito.junit.jupiter.MockitoExtension.class) +public class SMTPServiceTest { + + public static final String NEW_SMTP_HOST = "new.smtp.host"; + public static final int NEW_SMTP_PORT = 465; + public static final String NEW_SMTP_USER = "newUser"; + public static final String NEW_SMTP_PASSWORD = "newPassword"; + public static final String OLD_SMTP_HOST = "old.smtp.host"; + public static final int OLD_SMTP_PORT = 587; + public static final String OLD_SMTP_USER = "globalUser"; + public static final String OLD_SMTP_PASSWORD = "oldPassword"; + @Mock + private GlobalSMTPConfigurationPersistenceService smtpConfigurationPersistenceService; + + @Mock + private EnvironmentSMTPConfigurationProvider environmentSMTPConfigurationProvider; + + @Mock + private RealmService realmService; + + @Mock + private TenantRepository tenantRepository; + + @Mock + private EncryptionDecryptionService encryptionDecryptionService; + + @InjectMocks + private SMTPService smtpService; + + private SMTPConfiguration oldGlobalConfig; + private SMTPConfiguration overriddenConfig; + private List tenantEntities; + private final static String DEFAULT_PASSWORD = "**********"; + + + @BeforeEach + public void setUp() { + + smtpService = new SMTPService(smtpConfigurationPersistenceService, + environmentSMTPConfigurationProvider, + null, + realmService, + tenantRepository, + encryptionDecryptionService, + new ObjectMapper()); + + oldGlobalConfig = new SMTPConfiguration(); + oldGlobalConfig.setId("singleton"); + oldGlobalConfig.setHost(OLD_SMTP_HOST); + oldGlobalConfig.setPort(OLD_SMTP_PORT); + oldGlobalConfig.setUser(OLD_SMTP_USER); + oldGlobalConfig.setPassword(OLD_SMTP_PASSWORD); + + overriddenConfig = new SMTPConfiguration(); + overriddenConfig.setHost("custom.smtp.host"); + overriddenConfig.setPort(2525); + overriddenConfig.setUser("customUser"); + overriddenConfig.setPassword("encrypted_customPassword"); + + TenantEntity tenant1 = new TenantEntity(); + tenant1.setTenantId("tenant1"); + + TenantEntity tenant2 = new TenantEntity(); + tenant2.setTenantId("tenant2"); + + TenantEntity tenant3 = new TenantEntity(); + tenant3.setTenantId("tenant3"); + + tenantEntities = Arrays.asList(tenant1, tenant2, tenant3); + when(tenantRepository.findAll()).thenReturn(tenantEntities); + + GlobalSMTPConfigurationEntity globalEntity = new GlobalSMTPConfigurationEntity(); + globalEntity.setHost(oldGlobalConfig.getHost()); + globalEntity.setPort(oldGlobalConfig.getPort()); + globalEntity.setUserName(oldGlobalConfig.getUser()); + globalEntity.setPassword(oldGlobalConfig.getPassword()); + when(smtpConfigurationPersistenceService.getConfiguration()).thenReturn(globalEntity); + + for (TenantEntity tenant : tenantEntities) { + RealmResource tenantRealmResource = mock(RealmResource.class); + when(realmService.realm(tenant.getTenantId())).thenReturn(tenantRealmResource); + } + + when(encryptionDecryptionService.decrypt(anyString())).thenAnswer(invocation -> { + String encrypted = invocation.getArgument(0); + if (encrypted.startsWith("encrypted_")) { + return encrypted.substring("encrypted_".length()); + } + return encrypted; + }); + } + + + @Test + public void testSynchronizeSMTPConfigurations() { + + for (TenantEntity tenant : tenantEntities) { + RealmResource tenantRealmResource = mock(RealmResource.class); + when(realmService.realm(tenant.getTenantId())).thenReturn(tenantRealmResource); + // for this test also mock toRepresentation + RealmRepresentation realm = getRealmRepresentation(tenant, overriddenConfig, oldGlobalConfig); + when(tenantRealmResource.toRepresentation()).thenReturn(realm); + } + + when(encryptionDecryptionService.encrypt(anyString())).thenAnswer(invocation -> "encrypted_" + invocation.getArgument(0)); + + SMTPConfiguration newEnvConfig = new SMTPConfiguration(); + newEnvConfig.setId("singleton"); + newEnvConfig.setHost(NEW_SMTP_HOST); + newEnvConfig.setPort(NEW_SMTP_PORT); + newEnvConfig.setUser(NEW_SMTP_USER); + newEnvConfig.setPassword(NEW_SMTP_PASSWORD); + when(environmentSMTPConfigurationProvider.get()).thenReturn(newEnvConfig); + + smtpService.synchronizeSMTPConfigurations(); + + ArgumentCaptor globalConfigCaptor = ArgumentCaptor.forClass(GlobalSMTPConfigurationEntity.class); + verify(smtpConfigurationPersistenceService).updateConfiguration(globalConfigCaptor.capture()); + + GlobalSMTPConfigurationEntity updatedGlobalConfig = globalConfigCaptor.getValue(); + assertEquals(NEW_SMTP_HOST, updatedGlobalConfig.getHost()); + assertEquals(NEW_SMTP_PORT, updatedGlobalConfig.getPort()); + assertEquals(NEW_SMTP_USER, updatedGlobalConfig.getUserName()); + assertEquals("encrypted_" + NEW_SMTP_PASSWORD, updatedGlobalConfig.getPassword()); + + // Verify that tenant1 and tenant3 were not updated, and tenant2 was + for (TenantEntity tenant : tenantEntities) { + RealmResource tenantRealmResource = realmService.realm(tenant.getTenantId()); + + if ("tenant1".equals(tenant.getTenantId()) || "tenant3".equals(tenant.getTenantId())) { + // Verify that update() was called once for tenant1 and tenant3 + verify(tenantRealmResource, times(1)).update(any(RealmRepresentation.class)); + + ArgumentCaptor realmCaptor = ArgumentCaptor.forClass(RealmRepresentation.class); + verify(tenantRealmResource).update(realmCaptor.capture()); + + RealmRepresentation updatedRealm = realmCaptor.getValue(); + Map smtpServer = updatedRealm.getSmtpServer(); + smtpServer.putAll(updatedRealm.getAttributes()); + assertEquals(NEW_SMTP_HOST, smtpServer.get("host")); + assertEquals(String.valueOf(NEW_SMTP_PORT), smtpServer.get("port")); + assertEquals(NEW_SMTP_USER, smtpServer.get("user")); + assertEquals("encrypted_" + NEW_SMTP_PASSWORD, smtpServer.get("FFORESIGHT_SMTP_PASSWORD")); + } else if ("tenant2".equals(tenant.getTenantId())) { + // Verify that update() was never called for tenant2 + verify(tenantRealmResource, never()).update(any(RealmRepresentation.class)); + } + } + } + + + @Test + public void testSynchronizeSMTPConfigurationsWithoutGlobalChanges() { + + SMTPConfiguration newEnvConfig = new SMTPConfiguration(); + newEnvConfig.setId("singleton"); + newEnvConfig.setHost(OLD_SMTP_HOST); + newEnvConfig.setPort(OLD_SMTP_PORT); + newEnvConfig.setUser(OLD_SMTP_USER); + newEnvConfig.setPassword(OLD_SMTP_PASSWORD); + when(environmentSMTPConfigurationProvider.get()).thenReturn(newEnvConfig); + + smtpService.synchronizeSMTPConfigurations(); + + verify(smtpConfigurationPersistenceService, never()).updateConfiguration(any(GlobalSMTPConfigurationEntity.class)); + + // Verify that all tenants are unchanged + for (TenantEntity tenant : tenantEntities) { + RealmResource tenantRealmResource = realmService.realm(tenant.getTenantId()); + + verify(tenantRealmResource, never()).update(any(RealmRepresentation.class)); + } + } + + + @NotNull + private static RealmRepresentation getRealmRepresentation(TenantEntity tenant, SMTPConfiguration overriddenConfig, SMTPConfiguration oldGlobalConfig) { + + RealmRepresentation realm = new RealmRepresentation(); + Map smtpMap = new HashMap<>(); + smtpMap.put("password", DEFAULT_PASSWORD); + Map attributes = new HashMap<>(); + + if ("tenant2".equals(tenant.getTenantId())) { + // tenant2 has overridden SMTP config + smtpMap.put("host", overriddenConfig.getHost()); + smtpMap.put("port", String.valueOf(overriddenConfig.getPort())); + smtpMap.put("user", overriddenConfig.getUser()); + attributes.put("FFORESIGHT_SMTP_PASSWORD", overriddenConfig.getPassword()); + + } else { + // tenant1 and tenant3 have SMTP config matching global + smtpMap.put("host", oldGlobalConfig.getHost()); + smtpMap.put("port", String.valueOf(oldGlobalConfig.getPort())); + smtpMap.put("user", oldGlobalConfig.getUser()); + attributes.put("FFORESIGHT_SMTP_PASSWORD", oldGlobalConfig.getPassword()); + + } + realm.setSmtpServer(smtpMap); + + realm.setAttributes(attributes); + return realm; + } + +}