]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-22638 add OAuth authentication for sending emails
authorBogdana <bogdana.kushnir@sonarsource.com>
Tue, 30 Jul 2024 07:45:35 +0000 (09:45 +0200)
committersonartech <sonartech@sonarsource.com>
Fri, 16 Aug 2024 20:02:58 +0000 (20:02 +0000)
server/sonar-server-common/build.gradle
server/sonar-server-common/src/main/java/org/sonar/server/notification/email/EmailNotificationChannel.java
server/sonar-server-common/src/main/java/org/sonar/server/oauth/OAuthMicrosoftRestClient.java [new file with mode: 0644]
server/sonar-server-common/src/main/java/org/sonar/server/oauth/ScribeMicrosoftOauth2Api.java [new file with mode: 0644]
server/sonar-server-common/src/main/java/org/sonar/server/oauth/package-info.java [new file with mode: 0644]
server/sonar-server-common/src/test/java/org/sonar/server/notification/email/EmailNotificationChannelTest.java
server/sonar-server-common/src/test/java/org/sonar/server/oauth/OAuthMicrosoftRestClientTest.java [new file with mode: 0644]
server/sonar-server-common/src/test/java/org/sonar/server/oauth/ScribeMicrosoftOauth2ApiTest.java [new file with mode: 0644]

index 083eb70991d04585c84d6bc996efe7986f92c519..bc9f402f59098c869f3df9792ce4ae087b43a82d 100644 (file)
@@ -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'
index 4c9fcc001098da0a8cfcdfecb1dbc8ebca7a15af..9948abe4f1d94aa37df453f054c4fa3cb8f8086d 100644 (file)
@@ -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 (file)
index 0000000..3a7ada7
--- /dev/null
@@ -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 (file)
index 0000000..498abde
--- /dev/null
@@ -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 (file)
index 0000000..b21abe2
--- /dev/null
@@ -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;
index e4aad4cf909e8183cc8e9743bb268319a7197b74..bd524e9e6a2f477102d4bab2dce7287faeb086c9 100644 (file)
@@ -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 (file)
index 0000000..39e796d
--- /dev/null
@@ -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 (file)
index 0000000..7c52e41
--- /dev/null
@@ -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");
+  }
+}