aboutsummaryrefslogtreecommitdiffstats
path: root/server/sonar-webserver-common
diff options
context:
space:
mode:
Diffstat (limited to 'server/sonar-webserver-common')
-rw-r--r--server/sonar-webserver-common/src/it/java/org/sonar/server/common/email/config/EmailConfigurationBuilder.java119
-rw-r--r--server/sonar-webserver-common/src/it/java/org/sonar/server/common/email/config/EmailConfigurationServiceIT.java477
-rw-r--r--server/sonar-webserver-common/src/main/java/org/sonar/server/common/email/config/EmailConfiguration.java52
-rw-r--r--server/sonar-webserver-common/src/main/java/org/sonar/server/common/email/config/EmailConfigurationAuthMethod.java24
-rw-r--r--server/sonar-webserver-common/src/main/java/org/sonar/server/common/email/config/EmailConfigurationSecurityProtocol.java24
-rw-r--r--server/sonar-webserver-common/src/main/java/org/sonar/server/common/email/config/EmailConfigurationService.java284
-rw-r--r--server/sonar-webserver-common/src/main/java/org/sonar/server/common/email/config/UpdateEmailConfigurationRequest.java139
-rw-r--r--server/sonar-webserver-common/src/main/java/org/sonar/server/common/email/config/package-info.java23
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;