aboutsummaryrefslogtreecommitdiffstats
path: root/server/sonar-webserver-auth
diff options
context:
space:
mode:
authorZipeng WU <zipeng.wu@sonarsource.com>2022-07-05 10:00:38 +0200
committersonartech <sonartech@sonarsource.com>2022-07-06 20:03:56 +0000
commit6a401f73236a70f702b64646d8bdec7c5a90e15d (patch)
tree23fc39668ac04e0942d704fa8397e28193d55569 /server/sonar-webserver-auth
parentb31d435c35d2e104b225a6f722e459616b0cb8af (diff)
downloadsonarqube-6a401f73236a70f702b64646d8bdec7c5a90e15d.tar.gz
sonarqube-6a401f73236a70f702b64646d8bdec7c5a90e15d.zip
SONAR-16567 Notify the token creator about expiring tokens via email
Diffstat (limited to 'server/sonar-webserver-auth')
-rw-r--r--server/sonar-webserver-auth/src/main/java/org/sonar/server/usertoken/UserTokenModule.java10
-rw-r--r--server/sonar-webserver-auth/src/main/java/org/sonar/server/usertoken/notification/TokenExpirationEmail.java37
-rw-r--r--server/sonar-webserver-auth/src/main/java/org/sonar/server/usertoken/notification/TokenExpirationEmailComposer.java72
-rw-r--r--server/sonar-webserver-auth/src/main/java/org/sonar/server/usertoken/notification/TokenExpirationNotificationExecutorService.java27
-rw-r--r--server/sonar-webserver-auth/src/main/java/org/sonar/server/usertoken/notification/TokenExpirationNotificationExecutorServiceImpl.java37
-rw-r--r--server/sonar-webserver-auth/src/main/java/org/sonar/server/usertoken/notification/TokenExpirationNotificationInitializer.java38
-rw-r--r--server/sonar-webserver-auth/src/main/java/org/sonar/server/usertoken/notification/TokenExpirationNotificationScheduler.java27
-rw-r--r--server/sonar-webserver-auth/src/main/java/org/sonar/server/usertoken/notification/TokenExpirationNotificationSchedulerImpl.java70
-rw-r--r--server/sonar-webserver-auth/src/main/java/org/sonar/server/usertoken/notification/TokenExpirationNotificationSender.java56
-rw-r--r--server/sonar-webserver-auth/src/test/java/org/sonar/server/usertoken/UserTokenAuthenticationTest.java4
-rw-r--r--server/sonar-webserver-auth/src/test/java/org/sonar/server/usertoken/UserTokenModuleTest.java2
-rw-r--r--server/sonar-webserver-auth/src/test/java/org/sonar/server/usertoken/notification/TokenExpirationEmailComposerTest.java104
-rw-r--r--server/sonar-webserver-auth/src/test/java/org/sonar/server/usertoken/notification/TokenExpirationNotificationExecutorServiceImplTest.java35
-rw-r--r--server/sonar-webserver-auth/src/test/java/org/sonar/server/usertoken/notification/TokenExpirationNotificationInitializerTest.java46
-rw-r--r--server/sonar-webserver-auth/src/test/java/org/sonar/server/usertoken/notification/TokenExpirationNotificationSchedulerImplTest.java75
-rw-r--r--server/sonar-webserver-auth/src/test/java/org/sonar/server/usertoken/notification/TokenExpirationNotificationSenderTest.java87
16 files changed, 724 insertions, 3 deletions
diff --git a/server/sonar-webserver-auth/src/main/java/org/sonar/server/usertoken/UserTokenModule.java b/server/sonar-webserver-auth/src/main/java/org/sonar/server/usertoken/UserTokenModule.java
index 41ef0f18713..096e2492947 100644
--- a/server/sonar-webserver-auth/src/main/java/org/sonar/server/usertoken/UserTokenModule.java
+++ b/server/sonar-webserver-auth/src/main/java/org/sonar/server/usertoken/UserTokenModule.java
@@ -20,11 +20,21 @@
package org.sonar.server.usertoken;
import org.sonar.core.platform.Module;
+import org.sonar.server.usertoken.notification.TokenExpirationEmailComposer;
+import org.sonar.server.usertoken.notification.TokenExpirationNotificationExecutorServiceImpl;
+import org.sonar.server.usertoken.notification.TokenExpirationNotificationInitializer;
+import org.sonar.server.usertoken.notification.TokenExpirationNotificationSchedulerImpl;
+import org.sonar.server.usertoken.notification.TokenExpirationNotificationSender;
public class UserTokenModule extends Module {
@Override
protected void configureModule() {
add(
+ TokenExpirationEmailComposer.class,
+ TokenExpirationNotificationSchedulerImpl.class,
+ TokenExpirationNotificationExecutorServiceImpl.class,
+ TokenExpirationNotificationInitializer.class,
+ TokenExpirationNotificationSender.class,
UserTokenAuthentication.class,
TokenGeneratorImpl.class);
}
diff --git a/server/sonar-webserver-auth/src/main/java/org/sonar/server/usertoken/notification/TokenExpirationEmail.java b/server/sonar-webserver-auth/src/main/java/org/sonar/server/usertoken/notification/TokenExpirationEmail.java
new file mode 100644
index 00000000000..fa5c2ae4332
--- /dev/null
+++ b/server/sonar-webserver-auth/src/main/java/org/sonar/server/usertoken/notification/TokenExpirationEmail.java
@@ -0,0 +1,37 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2022 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.usertoken.notification;
+
+import java.util.Set;
+import org.sonar.db.user.UserTokenDto;
+import org.sonar.server.email.BasicEmail;
+
+public class TokenExpirationEmail extends BasicEmail {
+ private final UserTokenDto userToken;
+
+ public TokenExpirationEmail(String recipient, UserTokenDto userToken) {
+ super(Set.of(recipient));
+ this.userToken = userToken;
+ }
+
+ public UserTokenDto getUserToken() {
+ return userToken;
+ }
+}
diff --git a/server/sonar-webserver-auth/src/main/java/org/sonar/server/usertoken/notification/TokenExpirationEmailComposer.java b/server/sonar-webserver-auth/src/main/java/org/sonar/server/usertoken/notification/TokenExpirationEmailComposer.java
new file mode 100644
index 00000000000..d26497caf40
--- /dev/null
+++ b/server/sonar-webserver-auth/src/main/java/org/sonar/server/usertoken/notification/TokenExpirationEmailComposer.java
@@ -0,0 +1,72 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2022 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.usertoken.notification;
+
+import java.net.MalformedURLException;
+import java.time.Instant;
+import java.time.ZoneOffset;
+import java.time.format.DateTimeFormatter;
+import org.apache.commons.mail.EmailException;
+import org.apache.commons.mail.HtmlEmail;
+import org.sonar.api.config.EmailSettings;
+import org.sonar.db.user.UserTokenDto;
+import org.sonar.server.email.EmailSender;
+
+import static java.lang.String.format;
+import static org.sonar.db.user.TokenType.PROJECT_ANALYSIS_TOKEN;
+
+public class TokenExpirationEmailComposer extends EmailSender<TokenExpirationEmail> {
+
+ protected TokenExpirationEmailComposer(EmailSettings emailSettings) {
+ super(emailSettings);
+ }
+
+ @Override protected void addReportContent(HtmlEmail email, TokenExpirationEmail emailData) throws EmailException, MalformedURLException {
+ email.addTo(emailData.getRecipients().toArray(String[]::new));
+ UserTokenDto token = emailData.getUserToken();
+ if (token.isExpired()) {
+ email.setSubject(format("Your token with name \"%s\" has expired.", token.getName()));
+ } else {
+ email.setSubject(format("Your token with name \"%s\" will expire on %s.", token.getName(), parseDate(token.getExpirationDate())));
+ }
+ email.setHtmlMsg(composeEmailBody(token));
+ }
+
+ private String composeEmailBody(UserTokenDto token) {
+ StringBuilder builder = new StringBuilder();
+ builder.append("Token Summary<br/><br/>")
+ .append(format("Name: %s<br/>", token.getName()))
+ .append(format("Type: %s<br/>", token.getType()));
+ if (PROJECT_ANALYSIS_TOKEN.name().equals(token.getType())) {
+ builder.append(format("Project: %s<br/>", token.getProjectName()));
+ }
+ builder.append(format("Created on: %s<br/>", parseDate(token.getCreatedAt())));
+ if (token.getLastConnectionDate() != null) {
+ builder.append(format("Last used on: %s<br/>", parseDate(token.getLastConnectionDate())));
+ }
+ builder.append(format("%s on: %s<br/>", token.isExpired() ? "Expired" : "Expires", parseDate(token.getExpirationDate())))
+ .append(format("<br/>If this token is still needed, visit <a href=\"%s/account/security/\">here</a> to generate an equivalent.", emailSettings.getServerBaseURL()));
+ return builder.toString();
+ }
+
+ private static String parseDate(long timestamp) {
+ return Instant.ofEpochMilli(timestamp).atZone(ZoneOffset.UTC).toLocalDate().format(DateTimeFormatter.ofPattern("dd/MM/yyyy"));
+ }
+}
diff --git a/server/sonar-webserver-auth/src/main/java/org/sonar/server/usertoken/notification/TokenExpirationNotificationExecutorService.java b/server/sonar-webserver-auth/src/main/java/org/sonar/server/usertoken/notification/TokenExpirationNotificationExecutorService.java
new file mode 100644
index 00000000000..32772c83d3b
--- /dev/null
+++ b/server/sonar-webserver-auth/src/main/java/org/sonar/server/usertoken/notification/TokenExpirationNotificationExecutorService.java
@@ -0,0 +1,27 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2022 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.usertoken.notification;
+
+import java.util.concurrent.ScheduledExecutorService;
+import org.sonar.api.server.ServerSide;
+
+@ServerSide
+public interface TokenExpirationNotificationExecutorService extends ScheduledExecutorService {
+}
diff --git a/server/sonar-webserver-auth/src/main/java/org/sonar/server/usertoken/notification/TokenExpirationNotificationExecutorServiceImpl.java b/server/sonar-webserver-auth/src/main/java/org/sonar/server/usertoken/notification/TokenExpirationNotificationExecutorServiceImpl.java
new file mode 100644
index 00000000000..bf255e1a2f0
--- /dev/null
+++ b/server/sonar-webserver-auth/src/main/java/org/sonar/server/usertoken/notification/TokenExpirationNotificationExecutorServiceImpl.java
@@ -0,0 +1,37 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2022 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.usertoken.notification;
+
+import com.google.common.util.concurrent.ThreadFactoryBuilder;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import org.sonar.server.util.AbstractStoppableScheduledExecutorServiceImpl;
+
+public class TokenExpirationNotificationExecutorServiceImpl
+ extends AbstractStoppableScheduledExecutorServiceImpl<ScheduledExecutorService>
+ implements TokenExpirationNotificationExecutorService {
+ public TokenExpirationNotificationExecutorServiceImpl() {
+ super(Executors.newSingleThreadScheduledExecutor(
+ new ThreadFactoryBuilder()
+ .setDaemon(false)
+ .setNameFormat("Token-expiration-notification-%d")
+ .build()));
+ }
+}
diff --git a/server/sonar-webserver-auth/src/main/java/org/sonar/server/usertoken/notification/TokenExpirationNotificationInitializer.java b/server/sonar-webserver-auth/src/main/java/org/sonar/server/usertoken/notification/TokenExpirationNotificationInitializer.java
new file mode 100644
index 00000000000..aae268171fd
--- /dev/null
+++ b/server/sonar-webserver-auth/src/main/java/org/sonar/server/usertoken/notification/TokenExpirationNotificationInitializer.java
@@ -0,0 +1,38 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2022 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.usertoken.notification;
+
+import javax.annotation.Nullable;
+import org.sonar.api.platform.Server;
+import org.sonar.api.platform.ServerStartHandler;
+
+public class TokenExpirationNotificationInitializer implements ServerStartHandler {
+ private final TokenExpirationNotificationScheduler scheduler;
+
+ public TokenExpirationNotificationInitializer(@Nullable TokenExpirationNotificationScheduler scheduler) {
+ this.scheduler = scheduler;
+ }
+
+ @Override public void onServerStart(Server server) {
+ if (scheduler != null) {
+ scheduler.startScheduling();
+ }
+ }
+}
diff --git a/server/sonar-webserver-auth/src/main/java/org/sonar/server/usertoken/notification/TokenExpirationNotificationScheduler.java b/server/sonar-webserver-auth/src/main/java/org/sonar/server/usertoken/notification/TokenExpirationNotificationScheduler.java
new file mode 100644
index 00000000000..7f2457fdafa
--- /dev/null
+++ b/server/sonar-webserver-auth/src/main/java/org/sonar/server/usertoken/notification/TokenExpirationNotificationScheduler.java
@@ -0,0 +1,27 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2022 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.usertoken.notification;
+
+import org.sonar.api.server.ServerSide;
+
+@ServerSide
+public interface TokenExpirationNotificationScheduler {
+ void startScheduling();
+}
diff --git a/server/sonar-webserver-auth/src/main/java/org/sonar/server/usertoken/notification/TokenExpirationNotificationSchedulerImpl.java b/server/sonar-webserver-auth/src/main/java/org/sonar/server/usertoken/notification/TokenExpirationNotificationSchedulerImpl.java
new file mode 100644
index 00000000000..fc95425503c
--- /dev/null
+++ b/server/sonar-webserver-auth/src/main/java/org/sonar/server/usertoken/notification/TokenExpirationNotificationSchedulerImpl.java
@@ -0,0 +1,70 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2022 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.usertoken.notification;
+
+import com.google.common.annotations.VisibleForTesting;
+import java.time.Duration;
+import java.time.LocalDateTime;
+import org.sonar.api.utils.log.Logger;
+import org.sonar.api.utils.log.Loggers;
+import org.sonar.server.util.GlobalLockManager;
+
+import static java.util.concurrent.TimeUnit.DAYS;
+import static java.util.concurrent.TimeUnit.SECONDS;
+
+public class TokenExpirationNotificationSchedulerImpl implements TokenExpirationNotificationScheduler {
+ // Lock 23 hours in case of server restart or multiple nodes in data center edition
+ private static int LOCK_DURATION = 23 * 60 * 60;
+ private static String LOCK_NAME = "token-notif";
+ private static final Logger LOG = Loggers.get(TokenExpirationNotificationSchedulerImpl.class);
+ private final TokenExpirationNotificationExecutorService executorService;
+ private final GlobalLockManager lockManager;
+ private final TokenExpirationNotificationSender notificationSender;
+
+ public TokenExpirationNotificationSchedulerImpl(TokenExpirationNotificationExecutorService executorService, GlobalLockManager lockManager,
+ TokenExpirationNotificationSender notificationSender) {
+ this.executorService = executorService;
+ this.lockManager = lockManager;
+ this.notificationSender = notificationSender;
+ }
+
+ @Override
+ public void startScheduling() {
+ LocalDateTime now = LocalDateTime.now();
+ // schedule run at midnight everyday
+ LocalDateTime nextRun = now.plusDays(1).withHour(0).withMinute(0).withSecond(0);
+ long initialDelay = Duration.between(now, nextRun).getSeconds();
+ executorService.scheduleAtFixedRate(this::notifyTokenExpiration, initialDelay, DAYS.toSeconds(1), SECONDS);
+ }
+
+ @VisibleForTesting
+ void notifyTokenExpiration() {
+ try {
+ // Avoid notification multiple times in case of data center edition
+ if (!lockManager.tryLock(LOCK_NAME, LOCK_DURATION)) {
+ return;
+ }
+ notificationSender.sendNotifications();
+ } catch (RuntimeException e) {
+ LOG.error("Error in sending token expiration notification", e);
+ }
+ }
+
+}
diff --git a/server/sonar-webserver-auth/src/main/java/org/sonar/server/usertoken/notification/TokenExpirationNotificationSender.java b/server/sonar-webserver-auth/src/main/java/org/sonar/server/usertoken/notification/TokenExpirationNotificationSender.java
new file mode 100644
index 00000000000..d40cdcc67dd
--- /dev/null
+++ b/server/sonar-webserver-auth/src/main/java/org/sonar/server/usertoken/notification/TokenExpirationNotificationSender.java
@@ -0,0 +1,56 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2022 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.usertoken.notification;
+
+import java.util.Map;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+import org.sonar.api.utils.log.Logger;
+import org.sonar.api.utils.log.Loggers;
+import org.sonar.db.DbClient;
+import org.sonar.db.user.UserDto;
+import org.sonar.db.user.UserTokenDto;
+
+public class TokenExpirationNotificationSender {
+ private static final Logger LOG = Loggers.get(TokenExpirationNotificationSender.class);
+ private final DbClient dbClient;
+ private final TokenExpirationEmailComposer emailComposer;
+
+ public TokenExpirationNotificationSender(DbClient dbClient, TokenExpirationEmailComposer emailComposer) {
+ this.dbClient = dbClient;
+ this.emailComposer = emailComposer;
+ }
+
+ public void sendNotifications() {
+ if (!emailComposer.areEmailSettingsSet()) {
+ LOG.debug("Emails for token expiration notification have not been sent because email settings are not configured.");
+ return;
+ }
+ try (var dbSession = dbClient.openSession(false)) {
+ var expiringTokens = dbClient.userTokenDao().selectTokensExpiredInDays(dbSession, 7);
+ var expiredTokens = dbClient.userTokenDao().selectExpiredTokens(dbSession);
+ var tokensToNotify = Stream.concat(expiringTokens.stream(), expiredTokens.stream()).collect(Collectors.toList());
+ var usersToNotify = tokensToNotify.stream().map(UserTokenDto::getUserUuid).collect(Collectors.toSet());
+ Map<String, String> userUuidToEmail = dbClient.userDao().selectByUuids(dbSession, usersToNotify).stream()
+ .collect(Collectors.toMap(UserDto::getUuid, UserDto::getEmail));
+ tokensToNotify.stream().map(token -> new TokenExpirationEmail(userUuidToEmail.get(token.getUserUuid()), token)).forEach(emailComposer::send);
+ }
+ }
+}
diff --git a/server/sonar-webserver-auth/src/test/java/org/sonar/server/usertoken/UserTokenAuthenticationTest.java b/server/sonar-webserver-auth/src/test/java/org/sonar/server/usertoken/UserTokenAuthenticationTest.java
index 44d3cc141d8..96faea38141 100644
--- a/server/sonar-webserver-auth/src/test/java/org/sonar/server/usertoken/UserTokenAuthenticationTest.java
+++ b/server/sonar-webserver-auth/src/test/java/org/sonar/server/usertoken/UserTokenAuthenticationTest.java
@@ -19,7 +19,7 @@
*/
package org.sonar.server.usertoken;
-import java.time.ZoneId;
+import java.time.ZoneOffset;
import java.time.ZonedDateTime;
import java.util.Base64;
import java.util.Optional;
@@ -112,7 +112,7 @@ public class UserTokenAuthenticationTest {
String token = "known-token";
String tokenHash = "123456789";
- long expirationTimestamp = ZonedDateTime.now(ZoneId.systemDefault()).plusDays(10).toInstant().toEpochMilli();
+ long expirationTimestamp = ZonedDateTime.now(ZoneOffset.UTC).plusDays(10).toInstant().toEpochMilli();
when(request.getHeader(AUTHORIZATION_HEADER)).thenReturn("Basic " + toBase64(token + ":"));
when(tokenGenerator.hash(token)).thenReturn(tokenHash);
UserDto user1 = db.users().insertUser();
diff --git a/server/sonar-webserver-auth/src/test/java/org/sonar/server/usertoken/UserTokenModuleTest.java b/server/sonar-webserver-auth/src/test/java/org/sonar/server/usertoken/UserTokenModuleTest.java
index 563e316ab5c..3f34d454947 100644
--- a/server/sonar-webserver-auth/src/test/java/org/sonar/server/usertoken/UserTokenModuleTest.java
+++ b/server/sonar-webserver-auth/src/test/java/org/sonar/server/usertoken/UserTokenModuleTest.java
@@ -29,6 +29,6 @@ public class UserTokenModuleTest {
public void verify_count_of_added_components() {
ListContainer container = new ListContainer();
new UserTokenModule().configure(container);
- assertThat(container.getAddedObjects()).hasSize(2);
+ assertThat(container.getAddedObjects()).hasSize(7);
}
}
diff --git a/server/sonar-webserver-auth/src/test/java/org/sonar/server/usertoken/notification/TokenExpirationEmailComposerTest.java b/server/sonar-webserver-auth/src/test/java/org/sonar/server/usertoken/notification/TokenExpirationEmailComposerTest.java
new file mode 100644
index 00000000000..17a6ad0c936
--- /dev/null
+++ b/server/sonar-webserver-auth/src/test/java/org/sonar/server/usertoken/notification/TokenExpirationEmailComposerTest.java
@@ -0,0 +1,104 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2022 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.usertoken.notification;
+
+import java.net.MalformedURLException;
+import java.time.Instant;
+import java.time.LocalDate;
+import java.time.ZoneOffset;
+import java.time.format.DateTimeFormatter;
+import org.apache.commons.mail.EmailException;
+import org.apache.commons.mail.HtmlEmail;
+import org.junit.Before;
+import org.junit.Test;
+import org.sonar.api.config.EmailSettings;
+import org.sonar.db.user.TokenType;
+import org.sonar.db.user.UserTokenDto;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+public class TokenExpirationEmailComposerTest {
+ private final EmailSettings emailSettings = mock(EmailSettings.class);
+ private final long createdAt = LocalDate.parse("2022-01-01").atStartOfDay(ZoneOffset.UTC).toInstant().toEpochMilli();
+ private final TokenExpirationEmailComposer underTest = new TokenExpirationEmailComposer(emailSettings);
+
+ @Before
+ public void setup() {
+ when(emailSettings.getServerBaseURL()).thenReturn("http://localhost");
+ }
+
+ @Test
+ public void composer_email_with_expiring_project_token() throws MalformedURLException, EmailException {
+ long expiredDate = LocalDate.now().atStartOfDay(ZoneOffset.UTC).plusDays(7).toInstant().toEpochMilli();
+ var token = createToken("projectToken", "projectA", expiredDate);
+ var emailData = new TokenExpirationEmail("admin@sonarsource.com", token);
+ var email = mock(HtmlEmail.class);
+ underTest.addReportContent(email, emailData);
+ verify(email).setSubject(String.format("Your token with name \"projectToken\" will expire on %s.", parseDate(expiredDate)));
+ verify(email).setHtmlMsg(String.format("Token Summary<br/><br/>"
+ + "Name: projectToken<br/>"
+ + "Type: PROJECT_ANALYSIS_TOKEN<br/>"
+ + "Project: projectA<br/>"
+ + "Created on: 01/01/2022<br/>"
+ + "Last used on: 01/01/2022<br/>"
+ + "Expires on: %s<br/><br/>"
+ + "If this token is still needed, visit <a href=\"http://localhost/account/security/\">here</a> to generate an equivalent.",
+ parseDate(expiredDate)));
+ }
+
+ @Test
+ public void composer_email_with_expired_global_token() throws MalformedURLException, EmailException {
+ long expiredDate = LocalDate.now().atStartOfDay(ZoneOffset.UTC).toInstant().toEpochMilli();
+ var token = createToken("globalToken", null, expiredDate);
+ var emailData = new TokenExpirationEmail("admin@sonarsource.com", token);
+ var email = mock(HtmlEmail.class);
+ underTest.addReportContent(email, emailData);
+ verify(email).setSubject("Your token with name \"globalToken\" has expired.");
+ verify(email).setHtmlMsg(String.format("Token Summary<br/><br/>"
+ + "Name: globalToken<br/>"
+ + "Type: GLOBAL_ANALYSIS_TOKEN<br/>"
+ + "Created on: 01/01/2022<br/>"
+ + "Last used on: 01/01/2022<br/>"
+ + "Expired on: %s<br/><br/>"
+ + "If this token is still needed, visit <a href=\"http://localhost/account/security/\">here</a> to generate an equivalent.",
+ parseDate(expiredDate)));
+ }
+
+ private UserTokenDto createToken(String name, String project, long expired) {
+ var token = new UserTokenDto();
+ token.setName(name);
+ if (project != null) {
+ token.setType(TokenType.PROJECT_ANALYSIS_TOKEN.name());
+ token.setProjectName(project);
+ } else {
+ token.setType(TokenType.GLOBAL_ANALYSIS_TOKEN.name());
+ }
+ token.setCreatedAt(createdAt);
+ token.setLastConnectionDate(createdAt);
+ token.setExpirationDate(expired);
+ return token;
+ }
+
+ private String parseDate(long timestamp) {
+ return Instant.ofEpochMilli(timestamp).atZone(ZoneOffset.UTC).toLocalDate().format(DateTimeFormatter.ofPattern("dd/MM/yyyy"));
+ }
+}
diff --git a/server/sonar-webserver-auth/src/test/java/org/sonar/server/usertoken/notification/TokenExpirationNotificationExecutorServiceImplTest.java b/server/sonar-webserver-auth/src/test/java/org/sonar/server/usertoken/notification/TokenExpirationNotificationExecutorServiceImplTest.java
new file mode 100644
index 00000000000..f7c7bb54ded
--- /dev/null
+++ b/server/sonar-webserver-auth/src/test/java/org/sonar/server/usertoken/notification/TokenExpirationNotificationExecutorServiceImplTest.java
@@ -0,0 +1,35 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2022 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.usertoken.notification;
+
+import org.junit.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class TokenExpirationNotificationExecutorServiceImplTest {
+ @Test
+ public void constructor_createsExecutorDelegateThatIsReadyToAct(){
+ TokenExpirationNotificationExecutorService tokenExpirationNotificationExecutorService = new TokenExpirationNotificationExecutorServiceImpl();
+
+ assertThat(tokenExpirationNotificationExecutorService.isShutdown()).isFalse();
+ assertThat(tokenExpirationNotificationExecutorService.isTerminated()).isFalse();
+ }
+
+}
diff --git a/server/sonar-webserver-auth/src/test/java/org/sonar/server/usertoken/notification/TokenExpirationNotificationInitializerTest.java b/server/sonar-webserver-auth/src/test/java/org/sonar/server/usertoken/notification/TokenExpirationNotificationInitializerTest.java
new file mode 100644
index 00000000000..2069f783df4
--- /dev/null
+++ b/server/sonar-webserver-auth/src/test/java/org/sonar/server/usertoken/notification/TokenExpirationNotificationInitializerTest.java
@@ -0,0 +1,46 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2022 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.usertoken.notification;
+
+import org.junit.Test;
+import org.sonar.api.platform.Server;
+
+import static org.assertj.core.api.Assertions.assertThatNoException;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+public class TokenExpirationNotificationInitializerTest {
+ @Test
+ public void when_scheduler_should_start_on_server_start() {
+ var scheduler = mock(TokenExpirationNotificationScheduler.class);
+ var underTest = new TokenExpirationNotificationInitializer(scheduler);
+ underTest.onServerStart(mock(Server.class));
+ verify(scheduler, times(1)).startScheduling();
+ }
+
+ @Test
+ public void server_start_with_no_scheduler_still_work() {
+ var underTest = new TokenExpirationNotificationInitializer(null);
+ underTest.onServerStart(mock(Server.class));
+ assertThatNoException();
+ }
+
+}
diff --git a/server/sonar-webserver-auth/src/test/java/org/sonar/server/usertoken/notification/TokenExpirationNotificationSchedulerImplTest.java b/server/sonar-webserver-auth/src/test/java/org/sonar/server/usertoken/notification/TokenExpirationNotificationSchedulerImplTest.java
new file mode 100644
index 00000000000..b13f9c3e065
--- /dev/null
+++ b/server/sonar-webserver-auth/src/test/java/org/sonar/server/usertoken/notification/TokenExpirationNotificationSchedulerImplTest.java
@@ -0,0 +1,75 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2022 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.usertoken.notification;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.sonar.api.utils.log.LogAndArguments;
+import org.sonar.api.utils.log.LogTester;
+import org.sonar.api.utils.log.LoggerLevel;
+import org.sonar.server.util.GlobalLockManager;
+
+import static java.util.concurrent.TimeUnit.DAYS;
+import static java.util.concurrent.TimeUnit.SECONDS;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoInteractions;
+import static org.mockito.Mockito.when;
+
+public class TokenExpirationNotificationSchedulerImplTest {
+ @Rule
+ public LogTester logTester = new LogTester();
+ private final TokenExpirationNotificationExecutorService executorService = mock(TokenExpirationNotificationExecutorService.class);
+ private final GlobalLockManager lockManager = mock(GlobalLockManager.class);
+ private final TokenExpirationNotificationSender notificationSender = mock(TokenExpirationNotificationSender.class);
+ private final TokenExpirationNotificationSchedulerImpl underTest = new TokenExpirationNotificationSchedulerImpl(executorService, lockManager,
+ notificationSender);
+
+ @Test
+ public void startScheduling() {
+ underTest.startScheduling();
+ verify(executorService, times(1)).scheduleAtFixedRate(any(Runnable.class), anyLong(), eq(DAYS.toSeconds(1)), eq(SECONDS));
+ }
+
+ @Test
+ public void no_notification_if_it_is_already_sent() {
+ when(lockManager.tryLock(anyString(), anyInt())).thenReturn(false);
+ underTest.notifyTokenExpiration();
+ verifyNoInteractions(notificationSender);
+ }
+
+ @Test
+ public void log_error_if_exception_in_sending_notification() {
+ when(lockManager.tryLock(anyString(), anyInt())).thenReturn(true);
+ doThrow(new IllegalStateException()).when(notificationSender).sendNotifications();
+ underTest.notifyTokenExpiration();
+ assertThat(logTester.getLogs(LoggerLevel.ERROR))
+ .extracting(LogAndArguments::getFormattedMsg)
+ .containsExactly("Error in sending token expiration notification");
+ }
+}
diff --git a/server/sonar-webserver-auth/src/test/java/org/sonar/server/usertoken/notification/TokenExpirationNotificationSenderTest.java b/server/sonar-webserver-auth/src/test/java/org/sonar/server/usertoken/notification/TokenExpirationNotificationSenderTest.java
new file mode 100644
index 00000000000..39943e89735
--- /dev/null
+++ b/server/sonar-webserver-auth/src/test/java/org/sonar/server/usertoken/notification/TokenExpirationNotificationSenderTest.java
@@ -0,0 +1,87 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2022 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.usertoken.notification;
+
+import java.util.List;
+import org.junit.Rule;
+import org.junit.Test;
+import org.mockito.ArgumentCaptor;
+import org.sonar.api.utils.log.LogAndArguments;
+import org.sonar.api.utils.log.LogTester;
+import org.sonar.api.utils.log.LoggerLevel;
+import org.sonar.db.DbClient;
+import org.sonar.db.user.UserDao;
+import org.sonar.db.user.UserDto;
+import org.sonar.db.user.UserTokenDao;
+import org.sonar.db.user.UserTokenDto;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+public class TokenExpirationNotificationSenderTest {
+ @Rule
+ public LogTester logTester = new LogTester();
+ private final DbClient dbClient = mock(DbClient.class);
+ private final TokenExpirationEmailComposer emailComposer = mock(TokenExpirationEmailComposer.class);
+ private final TokenExpirationNotificationSender underTest = new TokenExpirationNotificationSender(dbClient, emailComposer);
+
+ @Test
+ public void no_notification_when_email_setting_is_not_set() {
+ when(emailComposer.areEmailSettingsSet()).thenReturn(false);
+ underTest.sendNotifications();
+ assertThat(logTester.getLogs(LoggerLevel.DEBUG))
+ .extracting(LogAndArguments::getFormattedMsg)
+ .containsExactly("Emails for token expiration notification have not been sent because email settings are not configured.");
+ }
+
+ @Test
+ public void send_notification() {
+ var expiringToken = new UserTokenDto().setUserUuid("admin");
+ var expiredToken = new UserTokenDto().setUserUuid("admin");
+ var user = mock(UserDto.class);
+ when(user.getUuid()).thenReturn("admin");
+ when(user.getEmail()).thenReturn("admin@admin.com");
+ var userTokenDao = mock(UserTokenDao.class);
+ var userDao = mock(UserDao.class);
+ when(userDao.selectByUuids(any(), any())).thenReturn(List.of(user));
+ when(userTokenDao.selectTokensExpiredInDays(any(), anyLong())).thenReturn(List.of(expiringToken));
+ when(userTokenDao.selectExpiredTokens(any())).thenReturn(List.of(expiredToken));
+ when(dbClient.userTokenDao()).thenReturn(userTokenDao);
+ when(dbClient.userDao()).thenReturn(userDao);
+ when(emailComposer.areEmailSettingsSet()).thenReturn(true);
+
+ underTest.sendNotifications();
+
+ var argumentCaptor = ArgumentCaptor.forClass(TokenExpirationEmail.class);
+ verify(emailComposer, times(2)).send(argumentCaptor.capture());
+ List<TokenExpirationEmail> emails = argumentCaptor.getAllValues();
+ assertThat(emails).hasSize(2);
+ assertThat(emails.get(0).getRecipients()).containsOnly("admin@admin.com");
+ assertThat(emails.get(0).getUserToken()).isEqualTo(expiringToken);
+ assertThat(emails.get(1).getRecipients()).containsOnly("admin@admin.com");
+ assertThat(emails.get(1).getUserToken()).isEqualTo(expiredToken);
+ }
+
+}