aboutsummaryrefslogtreecommitdiffstats
path: root/server/sonar-server-common
diff options
context:
space:
mode:
authorBogdana <bogdana.kushnir@sonarsource.com>2024-07-30 09:45:35 +0200
committersonartech <sonartech@sonarsource.com>2024-08-16 20:02:58 +0000
commitf7030b054783e5c4eefee9bbd7467988b46f9ba4 (patch)
tree8c4b80ce23499701a18be3c16a3494aec7baa290 /server/sonar-server-common
parent461a47d22b55a5450062883a6c1bd22e0a4ec3dd (diff)
downloadsonarqube-f7030b054783e5c4eefee9bbd7467988b46f9ba4.tar.gz
sonarqube-f7030b054783e5c4eefee9bbd7467988b46f9ba4.zip
SONAR-22638 add OAuth authentication for sending emails
Diffstat (limited to 'server/sonar-server-common')
-rw-r--r--server/sonar-server-common/build.gradle2
-rw-r--r--server/sonar-server-common/src/main/java/org/sonar/server/notification/email/EmailNotificationChannel.java27
-rw-r--r--server/sonar-server-common/src/main/java/org/sonar/server/oauth/OAuthMicrosoftRestClient.java47
-rw-r--r--server/sonar-server-common/src/main/java/org/sonar/server/oauth/ScribeMicrosoftOauth2Api.java45
-rw-r--r--server/sonar-server-common/src/main/java/org/sonar/server/oauth/package-info.java23
-rw-r--r--server/sonar-server-common/src/test/java/org/sonar/server/notification/email/EmailNotificationChannelTest.java20
-rw-r--r--server/sonar-server-common/src/test/java/org/sonar/server/oauth/OAuthMicrosoftRestClientTest.java34
-rw-r--r--server/sonar-server-common/src/test/java/org/sonar/server/oauth/ScribeMicrosoftOauth2ApiTest.java38
8 files changed, 223 insertions, 13 deletions
diff --git a/server/sonar-server-common/build.gradle b/server/sonar-server-common/build.gradle
index 083eb70991d..bc9f402f590 100644
--- a/server/sonar-server-common/build.gradle
+++ b/server/sonar-server-common/build.gradle
@@ -11,6 +11,8 @@ dependencies {
implementation 'com.fasterxml.jackson.core:jackson-databind'
api 'commons-io:commons-io'
+ api 'com.github.scribejava:scribejava-apis'
+ api 'com.github.scribejava:scribejava-core'
api 'com.google.guava:guava'
api 'com.squareup.okhttp3:okhttp'
api 'org.apache.commons:commons-email'
diff --git a/server/sonar-server-common/src/main/java/org/sonar/server/notification/email/EmailNotificationChannel.java b/server/sonar-server-common/src/main/java/org/sonar/server/notification/email/EmailNotificationChannel.java
index 4c9fcc00109..9948abe4f1d 100644
--- a/server/sonar-server-common/src/main/java/org/sonar/server/notification/email/EmailNotificationChannel.java
+++ b/server/sonar-server-common/src/main/java/org/sonar/server/notification/email/EmailNotificationChannel.java
@@ -23,6 +23,7 @@ import java.net.MalformedURLException;
import java.net.URL;
import java.time.Duration;
import java.util.Objects;
+import java.util.Properties;
import java.util.Set;
import java.util.regex.Pattern;
import javax.annotation.CheckForNull;
@@ -41,6 +42,7 @@ import org.sonar.db.DbClient;
import org.sonar.db.DbSession;
import org.sonar.db.user.UserDto;
import org.sonar.server.email.EmailSmtpConfiguration;
+import org.sonar.server.oauth.OAuthMicrosoftRestClient;
import org.sonar.server.issue.notification.EmailMessage;
import org.sonar.server.issue.notification.EmailTemplate;
import org.sonar.server.notification.NotificationChannel;
@@ -97,6 +99,7 @@ public class EmailNotificationChannel extends NotificationChannel {
private static final String SUBJECT_DEFAULT = "Notification";
private static final String SMTP_HOST_NOT_CONFIGURED_DEBUG_MSG = "SMTP host was not configured - email will not be sent";
private static final String MAIL_SENT_FROM = "%sMail sent from: %s";
+ private static final String OAUTH_AUTH_METHOD = "OAUTH";
private final EmailSmtpConfiguration configuration;
private final Server server;
@@ -289,14 +292,32 @@ public class EmailNotificationChannel extends NotificationChannel {
}
}
- private void setConnectionDetails(Email email) {
+ private void setConnectionDetails(Email email) throws EmailException {
email.setHostName(configuration.getSmtpHost());
configureSecureConnection(email);
+ email.setSocketConnectionTimeout(SOCKET_TIMEOUT);
+ email.setSocketTimeout(SOCKET_TIMEOUT);
+ if (OAUTH_AUTH_METHOD.equals(configuration.getAuthMethod())) {
+ setOauthAuthentication(email);
+ } else if (StringUtils.isNotBlank(configuration.getSmtpUsername()) || StringUtils.isNotBlank(configuration.getSmtpPassword())) {
+ setBasicAuthentication(email);
+ }
+ }
+
+ private void setOauthAuthentication(Email email) throws EmailException {
+ String token = OAuthMicrosoftRestClient.getAccessTokenFromClientCredentialsGrantFlow(configuration.getOAuthHost(), configuration.getOAuthClientId(),
+ configuration.getOAuthClientSecret(), configuration.getOAuthTenant(), configuration.getOAuthScope());
+ email.setAuthentication(configuration.getSmtpUsername(), token);
+ Properties props = email.getMailSession().getProperties();
+ props.put("mail.smtp.auth.mechanisms", "XOAUTH2");
+ props.put("mail.smtp.auth.login.disable", "true");
+ props.put("mail.smtp.auth.plain.disable", "true");
+ }
+
+ private void setBasicAuthentication(Email email) {
if (StringUtils.isNotBlank(configuration.getSmtpUsername()) || StringUtils.isNotBlank(configuration.getSmtpPassword())) {
email.setAuthentication(configuration.getSmtpUsername(), configuration.getSmtpPassword());
}
- email.setSocketConnectionTimeout(SOCKET_TIMEOUT);
- email.setSocketTimeout(SOCKET_TIMEOUT);
}
private void configureSecureConnection(Email email) {
diff --git a/server/sonar-server-common/src/main/java/org/sonar/server/oauth/OAuthMicrosoftRestClient.java b/server/sonar-server-common/src/main/java/org/sonar/server/oauth/OAuthMicrosoftRestClient.java
new file mode 100644
index 00000000000..3a7ada75440
--- /dev/null
+++ b/server/sonar-server-common/src/main/java/org/sonar/server/oauth/OAuthMicrosoftRestClient.java
@@ -0,0 +1,47 @@
+/*
+ * 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.oauth;
+
+import com.github.scribejava.core.builder.ServiceBuilder;
+import com.github.scribejava.core.oauth.OAuth20Service;
+import java.io.IOException;
+import java.util.concurrent.ExecutionException;
+
+public class OAuthMicrosoftRestClient {
+
+ private OAuthMicrosoftRestClient() {
+ // Only static method
+ }
+
+ public static String getAccessTokenFromClientCredentialsGrantFlow(String host, String clientId, String clientSecret, String tenant, String scope) {
+ final OAuth20Service service = new ServiceBuilder(clientId)
+ .apiSecret(clientSecret)
+ .defaultScope(scope)
+ .build(new ScribeMicrosoftOauth2Api(host, tenant));
+ try {
+ return service.getAccessTokenClientCredentialsGrant().getAccessToken();
+ } catch (IOException | ExecutionException e) {
+ throw new IllegalStateException("Unable to get a token: " + e);
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ throw new IllegalStateException("Interrupted while getting a token: " + e);
+ }
+ }
+}
diff --git a/server/sonar-server-common/src/main/java/org/sonar/server/oauth/ScribeMicrosoftOauth2Api.java b/server/sonar-server-common/src/main/java/org/sonar/server/oauth/ScribeMicrosoftOauth2Api.java
new file mode 100644
index 00000000000..498abdeed27
--- /dev/null
+++ b/server/sonar-server-common/src/main/java/org/sonar/server/oauth/ScribeMicrosoftOauth2Api.java
@@ -0,0 +1,45 @@
+/*
+ * 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.oauth;
+
+import com.github.scribejava.core.builder.api.DefaultApi20;
+
+public class ScribeMicrosoftOauth2Api extends DefaultApi20 {
+
+ private static final String OAUTH_V2 = "/oauth2/v2.0/";
+ private final String tenant;
+ private final String host;
+
+ public ScribeMicrosoftOauth2Api(String host, String tenant) {
+ this.tenant = tenant;
+ this.host = host;
+ }
+
+ @Override
+ public String getAccessTokenEndpoint() {
+ return host + "/" + tenant + OAUTH_V2 + "token";
+ }
+
+ @Override
+ protected String getAuthorizationBaseUrl() {
+ return host + "/" + tenant + OAUTH_V2 + "authorize";
+ }
+
+}
diff --git a/server/sonar-server-common/src/main/java/org/sonar/server/oauth/package-info.java b/server/sonar-server-common/src/main/java/org/sonar/server/oauth/package-info.java
new file mode 100644
index 00000000000..b21abe2add4
--- /dev/null
+++ b/server/sonar-server-common/src/main/java/org/sonar/server/oauth/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.oauth;
+
+import javax.annotation.ParametersAreNonnullByDefault;
diff --git a/server/sonar-server-common/src/test/java/org/sonar/server/notification/email/EmailNotificationChannelTest.java b/server/sonar-server-common/src/test/java/org/sonar/server/notification/email/EmailNotificationChannelTest.java
index e4aad4cf909..bd524e9e6a2 100644
--- a/server/sonar-server-common/src/test/java/org/sonar/server/notification/email/EmailNotificationChannelTest.java
+++ b/server/sonar-server-common/src/test/java/org/sonar/server/notification/email/EmailNotificationChannelTest.java
@@ -256,9 +256,9 @@ public class EmailNotificationChannelTest {
@Test
public void deliverAll_has_no_effect_if_set_is_empty() {
EmailSmtpConfiguration emailSettings = mock(EmailSmtpConfiguration.class);
- EmailNotificationChannel underTest = new EmailNotificationChannel(emailSettings, server, null, null);
+ EmailNotificationChannel emailNotificationChannel = new EmailNotificationChannel(emailSettings, server, null, null);
- int count = underTest.deliverAll(Collections.emptySet());
+ int count = emailNotificationChannel.deliverAll(Collections.emptySet());
assertThat(count).isZero();
verifyNoInteractions(emailSettings);
@@ -272,9 +272,9 @@ public class EmailNotificationChannelTest {
Set<EmailDeliveryRequest> requests = IntStream.range(0, 1 + new Random().nextInt(10))
.mapToObj(i -> new EmailDeliveryRequest("foo" + i + "@moo", mock(Notification.class)))
.collect(toSet());
- EmailNotificationChannel underTest = new EmailNotificationChannel(emailSettings, server, null, null);
+ EmailNotificationChannel emailNotificationChannel = new EmailNotificationChannel(emailSettings, server, null, null);
- int count = underTest.deliverAll(requests);
+ int count = emailNotificationChannel.deliverAll(requests);
assertThat(count).isZero();
verify(emailSettings).getSmtpHost();
@@ -290,9 +290,9 @@ public class EmailNotificationChannelTest {
Set<EmailDeliveryRequest> requests = IntStream.range(0, 1 + new Random().nextInt(10))
.mapToObj(i -> new EmailDeliveryRequest(emptyString, mock(Notification.class)))
.collect(toSet());
- EmailNotificationChannel underTest = new EmailNotificationChannel(emailSettings, server, null, null);
+ EmailNotificationChannel emailNotificationChannel = new EmailNotificationChannel(emailSettings, server, null, null);
- int count = underTest.deliverAll(requests);
+ int count = emailNotificationChannel.deliverAll(requests);
assertThat(count).isZero();
verify(emailSettings).getSmtpHost();
@@ -316,9 +316,9 @@ public class EmailNotificationChannelTest {
Set<EmailDeliveryRequest> requests = Stream.of(notification1, notification2, notification3)
.map(t -> new EmailDeliveryRequest(recipientEmail, t))
.collect(toSet());
- EmailNotificationChannel underTest = new EmailNotificationChannel(configuration, server, new EmailTemplate[] {template1, template3}, null);
+ EmailNotificationChannel emailNotificationChannel = new EmailNotificationChannel(configuration, server, new EmailTemplate[] {template1, template3}, null);
- int count = underTest.deliverAll(requests);
+ int count = emailNotificationChannel.deliverAll(requests);
assertThat(count).isEqualTo(2);
assertThat(smtpServer.getMessages()).hasSize(2);
@@ -356,9 +356,9 @@ public class EmailNotificationChannelTest {
when(template11.format(notification1)).thenReturn(emailMessage11);
when(template12.format(notification1)).thenReturn(emailMessage12);
EmailDeliveryRequest request = new EmailDeliveryRequest(recipientEmail, notification1);
- EmailNotificationChannel underTest = new EmailNotificationChannel(configuration, server, new EmailTemplate[] {template11, template12}, null);
+ EmailNotificationChannel emailNotificationChannel = new EmailNotificationChannel(configuration, server, new EmailTemplate[] {template11, template12}, null);
- int count = underTest.deliverAll(Collections.singleton(request));
+ int count = emailNotificationChannel.deliverAll(Collections.singleton(request));
assertThat(count).isOne();
assertThat(smtpServer.getMessages()).hasSize(1);
diff --git a/server/sonar-server-common/src/test/java/org/sonar/server/oauth/OAuthMicrosoftRestClientTest.java b/server/sonar-server-common/src/test/java/org/sonar/server/oauth/OAuthMicrosoftRestClientTest.java
new file mode 100644
index 00000000000..39e796d41a8
--- /dev/null
+++ b/server/sonar-server-common/src/test/java/org/sonar/server/oauth/OAuthMicrosoftRestClientTest.java
@@ -0,0 +1,34 @@
+/*
+ * 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.oauth;
+
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+class OAuthMicrosoftRestClientTest {
+
+ @Test
+ void getAccessTokenFromClientCredentialsGrantFlow_throwsException() {
+ assertThatThrownBy(() -> OAuthMicrosoftRestClient.getAccessTokenFromClientCredentialsGrantFlow("https://localhost", "clientId", "clientSecret", "tenant", "scope"))
+ .isInstanceOf(IllegalStateException.class)
+ .hasMessageStartingWith("Unable to get a token: ");
+ }
+}
diff --git a/server/sonar-server-common/src/test/java/org/sonar/server/oauth/ScribeMicrosoftOauth2ApiTest.java b/server/sonar-server-common/src/test/java/org/sonar/server/oauth/ScribeMicrosoftOauth2ApiTest.java
new file mode 100644
index 00000000000..7c52e41099a
--- /dev/null
+++ b/server/sonar-server-common/src/test/java/org/sonar/server/oauth/ScribeMicrosoftOauth2ApiTest.java
@@ -0,0 +1,38 @@
+/*
+ * 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.oauth;
+
+import org.junit.jupiter.api.Test;
+import static org.assertj.core.api.Assertions.assertThat;
+
+class ScribeMicrosoftOauth2ApiTest {
+
+ private final ScribeMicrosoftOauth2Api scribeMicrosoftOauth2Api = new ScribeMicrosoftOauth2Api("host", "tenant");
+
+ @Test
+ void getAccessToken_returnCorrectEndpoint() {
+ assertThat(scribeMicrosoftOauth2Api.getAccessTokenEndpoint()).isEqualTo("host/tenant/oauth2/v2.0/token");
+ }
+
+ @Test
+ void getAuthorizationBaseUrl_returnCorrectEndpoint() {
+ assertThat(scribeMicrosoftOauth2Api.getAuthorizationBaseUrl()).isEqualTo("host/tenant/oauth2/v2.0/authorize");
+ }
+}