diff options
author | Antoine Vigneau <antoine.vigneau@sonarsource.com> | 2024-08-06 13:45:14 +0200 |
---|---|---|
committer | sonartech <sonartech@sonarsource.com> | 2024-08-16 20:02:58 +0000 |
commit | 461a47d22b55a5450062883a6c1bd22e0a4ec3dd (patch) | |
tree | 7107a6598c46d4f3899f9659a14232f4d4b552b8 | |
parent | 808cba5b51d54a61ef2a01e6e78e3874ef53eea5 (diff) | |
download | sonarqube-461a47d22b55a5450062883a6c1bd22e0a4ec3dd.tar.gz sonarqube-461a47d22b55a5450062883a6c1bd22e0a4ec3dd.zip |
SONAR-22516 API v2 endpoints for managing the email configuration
32 files changed, 2891 insertions, 23 deletions
diff --git a/server/sonar-server-common/src/main/java/org/sonar/server/email/EmailSmtpConfiguration.java b/server/sonar-server-common/src/main/java/org/sonar/server/email/EmailSmtpConfiguration.java index b7873fab943..f8ab524b7e2 100644 --- a/server/sonar-server-common/src/main/java/org/sonar/server/email/EmailSmtpConfiguration.java +++ b/server/sonar-server-common/src/main/java/org/sonar/server/email/EmailSmtpConfiguration.java @@ -34,8 +34,6 @@ public class EmailSmtpConfiguration { public static final String EMAIL_CONFIG_SMTP_PORT_DEFAULT = "25"; public static final String EMAIL_CONFIG_SMTP_SECURE_CONNECTION = "email.smtp_secure_connection.secured"; public static final String EMAIL_CONFIG_SMTP_SECURE_CONNECTION_DEFAULT = ""; - public static final String EMAIL_CONFIG_SMTP_AUTH_METHOD= "email.smtp.auth.method"; - public static final String EMAIL_CONFIG_SMTP_AUTH_METHOD_DEFAULT = "BASIC"; // Email content public static final String EMAIL_CONFIG_FROM = "email.from"; public static final String EMAIL_CONFIG_FROM_DEFAULT = "noreply@nowhere"; @@ -43,19 +41,24 @@ public class EmailSmtpConfiguration { public static final String EMAIL_CONFIG_FROM_NAME_DEFAULT = "SonarQube"; public static final String EMAIL_CONFIG_PREFIX = "email.prefix"; public static final String EMAIL_CONFIG_PREFIX_DEFAULT = "[SONARQUBE]"; + // Auth selection + public static final String EMAIL_CONFIG_SMTP_AUTH_METHOD= "email.smtp.auth.method"; + public static final String EMAIL_CONFIG_SMTP_AUTH_METHOD_DEFAULT = "BASIC"; // Basic Auth public static final String EMAIL_CONFIG_SMTP_USERNAME = "email.smtp_username.secured"; public static final String EMAIL_CONFIG_SMTP_USERNAME_DEFAULT = ""; public static final String EMAIL_CONFIG_SMTP_PASSWORD = "email.smtp_password.secured"; public static final String EMAIL_CONFIG_SMTP_PASSWORD_DEFAULT = ""; - // Modern auth + // OAuth public static final String EMAIL_CONFIG_SMTP_OAUTH_HOST = "email.smtp.oauth.host"; public static final String EMAIL_CONFIG_SMTP_OAUTH_HOST_DEFAULT = "https://login.microsoftonline.com"; - public static final String EMAIL_CONFIG_SMTP_OAUTH_TENANT = "email.smtp.oauth.tenant"; public static final String EMAIL_CONFIG_SMTP_OAUTH_CLIENTID = "email.smtp.oauth.clientId"; public static final String EMAIL_CONFIG_SMTP_OAUTH_CLIENTSECRET = "email.smtp.oauth.clientSecret"; + public static final String EMAIL_CONFIG_SMTP_OAUTH_TENANT = "email.smtp.oauth.tenant"; public static final String EMAIL_CONFIG_SMTP_OAUTH_SCOPE = "email.smtp.oauth.scope"; - public static final String EMAIL_CONFIG_SMTP_OAUTH_SCOPE_DEFAULT = "client_credentials"; + public static final String EMAIL_CONFIG_SMTP_OAUTH_SCOPE_DEFAULT = "https://outlook.office365.com/.default"; + public static final String EMAIL_CONFIG_SMTP_OAUTH_GRANT = "email.smtp.oauth.grant"; + public static final String EMAIL_CONFIG_SMTP_OAUTH_GRANT_DEFAULT = "client_credentials"; private final DbClient dbClient; @@ -119,6 +122,10 @@ public class EmailSmtpConfiguration { return get(EMAIL_CONFIG_SMTP_OAUTH_SCOPE, EMAIL_CONFIG_SMTP_OAUTH_SCOPE_DEFAULT); } + public String getOAuthGrant() { + return get(EMAIL_CONFIG_SMTP_OAUTH_GRANT, EMAIL_CONFIG_SMTP_OAUTH_GRANT_DEFAULT); + } + private String get(String key, String defaultValue) { try (DbSession dbSession = dbClient.openSession(false)) { return dbClient.internalPropertiesDao().selectByKey(dbSession, key).orElse(defaultValue); diff --git a/server/sonar-server-common/src/main/java/org/sonar/server/notification/email/telemetry/package-info.java b/server/sonar-server-common/src/main/java/org/sonar/server/notification/email/telemetry/package-info.java new file mode 100644 index 00000000000..0722ecbfca8 --- /dev/null +++ b/server/sonar-server-common/src/main/java/org/sonar/server/notification/email/telemetry/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.notification.email.telemetry; + +import javax.annotation.ParametersAreNonnullByDefault; 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; diff --git a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/WebApiEndpoints.java b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/WebApiEndpoints.java index 0bd6af1386b..a705491096d 100644 --- a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/WebApiEndpoints.java +++ b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/WebApiEndpoints.java @@ -20,39 +20,36 @@ package org.sonar.server.v2; public class WebApiEndpoints { - private static final String SYSTEM_ENDPOINTS = "/system"; - public static final String LIVENESS_ENDPOINT = SYSTEM_ENDPOINTS + "/liveness"; - public static final String HEALTH_ENDPOINT = SYSTEM_ENDPOINTS + "/health"; - public static final String DATABASE_MIGRATIONS_ENDPOINT = SYSTEM_ENDPOINTS + "/migrations-status"; + public static final String JSON_MERGE_PATCH_CONTENT_TYPE = "application/merge-patch+json"; + public static final String INTERNAL = "internal"; + + public static final String SYSTEM_DOMAIN = "/system"; + public static final String LIVENESS_ENDPOINT = SYSTEM_DOMAIN + "/liveness"; + public static final String HEALTH_ENDPOINT = SYSTEM_DOMAIN + "/health"; + public static final String DATABASE_MIGRATIONS_ENDPOINT = SYSTEM_DOMAIN + "/migrations-status"; + public static final String EMAIL_CONFIGURATION_ENDPOINT = SYSTEM_DOMAIN + "/email-configurations"; public static final String USERS_MANAGEMENT_DOMAIN = "/users-management"; public static final String USER_ENDPOINT = USERS_MANAGEMENT_DOMAIN + "/users"; - public static final String JSON_MERGE_PATCH_CONTENT_TYPE = "application/merge-patch+json"; - public static final String AUTHORIZATIONS_DOMAIN = "/authorizations"; + public static final String AUTHORIZATIONS_DOMAIN = "/authorizations"; public static final String GROUPS_ENDPOINT = AUTHORIZATIONS_DOMAIN + "/groups"; public static final String GROUP_MEMBERSHIPS_ENDPOINT = AUTHORIZATIONS_DOMAIN + "/group-memberships"; - public static final String DOP_TRANSLATION_DOMAIN = "/dop-translation"; public static final String CLEAN_CODE_POLICY_DOMAIN = "/clean-code-policy"; public static final String RULES_ENDPOINT = CLEAN_CODE_POLICY_DOMAIN + "/rules"; + public static final String DOP_TRANSLATION_DOMAIN = "/dop-translation"; public static final String GITLAB_CONFIGURATION_ENDPOINT = DOP_TRANSLATION_DOMAIN + "/gitlab-configurations"; - public static final String GITHUB_CONFIGURATION_ENDPOINT = DOP_TRANSLATION_DOMAIN + "/github-configurations"; - public static final String BOUND_PROJECTS_ENDPOINT = DOP_TRANSLATION_DOMAIN + "/bound-projects"; - public static final String PROJECT_BINDINGS_ENDPOINT = DOP_TRANSLATION_DOMAIN + "/project-bindings"; - public static final String DOP_SETTINGS_ENDPOINT = DOP_TRANSLATION_DOMAIN + "/dop-settings"; - public static final String INTERNAL = "internal"; - - public static final String ANALYSIS_ENDPOINT = "/analysis"; - public static final String VERSION_ENDPOINT = ANALYSIS_ENDPOINT + "/version"; - public static final String JRE_ENDPOINT = ANALYSIS_ENDPOINT + "/jres"; - public static final String SCANNER_ENGINE_ENDPOINT = ANALYSIS_ENDPOINT + "/engine"; + public static final String ANALYSIS_DOMAIN = "/analysis"; + public static final String VERSION_ENDPOINT = ANALYSIS_DOMAIN + "/version"; + public static final String JRE_ENDPOINT = ANALYSIS_DOMAIN + "/jres"; + public static final String SCANNER_ENGINE_ENDPOINT = ANALYSIS_DOMAIN + "/engine"; private WebApiEndpoints() { } diff --git a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/email/config/controller/DefaultEmailConfigurationController.java b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/email/config/controller/DefaultEmailConfigurationController.java new file mode 100644 index 00000000000..debfde60dd6 --- /dev/null +++ b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/email/config/controller/DefaultEmailConfigurationController.java @@ -0,0 +1,164 @@ +/* + * 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.v2.api.email.config.controller; + +import java.util.List; +import org.apache.commons.lang.StringUtils; +import org.sonar.server.common.email.config.EmailConfiguration; +import org.sonar.server.common.email.config.EmailConfigurationAuthMethod; +import org.sonar.server.common.email.config.EmailConfigurationSecurityProtocol; +import org.sonar.server.common.email.config.EmailConfigurationService; +import org.sonar.server.common.email.config.UpdateEmailConfigurationRequest; +import org.sonar.server.user.UserSession; +import org.sonar.server.v2.api.email.config.request.EmailConfigurationCreateRestRequest; +import org.sonar.server.v2.api.email.config.request.EmailConfigurationUpdateRestRequest; +import org.sonar.server.v2.api.email.config.resource.EmailConfigurationResource; +import org.sonar.server.v2.api.email.config.response.EmailConfigurationSearchRestResponse; +import org.sonar.server.v2.api.response.PageRestResponse; + +import static org.sonar.server.common.email.config.EmailConfigurationService.UNIQUE_EMAIL_CONFIGURATION_ID; + +public class DefaultEmailConfigurationController implements EmailConfigurationController { + + private final UserSession userSession; + private final EmailConfigurationService emailConfigurationService; + + public DefaultEmailConfigurationController(UserSession userSession, EmailConfigurationService emailConfigurationService) { + this.userSession = userSession; + this.emailConfigurationService = emailConfigurationService; + } + + @Override + public EmailConfigurationResource createEmailConfiguration(EmailConfigurationCreateRestRequest createRequest) { + userSession.checkIsSystemAdministrator(); + EmailConfiguration createdConfiguration = emailConfigurationService.createConfiguration(toEmailConfiguration(createRequest)); + return toEmailConfigurationResource(createdConfiguration); + } + + private static EmailConfiguration toEmailConfiguration(EmailConfigurationCreateRestRequest createRestRequest) { + return new EmailConfiguration( + UNIQUE_EMAIL_CONFIGURATION_ID, + createRestRequest.host(), + createRestRequest.port(), + toSecurityProtocol(createRestRequest.securityProtocol()), + createRestRequest.fromAddress(), + createRestRequest.fromName(), + createRestRequest.subjectPrefix(), + toAuthMethod(createRestRequest.authMethod()), + createRestRequest.username(), + createRestRequest.basicPassword(), + createRestRequest.oauthAuthenticationHost(), + createRestRequest.oauthClientId(), + createRestRequest.oauthClientSecret(), + createRestRequest.oauthTenant() + ); + } + + @Override + public EmailConfigurationResource getEmailConfiguration(String id) { + userSession.checkIsSystemAdministrator(); + return getEmailConfigurationResource(id); + } + + private EmailConfigurationResource getEmailConfigurationResource(String id) { + return toEmailConfigurationResource(emailConfigurationService.getConfiguration(id)); + } + + @Override + public EmailConfigurationSearchRestResponse searchEmailConfigurations() { + userSession.checkIsSystemAdministrator(); + + List<EmailConfigurationResource> emailConfigurationResources = emailConfigurationService.findConfigurations() + .stream() + .map(DefaultEmailConfigurationController::toEmailConfigurationResource) + .toList(); + + PageRestResponse pageRestResponse = new PageRestResponse(1, 1000, emailConfigurationResources.size()); + return new EmailConfigurationSearchRestResponse(emailConfigurationResources, pageRestResponse); + } + + @Override + public EmailConfigurationResource updateEmailConfiguration(String id, EmailConfigurationUpdateRestRequest updateRequest) { + userSession.checkIsSystemAdministrator(); + + UpdateEmailConfigurationRequest updateEmailConfigurationRequest = toUpdateEmailConfigurationRequest(id, updateRequest); + return toEmailConfigurationResource(emailConfigurationService.updateConfiguration(updateEmailConfigurationRequest)); + } + + private static UpdateEmailConfigurationRequest toUpdateEmailConfigurationRequest(String id, EmailConfigurationUpdateRestRequest updateRequest) { + return UpdateEmailConfigurationRequest.builder() + .emailConfigurationId(id) + .host(updateRequest.getHost().toNonNullUpdatedValue()) + .port(updateRequest.getPort().toNonNullUpdatedValue()) + .securityProtocol(updateRequest.getSecurityProtocol().map(DefaultEmailConfigurationController::toSecurityProtocol).toNonNullUpdatedValue()) + .fromAddress(updateRequest.getFromAddress().toNonNullUpdatedValue()) + .fromName(updateRequest.getFromName().toNonNullUpdatedValue()) + .subjectPrefix(updateRequest.getSubjectPrefix().toNonNullUpdatedValue()) + .authMethod(updateRequest.getAuthMethod().map(DefaultEmailConfigurationController::toAuthMethod).toNonNullUpdatedValue()) + .username(updateRequest.getUsername().toNonNullUpdatedValue()) + .basicPassword(updateRequest.getBasicPassword().toNonNullUpdatedValue()) + .oauthAuthenticationHost(updateRequest.getOauthAuthenticationHost().toNonNullUpdatedValue()) + .oauthClientId(updateRequest.getOauthClientId().toNonNullUpdatedValue()) + .oauthClientSecret(updateRequest.getOauthClientSecret().toNonNullUpdatedValue()) + .oauthTenant(updateRequest.getOauthTenant().toNonNullUpdatedValue()) + .build(); + } + + private static EmailConfigurationResource toEmailConfigurationResource(EmailConfiguration configuration) { + return new EmailConfigurationResource( + configuration.id(), + configuration.host(), + configuration.port(), + toRestSecurityProtocol(configuration.securityProtocol()), + configuration.fromAddress(), + configuration.fromName(), + configuration.subjectPrefix(), + toRestAuthMethod(configuration.authMethod()), + configuration.username(), + StringUtils.isNotEmpty(configuration.basicPassword()), + configuration.oauthAuthenticationHost(), + StringUtils.isNotEmpty(configuration.oauthClientId()), + StringUtils.isNotEmpty(configuration.oauthClientSecret()), + configuration.oauthTenant() + ); + } + + @Override + public void deleteEmailConfiguration(String id) { + userSession.checkIsSystemAdministrator(); + emailConfigurationService.deleteConfiguration(id); + } + + private static EmailConfigurationSecurityProtocol toSecurityProtocol(org.sonar.server.v2.api.email.config.resource.EmailConfigurationSecurityProtocol restSecurityProtocol) { + return EmailConfigurationSecurityProtocol.valueOf(restSecurityProtocol.name()); + } + + private static EmailConfigurationAuthMethod toAuthMethod(org.sonar.server.v2.api.email.config.resource.EmailConfigurationAuthMethod restAuthMethod) { + return EmailConfigurationAuthMethod.valueOf(restAuthMethod.name()); + } + + private static org.sonar.server.v2.api.email.config.resource.EmailConfigurationSecurityProtocol toRestSecurityProtocol(EmailConfigurationSecurityProtocol securityProtocol) { + return org.sonar.server.v2.api.email.config.resource.EmailConfigurationSecurityProtocol.valueOf(securityProtocol.name()); + } + + private static org.sonar.server.v2.api.email.config.resource.EmailConfigurationAuthMethod toRestAuthMethod(EmailConfigurationAuthMethod authMethod) { + return org.sonar.server.v2.api.email.config.resource.EmailConfigurationAuthMethod.valueOf(authMethod.name()); + } +} diff --git a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/email/config/controller/EmailConfigurationController.java b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/email/config/controller/EmailConfigurationController.java new file mode 100644 index 00000000000..01eef1272c1 --- /dev/null +++ b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/email/config/controller/EmailConfigurationController.java @@ -0,0 +1,97 @@ +/* + * 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.v2.api.email.config.controller; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.enums.ParameterIn; +import io.swagger.v3.oas.annotations.extensions.Extension; +import io.swagger.v3.oas.annotations.extensions.ExtensionProperty; +import javax.validation.Valid; +import org.sonar.server.v2.api.email.config.request.EmailConfigurationCreateRestRequest; +import org.sonar.server.v2.api.email.config.request.EmailConfigurationUpdateRestRequest; +import org.sonar.server.v2.api.email.config.resource.EmailConfigurationResource; +import org.sonar.server.v2.api.email.config.response.EmailConfigurationSearchRestResponse; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PatchMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestController; + +import static org.sonar.server.v2.WebApiEndpoints.EMAIL_CONFIGURATION_ENDPOINT; +import static org.sonar.server.v2.WebApiEndpoints.INTERNAL; +import static org.sonar.server.v2.WebApiEndpoints.JSON_MERGE_PATCH_CONTENT_TYPE; + +@RequestMapping(EMAIL_CONFIGURATION_ENDPOINT) +@RestController +public interface EmailConfigurationController { + + @PostMapping + @Operation(summary = "Create an email configuration", description = """ + Create a new email configuration. + Note that only a single configuration can exist at a time. + Requires 'Administer System' permission. + """, + extensions = @Extension(properties = {@ExtensionProperty(name = INTERNAL, value = "true")})) + EmailConfigurationResource createEmailConfiguration(@Valid @RequestBody EmailConfigurationCreateRestRequest createRequest); + + @GetMapping(path = "/{id}") + @ResponseStatus(HttpStatus.OK) + @Operation(summary = "Fetch an email configuration", description = """ + Fetch a Email configuration. Requires 'Administer System' permission. + """, + extensions = @Extension(properties = {@ExtensionProperty(name = INTERNAL, value = "true")})) + EmailConfigurationResource getEmailConfiguration( + @PathVariable("id") @Parameter(description = "The id of the configuration to fetch.", required = true, in = ParameterIn.PATH) String id); + + @GetMapping + @Operation(summary = "Search email configurations", description = """ + Get the list of email configurations. + Note that a single configuration is supported at this time. + Requires 'Administer System' permission. + """, + extensions = @Extension(properties = {@ExtensionProperty(name = INTERNAL, value = "true")})) + EmailConfigurationSearchRestResponse searchEmailConfigurations(); + + @PatchMapping(path = "/{id}", consumes = JSON_MERGE_PATCH_CONTENT_TYPE, produces = MediaType.APPLICATION_JSON_VALUE) + @ResponseStatus(HttpStatus.OK) + @Operation(summary = "Update an email configuration", description = """ + Update an email configuration. Requires 'Administer System' permission. + """, + extensions = @Extension(properties = {@ExtensionProperty(name = INTERNAL, value = "true")})) + EmailConfigurationResource updateEmailConfiguration(@PathVariable("id") String id, @Valid @RequestBody EmailConfigurationUpdateRestRequest updateRequest); + + @DeleteMapping(path = "/{id}") + @ResponseStatus(HttpStatus.NO_CONTENT) + @Operation(summary = "Delete an email configuration", description = """ + Delete an email configuration. + Requires 'Administer System' permission. + """, + extensions = @Extension(properties = {@ExtensionProperty(name = INTERNAL, value = "true")})) + void deleteEmailConfiguration( + @PathVariable("id") @Parameter(description = "The id of the configuration to delete.", required = true, in = ParameterIn.PATH) String id); + +} diff --git a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/email/config/controller/package-info.java b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/email/config/controller/package-info.java new file mode 100644 index 00000000000..f1bf08237e6 --- /dev/null +++ b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/email/config/controller/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.v2.api.email.config.controller; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/email/config/request/EmailConfigurationCreateRestRequest.java b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/email/config/request/EmailConfigurationCreateRestRequest.java new file mode 100644 index 00000000000..0034d8feacb --- /dev/null +++ b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/email/config/request/EmailConfigurationCreateRestRequest.java @@ -0,0 +1,84 @@ +/* + * 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.v2.api.email.config.request; + +import io.swagger.v3.oas.annotations.media.Schema; +import javax.annotation.Nullable; +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; +import org.sonar.server.v2.api.email.config.resource.EmailConfigurationAuthMethod; +import org.sonar.server.v2.api.email.config.resource.EmailConfigurationSecurityProtocol; + +public record EmailConfigurationCreateRestRequest( + + @NotEmpty + @Schema(description = "URL of your SMTP server") + String host, + + @NotEmpty + @Schema(description = "Port of your SMTP server (usually 25, 587 or 465)") + String port, + + @NotNull + @Schema(description = "Security protocol used to connect to your SMTP server (SSLTLS is recommended)") + EmailConfigurationSecurityProtocol securityProtocol, + + @NotEmpty + @Schema(description = "Address emails will come from") + String fromAddress, + + @NotEmpty + @Schema(description = "Name emails will come from (usually \"SonarQube\")") + String fromName, + + @NotEmpty + @Schema(description = "Prefix added to email so they can be easily recognized (usually \"[SonarQube]\")") + String subjectPrefix, + + @NotNull + @Schema(description = "Authentication method used to connect to the SMTP server. OAuth is only supported for Microsoft Exchange") + EmailConfigurationAuthMethod authMethod, + + @NotEmpty + @Schema(description = "For Basic and OAuth authentication: username used to authenticate to the SMTP server") + String username, + + @Nullable + @Schema(accessMode = Schema.AccessMode.WRITE_ONLY, description = "For basic authentication: password used to authenticate to the SMTP server") + String basicPassword, + + @Nullable + @Schema(description = "For OAuth authentication: host of the Identity Provider issuing access tokens") + String oauthAuthenticationHost, + + @Nullable + @Schema(accessMode = Schema.AccessMode.WRITE_ONLY, description = "For OAuth authentication: Client ID provided by Microsoft Exchange when registering the application") + String oauthClientId, + + @Nullable + @Schema(accessMode = Schema.AccessMode.WRITE_ONLY, description = "For OAuth authentication: Client secret provided by Microsoft Exchange when registering the application") + String oauthClientSecret, + + @Nullable + @Schema(description = "For OAuth authentication: Microsoft tenant") + String oauthTenant + +) { +} diff --git a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/email/config/request/EmailConfigurationUpdateRestRequest.java b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/email/config/request/EmailConfigurationUpdateRestRequest.java new file mode 100644 index 00000000000..8527145a440 --- /dev/null +++ b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/email/config/request/EmailConfigurationUpdateRestRequest.java @@ -0,0 +1,162 @@ +/* + * 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.v2.api.email.config.request; + +import io.swagger.v3.oas.annotations.media.Schema; +import org.sonar.server.v2.api.email.config.resource.EmailConfigurationAuthMethod; +import org.sonar.server.v2.api.email.config.resource.EmailConfigurationSecurityProtocol; +import org.sonar.server.v2.common.model.NullOrNotEmpty; +import org.sonar.server.v2.common.model.UpdateField; + +public class EmailConfigurationUpdateRestRequest { + + private UpdateField<@NullOrNotEmpty String> host = UpdateField.undefined(); + private UpdateField<@NullOrNotEmpty String> port = UpdateField.undefined(); + private UpdateField<EmailConfigurationSecurityProtocol> securityProtocol = UpdateField.undefined(); + private UpdateField<@NullOrNotEmpty String> fromAddress = UpdateField.undefined(); + private UpdateField<@NullOrNotEmpty String> fromName = UpdateField.undefined(); + private UpdateField<@NullOrNotEmpty String> subjectPrefix = UpdateField.undefined(); + private UpdateField<EmailConfigurationAuthMethod> authMethod = UpdateField.undefined(); + private UpdateField<@NullOrNotEmpty String> username = UpdateField.undefined(); + private UpdateField<String> basicPassword = UpdateField.undefined(); + private UpdateField<String> oauthAuthenticationHost = UpdateField.undefined(); + private UpdateField<String> oauthClientId = UpdateField.undefined(); + private UpdateField<String> oauthClientSecret = UpdateField.undefined(); + private UpdateField<String> oauthTenant = UpdateField.undefined(); + + @Schema(implementation = String.class, description = "URL of your SMTP server") + public UpdateField<String> getHost() { + return host; + } + + public void setHost(String host) { + this.host = UpdateField.withValue(host); + } + + @Schema(implementation = String.class, description = "Port of your SMTP server (usually 25, 587 or 465)") + public UpdateField<String> getPort() { + return port; + } + + public void setPort(String port) { + this.port = UpdateField.withValue(port); + } + + @Schema(implementation = EmailConfigurationSecurityProtocol.class, description = "Security protocol used to connect to your SMTP server (SSLTLS is recommended)") + public UpdateField<EmailConfigurationSecurityProtocol> getSecurityProtocol() { + return securityProtocol; + } + + public void setSecurityProtocol(EmailConfigurationSecurityProtocol securityProtocol) { + this.securityProtocol = UpdateField.withValue(securityProtocol); + } + + @Schema(implementation = String.class, description = "Address emails will come from") + public UpdateField<String> getFromAddress() { + return fromAddress; + } + + public void setFromAddress(String fromAddress) { + this.fromAddress = UpdateField.withValue(fromAddress); + } + + @Schema(implementation = String.class, description = "Name emails will come from (usually \"SonarQube\")") + public UpdateField<String> getFromName() { + return fromName; + } + + public void setFromName(String fromName) { + this.fromName = UpdateField.withValue(fromName); + } + + @Schema(implementation = String.class, description = "Prefix added to email so they can be easily recognized (usually \"[SonarQube]\")") + public UpdateField<String> getSubjectPrefix() { + return subjectPrefix; + } + + public void setSubjectPrefix(String subjectPrefix) { + this.subjectPrefix = UpdateField.withValue(subjectPrefix); + } + + @Schema(implementation = EmailConfigurationAuthMethod.class, + description = "Authentication method used to connect to the SMTP server. OAuth is only supported for Microsoft Exchange") + public UpdateField<EmailConfigurationAuthMethod> getAuthMethod() { + return authMethod; + } + + public void setAuthMethod(EmailConfigurationAuthMethod authMethod) { + this.authMethod = UpdateField.withValue(authMethod); + } + + @Schema(implementation = String.class, description = "For Basic and OAuth authentication: username used to authenticate to the SMTP server") + public UpdateField<String> getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = UpdateField.withValue(username); + } + + @Schema(implementation = String.class, description = "For basic authentication: password used to authenticate to the SMTP server") + public UpdateField<String> getBasicPassword() { + return basicPassword; + } + + public void setBasicPassword(String basicPassword) { + this.basicPassword = UpdateField.withValue(basicPassword); + } + + @Schema(implementation = String.class, description = "For OAuth authentication: host of the Identity Provider issuing access tokens") + public UpdateField<String> getOauthAuthenticationHost() { + return oauthAuthenticationHost; + } + + public void setOauthAuthenticationHost(String oauthAuthenticationHost) { + this.oauthAuthenticationHost = UpdateField.withValue(oauthAuthenticationHost); + } + + @Schema(implementation = String.class, description = "For OAuth authentication: Client ID provided by Microsoft Exchange when registering the application") + public UpdateField<String> getOauthClientId() { + return oauthClientId; + } + + public void setOauthClientId(String oauthClientId) { + this.oauthClientId = UpdateField.withValue(oauthClientId); + } + + @Schema(implementation = String.class, description = "For OAuth authentication: Client password provided by Microsoft Exchange when registering the application") + public UpdateField<String> getOauthClientSecret() { + return oauthClientSecret; + } + + public void setOauthClientSecret(String oauthClientSecret) { + this.oauthClientSecret = UpdateField.withValue(oauthClientSecret); + } + + @Schema(implementation = String.class, description = "For OAuth authentication: Microsoft tenant") + public UpdateField<String> getOauthTenant() { + return oauthTenant; + } + + public void setOauthTenant(String oauthTenant) { + this.oauthTenant = UpdateField.withValue(oauthTenant); + } + +} diff --git a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/email/config/request/package-info.java b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/email/config/request/package-info.java new file mode 100644 index 00000000000..26b888985dd --- /dev/null +++ b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/email/config/request/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.v2.api.email.config.request; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/email/config/resource/EmailConfigurationAuthMethod.java b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/email/config/resource/EmailConfigurationAuthMethod.java new file mode 100644 index 00000000000..5d9e14c5ab7 --- /dev/null +++ b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/email/config/resource/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.v2.api.email.config.resource; + +public enum EmailConfigurationAuthMethod { + BASIC, OAUTH +} diff --git a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/email/config/resource/EmailConfigurationResource.java b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/email/config/resource/EmailConfigurationResource.java new file mode 100644 index 00000000000..ca2a6db0638 --- /dev/null +++ b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/email/config/resource/EmailConfigurationResource.java @@ -0,0 +1,74 @@ +/* + * 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.v2.api.email.config.resource; + +import io.swagger.v3.oas.annotations.media.Schema; +import javax.annotation.Nullable; + +public record EmailConfigurationResource( + + @Schema(accessMode = Schema.AccessMode.READ_ONLY) + String id, + + @Schema(description = "URL of your SMTP server") + String host, + + @Schema(description = "Port of your SMTP server (usually 25, 587 or 465)") + String port, + + @Schema(description = "Security protocol used to connect to your SMTP server (SSLTLS is recommended)") + EmailConfigurationSecurityProtocol securityProtocol, + + @Schema(description = "Address emails will come from") + String fromAddress, + + @Schema(description = "Name emails will come from (usually \"SonarQube\")") + String fromName, + + @Schema(description = "Prefix added to email so they can be easily recognized (usually \"[SonarQube]\")") + String subjectPrefix, + + @Schema(description = "Authentication method used to connect to the SMTP server. OAuth is only supported for Microsoft Exchange") + EmailConfigurationAuthMethod authMethod, + + @Nullable + @Schema(description = "For Basic and OAuth authentication: username used to authenticate to the SMTP server") + String username, + + @Schema(description = "For Basic authentication: has the password field been set?") + boolean isBasicPasswordSet, + + @Nullable + @Schema(description = "For OAuth authentication: host of the Identity Provider issuing access tokens") + String oauthAuthenticationHost, + + @Schema(description = "For OAuth authentication: has the Client ID field been set?") + boolean isOauthClientIdSet, + + @Schema(description = "For OAuth authentication: has the Client secret field been set?") + boolean isOauthClientSecretSet, + + @Nullable + @Schema(description = "For OAuth authentication: Microsoft tenant") + String oauthTenant + +) { +} + diff --git a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/email/config/resource/EmailConfigurationSecurityProtocol.java b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/email/config/resource/EmailConfigurationSecurityProtocol.java new file mode 100644 index 00000000000..3852ddef59a --- /dev/null +++ b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/email/config/resource/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.v2.api.email.config.resource; + +public enum EmailConfigurationSecurityProtocol { + NONE, SSLTLS, STARTTLS +} diff --git a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/email/config/resource/package-info.java b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/email/config/resource/package-info.java new file mode 100644 index 00000000000..6800ffa2b84 --- /dev/null +++ b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/email/config/resource/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.v2.api.email.config.resource; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/email/config/response/EmailConfigurationSearchRestResponse.java b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/email/config/response/EmailConfigurationSearchRestResponse.java new file mode 100644 index 00000000000..c17f4785f4f --- /dev/null +++ b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/email/config/response/EmailConfigurationSearchRestResponse.java @@ -0,0 +1,27 @@ +/* + * 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.v2.api.email.config.response; + +import java.util.List; +import org.sonar.server.v2.api.email.config.resource.EmailConfigurationResource; +import org.sonar.server.v2.api.response.PageRestResponse; + +public record EmailConfigurationSearchRestResponse(List<EmailConfigurationResource> emailConfigurations, PageRestResponse page) {} + diff --git a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/email/config/response/package-info.java b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/email/config/response/package-info.java new file mode 100644 index 00000000000..4aebb920e55 --- /dev/null +++ b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/email/config/response/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.v2.api.email.config.response; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/common/model/NullOrNotEmpty.java b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/common/model/NullOrNotEmpty.java new file mode 100644 index 00000000000..7f69d3cad44 --- /dev/null +++ b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/common/model/NullOrNotEmpty.java @@ -0,0 +1,44 @@ +/* + * 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.v2.common.model; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import javax.validation.Constraint; +import javax.validation.Payload; + +import static java.lang.annotation.ElementType.ANNOTATION_TYPE; +import static java.lang.annotation.ElementType.CONSTRUCTOR; +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.PARAMETER; +import static java.lang.annotation.ElementType.TYPE_USE; + +@Documented +@Constraint(validatedBy = NullOrNotEmptyValidator.class) +@Target( {METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE }) +@Retention(RetentionPolicy.RUNTIME) +public @interface NullOrNotEmpty { + String message() default "must not be empty"; + Class<?>[] groups() default {}; + Class<? extends Payload>[] payload() default {}; +} diff --git a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/common/model/NullOrNotEmptyValidator.java b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/common/model/NullOrNotEmptyValidator.java new file mode 100644 index 00000000000..b6a40c694d0 --- /dev/null +++ b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/common/model/NullOrNotEmptyValidator.java @@ -0,0 +1,31 @@ +/* + * 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.v2.common.model; + +import javax.annotation.Nullable; +import javax.validation.ConstraintValidator; +import javax.validation.ConstraintValidatorContext; + +public class NullOrNotEmptyValidator implements ConstraintValidator<NullOrNotEmpty, String> { + @Override + public boolean isValid(@Nullable String value, ConstraintValidatorContext context) { + return value == null || !value.isEmpty(); + } +} diff --git a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/config/PlatformLevel4WebConfig.java b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/config/PlatformLevel4WebConfig.java index 7394832467b..87ef131bb29 100644 --- a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/config/PlatformLevel4WebConfig.java +++ b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/config/PlatformLevel4WebConfig.java @@ -24,6 +24,7 @@ import org.sonar.api.platform.Server; import org.sonar.api.resources.Languages; import org.sonar.db.Database; import org.sonar.db.DbClient; +import org.sonar.server.common.email.config.EmailConfigurationService; import org.sonar.server.common.github.config.GithubConfigurationService; import org.sonar.server.common.gitlab.config.GitlabConfigurationService; import org.sonar.server.common.group.service.GroupMembershipService; @@ -60,6 +61,8 @@ import org.sonar.server.v2.api.analysis.service.ScannerEngineHandler; import org.sonar.server.v2.api.analysis.service.ScannerEngineHandlerImpl; import org.sonar.server.v2.api.dop.controller.DefaultDopSettingsController; import org.sonar.server.v2.api.dop.controller.DopSettingsController; +import org.sonar.server.v2.api.email.config.controller.DefaultEmailConfigurationController; +import org.sonar.server.v2.api.email.config.controller.EmailConfigurationController; import org.sonar.server.v2.api.github.config.controller.DefaultGithubConfigurationController; import org.sonar.server.v2.api.github.config.controller.GithubConfigurationController; import org.sonar.server.v2.api.gitlab.config.controller.DefaultGitlabConfigurationController; @@ -206,4 +209,9 @@ public class PlatformLevel4WebConfig { return new DefaultScannerEngineController(scannerEngineHandler); } + @Bean + public EmailConfigurationController emailConfigurationController(UserSession userSession, EmailConfigurationService emailConfigurationService) { + return new DefaultEmailConfigurationController(userSession, emailConfigurationService); + } + } diff --git a/server/sonar-webserver-webapi-v2/src/test/java/org/sonar/server/v2/api/email/config/DefaultEmailConfigurationControllerTest.java b/server/sonar-webserver-webapi-v2/src/test/java/org/sonar/server/v2/api/email/config/DefaultEmailConfigurationControllerTest.java new file mode 100644 index 00000000000..c2e0b811da6 --- /dev/null +++ b/server/sonar-webserver-webapi-v2/src/test/java/org/sonar/server/v2/api/email/config/DefaultEmailConfigurationControllerTest.java @@ -0,0 +1,597 @@ +/* + * 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.v2.api.email.config; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import java.util.Optional; +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.ValueSource; +import org.sonar.server.common.NonNullUpdatedValue; +import org.sonar.server.common.email.config.EmailConfiguration; +import org.sonar.server.common.email.config.EmailConfigurationAuthMethod; +import org.sonar.server.common.email.config.EmailConfigurationSecurityProtocol; +import org.sonar.server.common.email.config.EmailConfigurationService; +import org.sonar.server.common.email.config.UpdateEmailConfigurationRequest; +import org.sonar.server.exceptions.NotFoundException; +import org.sonar.server.tester.UserSessionRule; +import org.sonar.server.v2.api.ControllerTester; +import org.sonar.server.v2.api.email.config.controller.DefaultEmailConfigurationController; +import org.sonar.server.v2.api.email.config.resource.EmailConfigurationResource; +import org.sonar.server.v2.api.email.config.response.EmailConfigurationSearchRestResponse; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.MvcResult; + +import static java.lang.String.format; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.sonar.server.common.email.config.EmailConfigurationService.UNIQUE_EMAIL_CONFIGURATION_ID; +import static org.sonar.server.v2.WebApiEndpoints.EMAIL_CONFIGURATION_ENDPOINT; +import static org.sonar.server.v2.WebApiEndpoints.JSON_MERGE_PATCH_CONTENT_TYPE; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.patch; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +class DefaultEmailConfigurationControllerTest { + + private static final Gson GSON = new GsonBuilder().create(); + + private static final EmailConfiguration EMAIL_BASIC_CONFIGURATION = new EmailConfiguration( + UNIQUE_EMAIL_CONFIGURATION_ID, + "host", + "port", + EmailConfigurationSecurityProtocol.SSLTLS, + "fromAddress", + "fromName", + "subjectPrefix", + EmailConfigurationAuthMethod.BASIC, + "username", + "basicPassword", + null, + null, + null, + null + ); + + private static final EmailConfiguration EMAIL_OAUTH_CONFIGURATION = new EmailConfiguration( + UNIQUE_EMAIL_CONFIGURATION_ID, + "host", + "port", + EmailConfigurationSecurityProtocol.STARTTLS, + "fromAddress", + "fromName", + "subjectPrefix", + EmailConfigurationAuthMethod.OAUTH, + "username", + null, + "oauthAuthenticationHost", + "oauthClientId", + "oauthClientSecret", + "oauthTenant" + ); + + private static final String EXPECTED_BASIC_CONFIGURATION = """ + { + "host": "host", + "port": "port", + "securityProtocol": "SSLTLS", + "fromAddress": "fromAddress", + "fromName": "fromName", + "subjectPrefix": "subjectPrefix", + "authMethod": "BASIC", + "username": "username", + "isBasicPasswordSet": true + } + """; + + private static final String EXPECTED_OAUTH_CONFIGURATION = """ + { + "host": "host", + "port": "port", + "securityProtocol": "STARTTLS", + "fromAddress": "fromAddress", + "fromName": "fromName", + "subjectPrefix": "subjectPrefix", + "authMethod": "OAUTH", + "username": "username", + "isBasicPasswordSet": false, + "oauthAuthenticationHost": "oauthAuthenticationHost", + "isOauthClientIdSet": true, + "isOauthClientSecretSet": true, + "oauthTenant": "oauthTenant" + } + """; + + @RegisterExtension + public UserSessionRule userSession = UserSessionRule.standalone(); + private final EmailConfigurationService emailConfigurationService = mock(); + private final MockMvc mockMvc = ControllerTester.getMockMvc(new DefaultEmailConfigurationController(userSession, emailConfigurationService)); + + @Test + void createEmailConfiguration_whenUserIsNotAdmin_shouldReturnForbidden() throws Exception { + userSession.logIn().setNonSystemAdministrator(); + + mockMvc.perform( + post(EMAIL_CONFIGURATION_ENDPOINT) + .contentType(MediaType.APPLICATION_JSON_VALUE) + .content(""" + { + "host": "host", + "port": "port", + "securityProtocol": "NONE", + "fromAddress": "fromAddress", + "fromName": "fromName", + "subjectPrefix": "subjectPrefix", + "authMethod": "BASIC", + "username": "username", + "basicPassword": "basicPassword" + } + """)) + .andExpectAll( + status().isForbidden(), + content().json("{\"message\":\"Insufficient privileges\"}")); + } + + @ParameterizedTest + @ValueSource(strings = {"host", "port", "fromAddress", "fromName", "subjectPrefix", "username"}) + void create_whenRequiredFieldEmpty_shouldReturnBadRequest(String field) throws Exception { + userSession.logIn().setSystemAdministrator(); + + String payload = format(""" + { + "host": "%s", + "port": "%s", + "securityProtocol": "NONE", + "fromAddress": "%s", + "fromName": "%s", + "subjectPrefix": "%s", + "authMethod": "BASIC", + "username": "%s" + } + """, + field.equals("host") ? "" : "host", + field.equals("port") ? "" : "port", + field.equals("fromAddress") ? "" : "fromAddress", + field.equals("fromName") ? "" : "fromName", + field.equals("subjectPrefix") ? "" : "subjectPrefix", + field.equals("username") ? "" : "username"); + + mockMvc.perform(post(EMAIL_CONFIGURATION_ENDPOINT) + .contentType(MediaType.APPLICATION_JSON_VALUE) + .content(payload)) + .andExpectAll( + status().isBadRequest(), + content().json(format("{\"message\":\"Value for field %s was rejected. Error: must not be empty.\"}", field))); + } + + @ParameterizedTest + @ValueSource(strings = {"host", "port", "fromAddress", "fromName", "subjectPrefix", "username"}) + void create_whenRequiredStringFieldNull_shouldReturnBadRequest(String field) throws Exception { + userSession.logIn().setSystemAdministrator(); + + String payload = format(""" + { + %s + %s + %s + %s + %s + %s + "securityProtocol": "NONE", + "authMethod": "BASIC" + } + """, + field.equals("host") ? "" : "\"host\" : \"host\",", + field.equals("port") ? "" : "\"port\" : \"port\",", + field.equals("fromAddress") ? "" : "\"fromAddress\" : \"fromAddress\",", + field.equals("fromName") ? "" : "\"fromName\" : \"fromName\",", + field.equals("subjectPrefix") ? "" : "\"subjectPrefix\" : \"subjectPrefix\",", + field.equals("username") ? "" : "\"username\" : \"username\","); + + mockMvc.perform(post(EMAIL_CONFIGURATION_ENDPOINT) + .contentType(MediaType.APPLICATION_JSON_VALUE) + .content(payload)) + .andExpectAll( + status().isBadRequest(), + content().json(format("{\"message\":\"Value {} for field %s was rejected. Error: must not be empty.\"}", field))); + } + + @ParameterizedTest + @ValueSource(strings = {"securityProtocol", "authMethod"}) + void create_whenRequiredEnumFieldNull_shouldReturnBadRequest(String field) throws Exception { + userSession.logIn().setSystemAdministrator(); + + String payload = format(""" + { + %s + %s + "host": "host", + "port": "port", + "fromAddress": "fromAddress", + "fromName": "fromName", + "subjectPrefix": "subjectPrefix", + "username": "username" + } + """, + field.equals("securityProtocol") ? "" : "\"securityProtocol\" : \"NONE\",", + field.equals("authMethod") ? "" : "\"authMethod\" : \"BASIC\","); + + mockMvc.perform(post(EMAIL_CONFIGURATION_ENDPOINT) + .contentType(MediaType.APPLICATION_JSON_VALUE) + .content(payload)) + .andExpectAll( + status().isBadRequest(), + content().json(format("{\"message\":\"Value {} for field %s was rejected. Error: must not be null.\"}", field))); + } + + @Test + void create_whenBasicConfigCreated_returnsItWithoutSecrets() throws Exception { + userSession.logIn().setSystemAdministrator(); + when(emailConfigurationService.createConfiguration(any())).thenReturn(EMAIL_BASIC_CONFIGURATION); + + mockMvc.perform( + post(EMAIL_CONFIGURATION_ENDPOINT) + .contentType(MediaType.APPLICATION_JSON_VALUE) + .content(""" + { + "host": "host", + "port": "port", + "securityProtocol": "SSLTLS", + "fromAddress": "fromAddress", + "fromName": "fromName", + "subjectPrefix": "subjectPrefix", + "authMethod": "BASIC", + "username": "username", + "basicPassword": "basicPassword" + } + """)) + .andExpectAll( + status().isOk(), + content().json(""" + { + "id": "email-configuration", + "host": "host", + "port": "port", + "securityProtocol": "SSLTLS", + "fromAddress": "fromAddress", + "fromName": "fromName", + "subjectPrefix": "subjectPrefix", + "authMethod": "BASIC", + "username": "username", + "isBasicPasswordSet": true + } + """)); + + verify(emailConfigurationService).createConfiguration(any()); + } + + @Test + void create_whenOauthConfigCreated_returnsItWithoutSecrets() throws Exception { + userSession.logIn().setSystemAdministrator(); + when(emailConfigurationService.createConfiguration(any())).thenReturn(EMAIL_OAUTH_CONFIGURATION); + + mockMvc.perform( + post(EMAIL_CONFIGURATION_ENDPOINT) + .contentType(MediaType.APPLICATION_JSON_VALUE) + .content(""" + { + "host": "host", + "port": "port", + "securityProtocol": "STARTTLS", + "fromAddress": "fromAddress", + "fromName": "fromName", + "subjectPrefix": "subjectPrefix", + "authMethod": "OAUTH", + "username": "username", + "oauthAuthenticationHost": "oauthAuthenticationHost", + "oauthClientId": "oauthClientId", + "oauthClientSecret": "oauthClientSecret", + "oauthTenant": "oauthTenant" + } + """)) + .andExpectAll( + status().isOk(), + content().json(""" + { + "id": "email-configuration", + "host": "host", + "port": "port", + "securityProtocol": "STARTTLS", + "fromAddress": "fromAddress", + "fromName": "fromName", + "subjectPrefix": "subjectPrefix", + "authMethod": "OAUTH", + "username": "username", + "isBasicPasswordSet": false, + "oauthAuthenticationHost": "oauthAuthenticationHost", + "isOauthClientIdSet": true, + "isOauthClientSecretSet": true, + "oauthTenant": "oauthTenant" + } + """)); + + verify(emailConfigurationService).createConfiguration(any()); + } + + @Test + void getEmailConfiguration_whenUserIsNotAdmin_shouldReturnForbidden() throws Exception { + userSession.logIn().setNonSystemAdministrator(); + + mockMvc.perform(get(EMAIL_CONFIGURATION_ENDPOINT + "/whatever-id")) + .andExpectAll( + status().isForbidden(), + content().json("{\"message\":\"Insufficient privileges\"}")); + } + + @Test + void getEmailConfiguration_whenConfigNotFound_throws() throws Exception { + userSession.logIn().setSystemAdministrator(); + when(emailConfigurationService.getConfiguration("not-existing-id")).thenThrow(new NotFoundException("Not found")); + + mockMvc.perform(get(EMAIL_CONFIGURATION_ENDPOINT + "/not-existing-id")) + .andExpectAll( + status().isNotFound(), + content().json("{\"message\":\"Not found\"}")); + + verify(emailConfigurationService).getConfiguration("not-existing-id"); + } + + @Test + void getEmailConfiguration_whenConfigFound_returnsIt() throws Exception { + userSession.logIn().setSystemAdministrator(); + when(emailConfigurationService.getConfiguration("existing-id")).thenReturn(EMAIL_BASIC_CONFIGURATION); + + mockMvc.perform(get(EMAIL_CONFIGURATION_ENDPOINT + "/existing-id")) + .andExpectAll( + status().isOk(), + content().json(EXPECTED_BASIC_CONFIGURATION)); + + verify(emailConfigurationService).getConfiguration("existing-id"); + } + + @Test + void searchEmailConfigurations_whenUserIsNotAdmin_shouldReturnForbidden() throws Exception { + userSession.logIn().setNonSystemAdministrator(); + + mockMvc.perform(get(EMAIL_CONFIGURATION_ENDPOINT + "/whatever-id")) + .andExpectAll( + status().isForbidden(), + content().json("{\"message\":\"Insufficient privileges\"}")); + } + + @Test + void searchEmailConfigurations_whenNoParams_shouldReturnDefault() throws Exception { + userSession.logIn().setSystemAdministrator(); + when(emailConfigurationService.findConfigurations()).thenReturn(Optional.of(EMAIL_BASIC_CONFIGURATION)); + + MvcResult mvcResult = mockMvc.perform(get(EMAIL_CONFIGURATION_ENDPOINT)) + .andExpect(status().isOk()) + .andReturn(); + + EmailConfigurationSearchRestResponse response = GSON.fromJson(mvcResult.getResponse().getContentAsString(), EmailConfigurationSearchRestResponse.class); + + assertThat(response.page().pageSize()).isEqualTo(1000); + assertThat(response.page().pageIndex()).isEqualTo(1); + assertThat(response.page().total()).isEqualTo(1); + assertThat(response.emailConfigurations()).containsExactly(toEmailConfigurationResource(EMAIL_BASIC_CONFIGURATION)); + verify(emailConfigurationService).findConfigurations(); + } + + private EmailConfigurationResource toEmailConfigurationResource(EmailConfiguration emailConfiguration) { + return new EmailConfigurationResource( + emailConfiguration.id(), + emailConfiguration.host(), + emailConfiguration.port(), + toRestSecurityProtocol(emailConfiguration.securityProtocol()), + emailConfiguration.fromAddress(), + emailConfiguration.fromName(), + emailConfiguration.subjectPrefix(), + toRestAuthMethod(emailConfiguration.authMethod()), + emailConfiguration.username(), + emailConfiguration.basicPassword() != null, + emailConfiguration.oauthAuthenticationHost(), + emailConfiguration.oauthClientId() != null, + emailConfiguration.oauthClientSecret() != null, + emailConfiguration.oauthTenant() + ); + } + + private static org.sonar.server.v2.api.email.config.resource.EmailConfigurationSecurityProtocol toRestSecurityProtocol(EmailConfigurationSecurityProtocol securityProtocol) { + return org.sonar.server.v2.api.email.config.resource.EmailConfigurationSecurityProtocol.valueOf(securityProtocol.name()); + } + + private static org.sonar.server.v2.api.email.config.resource.EmailConfigurationAuthMethod toRestAuthMethod(EmailConfigurationAuthMethod authMethod) { + return org.sonar.server.v2.api.email.config.resource.EmailConfigurationAuthMethod.valueOf(authMethod.name()); + } + + @Test + void searchEmailConfigurations_whenNoParamAndNoConfig_shouldReturnEmptyList() throws Exception { + userSession.logIn().setSystemAdministrator(); + when(emailConfigurationService.findConfigurations()).thenReturn(Optional.empty()); + + MvcResult mvcResult = mockMvc.perform(get(EMAIL_CONFIGURATION_ENDPOINT)) + .andExpect(status().isOk()) + .andReturn(); + + EmailConfigurationSearchRestResponse response = GSON.fromJson(mvcResult.getResponse().getContentAsString(), EmailConfigurationSearchRestResponse.class); + + assertThat(response.page().pageSize()).isEqualTo(1000); + assertThat(response.page().pageIndex()).isEqualTo(1); + assertThat(response.page().total()).isZero(); + assertThat(response.emailConfigurations()).isEmpty(); + verify(emailConfigurationService).findConfigurations(); + } + + @Test + void updateConfiguration_whenUserIsNotAdmin_shouldReturnForbidden() throws Exception { + userSession.logIn().setNonSystemAdministrator(); + + mockMvc.perform(patch(EMAIL_CONFIGURATION_ENDPOINT + "/whatever-id") + .contentType(JSON_MERGE_PATCH_CONTENT_TYPE) + .content("{}")) + .andExpectAll( + status().isForbidden(), + content().json("{\"message\":\"Insufficient privileges\"}")); + } + + @ParameterizedTest + @ValueSource(strings = {"host", "port", "fromAddress", "fromName", "subjectPrefix", "username"}) + void update_whenRequiredFieldEmpty_shouldReturnBadRequest(String field) throws Exception { + userSession.logIn().setSystemAdministrator(); + + mockMvc.perform(patch(EMAIL_CONFIGURATION_ENDPOINT + "/" + UNIQUE_EMAIL_CONFIGURATION_ID) + .contentType(JSON_MERGE_PATCH_CONTENT_TYPE) + .content(format(""" + { + "%s": "" + } + """, field))) + .andExpectAll( + status().isBadRequest(), + content().json(format("{\"message\":\"Value for field %s was rejected. Error: must not be empty.\"}", field))); + } + + @Test + void updateConfiguration_whenAllFieldsUpdated_performUpdates() throws Exception { + userSession.logIn().setSystemAdministrator(); + when(emailConfigurationService.updateConfiguration(any())).thenReturn(EMAIL_OAUTH_CONFIGURATION); + + mockMvc.perform(patch(EMAIL_CONFIGURATION_ENDPOINT + "/" + UNIQUE_EMAIL_CONFIGURATION_ID) + .contentType(JSON_MERGE_PATCH_CONTENT_TYPE) + .content(""" + { + "host": "host", + "port": "port", + "securityProtocol": "STARTTLS", + "fromAddress": "fromAddress", + "fromName": "fromName", + "subjectPrefix": "subjectPrefix", + "authMethod": "OAUTH", + "username": "username", + "basicPassword": "basicPassword", + "oauthAuthenticationHost": "oauthAuthenticationHost", + "oauthClientId": "oauthClientId", + "oauthClientSecret": "oauthClientSecret", + "oauthTenant": "oauthTenant" + } + """)) + .andExpectAll( + status().isOk(), + content().json(EXPECTED_OAUTH_CONFIGURATION)); + + verify(emailConfigurationService).updateConfiguration(new UpdateEmailConfigurationRequest( + UNIQUE_EMAIL_CONFIGURATION_ID, + NonNullUpdatedValue.withValueOrThrow("host"), + NonNullUpdatedValue.withValueOrThrow("port"), + NonNullUpdatedValue.withValueOrThrow(EmailConfigurationSecurityProtocol.STARTTLS), + NonNullUpdatedValue.withValueOrThrow("fromAddress"), + NonNullUpdatedValue.withValueOrThrow("fromName"), + NonNullUpdatedValue.withValueOrThrow("subjectPrefix"), + NonNullUpdatedValue.withValueOrThrow(EmailConfigurationAuthMethod.OAUTH), + NonNullUpdatedValue.withValueOrThrow("username"), + NonNullUpdatedValue.withValueOrThrow("basicPassword"), + NonNullUpdatedValue.withValueOrThrow("oauthAuthenticationHost"), + NonNullUpdatedValue.withValueOrThrow("oauthClientId"), + NonNullUpdatedValue.withValueOrThrow("oauthClientSecret"), + NonNullUpdatedValue.withValueOrThrow("oauthTenant") + )); + } + + @Test + void updateConfiguration_whenSomeFieldsUpdated_performUpdates() throws Exception { + userSession.logIn().setSystemAdministrator(); + when(emailConfigurationService.updateConfiguration(any())).thenReturn(EMAIL_OAUTH_CONFIGURATION); + + mockMvc.perform(patch(EMAIL_CONFIGURATION_ENDPOINT + "/" + UNIQUE_EMAIL_CONFIGURATION_ID) + .contentType(JSON_MERGE_PATCH_CONTENT_TYPE) + .content(""" + { + "host": "host", + "oauthTenant": "oauthTenant" + } + """)) + .andExpectAll( + status().isOk(), + content().json(EXPECTED_OAUTH_CONFIGURATION)); + + verify(emailConfigurationService).updateConfiguration(new UpdateEmailConfigurationRequest( + UNIQUE_EMAIL_CONFIGURATION_ID, + NonNullUpdatedValue.withValueOrThrow("host"), + NonNullUpdatedValue.undefined(), + NonNullUpdatedValue.undefined(), + NonNullUpdatedValue.undefined(), + NonNullUpdatedValue.undefined(), + NonNullUpdatedValue.undefined(), + NonNullUpdatedValue.undefined(), + NonNullUpdatedValue.undefined(), + NonNullUpdatedValue.undefined(), + NonNullUpdatedValue.undefined(), + NonNullUpdatedValue.undefined(), + NonNullUpdatedValue.undefined(), + NonNullUpdatedValue.withValueOrThrow("oauthTenant") + )); + } + + @Test + void delete_whenUserIsNotAdmin_shouldReturnForbidden() throws Exception { + userSession.logIn().setNonSystemAdministrator(); + + mockMvc.perform( + delete(EMAIL_CONFIGURATION_ENDPOINT + "/" + UNIQUE_EMAIL_CONFIGURATION_ID)) + .andExpectAll( + status().isForbidden(), + content().json("{\"message\":\"Insufficient privileges\"}")); + } + + @Test + void delete_whenConfigIsDeleted_returnsNoContent() throws Exception { + userSession.logIn().setSystemAdministrator(); + + mockMvc.perform( + delete(EMAIL_CONFIGURATION_ENDPOINT + "/" + UNIQUE_EMAIL_CONFIGURATION_ID)) + .andExpectAll( + status().isNoContent()); + + verify(emailConfigurationService).deleteConfiguration(UNIQUE_EMAIL_CONFIGURATION_ID); + } + + @Test + void delete_whenConfigNotFound_returnsNotFound() throws Exception { + userSession.logIn().setSystemAdministrator(); + doThrow(new NotFoundException("Not found")).when(emailConfigurationService).deleteConfiguration("not-existing-id"); + + mockMvc.perform( + delete(EMAIL_CONFIGURATION_ENDPOINT + "/not-existing-id")) + .andExpectAll( + status().isNotFound(), + content().json("{\"message\":\"Not found\"}")); + } + +} diff --git a/server/sonar-webserver-webapi-v2/src/test/java/org/sonar/server/v2/common/model/NullOrNotEmptyValidatorTest.java b/server/sonar-webserver-webapi-v2/src/test/java/org/sonar/server/v2/common/model/NullOrNotEmptyValidatorTest.java new file mode 100644 index 00000000000..2b9799bda63 --- /dev/null +++ b/server/sonar-webserver-webapi-v2/src/test/java/org/sonar/server/v2/common/model/NullOrNotEmptyValidatorTest.java @@ -0,0 +1,49 @@ +/* + * 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.v2.common.model; + +import javax.validation.ConstraintValidatorContext; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; + +class NullOrNotEmptyValidatorTest { + + NullOrNotEmptyValidator validator = new NullOrNotEmptyValidator(); + ConstraintValidatorContext context = mock(); + + @Test + void isValid_shouldValidateNull() { + assertTrue(validator.isValid(null, context)); + } + + @Test + void isValid_shouldValidateNotEmptyString() { + assertTrue(validator.isValid("not-empty", context)); + } + + @Test + void isValid_shouldNotValidateEmptyString() { + assertFalse(validator.isValid("", context)); + } + +}
\ No newline at end of file diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/email/ws/EmailsWsModule.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/email/ws/EmailsWsModule.java index 6c54426c691..ad5226988ff 100644 --- a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/email/ws/EmailsWsModule.java +++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/email/ws/EmailsWsModule.java @@ -20,11 +20,13 @@ package org.sonar.server.email.ws; import org.sonar.core.platform.Module; +import org.sonar.server.common.email.config.EmailConfigurationService; public class EmailsWsModule extends Module { @Override protected void configureModule() { add( + EmailConfigurationService.class, EmailsWs.class, SendAction.class); } diff --git a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/email/ws/EmailsWsModuleTest.java b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/email/ws/EmailsWsModuleTest.java index 8e076cb9c20..e5775c25f9f 100644 --- a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/email/ws/EmailsWsModuleTest.java +++ b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/email/ws/EmailsWsModuleTest.java @@ -29,6 +29,6 @@ public class EmailsWsModuleTest { public void verify_count_of_added_components() { ListContainer container = new ListContainer(); new EmailsWsModule().configure(container); - assertThat(container.getAddedObjects()).hasSize(2); + assertThat(container.getAddedObjects()).hasSize(3); } } diff --git a/sonar-ws/src/main/java/org/sonarqube/ws/client/emails/EmailConfigurationService.java b/sonar-ws/src/main/java/org/sonarqube/ws/client/emails/EmailConfigurationService.java new file mode 100644 index 00000000000..785d61b88e4 --- /dev/null +++ b/sonar-ws/src/main/java/org/sonarqube/ws/client/emails/EmailConfigurationService.java @@ -0,0 +1,91 @@ +/* + * 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.sonarqube.ws.client.emails; + +import com.google.gson.Gson; +import com.google.gson.JsonObject; +import org.sonarqube.ws.client.BaseService; +import org.sonarqube.ws.client.DeleteRequest; +import org.sonarqube.ws.client.HttpException; +import org.sonarqube.ws.client.PostRequest; +import org.sonarqube.ws.client.WsConnector; +import org.sonarqube.ws.client.WsResponse; + +import static java.net.HttpURLConnection.HTTP_NOT_FOUND; + +public class EmailConfigurationService extends BaseService { + + + private static final String EMAIL_CONFIGURATION_ID = "email-configuration"; + + public EmailConfigurationService(WsConnector wsConnector) { + super(wsConnector, "api/v2/system/email-configurations"); + } + + public String createEmailConfiguration(WsEmailConfiguration config) { + String body = String.format(""" + { + "host": "%s", + "port": "%s", + "securityProtocol": "%s", + "fromAddress": "%s", + "fromName": "%s", + "subjectPrefix": "%s", + "authMethod": "%s", + "username": "%s", + "basicPassword": "%s", + "oauthAuthenticationHost": "%s", + "oauthClientId": "%s", + "oauthClientSecret": "%s", + "oauthTenant": "%s" + } + """, + config.host(), + config.port(), + config.securityProtocol(), + config.fromAddress(), + config.fromName(), + config.subjectPrefix(), + config.authMethod(), + config.username(), + config.basicPassword(), + config.oauthAuthenticationHost(), + config.oauthClientId(), + config.oauthClientSecret(), + config.oauthTenant() + ); + + WsResponse response = call( + new PostRequest(path()).setBody(body) + ); + return new Gson().fromJson(response.content(), JsonObject.class).get("id").getAsString(); + } + + public void deleteEmailConfiguration() { + try { + call(new DeleteRequest(path() + "/" + EMAIL_CONFIGURATION_ID)); + } catch (HttpException e) { + // We ignore if it gets deleted while there is no configuration + if (e.code() != HTTP_NOT_FOUND) { + throw e; + } + } + } +} diff --git a/sonar-ws/src/main/java/org/sonarqube/ws/client/emails/WsEmailConfiguration.java b/sonar-ws/src/main/java/org/sonarqube/ws/client/emails/WsEmailConfiguration.java new file mode 100644 index 00000000000..a44f47edc3b --- /dev/null +++ b/sonar-ws/src/main/java/org/sonarqube/ws/client/emails/WsEmailConfiguration.java @@ -0,0 +1,129 @@ +/* + * 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.sonarqube.ws.client.emails; + +public record WsEmailConfiguration( + String host, + String port, + String securityProtocol, + String fromAddress, + String fromName, + String subjectPrefix, + String authMethod, + String username, + String basicPassword, + String oauthAuthenticationHost, + String oauthClientId, + String oauthClientSecret, + String oauthTenant + ) { + + public static WsEmailConfigurationBuilder builder() { + return new WsEmailConfigurationBuilder(); + } + + public static final class WsEmailConfigurationBuilder { + private String host; + private String port; + private String securityProtocol; + private String fromAddress; + private String fromName; + private String subjectPrefix; + private String authMethod; + private String username; + private String basicPassword; + private String oauthAuthenticationHost; + private String oauthClientId; + private String oauthClientSecret; + private String oauthTenant; + + private WsEmailConfigurationBuilder() { + } + + public WsEmailConfigurationBuilder host(String host) { + this.host = host; + return this; + } + + public WsEmailConfigurationBuilder port(String port) { + this.port = port; + return this; + } + + public WsEmailConfigurationBuilder securityProtocol(String securityProtocol) { + this.securityProtocol = securityProtocol; + return this; + } + + public WsEmailConfigurationBuilder fromAddress(String fromAddress) { + this.fromAddress = fromAddress; + return this; + } + + public WsEmailConfigurationBuilder fromName(String fromName) { + this.fromName = fromName; + return this; + } + + public WsEmailConfigurationBuilder subjectPrefix(String subjectPrefix) { + this.subjectPrefix = subjectPrefix; + return this; + } + + public WsEmailConfigurationBuilder authMethod(String authMethod) { + this.authMethod = authMethod; + return this; + } + + public WsEmailConfigurationBuilder username(String username) { + this.username = username; + return this; + } + + public WsEmailConfigurationBuilder basicPassword(String basicPassword) { + this.basicPassword = basicPassword; + return this; + } + + public WsEmailConfigurationBuilder oauthAuthenticationHost(String oauthAuthenticationHost) { + this.oauthAuthenticationHost = oauthAuthenticationHost; + return this; + } + + public WsEmailConfigurationBuilder oauthClientId(String oauthClientId) { + this.oauthClientId = oauthClientId; + return this; + } + + public WsEmailConfigurationBuilder oauthClientSecret(String oauthClientSecret) { + this.oauthClientSecret = oauthClientSecret; + return this; + } + + public WsEmailConfigurationBuilder oauthTenant(String oauthTenant) { + this.oauthTenant = oauthTenant; + return this; + } + + public WsEmailConfiguration build() { + return new WsEmailConfiguration(host, port, securityProtocol, fromAddress, fromName, subjectPrefix, authMethod, username, basicPassword, oauthAuthenticationHost, oauthClientId, oauthClientSecret, oauthTenant); + } + } +} |