diff options
Diffstat (limited to 'server/sonar-webserver-common')
8 files changed, 1142 insertions, 0 deletions
diff --git a/server/sonar-webserver-common/src/it/java/org/sonar/server/common/email/config/EmailConfigurationBuilder.java b/server/sonar-webserver-common/src/it/java/org/sonar/server/common/email/config/EmailConfigurationBuilder.java new file mode 100644 index 00000000000..ae619646223 --- /dev/null +++ b/server/sonar-webserver-common/src/it/java/org/sonar/server/common/email/config/EmailConfigurationBuilder.java @@ -0,0 +1,119 @@ +/* + * SonarQube + * Copyright (C) 2009-2024 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.common.email.config; + +public final class EmailConfigurationBuilder { + private String id; + private String host; + private String port; + private EmailConfigurationSecurityProtocol securityProtocol; + private String fromAddress; + private String fromName; + private String subjectPrefix; + private EmailConfigurationAuthMethod authMethod; + private String username; + private String basicPassword; + private String oauthAuthenticationHost; + private String oauthClientId; + private String oauthClientSecret; + private String oauthTenant; + + private EmailConfigurationBuilder() { + } + + public static EmailConfigurationBuilder builder() { + return new EmailConfigurationBuilder(); + } + + public EmailConfigurationBuilder id(String id) { + this.id = id; + return this; + } + + public EmailConfigurationBuilder host(String host) { + this.host = host; + return this; + } + + public EmailConfigurationBuilder port(String port) { + this.port = port; + return this; + } + + public EmailConfigurationBuilder securityProtocol(EmailConfigurationSecurityProtocol securityProtocol) { + this.securityProtocol = securityProtocol; + return this; + } + + public EmailConfigurationBuilder fromAddress(String fromAddress) { + this.fromAddress = fromAddress; + return this; + } + + public EmailConfigurationBuilder fromName(String fromName) { + this.fromName = fromName; + return this; + } + + public EmailConfigurationBuilder subjectPrefix(String subjectPrefix) { + this.subjectPrefix = subjectPrefix; + return this; + } + + public EmailConfigurationBuilder authMethod(EmailConfigurationAuthMethod authMethod) { + this.authMethod = authMethod; + return this; + } + + public EmailConfigurationBuilder username(String username) { + this.username = username; + return this; + } + + public EmailConfigurationBuilder basicPassword(String basicPassword) { + this.basicPassword = basicPassword; + return this; + } + + public EmailConfigurationBuilder oauthAuthenticationHost(String oauthAuthenticationHost) { + this.oauthAuthenticationHost = oauthAuthenticationHost; + return this; + } + + public EmailConfigurationBuilder oauthClientId(String oauthClientId) { + this.oauthClientId = oauthClientId; + return this; + } + + public EmailConfigurationBuilder oauthClientSecret(String oauthClientSecret) { + this.oauthClientSecret = oauthClientSecret; + return this; + } + + public EmailConfigurationBuilder oauthTenant(String oauthTenant) { + this.oauthTenant = oauthTenant; + return this; + } + + public EmailConfiguration build() { + return new EmailConfiguration(id, host, port, securityProtocol, fromAddress, fromName, subjectPrefix, authMethod, username, basicPassword, oauthAuthenticationHost, + oauthClientId, oauthClientSecret, oauthTenant); + } +} diff --git a/server/sonar-webserver-common/src/it/java/org/sonar/server/common/email/config/EmailConfigurationServiceIT.java b/server/sonar-webserver-common/src/it/java/org/sonar/server/common/email/config/EmailConfigurationServiceIT.java new file mode 100644 index 00000000000..2ebb26e580c --- /dev/null +++ b/server/sonar-webserver-common/src/it/java/org/sonar/server/common/email/config/EmailConfigurationServiceIT.java @@ -0,0 +1,477 @@ +/* + * SonarQube + * Copyright (C) 2009-2024 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.common.email.config; + +import java.util.List; +import java.util.Map; +import javax.annotation.Nullable; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; +import org.junit.platform.commons.util.StringUtils; +import org.sonar.db.DbSession; +import org.sonar.db.DbTester; +import org.sonar.server.exceptions.BadRequestException; +import org.sonar.server.exceptions.NotFoundException; + +import static java.lang.String.format; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.sonar.server.common.NonNullUpdatedValue.undefined; +import static org.sonar.server.common.NonNullUpdatedValue.withValueOrThrow; +import static org.sonar.server.common.email.config.EmailConfigurationService.UNIQUE_EMAIL_CONFIGURATION_ID; +import static org.sonar.server.common.email.config.UpdateEmailConfigurationRequest.builder; +import static org.sonar.server.email.EmailSmtpConfiguration.EMAIL_CONFIG_SMTP_HOST; +import static org.sonar.server.email.EmailSmtpConfiguration.EMAIL_CONFIG_SMTP_OAUTH_CLIENTID; +import static org.sonar.server.email.EmailSmtpConfiguration.EMAIL_CONFIG_SMTP_OAUTH_CLIENTSECRET; +import static org.sonar.server.email.EmailSmtpConfiguration.EMAIL_CONFIG_SMTP_OAUTH_HOST; +import static org.sonar.server.email.EmailSmtpConfiguration.EMAIL_CONFIG_SMTP_OAUTH_TENANT; +import static org.sonar.server.email.EmailSmtpConfiguration.EMAIL_CONFIG_SMTP_PASSWORD; +import static org.sonar.server.email.EmailSmtpConfiguration.EMAIL_CONFIG_SMTP_USERNAME; + +class EmailConfigurationServiceIT { + + private static final String OAUTH_URLS_ERROR_MESSAGE = "For security reasons, OAuth urls can't be updated without providing the client secret."; + private static final String BASIC_URLS_ERROR_MESSAGE = "For security reasons, the host can't be updated without providing the password."; + + private static final EmailConfigurationBuilder EMAIL_BASIC_CONFIG_BUILDER = EmailConfigurationBuilder.builder() + .id(UNIQUE_EMAIL_CONFIGURATION_ID) + .host("host") + .port("port") + .securityProtocol(EmailConfigurationSecurityProtocol.NONE) + .fromAddress("fromAddress") + .fromName("fromName") + .subjectPrefix("subjectPrefix") + .authMethod(EmailConfigurationAuthMethod.BASIC) + .username("username") + .basicPassword("basicPassword") + .oauthAuthenticationHost("oauthAuthenticationHost") + .oauthClientId("oauthClientId") + .oauthClientSecret("oauthClientSecret") + .oauthTenant("oauthTenant"); + + private static final EmailConfigurationBuilder EMAIL_OAUTH_CONFIG_BUILDER = EmailConfigurationBuilder.builder() + .id(UNIQUE_EMAIL_CONFIGURATION_ID) + .host("hostOAuth") + .port("portOAuth") + .securityProtocol(EmailConfigurationSecurityProtocol.SSLTLS) + .fromAddress("fromAddressOAuth") + .fromName("fromNameOAuth") + .subjectPrefix("subjectPrefixOAuth") + .authMethod(EmailConfigurationAuthMethod.OAUTH) + .username("usernameOAuth") + .basicPassword("basicPasswordOAuth") + .oauthAuthenticationHost("oauthAuthenticationHostOAuth") + .oauthClientId("oauthClientIdOAuth") + .oauthClientSecret("oauthClientSecretOAuth") + .oauthTenant("oauthTenantOAuth"); + + @RegisterExtension + public DbTester dbTester = DbTester.create(); + + private EmailConfigurationService underTest; + + @BeforeEach + void setUp() { + underTest = new EmailConfigurationService(dbTester.getDbClient()); + } + + @Test + void createConfiguration_whenConfigExists_shouldFail() { + dbTester.getDbClient().internalPropertiesDao().save(dbTester.getSession(), EMAIL_CONFIG_SMTP_HOST, "localhost"); + dbTester.commit(); + + EmailConfiguration emailConfiguration = EMAIL_BASIC_CONFIG_BUILDER.build(); + assertThatThrownBy(() -> underTest.createConfiguration(emailConfiguration)) + .isInstanceOf(BadRequestException.class) + .hasMessage("Email configuration already exists. Only one Email configuration is supported."); + } + + @ParameterizedTest + @MethodSource("configCreationParamConstraints") + void createConfiguration_whenFieldsAreMissing_shouldThrow(EmailConfigurationAuthMethod authMethod, String missingField, String errorMessage) { + EmailConfiguration config = new EmailConfiguration( + UNIQUE_EMAIL_CONFIGURATION_ID, + "smtpHost", + "smtpPort", + EmailConfigurationSecurityProtocol.NONE, + "fromAddress", + "fromName", + "subjectPrefix", + authMethod, + "username", + "basicPassword".equals(missingField) ? null : "basicPassword", + "oauthAuthenticationHost".equals(missingField) ? null : "oauthAuthenticationHost", + "oauthClientId".equals(missingField) ? null : "oauthClientId", + "oauthClientSecret".equals(missingField) ? null : "oauthClientSecret", + "oauthTenant".equals(missingField) ? null : "oauthTenant" + ); + + assertThatThrownBy(() -> underTest.createConfiguration(config)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage(errorMessage); + } + + static Object[][] configCreationParamConstraints() { + return new Object[][]{ + {EmailConfigurationAuthMethod.BASIC, "basicPassword", "Password is required."}, + {EmailConfigurationAuthMethod.OAUTH, "oauthAuthenticationHost", "OAuth authentication host is required."}, + {EmailConfigurationAuthMethod.OAUTH, "oauthClientId", "OAuth client id is required."}, + {EmailConfigurationAuthMethod.OAUTH, "oauthClientSecret", "OAuth client secret is required."}, + {EmailConfigurationAuthMethod.OAUTH, "oauthTenant", "OAuth tenant is required."} + }; + } + + @Test + void createConfiguration_whenConfigDoesNotExist_shouldCreateConfig() { + EmailConfiguration configuration = EMAIL_BASIC_CONFIG_BUILDER.build(); + EmailConfiguration createdConfig = underTest.createConfiguration(configuration); + + assertThatConfigurationIsCorrect(configuration, createdConfig); + } + + @Test + void getConfiguration_whenWrongId_shouldThrow() { + assertThatThrownBy(() -> underTest.getConfiguration("wrongId")) + .isInstanceOf(NotFoundException.class) + .hasMessage("Email configuration with id wrongId not found"); + } + + @Test + void getConfiguration_whenNoConfig_shouldThrow() { + assertThatThrownBy(() -> underTest.getConfiguration(UNIQUE_EMAIL_CONFIGURATION_ID)) + .isInstanceOf(NotFoundException.class) + .hasMessage("Email configuration doesn't exist."); + } + + @Test + void getConfiguration_whenConfigExists_shouldReturnConfig() { + EmailConfiguration configuration = EMAIL_BASIC_CONFIG_BUILDER.build(); + underTest.createConfiguration(configuration); + + EmailConfiguration retrievedConfig = underTest.getConfiguration(UNIQUE_EMAIL_CONFIGURATION_ID); + + assertThatConfigurationIsCorrect(configuration, retrievedConfig); + } + + @Test + void findConfiguration_whenNoConfig_shouldReturnEmpty() { + assertThat(underTest.findConfigurations()).isEmpty(); + } + + @Test + void findConfiguration_whenConfigExists_shouldReturnConfig() { + EmailConfiguration configuration = underTest.createConfiguration(EMAIL_BASIC_CONFIG_BUILDER.build()); + + assertThat(underTest.findConfigurations()).contains(configuration); + } + + @Test + void updateConfiguration_whenConfigDoesNotExist_shouldThrow() { + UpdateEmailConfigurationRequest updateRequest = getUpdateEmailConfigurationRequestFromConfig(EMAIL_OAUTH_CONFIG_BUILDER.build()); + + assertThatThrownBy(() -> underTest.updateConfiguration(updateRequest)) + .isInstanceOf(NotFoundException.class) + .hasMessage("Email configuration doesn't exist."); + } + + @ParameterizedTest + @MethodSource("configUpdateParamConstraints") + void updateConfiguration_shouldApplyParamConstraints(ConfigTypeAndOrigin configTypeAndOrigin, List<Param> params, boolean shouldThrow, String errorMessage) { + UpdateEmailConfigurationRequest updateRequest = prepareUpdateRequestFromParams(configTypeAndOrigin, params); + + if (shouldThrow) { + assertThatThrownBy(() -> underTest.updateConfiguration(updateRequest)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage(errorMessage); + } else { + EmailConfiguration updatedConfig = underTest.updateConfiguration(updateRequest); + assertUpdatesMadeFromParams(configTypeAndOrigin, params, updatedConfig); + } + } + + static Object[][] configUpdateParamConstraints() { + return new Object[][]{ + // OAuth URLs update constraints + {ConfigTypeAndOrigin.OAUTH_BY_REQUEST, List.of(), false, ""}, + {ConfigTypeAndOrigin.OAUTH_BY_CONFIG, List.of(), false, ""}, + {ConfigTypeAndOrigin.OAUTH_BY_REQUEST, List.of(new Param("host", ParamOrigin.REQUEST, "newHost")), true, OAUTH_URLS_ERROR_MESSAGE}, + {ConfigTypeAndOrigin.OAUTH_BY_CONFIG, List.of(new Param("host", ParamOrigin.REQUEST, "newHost")), true, OAUTH_URLS_ERROR_MESSAGE}, + {ConfigTypeAndOrigin.OAUTH_BY_REQUEST, List.of(new Param("host", ParamOrigin.REQUEST, "newHost"), new Param("oauthClientSecret", ParamOrigin.REQUEST, "newSecret")), false, ""}, + {ConfigTypeAndOrigin.OAUTH_BY_CONFIG, List.of(new Param("host", ParamOrigin.REQUEST, "newHost"), new Param("oauthClientSecret", ParamOrigin.REQUEST, "newSecret")), false, ""}, + {ConfigTypeAndOrigin.OAUTH_BY_REQUEST, List.of(new Param("oauthAuthenticationHost", ParamOrigin.REQUEST, "newAuthHost")), true, OAUTH_URLS_ERROR_MESSAGE}, + {ConfigTypeAndOrigin.OAUTH_BY_CONFIG, List.of(new Param("oauthAuthenticationHost", ParamOrigin.REQUEST, "newAuthHost")), true, OAUTH_URLS_ERROR_MESSAGE}, + {ConfigTypeAndOrigin.OAUTH_BY_REQUEST, List.of(new Param("oauthAuthenticationHost", ParamOrigin.REQUEST, "newAuthHost"), new Param("oauthClientSecret", ParamOrigin.REQUEST, "newSecret")), false, ""}, + {ConfigTypeAndOrigin.OAUTH_BY_CONFIG, List.of(new Param("oauthAuthenticationHost", ParamOrigin.REQUEST, "newAuthHost"), new Param("oauthClientSecret", ParamOrigin.REQUEST, "newSecret")), false, ""}, + // Basic URLs update constraints + {ConfigTypeAndOrigin.BASIC_BY_REQUEST, List.of(), false, ""}, + {ConfigTypeAndOrigin.BASIC_BY_CONFIG, List.of(), false, ""}, + {ConfigTypeAndOrigin.BASIC_BY_REQUEST, List.of(new Param("host", ParamOrigin.REQUEST, "newHost")), true, BASIC_URLS_ERROR_MESSAGE}, + {ConfigTypeAndOrigin.BASIC_BY_CONFIG, List.of(new Param("host", ParamOrigin.REQUEST, "newHost")), true, BASIC_URLS_ERROR_MESSAGE}, + {ConfigTypeAndOrigin.BASIC_BY_REQUEST, List.of(new Param("host", ParamOrigin.REQUEST, "newHost"), new Param("basicPassword", ParamOrigin.REQUEST, "newPassword")), false, ""}, + {ConfigTypeAndOrigin.BASIC_BY_CONFIG, List.of(new Param("host", ParamOrigin.REQUEST, "newHost"), new Param("basicPassword", ParamOrigin.REQUEST, "newPassword")), false, ""}, + // OAuth param existence update constraints + {ConfigTypeAndOrigin.OAUTH_BY_REQUEST, List.of(new Param("oauthAuthenticationHost", ParamOrigin.CONFIG, "")), true, "OAuth authentication host is required."}, + {ConfigTypeAndOrigin.OAUTH_BY_REQUEST, List.of(new Param("oauthAuthenticationHost", ParamOrigin.CONFIG, ""), new Param("oauthAuthenticationHost", ParamOrigin.REQUEST, ""), new Param("oauthClientSecret", ParamOrigin.REQUEST, "newSecret")), true, "OAuth authentication host is required."}, + {ConfigTypeAndOrigin.OAUTH_BY_REQUEST, List.of(new Param("oauthAuthenticationHost", ParamOrigin.CONFIG, ""), new Param("oauthAuthenticationHost", ParamOrigin.REQUEST, "newHost"), new Param("oauthClientSecret", ParamOrigin.REQUEST, "newSecret")), false, ""}, + {ConfigTypeAndOrigin.OAUTH_BY_REQUEST, List.of(new Param("oauthClientId", ParamOrigin.CONFIG, "")), true, "OAuth client id is required."}, + {ConfigTypeAndOrigin.OAUTH_BY_REQUEST, List.of(new Param("oauthClientId", ParamOrigin.CONFIG, ""), new Param("oauthClientId", ParamOrigin.REQUEST, "")), true, "OAuth client id is required."}, + {ConfigTypeAndOrigin.OAUTH_BY_REQUEST, List.of(new Param("oauthClientId", ParamOrigin.CONFIG, ""), new Param("oauthClientId", ParamOrigin.REQUEST, "newId")), false, ""}, + {ConfigTypeAndOrigin.OAUTH_BY_REQUEST, List.of(new Param("oauthClientSecret", ParamOrigin.CONFIG, "")), true, "OAuth client secret is required."}, + {ConfigTypeAndOrigin.OAUTH_BY_REQUEST, List.of(new Param("oauthClientSecret", ParamOrigin.CONFIG, ""), new Param("oauthClientSecret", ParamOrigin.REQUEST, "")), true, "OAuth client secret is required."}, + {ConfigTypeAndOrigin.OAUTH_BY_REQUEST, List.of(new Param("oauthClientSecret", ParamOrigin.CONFIG, ""), new Param("oauthClientSecret", ParamOrigin.REQUEST, "newSecret")), false, ""}, + {ConfigTypeAndOrigin.OAUTH_BY_REQUEST, List.of(new Param("oauthTenant", ParamOrigin.CONFIG, "")), true, "OAuth tenant is required."}, + {ConfigTypeAndOrigin.OAUTH_BY_REQUEST, List.of(new Param("oauthTenant", ParamOrigin.CONFIG, ""), new Param("oauthTenant", ParamOrigin.REQUEST, "")), true, "OAuth tenant is required."}, + {ConfigTypeAndOrigin.OAUTH_BY_REQUEST, List.of(new Param("oauthTenant", ParamOrigin.CONFIG, ""), new Param("oauthTenant", ParamOrigin.REQUEST, "newTenant")), false, ""}, + {ConfigTypeAndOrigin.OAUTH_BY_REQUEST, List.of(new Param("username", ParamOrigin.CONFIG, "")), true, "Username is required."}, + {ConfigTypeAndOrigin.OAUTH_BY_REQUEST, List.of(new Param("username", ParamOrigin.CONFIG, ""), new Param("username", ParamOrigin.REQUEST, "")), true, "Username is required."}, + {ConfigTypeAndOrigin.OAUTH_BY_REQUEST, List.of(new Param("username", ParamOrigin.CONFIG, ""), new Param("username", ParamOrigin.REQUEST, "newUsername")), false, ""}, + // Basic param existence update constraints + {ConfigTypeAndOrigin.BASIC_BY_REQUEST, List.of(new Param("username", ParamOrigin.CONFIG, "")), true, "Username is required."}, + {ConfigTypeAndOrigin.BASIC_BY_REQUEST, List.of(new Param("username", ParamOrigin.CONFIG, ""), new Param("username", ParamOrigin.REQUEST, "")), true, "Username is required."}, + {ConfigTypeAndOrigin.BASIC_BY_REQUEST, List.of(new Param("username", ParamOrigin.CONFIG, ""), new Param("username", ParamOrigin.REQUEST, "newUsername")), false, ""}, + {ConfigTypeAndOrigin.BASIC_BY_REQUEST, List.of(new Param("basicPassword", ParamOrigin.CONFIG, "")), true, "Password is required."}, + {ConfigTypeAndOrigin.BASIC_BY_REQUEST, List.of(new Param("basicPassword", ParamOrigin.CONFIG, ""), new Param("basicPassword", ParamOrigin.REQUEST, "")), true, "Password is required."}, + {ConfigTypeAndOrigin.BASIC_BY_REQUEST, List.of(new Param("basicPassword", ParamOrigin.CONFIG, ""), new Param("basicPassword", ParamOrigin.REQUEST, "newPassword")), false, ""}, + }; + } + + @Test + void updateConfiguration_whenConfigExists_shouldUpdateConfig() { + underTest.createConfiguration(EMAIL_BASIC_CONFIG_BUILDER.build()); + EmailConfiguration newConfig = EMAIL_OAUTH_CONFIG_BUILDER.build(); + UpdateEmailConfigurationRequest updateRequest = getUpdateEmailConfigurationRequestFromConfig(newConfig); + + EmailConfiguration updatedConfig = underTest.updateConfiguration(updateRequest); + + assertThatConfigurationIsCorrect(newConfig, updatedConfig); + } + + private static UpdateEmailConfigurationRequest getUpdateEmailConfigurationRequestFromConfig(EmailConfiguration updatedConfig) { + return builder() + .emailConfigurationId(updatedConfig.id()) + .host(withValueOrThrow(updatedConfig.host())) + .port(withValueOrThrow(updatedConfig.port())) + .securityProtocol(withValueOrThrow(updatedConfig.securityProtocol())) + .fromAddress(withValueOrThrow(updatedConfig.fromAddress())) + .fromName(withValueOrThrow(updatedConfig.fromName())) + .subjectPrefix(withValueOrThrow(updatedConfig.subjectPrefix())) + .authMethod(withValueOrThrow(updatedConfig.authMethod())) + .username(withValueOrThrow(updatedConfig.username())) + .basicPassword(withValueOrThrow(updatedConfig.basicPassword())) + .oauthAuthenticationHost(withValueOrThrow(updatedConfig.oauthAuthenticationHost())) + .oauthClientId(withValueOrThrow(updatedConfig.oauthClientId())) + .oauthClientSecret(withValueOrThrow(updatedConfig.oauthClientSecret())) + .oauthTenant(withValueOrThrow(updatedConfig.oauthTenant())) + .build(); + } + + private void assertThatConfigurationIsCorrect(EmailConfiguration expectedConfig, EmailConfiguration actualConfig) { + assertThat(actualConfig.id()).isEqualTo(expectedConfig.id()); + assertThat(actualConfig.host()).isEqualTo(expectedConfig.host()); + assertThat(actualConfig.port()).isEqualTo(expectedConfig.port()); + assertThat(actualConfig.securityProtocol()).isEqualTo(expectedConfig.securityProtocol()); + assertThat(actualConfig.fromAddress()).isEqualTo(expectedConfig.fromAddress()); + assertThat(actualConfig.fromName()).isEqualTo(expectedConfig.fromName()); + assertThat(actualConfig.subjectPrefix()).isEqualTo(expectedConfig.subjectPrefix()); + assertThat(actualConfig.authMethod()).isEqualTo(expectedConfig.authMethod()); + assertThat(actualConfig.username()).isEqualTo(expectedConfig.username()); + assertThat(actualConfig.basicPassword()).isEqualTo(expectedConfig.basicPassword()); + assertThat(actualConfig.oauthAuthenticationHost()).isEqualTo(expectedConfig.oauthAuthenticationHost()); + assertThat(actualConfig.oauthClientId()).isEqualTo(expectedConfig.oauthClientId()); + assertThat(actualConfig.oauthClientSecret()).isEqualTo(expectedConfig.oauthClientSecret()); + assertThat(actualConfig.oauthTenant()).isEqualTo(expectedConfig.oauthTenant()); + assertThat(actualConfig.oauthScope()).isEqualTo(expectedConfig.oauthScope()); + assertThat(actualConfig.oauthGrant()).isEqualTo(expectedConfig.oauthGrant()); + } + + @Test + void deleteConfiguration_whenConfigDoesNotExist_shouldThrow() { + assertThatThrownBy(() -> underTest.deleteConfiguration(UNIQUE_EMAIL_CONFIGURATION_ID)) + .isInstanceOf(NotFoundException.class) + .hasMessage("Email configuration doesn't exist."); + } + + @Test + void deleteConfiguration_whenConfigExists_shouldDeleteConfig() { + underTest.createConfiguration(EMAIL_BASIC_CONFIG_BUILDER.build()); + + underTest.deleteConfiguration(UNIQUE_EMAIL_CONFIGURATION_ID); + + assertThatThrownBy(() -> underTest.getConfiguration(UNIQUE_EMAIL_CONFIGURATION_ID)) + .isInstanceOf(NotFoundException.class) + .hasMessage("Email configuration doesn't exist."); + } + + + private UpdateEmailConfigurationRequest prepareUpdateRequestFromParams(ConfigTypeAndOrigin configTypeAndOrigin, List<Param> params) { + createOriginalConfiguration(configTypeAndOrigin, params); + UpdateEmailConfigurationRequest.Builder requestBuilder = getOriginalBuilder(); + + switch (configTypeAndOrigin) { + case OAUTH_BY_REQUEST, OAUTH_BY_CONFIG: + requestBuilder.authMethod(withValueOrThrow(EmailConfigurationAuthMethod.OAUTH)); + break; + case BASIC_BY_REQUEST, BASIC_BY_CONFIG: + requestBuilder.authMethod(withValueOrThrow(EmailConfigurationAuthMethod.BASIC)); + break; + default: + throw new IllegalArgumentException(format("Invalid test input: config %s not supported", configTypeAndOrigin.name())); + } + + for (Param param : params) { + if (param.paramOrigin.equals(ParamOrigin.REQUEST)) { + switch (param.paramName()) { + case "host": + requestBuilder.host(withValueOrThrow(param.value())); + break; + case "basicPassword": + requestBuilder.basicPassword(withValueOrThrow(param.value())); + break; + case "username": + requestBuilder.username(withValueOrThrow(param.value())); + break; + case "oauthAuthenticationHost": + requestBuilder.oauthAuthenticationHost(withValueOrThrow(param.value())); + break; + case "oauthClientSecret": + requestBuilder.oauthClientSecret(withValueOrThrow(param.value())); + break; + case "oauthClientId": + requestBuilder.oauthClientId(withValueOrThrow(param.value())); + break; + case "oauthTenant": + requestBuilder.oauthTenant(withValueOrThrow(param.value())); + break; + default: + throw new IllegalArgumentException(format("Invalid test input: param %s not supported.", param.paramName())); + } + } + } + + return requestBuilder.build(); + } + + private void createOriginalConfiguration(ConfigTypeAndOrigin configTypeAndOrigin, List<Param> params) { + EmailConfigurationBuilder configBuilder; + + if (configTypeAndOrigin == ConfigTypeAndOrigin.OAUTH_BY_CONFIG || configTypeAndOrigin == ConfigTypeAndOrigin.OAUTH_BY_REQUEST) { + configBuilder = EMAIL_OAUTH_CONFIG_BUILDER; + } else { + configBuilder = EMAIL_BASIC_CONFIG_BUILDER; + } + + underTest.createConfiguration(configBuilder.build()); + + // We manually alter the Param config to bypass service constraints of EmailConfigurationService.createConfiguration() + Map<String, String> paramNameToPropertyKey = Map.of( + "username", EMAIL_CONFIG_SMTP_USERNAME, + "basicPassword", EMAIL_CONFIG_SMTP_PASSWORD, + "oauthAuthenticationHost", EMAIL_CONFIG_SMTP_OAUTH_HOST, + "oauthClientId", EMAIL_CONFIG_SMTP_OAUTH_CLIENTID, + "oauthClientSecret", EMAIL_CONFIG_SMTP_OAUTH_CLIENTSECRET, + "oauthTenant", EMAIL_CONFIG_SMTP_OAUTH_TENANT + ); + params.stream() + .filter(param -> param.paramOrigin.equals(ParamOrigin.CONFIG)) + .forEach(param -> setInternalProperty(dbTester.getSession(), paramNameToPropertyKey.get(param.paramName()), param.value)); + dbTester.commit(); + + } + + private void setInternalProperty(DbSession dbSession, String propertyName, @Nullable String value) { + if (StringUtils.isBlank(value)) { + dbTester.getDbClient().internalPropertiesDao().delete(dbSession, propertyName); + } else { + dbTester.getDbClient().internalPropertiesDao().save(dbSession, propertyName, value); + } + } + + private static UpdateEmailConfigurationRequest.Builder getOriginalBuilder() { + return builder() + .emailConfigurationId(UNIQUE_EMAIL_CONFIGURATION_ID) + .host(undefined()) + .port(undefined()) + .securityProtocol(undefined()) + .fromAddress(undefined()) + .fromName(undefined()) + .subjectPrefix(undefined()) + .authMethod(undefined()) + .username(undefined()) + .basicPassword(undefined()) + .oauthAuthenticationHost(undefined()) + .oauthClientId(undefined()) + .oauthClientSecret(undefined()) + .oauthTenant(undefined()); + } + + private void assertUpdatesMadeFromParams(ConfigTypeAndOrigin configTypeAndOrigin, List<Param> params, EmailConfiguration updatedConfig) { + for (Param param : params) { + if (param.paramOrigin.equals(ParamOrigin.REQUEST)) { + switch (param.paramName()) { + case "host": + assertThat(updatedConfig.host()).isEqualTo(param.value()); + break; + case "basicPassword": + assertThat(updatedConfig.basicPassword()).isEqualTo(param.value()); + break; + case "username": + assertThat(updatedConfig.username()).isEqualTo(param.value()); + break; + case "oauthAuthenticationHost": + assertThat(updatedConfig.oauthAuthenticationHost()).isEqualTo(param.value()); + break; + case "oauthClientId": + assertThat(updatedConfig.oauthClientId()).isEqualTo(param.value()); + break; + case "oauthClientSecret": + assertThat(updatedConfig.oauthClientSecret()).isEqualTo(param.value()); + break; + case "oauthTenant": + assertThat(updatedConfig.oauthTenant()).isEqualTo(param.value()); + break; + default: + throw new IllegalArgumentException(format("Invalid test input: param %s not supported.", param.paramName())); + } + } + } + + if (configTypeAndOrigin == ConfigTypeAndOrigin.OAUTH_BY_REQUEST) { + assertThat(updatedConfig.authMethod()).isEqualTo(EmailConfigurationAuthMethod.OAUTH); + } + if (configTypeAndOrigin == ConfigTypeAndOrigin.BASIC_BY_REQUEST) { + assertThat(updatedConfig.authMethod()).isEqualTo(EmailConfigurationAuthMethod.BASIC); + } + } + + private enum ConfigTypeAndOrigin { + BASIC_BY_CONFIG, BASIC_BY_REQUEST, OAUTH_BY_CONFIG, OAUTH_BY_REQUEST; + } + + private enum ParamOrigin { + REQUEST, CONFIG; + } + + private record Param(String paramName, ParamOrigin paramOrigin, @Nullable String value) {} + +} + diff --git a/server/sonar-webserver-common/src/main/java/org/sonar/server/common/email/config/EmailConfiguration.java b/server/sonar-webserver-common/src/main/java/org/sonar/server/common/email/config/EmailConfiguration.java new file mode 100644 index 00000000000..8b5a2d19745 --- /dev/null +++ b/server/sonar-webserver-common/src/main/java/org/sonar/server/common/email/config/EmailConfiguration.java @@ -0,0 +1,52 @@ +/* + * SonarQube + * Copyright (C) 2009-2024 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.common.email.config; + +import javax.annotation.Nullable; + +import static org.sonar.server.email.EmailSmtpConfiguration.EMAIL_CONFIG_SMTP_OAUTH_GRANT_DEFAULT; +import static org.sonar.server.email.EmailSmtpConfiguration.EMAIL_CONFIG_SMTP_OAUTH_SCOPE_DEFAULT; + +public record EmailConfiguration( + String id, + String host, + String port, + EmailConfigurationSecurityProtocol securityProtocol, + String fromAddress, + String fromName, + String subjectPrefix, + EmailConfigurationAuthMethod authMethod, + String username, + @Nullable String basicPassword, + @Nullable String oauthAuthenticationHost, + @Nullable String oauthClientId, + @Nullable String oauthClientSecret, + @Nullable String oauthTenant, + @Nullable String oauthScope, + @Nullable String oauthGrant +) { + + public EmailConfiguration(String id, String host, String port, EmailConfigurationSecurityProtocol securityProtocol, String fromAddress, String fromName, String subjectPrefix, + EmailConfigurationAuthMethod authMethod, String username, @Nullable String basicPassword, @Nullable String oauthAuthenticationHost, + @Nullable String oauthClientId, @Nullable String oauthClientSecret, @Nullable String oauthTenant) { + this(id, host, port, securityProtocol, fromAddress, fromName, subjectPrefix, authMethod, username, basicPassword, oauthAuthenticationHost, oauthClientId, + oauthClientSecret, oauthTenant, EMAIL_CONFIG_SMTP_OAUTH_SCOPE_DEFAULT, EMAIL_CONFIG_SMTP_OAUTH_GRANT_DEFAULT); + } +} diff --git a/server/sonar-webserver-common/src/main/java/org/sonar/server/common/email/config/EmailConfigurationAuthMethod.java b/server/sonar-webserver-common/src/main/java/org/sonar/server/common/email/config/EmailConfigurationAuthMethod.java new file mode 100644 index 00000000000..2269794ac2b --- /dev/null +++ b/server/sonar-webserver-common/src/main/java/org/sonar/server/common/email/config/EmailConfigurationAuthMethod.java @@ -0,0 +1,24 @@ +/* + * SonarQube + * Copyright (C) 2009-2024 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.common.email.config; + +public enum EmailConfigurationAuthMethod { + BASIC, OAUTH +} diff --git a/server/sonar-webserver-common/src/main/java/org/sonar/server/common/email/config/EmailConfigurationSecurityProtocol.java b/server/sonar-webserver-common/src/main/java/org/sonar/server/common/email/config/EmailConfigurationSecurityProtocol.java new file mode 100644 index 00000000000..7e8eaf57c9e --- /dev/null +++ b/server/sonar-webserver-common/src/main/java/org/sonar/server/common/email/config/EmailConfigurationSecurityProtocol.java @@ -0,0 +1,24 @@ +/* + * SonarQube + * Copyright (C) 2009-2024 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.common.email.config; + +public enum EmailConfigurationSecurityProtocol { + NONE, SSLTLS, STARTTLS +} diff --git a/server/sonar-webserver-common/src/main/java/org/sonar/server/common/email/config/EmailConfigurationService.java b/server/sonar-webserver-common/src/main/java/org/sonar/server/common/email/config/EmailConfigurationService.java new file mode 100644 index 00000000000..fdb90787dc7 --- /dev/null +++ b/server/sonar-webserver-common/src/main/java/org/sonar/server/common/email/config/EmailConfigurationService.java @@ -0,0 +1,284 @@ +/* + * SonarQube + * Copyright (C) 2009-2024 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.common.email.config; + +import java.util.List; +import java.util.Optional; +import javax.annotation.Nullable; +import org.apache.commons.lang.StringUtils; +import org.sonar.api.server.ServerSide; +import org.sonar.db.DbClient; +import org.sonar.db.DbSession; +import org.sonar.server.common.NonNullUpdatedValue; +import org.sonar.server.common.UpdatedValue; +import org.sonar.server.exceptions.BadRequestException; +import org.sonar.server.exceptions.NotFoundException; + +import static java.lang.String.format; +import static org.sonar.server.email.EmailSmtpConfiguration.EMAIL_CONFIG_FROM; +import static org.sonar.server.email.EmailSmtpConfiguration.EMAIL_CONFIG_FROM_NAME; +import static org.sonar.server.email.EmailSmtpConfiguration.EMAIL_CONFIG_PREFIX; +import static org.sonar.server.email.EmailSmtpConfiguration.EMAIL_CONFIG_SMTP_AUTH_METHOD; +import static org.sonar.server.email.EmailSmtpConfiguration.EMAIL_CONFIG_SMTP_HOST; +import static org.sonar.server.email.EmailSmtpConfiguration.EMAIL_CONFIG_SMTP_OAUTH_CLIENTID; +import static org.sonar.server.email.EmailSmtpConfiguration.EMAIL_CONFIG_SMTP_OAUTH_CLIENTSECRET; +import static org.sonar.server.email.EmailSmtpConfiguration.EMAIL_CONFIG_SMTP_OAUTH_GRANT; +import static org.sonar.server.email.EmailSmtpConfiguration.EMAIL_CONFIG_SMTP_OAUTH_GRANT_DEFAULT; +import static org.sonar.server.email.EmailSmtpConfiguration.EMAIL_CONFIG_SMTP_OAUTH_HOST; +import static org.sonar.server.email.EmailSmtpConfiguration.EMAIL_CONFIG_SMTP_OAUTH_SCOPE; +import static org.sonar.server.email.EmailSmtpConfiguration.EMAIL_CONFIG_SMTP_OAUTH_SCOPE_DEFAULT; +import static org.sonar.server.email.EmailSmtpConfiguration.EMAIL_CONFIG_SMTP_OAUTH_TENANT; +import static org.sonar.server.email.EmailSmtpConfiguration.EMAIL_CONFIG_SMTP_PASSWORD; +import static org.sonar.server.email.EmailSmtpConfiguration.EMAIL_CONFIG_SMTP_PORT; +import static org.sonar.server.email.EmailSmtpConfiguration.EMAIL_CONFIG_SMTP_SECURE_CONNECTION; +import static org.sonar.server.email.EmailSmtpConfiguration.EMAIL_CONFIG_SMTP_USERNAME; +import static org.sonarqube.ws.WsUtils.checkArgument; + +@ServerSide +public class EmailConfigurationService { + + private static final List<String> EMAIL_CONFIGURATION_PROPERTIES = List.of( + EMAIL_CONFIG_SMTP_HOST, + EMAIL_CONFIG_SMTP_PORT, + EMAIL_CONFIG_SMTP_SECURE_CONNECTION, + EMAIL_CONFIG_FROM, + EMAIL_CONFIG_FROM_NAME, + EMAIL_CONFIG_PREFIX, + EMAIL_CONFIG_SMTP_AUTH_METHOD, + EMAIL_CONFIG_SMTP_USERNAME, + EMAIL_CONFIG_SMTP_PASSWORD, + EMAIL_CONFIG_SMTP_OAUTH_HOST, + EMAIL_CONFIG_SMTP_OAUTH_CLIENTID, + EMAIL_CONFIG_SMTP_OAUTH_CLIENTSECRET, + EMAIL_CONFIG_SMTP_OAUTH_TENANT, + EMAIL_CONFIG_SMTP_OAUTH_SCOPE, + EMAIL_CONFIG_SMTP_OAUTH_GRANT + ); + + public static final String UNIQUE_EMAIL_CONFIGURATION_ID = "email-configuration"; + + private final DbClient dbClient; + + public EmailConfigurationService(DbClient dbClient) { + this.dbClient = dbClient; + } + + public EmailConfiguration createConfiguration(EmailConfiguration configuration) { + try (DbSession dbSession = dbClient.openSession(false)) { + throwIfConfigurationAlreadyExists(dbSession); + throwIfParamsConstraintsAreNotMetForCreation(configuration); + + setInternalProperty(dbSession, EMAIL_CONFIG_SMTP_HOST, configuration.host()); + setInternalProperty(dbSession, EMAIL_CONFIG_SMTP_PORT, configuration.port()); + setInternalProperty(dbSession, EMAIL_CONFIG_SMTP_SECURE_CONNECTION, configuration.securityProtocol().name()); + setInternalProperty(dbSession, EMAIL_CONFIG_FROM, configuration.fromAddress()); + setInternalProperty(dbSession, EMAIL_CONFIG_FROM_NAME, configuration.fromName()); + setInternalProperty(dbSession, EMAIL_CONFIG_PREFIX, configuration.subjectPrefix()); + setInternalProperty(dbSession, EMAIL_CONFIG_SMTP_AUTH_METHOD, configuration.authMethod().name()); + setInternalProperty(dbSession, EMAIL_CONFIG_SMTP_USERNAME, configuration.username()); + setInternalProperty(dbSession, EMAIL_CONFIG_SMTP_PASSWORD, configuration.basicPassword()); + setInternalProperty(dbSession, EMAIL_CONFIG_SMTP_OAUTH_HOST, configuration.oauthAuthenticationHost()); + setInternalProperty(dbSession, EMAIL_CONFIG_SMTP_OAUTH_CLIENTID, configuration.oauthClientId()); + setInternalProperty(dbSession, EMAIL_CONFIG_SMTP_OAUTH_CLIENTSECRET, configuration.oauthClientSecret()); + setInternalProperty(dbSession, EMAIL_CONFIG_SMTP_OAUTH_TENANT, configuration.oauthTenant()); + setInternalProperty(dbSession, EMAIL_CONFIG_SMTP_OAUTH_SCOPE, EMAIL_CONFIG_SMTP_OAUTH_SCOPE_DEFAULT); + setInternalProperty(dbSession, EMAIL_CONFIG_SMTP_OAUTH_GRANT, EMAIL_CONFIG_SMTP_OAUTH_GRANT_DEFAULT); + + EmailConfiguration createdConfiguration = getConfiguration(UNIQUE_EMAIL_CONFIGURATION_ID, dbSession); + dbSession.commit(); + return createdConfiguration; + } + } + + private static void throwIfParamsConstraintsAreNotMetForCreation(EmailConfiguration configuration) { + if (configuration.authMethod().equals(EmailConfigurationAuthMethod.OAUTH)) { + checkArgument(StringUtils.isNotEmpty(configuration.oauthAuthenticationHost()), "OAuth authentication host is required."); + checkArgument(StringUtils.isNotEmpty(configuration.oauthClientId()), "OAuth client id is required."); + checkArgument(StringUtils.isNotEmpty(configuration.oauthClientSecret()), "OAuth client secret is required."); + checkArgument(StringUtils.isNotEmpty(configuration.oauthTenant()), "OAuth tenant is required."); + } else if (configuration.authMethod().equals(EmailConfigurationAuthMethod.BASIC)) { + checkArgument(StringUtils.isNotEmpty(configuration.basicPassword()), "Password is required."); + } + } + + private void throwIfConfigurationAlreadyExists(DbSession dbSession) { + if (configurationExists(dbSession)) { + throw BadRequestException.create("Email configuration already exists. Only one Email configuration is supported."); + } + } + + public EmailConfiguration getConfiguration(String id) { + try (DbSession dbSession = dbClient.openSession(false)) { + throwIfNotUniqueConfigurationId(id); + throwIfConfigurationDoesntExist(dbSession); + return getConfiguration(id, dbSession); + } + } + + private EmailConfiguration getConfiguration(String id, DbSession dbSession) { + throwIfNotUniqueConfigurationId(id); + throwIfConfigurationDoesntExist(dbSession); + return new EmailConfiguration( + UNIQUE_EMAIL_CONFIGURATION_ID, + getStringInternalPropertyOrEmpty(dbSession, EMAIL_CONFIG_SMTP_HOST), + getStringInternalPropertyOrEmpty(dbSession, EMAIL_CONFIG_SMTP_PORT), + EmailConfigurationSecurityProtocol.valueOf(getStringInternalPropertyOrEmpty(dbSession, EMAIL_CONFIG_SMTP_SECURE_CONNECTION)), + getStringInternalPropertyOrEmpty(dbSession, EMAIL_CONFIG_FROM), + getStringInternalPropertyOrEmpty(dbSession, EMAIL_CONFIG_FROM_NAME), + getStringInternalPropertyOrEmpty(dbSession, EMAIL_CONFIG_PREFIX), + EmailConfigurationAuthMethod.valueOf(getStringInternalPropertyOrEmpty(dbSession, EMAIL_CONFIG_SMTP_AUTH_METHOD)), + getStringInternalPropertyOrEmpty(dbSession, EMAIL_CONFIG_SMTP_USERNAME), + getStringInternalPropertyOrEmpty(dbSession, EMAIL_CONFIG_SMTP_PASSWORD), + getStringInternalPropertyOrEmpty(dbSession, EMAIL_CONFIG_SMTP_OAUTH_HOST), + getStringInternalPropertyOrEmpty(dbSession, EMAIL_CONFIG_SMTP_OAUTH_CLIENTID), + getStringInternalPropertyOrEmpty(dbSession, EMAIL_CONFIG_SMTP_OAUTH_CLIENTSECRET), + getStringInternalPropertyOrEmpty(dbSession, EMAIL_CONFIG_SMTP_OAUTH_TENANT), + getStringInternalPropertyOrEmpty(dbSession, EMAIL_CONFIG_SMTP_OAUTH_SCOPE), + getStringInternalPropertyOrEmpty(dbSession, EMAIL_CONFIG_SMTP_OAUTH_GRANT)); + } + + private static void throwIfNotUniqueConfigurationId(String id) { + if (!UNIQUE_EMAIL_CONFIGURATION_ID.equals(id)) { + throw new NotFoundException(format("Email configuration with id %s not found", id)); + } + } + + private String getStringInternalPropertyOrEmpty(DbSession dbSession, String property) { + return dbClient.internalPropertiesDao().selectByKey(dbSession, property).orElse(""); + } + + public Optional<EmailConfiguration> findConfigurations() { + try (DbSession dbSession = dbClient.openSession(false)) { + if (configurationExists(dbSession)) { + return Optional.of(getConfiguration(UNIQUE_EMAIL_CONFIGURATION_ID, dbSession)); + } + return Optional.empty(); + } + } + + public EmailConfiguration updateConfiguration(UpdateEmailConfigurationRequest updateRequest) { + try (DbSession dbSession = dbClient.openSession(true)) { + throwIfConfigurationDoesntExist(dbSession); + EmailConfiguration existingConfig = getConfiguration(UNIQUE_EMAIL_CONFIGURATION_ID, dbSession); + throwIfUrlIsUpdatedWithoutCredentials(existingConfig, updateRequest); + throwIfParamsConstraintsAreNotMetForUpdate(existingConfig, updateRequest); + + setInternalIfDefined(dbSession, EMAIL_CONFIG_SMTP_HOST, updateRequest.host()); + setInternalIfDefined(dbSession, EMAIL_CONFIG_SMTP_PORT, updateRequest.port()); + setInternalIfDefined(dbSession, EMAIL_CONFIG_SMTP_SECURE_CONNECTION, updateRequest.securityProtocol().map(EmailConfigurationSecurityProtocol::name)); + setInternalIfDefined(dbSession, EMAIL_CONFIG_FROM, updateRequest.fromAddress()); + setInternalIfDefined(dbSession, EMAIL_CONFIG_FROM_NAME, updateRequest.fromName()); + setInternalIfDefined(dbSession, EMAIL_CONFIG_PREFIX, updateRequest.subjectPrefix()); + setInternalIfDefined(dbSession, EMAIL_CONFIG_SMTP_AUTH_METHOD, updateRequest.authMethod().map(EmailConfigurationAuthMethod::name)); + setInternalIfDefined(dbSession, EMAIL_CONFIG_SMTP_USERNAME, updateRequest.username()); + setInternalIfDefined(dbSession, EMAIL_CONFIG_SMTP_PASSWORD, updateRequest.basicPassword()); + setInternalIfDefined(dbSession, EMAIL_CONFIG_SMTP_OAUTH_HOST, updateRequest.oauthAuthenticationHost()); + setInternalIfDefined(dbSession, EMAIL_CONFIG_SMTP_OAUTH_CLIENTID, updateRequest.oauthClientId()); + setInternalIfDefined(dbSession, EMAIL_CONFIG_SMTP_OAUTH_CLIENTSECRET, updateRequest.oauthClientSecret()); + setInternalIfDefined(dbSession, EMAIL_CONFIG_SMTP_OAUTH_TENANT, updateRequest.oauthTenant()); + + dbSession.commit(); + + return getConfiguration(UNIQUE_EMAIL_CONFIGURATION_ID, dbSession); + } + } + + private static void throwIfUrlIsUpdatedWithoutCredentials(EmailConfiguration existingConfig, UpdateEmailConfigurationRequest request) { + if (isOauthDefinedByExistingConfigOrRequest(existingConfig, request)) { + // For OAuth config, we make sure that the client secret is provided when the host or authentication host is updated + if (isRequestParameterDefined(request.host()) || isRequestParameterDefined(request.oauthAuthenticationHost())) { + checkArgument(isRequestParameterDefined(request.oauthClientSecret()), "For security reasons, OAuth urls can't be updated without providing the client secret."); + } + } else { + // For Basic config, we make sure that the password is provided when the host is updated + if (isRequestParameterDefined(request.host())) { + checkArgument(isRequestParameterDefined(request.basicPassword()), "For security reasons, the host can't be updated without providing the password."); + } + } + } + + private static void throwIfParamsConstraintsAreNotMetForUpdate(EmailConfiguration existingConfig, UpdateEmailConfigurationRequest updateRequest) { + checkArgument(isFieldDefinedByExistingConfigOrRequest(existingConfig.username(), updateRequest.username()), + "Username is required."); + if (isOauthDefinedByExistingConfigOrRequest(existingConfig, updateRequest)) { + checkArgument(isFieldDefinedByExistingConfigOrRequest(existingConfig.oauthAuthenticationHost(), updateRequest.oauthAuthenticationHost()), + "OAuth authentication host is required."); + checkArgument(isFieldDefinedByExistingConfigOrRequest(existingConfig.oauthClientId(), updateRequest.oauthClientId()), + "OAuth client id is required."); + checkArgument(isFieldDefinedByExistingConfigOrRequest(existingConfig.oauthClientSecret(), updateRequest.oauthClientSecret()), + "OAuth client secret is required."); + checkArgument(isFieldDefinedByExistingConfigOrRequest(existingConfig.oauthTenant(), updateRequest.oauthTenant()), + "OAuth tenant is required."); + } else { + checkArgument(isFieldDefinedByExistingConfigOrRequest(existingConfig.basicPassword(), updateRequest.basicPassword()), + "Password is required."); + } + } + + private static boolean isFieldDefinedByExistingConfigOrRequest(@Nullable String existingParam, NonNullUpdatedValue<String> requestParam) { + return StringUtils.isNotEmpty(existingParam) || (requestParam.isDefined() && !requestParam.contains("")); + } + + private static boolean isOauthDefinedByExistingConfigOrRequest(EmailConfiguration existingConfig, UpdateEmailConfigurationRequest request) { + // Either the request update the config to OAuth, or the existing config is OAuth + if (isRequestParameterDefined(request.authMethod())) { + return request.authMethod().contains(EmailConfigurationAuthMethod.OAUTH); + } + return existingConfig.authMethod().equals(EmailConfigurationAuthMethod.OAUTH); + } + + private static boolean isRequestParameterDefined(@Nullable NonNullUpdatedValue<?> parameter) { + return parameter != null && parameter.isDefined(); + } + + public void deleteConfiguration(String id) { + throwIfNotUniqueConfigurationId(id); + try (DbSession dbSession = dbClient.openSession(false)) { + throwIfConfigurationDoesntExist(dbSession); + EMAIL_CONFIGURATION_PROPERTIES.forEach(propertyKey -> dbClient.internalPropertiesDao().delete(dbSession, propertyKey)); + dbSession.commit(); + } + } + + private void throwIfConfigurationDoesntExist(DbSession dbSession) { + if (!configurationExists(dbSession)) { + throw new NotFoundException("Email configuration doesn't exist."); + } + } + + private boolean configurationExists(DbSession dbSession) { + String property = getStringInternalPropertyOrEmpty(dbSession, EMAIL_CONFIG_SMTP_HOST); + return StringUtils.isNotEmpty(property); + } + + private void setInternalIfDefined(DbSession dbSession, String propertyKey, @Nullable UpdatedValue<String> value) { + if (value != null) { + value.applyIfDefined(propertyValue -> setInternalProperty(dbSession, propertyKey, propertyValue)); + } + } + + private void setInternalProperty(DbSession dbSession, String propertyKey, @Nullable String value) { + if (StringUtils.isNotEmpty(value)) { + dbClient.internalPropertiesDao().save(dbSession, propertyKey, value); + } else { + dbClient.internalPropertiesDao().delete(dbSession, propertyKey); + } + } +} diff --git a/server/sonar-webserver-common/src/main/java/org/sonar/server/common/email/config/UpdateEmailConfigurationRequest.java b/server/sonar-webserver-common/src/main/java/org/sonar/server/common/email/config/UpdateEmailConfigurationRequest.java new file mode 100644 index 00000000000..3f709386ccb --- /dev/null +++ b/server/sonar-webserver-common/src/main/java/org/sonar/server/common/email/config/UpdateEmailConfigurationRequest.java @@ -0,0 +1,139 @@ +/* + * SonarQube + * Copyright (C) 2009-2024 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.common.email.config; + +import org.sonar.server.common.NonNullUpdatedValue; + +public record UpdateEmailConfigurationRequest( + String emailConfigurationId, + NonNullUpdatedValue<String> host, + NonNullUpdatedValue<String> port, + NonNullUpdatedValue<EmailConfigurationSecurityProtocol> securityProtocol, + NonNullUpdatedValue<String> fromAddress, + NonNullUpdatedValue<String> fromName, + NonNullUpdatedValue<String> subjectPrefix, + NonNullUpdatedValue<EmailConfigurationAuthMethod> authMethod, + NonNullUpdatedValue<String> username, + NonNullUpdatedValue<String> basicPassword, + NonNullUpdatedValue<String> oauthAuthenticationHost, + NonNullUpdatedValue<String> oauthClientId, + NonNullUpdatedValue<String> oauthClientSecret, + NonNullUpdatedValue<String> oauthTenant +) { + + public static Builder builder() { + return new Builder(); + } + + public static final class Builder { + private String emailConfigurationId; + private NonNullUpdatedValue<String> host; + private NonNullUpdatedValue<String> port; + private NonNullUpdatedValue<EmailConfigurationSecurityProtocol> securityProtocol; + private NonNullUpdatedValue<String> fromAddress; + private NonNullUpdatedValue<String> fromName; + private NonNullUpdatedValue<String> subjectPrefix; + private NonNullUpdatedValue<EmailConfigurationAuthMethod> authMethod; + private NonNullUpdatedValue<String> username; + private NonNullUpdatedValue<String> basicPassword; + private NonNullUpdatedValue<String> oauthAuthenticationHost; + private NonNullUpdatedValue<String> oauthClientId; + private NonNullUpdatedValue<String> oauthClientSecret; + private NonNullUpdatedValue<String> oauthTenant; + + private Builder() { + } + + public Builder emailConfigurationId(String emailConfigurationId) { + this.emailConfigurationId = emailConfigurationId; + return this; + } + + public Builder host(NonNullUpdatedValue<String> host) { + this.host = host; + return this; + } + + public Builder port(NonNullUpdatedValue<String> port) { + this.port = port; + return this; + } + + public Builder securityProtocol(NonNullUpdatedValue<EmailConfigurationSecurityProtocol> securityProtocol) { + this.securityProtocol = securityProtocol; + return this; + } + + public Builder fromAddress(NonNullUpdatedValue<String> fromAddress) { + this.fromAddress = fromAddress; + return this; + } + + public Builder fromName(NonNullUpdatedValue<String> fromName) { + this.fromName = fromName; + return this; + } + + public Builder subjectPrefix(NonNullUpdatedValue<String> subjectPrefix) { + this.subjectPrefix = subjectPrefix; + return this; + } + + public Builder authMethod(NonNullUpdatedValue<EmailConfigurationAuthMethod> authMethod) { + this.authMethod = authMethod; + return this; + } + + public Builder username(NonNullUpdatedValue<String> username) { + this.username = username; + return this; + } + + public Builder basicPassword(NonNullUpdatedValue<String> basicPassword) { + this.basicPassword = basicPassword; + return this; + } + + public Builder oauthAuthenticationHost(NonNullUpdatedValue<String> oauthAuthenticationHost) { + this.oauthAuthenticationHost = oauthAuthenticationHost; + return this; + } + + public Builder oauthClientId(NonNullUpdatedValue<String> oauthClientId) { + this.oauthClientId = oauthClientId; + return this; + } + + public Builder oauthClientSecret(NonNullUpdatedValue<String> oauthClientSecret) { + this.oauthClientSecret = oauthClientSecret; + return this; + } + + public Builder oauthTenant(NonNullUpdatedValue<String> oauthTenant) { + this.oauthTenant = oauthTenant; + return this; + } + + public UpdateEmailConfigurationRequest build() { + return new UpdateEmailConfigurationRequest(emailConfigurationId, host, port, securityProtocol, fromAddress, fromName, subjectPrefix, authMethod, username, + basicPassword, oauthAuthenticationHost, oauthClientId, oauthClientSecret, oauthTenant); + } + } +} diff --git a/server/sonar-webserver-common/src/main/java/org/sonar/server/common/email/config/package-info.java b/server/sonar-webserver-common/src/main/java/org/sonar/server/common/email/config/package-info.java new file mode 100644 index 00000000000..dbfa3d62f68 --- /dev/null +++ b/server/sonar-webserver-common/src/main/java/org/sonar/server/common/email/config/package-info.java @@ -0,0 +1,23 @@ +/* + * SonarQube + * Copyright (C) 2009-2024 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +@ParametersAreNonnullByDefault +package org.sonar.server.common.email.config; + +import javax.annotation.ParametersAreNonnullByDefault; |