]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-16567 Use user-friendly date format and improve notification message
authorZipeng WU <zipeng.wu@sonarsource.com>
Fri, 15 Jul 2022 13:24:01 +0000 (15:24 +0200)
committersonartech <sonartech@sonarsource.com>
Mon, 18 Jul 2022 20:03:26 +0000 (20:03 +0000)
server/sonar-docs/src/pages/setup/operate-server.md
server/sonar-webserver-auth/src/main/java/org/sonar/server/authentication/UserSessionInitializer.java
server/sonar-webserver-auth/src/main/java/org/sonar/server/usertoken/notification/TokenExpirationEmailComposer.java
server/sonar-webserver-auth/src/test/java/org/sonar/server/authentication/UserSessionInitializerTest.java
server/sonar-webserver-auth/src/test/java/org/sonar/server/usertoken/notification/TokenExpirationEmailComposerTest.java
server/sonar-webserver-webapi/src/main/java/org/sonar/server/usertoken/ws/GenerateAction.java
sonar-scanner-engine/src/main/java/org/sonar/scanner/bootstrap/DefaultScannerWsClient.java
sonar-scanner-engine/src/test/java/org/sonar/scanner/bootstrap/DefaultScannerWsClientTest.java
sonar-ws/src/main/java/org/sonarqube/ws/client/MockWsResponse.java

