diff options
author | Zipeng WU <zipeng.wu@sonarsource.com> | 2022-07-05 10:00:38 +0200 |
---|---|---|
committer | sonartech <sonartech@sonarsource.com> | 2022-07-06 20:03:56 +0000 |
commit | 6a401f73236a70f702b64646d8bdec7c5a90e15d (patch) | |
tree | 23fc39668ac04e0942d704fa8397e28193d55569 /server/sonar-webserver-auth | |
parent | b31d435c35d2e104b225a6f722e459616b0cb8af (diff) | |
download | sonarqube-6a401f73236a70f702b64646d8bdec7c5a90e15d.tar.gz sonarqube-6a401f73236a70f702b64646d8bdec7c5a90e15d.zip |
SONAR-16567 Notify the token creator about expiring tokens via email
Diffstat (limited to 'server/sonar-webserver-auth')
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); + } + +} |