index b2e79ffdcc234d4899451576ca8aa279fa288863..d336946dd7ce3f4bd4837c8ffe06e59f2a306e52 100644 (file)
@@ -200,7 +200,7 @@ server {
 ```
 
 ### Forward SonarQube custom headers
-SonarQube adds custom HTTP headers in order for some features to function properly. The reverse proxy should be configured to forward the following header: `sq-authentication-token-expiration`.
+SonarQube adds custom HTTP headers in order for some features to function properly. The reverse proxy should be configured to forward the following header: `SonarQube-Authentication-Token-Expiration`.
 
 ## Secure your Network
 
index 880fc46ccb5dffb92c4cdf4f485e367109e67d0f..1ccb613385b09ae245c62405804428cab1925d27 100644 (file)
@@ -52,7 +52,7 @@ public class UserSessionInitializer {
    */
   private static final String ACCESS_LOG_LOGIN = "LOGIN";
 
-  private static final String SQ_AUTHENTICATION_TOKEN_EXPIRATION = "sq-authentication-token-expiration";
+  private static final String SQ_AUTHENTICATION_TOKEN_EXPIRATION = "SonarQube-Authentication-Token-Expiration";
 
   // SONAR-6546 these urls should be get from WebService
   private static final Set<String> SKIPPED_URLS = Set.of(
index d26497caf4001ae66fe8f0ac678f519d1d946f4d..3202d08ef0588ccb446e051841148ca989bf1e6c 100644 (file)
@@ -42,16 +42,22 @@ public class TokenExpirationEmailComposer extends EmailSender<TokenExpirationEma
     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()));
+      email.setSubject(format("Your token \"%s\" has expired.", token.getName()));
     } else {
-      email.setSubject(format("Your token with name \"%s\" will expire on %s.", token.getName(), parseDate(token.getExpirationDate())));
+      email.setSubject(format("Your token \"%s\" will expire.", token.getName()));
     }
     email.setHtmlMsg(composeEmailBody(token));
   }
 
   private String composeEmailBody(UserTokenDto token) {
     StringBuilder builder = new StringBuilder();
-    builder.append("Token Summary<br/><br/>")
+    if (token.isExpired()) {
+      builder.append(format("Your token \"%s\" has expired.<br/><br/>", token.getName()));
+    } else {
+      builder.append(format("Your token \"%s\" will expire on %s.<br/><br/>", token.getName(), parseDate(token.getExpirationDate())));
+    }
+    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())) {
@@ -62,11 +68,16 @@ public class TokenExpirationEmailComposer extends EmailSender<TokenExpirationEma
       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()));
+      .append(
+        format("<br/>If this token is still needed, please consider <a href=\"%s/account/security/\">generating</a> an equivalent.<br/><br/>", emailSettings.getServerBaseURL()))
+      .append("Don't forget to update the token in the locations where it is in use. "
+        + "This may include the CI pipeline that analyzes your projects, "
+        + "the IDE settings that connect SonarLint to SonarQube, "
+        + "and any places where you make calls to web services.");
     return builder.toString();
   }
 
   private static String parseDate(long timestamp) {
-    return Instant.ofEpochMilli(timestamp).atZone(ZoneOffset.UTC).toLocalDate().format(DateTimeFormatter.ofPattern("dd/MM/yyyy"));
+    return Instant.ofEpochMilli(timestamp).atZone(ZoneOffset.UTC).toLocalDate().format(DateTimeFormatter.ofPattern("MMMM dd, yyyy"));
   }
 }
index f577adaea425984b3ebc7753f17bfef71f151568..4e1608ef11904f4db61e3812ad770353cb3d232d 100644 (file)
@@ -212,7 +212,7 @@ public class UserSessionInitializerTest {
     when(threadLocalSession.isLoggedIn()).thenReturn(true);
 
     assertThat(underTest.initUserSession(request, response)).isTrue();
-    verify(response).addHeader("sq-authentication-token-expiration", formatDateTime(expirationTimestamp));
+    verify(response).addHeader("SonarQube-Authentication-Token-Expiration", formatDateTime(expirationTimestamp));
   }
 
   private void assertPathIsIgnored(String path) {
index 17a6ad0c936bf02273ffd1bd4c2c92c82dfd20bf..d5d1513be45f9f0fa15e1e078128668eaab777cd 100644 (file)
@@ -53,16 +53,19 @@ public class TokenExpirationEmailComposerTest {
     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)));
+    verify(email).setSubject(String.format("Your token \"projectToken\" will expire."));
+    verify(email).setHtmlMsg(
+      String.format("Your token \"projectToken\" will expire on %s.<br/><br/>"
+          + "Token Summary<br/><br/>"
+          + "Name: projectToken<br/>"
+          + "Type: PROJECT_ANALYSIS_TOKEN<br/>"
+          + "Project: projectA<br/>"
+          + "Created on: January 01, 2022<br/>"
+          + "Last used on: January 01, 2022<br/>"
+          + "Expires on: %s<br/><br/>"
+          + "If this token is still needed, please consider <a href=\"http://localhost/account/security/\">generating</a> an equivalent.<br/><br/>"
+          + "Don't forget to update the token in the locations where it is in use. This may include the CI pipeline that analyzes your projects, the IDE settings that connect SonarLint to SonarQube, and any places where you make calls to web services.",
+        parseDate(expiredDate), parseDate(expiredDate)));
   }
 
   @Test
@@ -72,15 +75,18 @@ public class TokenExpirationEmailComposerTest {
     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)));
+    verify(email).setSubject("Your token \"globalToken\" has expired.");
+    verify(email).setHtmlMsg(
+      String.format("Your token \"globalToken\" has expired.<br/><br/>"
+          + "Token Summary<br/><br/>"
+          + "Name: globalToken<br/>"
+          + "Type: GLOBAL_ANALYSIS_TOKEN<br/>"
+          + "Created on: January 01, 2022<br/>"
+          + "Last used on: January 01, 2022<br/>"
+          + "Expired on: %s<br/><br/>"
+          + "If this token is still needed, please consider <a href=\"http://localhost/account/security/\">generating</a> an equivalent.<br/><br/>"
+          + "Don't forget to update the token in the locations where it is in use. This may include the CI pipeline that analyzes your projects, the IDE settings that connect SonarLint to SonarQube, and any places where you make calls to web services.",
+        parseDate(expiredDate)));
   }
 
   private UserTokenDto createToken(String name, String project, long expired) {
@@ -99,6 +105,6 @@ public class TokenExpirationEmailComposerTest {
   }
 
   private String parseDate(long timestamp) {
-    return Instant.ofEpochMilli(timestamp).atZone(ZoneOffset.UTC).toLocalDate().format(DateTimeFormatter.ofPattern("dd/MM/yyyy"));
+    return Instant.ofEpochMilli(timestamp).atZone(ZoneOffset.UTC).toLocalDate().format(DateTimeFormatter.ofPattern("MMMM dd, yyyy"));
   }
 }
index 95ed509eb323e3a3d55d09ca5b0bb14c9cfc1a71..67599db213d24330ab3e7f5e1ba5f6c406541361 100644 (file)
@@ -108,7 +108,7 @@ public class GenerateAction implements UserTokensWsAction {
 
     action.createParam(PARAM_EXPIRATION_DATE)
       .setSince("9.6")
-      .setDescription("The expiration date of the token being generated, in ISO 8601 format (YYYY-MM-DD).");
+      .setDescription("The expiration date of the token being generated, in ISO 8601 format (YYYY-MM-DD). If not set, default to no expiration.");
   }
 
   @Override
index 0be294a4f996037b1c0ad4c9aec38a4692805b99..b1ec13c67321bd16f58e2fd0b0ef15e723ce99b7 100644 (file)
@@ -23,7 +23,6 @@ import com.google.gson.JsonArray;
 import com.google.gson.JsonElement;
 import com.google.gson.JsonObject;
 import com.google.gson.JsonParser;
-import java.time.LocalDateTime;
 import java.time.ZoneOffset;
 import java.time.ZonedDateTime;
 import java.time.format.DateTimeFormatter;
@@ -55,7 +54,8 @@ import static org.sonar.api.utils.Preconditions.checkState;
 
 public class DefaultScannerWsClient implements ScannerWsClient {
   private static final int MAX_ERROR_MSG_LEN = 128;
-  private static final String SQ_TOKEN_EXPIRATION_HEADER = "sq-authentication-token-expiration";
+  private static final String SQ_TOKEN_EXPIRATION_HEADER = "SonarQube-Authentication-Token-Expiration";
+  private static final DateTimeFormatter USER_FRIENDLY_DATETIME_FORMAT = DateTimeFormatter.ofPattern("MMMM dd, yyyy");
   private static final Logger LOG = Loggers.get(DefaultScannerWsClient.class);
 
   private final Set<String> warningMessages = new HashSet<>();
@@ -129,28 +129,30 @@ public class DefaultScannerWsClient implements ScannerWsClient {
   private void checkAuthenticationWarnings(WsResponse response) {
     if (response.code() == HTTP_OK) {
       response.header(SQ_TOKEN_EXPIRATION_HEADER).ifPresent(expirationDate -> {
-        if (isTokenExpiringInOneWeek(expirationDate)) {
-          addAnalysisWarning(expirationDate);
+        var datetimeInUTC = ZonedDateTime.from(DateTimeFormatter.ofPattern(DATETIME_FORMAT)
+          .parse(expirationDate)).withZoneSameInstant(ZoneOffset.UTC);
+        if (isTokenExpiringInOneWeek(datetimeInUTC)) {
+          addAnalysisWarning(datetimeInUTC);
         }
       });
     }
   }
 
-  private static boolean isTokenExpiringInOneWeek(String expirationDate) {
+  private static boolean isTokenExpiringInOneWeek(ZonedDateTime expirationDate) {
     ZonedDateTime localDateTime = ZonedDateTime.now(ZoneOffset.UTC);
-    ZonedDateTime headerDateTime = LocalDateTime.from(DateTimeFormatter.ofPattern(DATETIME_FORMAT)
-      .parse(expirationDate)).minusDays(7).atZone(ZoneOffset.UTC);
+    ZonedDateTime headerDateTime = expirationDate.minusDays(7);
     return localDateTime.isAfter(headerDateTime);
   }
 
-  private void addAnalysisWarning(String tokenExpirationDate) {
-    String warningMessage = "The token used for this analysis will expire on: " + tokenExpirationDate;
+  private void addAnalysisWarning(ZonedDateTime tokenExpirationDate) {
+    String warningMessage = "The token used for this analysis will expire on: " + tokenExpirationDate.format(USER_FRIENDLY_DATETIME_FORMAT);
     if (!warningMessages.contains(warningMessage)) {
       warningMessages.add(warningMessage);
       LOG.warn(warningMessage);
-      LOG.warn("Analysis executed with this token after the expiration date will fail.");
+      LOG.warn("Analysis executed with this token will fail after the expiration date.");
     }
-    analysisWarnings.addUnique(warningMessage + "\nAnalysis executed with this token after the expiration date will fail.");
+    analysisWarnings.addUnique(warningMessage + "\nAfter this date, the token can no longer be used to execute the analysis. "
+      + "Please consider generating a new token and updating it in the locations where it is in use.");
   }
 
   /**
index 3da98be890153e353a303e2e3c17361e1a0251d0..a1075f4b100b1cef9cbe91d81d6ff6fc1e8bcf9b 100644 (file)
@@ -133,9 +133,10 @@ public class DefaultScannerWsClientTest {
   @Test
   public void warnings_are_added_when_expiration_approaches() {
     WsRequest request = newRequest();
+    var fiveDaysLatter = LocalDateTime.now().atZone(ZoneOffset.UTC).plusDays(5);
     String expirationDate = DateTimeFormatter
       .ofPattern(DATETIME_FORMAT)
-      .format(LocalDateTime.now().atOffset(ZoneOffset.UTC).plusDays(5));
+      .format(fiveDaysLatter);
     WsResponse response = newResponse()
       .setCode(200)
       .setExpirationDate(expirationDate);
@@ -150,8 +151,8 @@ public class DefaultScannerWsClientTest {
     // check logs
     List<String> warningLogs = logTester.logs(LoggerLevel.WARN);
     assertThat(warningLogs).hasSize(2);
-    assertThat(warningLogs.get(0)).contains("The token used for this analysis will expire on: " + expirationDate);
-    assertThat(warningLogs.get(1)).contains("Analysis executed with this token after the expiration date will fail.");
+    assertThat(warningLogs.get(0)).contains("The token used for this analysis will expire on: " + fiveDaysLatter.format(DateTimeFormatter.ofPattern("MMMM dd, yyyy")));
+    assertThat(warningLogs.get(1)).contains("Analysis executed with this token will fail after the expiration date.");
   }
 
   @Test
index 2e799b1806db03f2d324e81b6be0da2aab03b168..855586d1eaebc0579f6a8d5efcd1941a69635132 100644 (file)
@@ -35,7 +35,7 @@ import static java.util.Objects.requireNonNull;
 public class MockWsResponse extends BaseResponse {
 
   private static final String CONTENT_TYPE_HEADER = "Content-Type";
-  private static final String SQ_TOKEN_EXPIRATION_HEADER = "sq-authentication-token-expiration";
+  private static final String SQ_TOKEN_EXPIRATION_HEADER = "SonarQube-Authentication-Token-Expiration";
 
   private int code = HttpURLConnection.HTTP_OK;
   private String requestUrl;