]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-16565 update the API api/user_tokens/generate for accepting the token expiratio...
authorMatteo Mara <matteo.mara@sonarsource.com>
Wed, 29 Jun 2022 13:31:28 +0000 (15:31 +0200)
committersonartech <sonartech@sonarsource.com>
Fri, 1 Jul 2022 20:03:06 +0000 (20:03 +0000)
server/sonar-db-dao/src/main/java/org/sonar/db/user/UserTokenDto.java
server/sonar-webserver-webapi/src/main/java/org/sonar/server/usertoken/ws/GenerateAction.java
server/sonar-webserver-webapi/src/main/java/org/sonar/server/usertoken/ws/UserTokenSupport.java
server/sonar-webserver-webapi/src/main/resources/org/sonar/server/usertoken/ws/generate-example.json
server/sonar-webserver-webapi/src/test/java/org/sonar/server/usertoken/ws/GenerateActionTest.java
sonar-ws/src/main/protobuf/ws-user_tokens.proto

index 8c7cd8476e4ccac3a00caabb9d226b4f9299cb31..bcd9572efeeaf584c09ba0d8e74d5a4a14c6ff54 100644 (file)
@@ -144,7 +144,8 @@ public class UserTokenDto {
     return projectUuid;
   }
 
-  public void setProjectUuid(String projectUuid) {
+  public UserTokenDto setProjectUuid(@Nullable String projectUuid) {
     this.projectUuid = projectUuid;
+    return this;
   }
 }
index ccecb44f1539a3828d024b197e74f5dbeaee1f61..cfb002233e9b8fafc5f0f84796ec517a271fc28b 100644 (file)
  */
 package org.sonar.server.usertoken.ws;
 
+import java.time.LocalDate;
+import java.time.ZoneId;
+import java.time.format.DateTimeFormatter;
+import java.time.format.DateTimeParseException;
 import java.util.Optional;
-import javax.annotation.Nullable;
+import org.jetbrains.annotations.NotNull;
+import org.sonar.api.server.ws.Change;
 import org.sonar.api.server.ws.Request;
 import org.sonar.api.server.ws.Response;
 import org.sonar.api.server.ws.WebService;
 import org.sonar.api.utils.System2;
 import org.sonar.db.DbClient;
 import org.sonar.db.DbSession;
+import org.sonar.db.user.TokenType;
 import org.sonar.db.user.UserDto;
 import org.sonar.db.user.UserTokenDto;
 import org.sonar.server.exceptions.ServerException;
 import org.sonar.server.usertoken.TokenGenerator;
-import org.sonar.db.user.TokenType;
 import org.sonarqube.ws.UserTokens;
 import org.sonarqube.ws.UserTokens.GenerateWsResponse;
 
 import static com.google.common.base.Preconditions.checkArgument;
 import static java.net.HttpURLConnection.HTTP_INTERNAL_ERROR;
 import static org.sonar.api.utils.DateUtils.formatDateTime;
-import static org.sonar.server.exceptions.BadRequestException.checkRequest;
 import static org.sonar.db.user.TokenType.GLOBAL_ANALYSIS_TOKEN;
 import static org.sonar.db.user.TokenType.PROJECT_ANALYSIS_TOKEN;
 import static org.sonar.db.user.TokenType.USER_TOKEN;
+import static org.sonar.server.exceptions.BadRequestException.checkRequest;
 import static org.sonar.server.usertoken.ws.UserTokenSupport.ACTION_GENERATE;
+import static org.sonar.server.usertoken.ws.UserTokenSupport.PARAM_EXPIRATION_DATE;
 import static org.sonar.server.usertoken.ws.UserTokenSupport.PARAM_LOGIN;
 import static org.sonar.server.usertoken.ws.UserTokenSupport.PARAM_NAME;
 import static org.sonar.server.usertoken.ws.UserTokenSupport.PARAM_PROJECT_KEY;
@@ -73,6 +79,8 @@ public class GenerateAction implements UserTokensWsAction {
       .setDescription("Generate a user access token. <br />" +
         "Please keep your tokens secret. They enable to authenticate and analyze projects.<br />" +
         "It requires administration permissions to specify a 'login' and generate a token for another user. Otherwise, a token is generated for the current user.")
+      .setChangelog(
+        new Change("9.6", "Response field 'expirationDate' added"))
       .setResponseExample(getClass().getResource("generate-example.json"))
       .setHandler(this);
 
@@ -95,6 +103,10 @@ public class GenerateAction implements UserTokensWsAction {
     action.createParam(PARAM_PROJECT_KEY)
       .setSince("9.5")
       .setDescription("The key of the only project that can be analyzed by the " + PROJECT_ANALYSIS_TOKEN.name() + " being generated.");
+
+    action.createParam(PARAM_EXPIRATION_DATE)
+      .setSince("9.6")
+      .setDescription("The expiration date of the token being generated, in ISO 8601 format (YYYY-MM-DD).");
   }
 
   @Override
@@ -105,18 +117,63 @@ public class GenerateAction implements UserTokensWsAction {
 
   private UserTokens.GenerateWsResponse doHandle(Request request) {
     try (DbSession dbSession = dbClient.openSession(false)) {
-      String name = request.mandatoryParam(PARAM_NAME).trim();
-      UserDto user = userTokenSupport.getUser(dbSession, request);
-      checkTokenDoesNotAlreadyExists(dbSession, user, name);
-
       String token = generateToken(request, dbSession);
       String tokenHash = hashToken(dbSession, token);
-      String projectKey = getProjecKeyFromRequest(request).orElse(null);
-      UserTokenDto userTokenDto = insertTokenInDb(dbSession, user, name, tokenHash, getTokenTypeFromRequest(request), projectKey);
+
+      UserTokenDto userTokenDtoFromRequest = getUserTokenDtoFromRequest(request);
+      userTokenDtoFromRequest.setTokenHash(tokenHash);
+
+      UserDto user = userTokenSupport.getUser(dbSession, request);
+      userTokenDtoFromRequest.setUserUuid(user.getUuid());
+
+      UserTokenDto userTokenDto = insertTokenInDb(dbSession, user, userTokenDtoFromRequest);
+
       return buildResponse(userTokenDto, token, user);
     }
   }
 
+  private UserTokenDto getUserTokenDtoFromRequest(Request request) {
+    UserTokenDto userTokenDtoFromRequest = new UserTokenDto()
+      .setName(request.mandatoryParam(PARAM_NAME).trim())
+      .setCreatedAt(system.now())
+      .setType(getTokenTypeFromRequest(request).name())
+      .setExpirationDate(getExpirationDateFromRequest(request));
+
+    getProjectKeyFromRequest(request).ifPresent(userTokenDtoFromRequest::setProjectKey);
+
+    return userTokenDtoFromRequest;
+  }
+
+  private static Long getExpirationDateFromRequest(Request request) {
+    String expirationDateString = request.param(PARAM_EXPIRATION_DATE);
+    Long expirationDateOpt = null;
+
+    if (expirationDateString != null) {
+      try {
+        expirationDateOpt = getExpirationDateFromString(expirationDateString);
+      } catch (DateTimeParseException e) {
+        throw new IllegalArgumentException(String.format("Supplied date format for parameter %s is wrong. Please supply date in the ISO 8601 " +
+          "date format (YYYY-MM-DD)", PARAM_EXPIRATION_DATE));
+      }
+    }
+
+    return expirationDateOpt;
+  }
+
+  @NotNull
+  private static Long getExpirationDateFromString(String expirationDateString) {
+    LocalDate expirationDate = LocalDate.parse(expirationDateString, DateTimeFormatter.ISO_DATE);
+    validateExpirationDateValue(expirationDate);
+    return expirationDate.atStartOfDay(ZoneId.systemDefault()).toInstant().toEpochMilli();
+  }
+
+  private static void validateExpirationDateValue(LocalDate localDate) {
+    if (localDate.isBefore(LocalDate.now().plusDays(1))) {
+      throw new IllegalArgumentException(
+        String.format("The minimum value for parameter %s is %s.", PARAM_EXPIRATION_DATE, LocalDate.now().plusDays(1).format(DateTimeFormatter.ISO_DATE)));
+    }
+  }
+
   private String generateToken(Request request, DbSession dbSession) {
     TokenType tokenType = getTokenTypeFromRequest(request);
     validateParametersCombination(dbSession, request, tokenType);
@@ -134,7 +191,7 @@ public class GenerateAction implements UserTokensWsAction {
   private void validateProjectAnalysisParameters(DbSession dbSession, Request request) {
     checkArgument(userTokenSupport.sameLoginAsConnectedUser(request), "A Project Analysis Token cannot be generated for another user.");
     checkArgument(request.param(PARAM_PROJECT_KEY) != null, "A projectKey is needed when creating Project Analysis Token");
-    userTokenSupport.validateProjectScanPermission(dbSession, getProjecKeyFromRequest(request).orElse(""));
+    userTokenSupport.validateProjectScanPermission(dbSession, getProjectKeyFromRequest(request).orElse(""));
   }
 
   private void validateGlobalAnalysisParameters(Request request) {
@@ -142,7 +199,7 @@ public class GenerateAction implements UserTokensWsAction {
     userTokenSupport.validateGlobalScanPermission();
   }
 
-  private static Optional<String> getProjecKeyFromRequest(Request request) {
+  private static Optional<String> getProjectKeyFromRequest(Request request) {
     String projectKey = null;
     if (PROJECT_ANALYSIS_TOKEN.equals(getTokenTypeFromRequest(request))) {
       projectKey = request.mandatoryParam(PARAM_PROJECT_KEY).trim();
@@ -164,28 +221,18 @@ public class GenerateAction implements UserTokensWsAction {
     throw new ServerException(HTTP_INTERNAL_ERROR, "Error while generating token. Please try again.");
   }
 
-  private void checkTokenDoesNotAlreadyExists(DbSession dbSession, UserDto user, String name) {
-    UserTokenDto userTokenDto = dbClient.userTokenDao().selectByUserAndName(dbSession, user, name);
-    checkRequest(userTokenDto == null, "A user token for login '%s' and name '%s' already exists", user.getLogin(), name);
-  }
-
-  private UserTokenDto insertTokenInDb(DbSession dbSession, UserDto user, String name, String tokenHash, TokenType tokenType, @Nullable String projectKey) {
-    UserTokenDto userTokenDto = new UserTokenDto()
-      .setUserUuid(user.getUuid())
-      .setName(name)
-      .setTokenHash(tokenHash)
-      .setCreatedAt(system.now())
-      .setType(tokenType.name());
-
-    if (projectKey != null) {
-      userTokenDto.setProjectKey(projectKey);
-    }
-
+  private UserTokenDto insertTokenInDb(DbSession dbSession, UserDto user,UserTokenDto userTokenDto) {
+    checkTokenDoesNotAlreadyExists(dbSession, user, userTokenDto.getName());
     dbClient.userTokenDao().insert(dbSession, userTokenDto, user.getLogin());
     dbSession.commit();
     return userTokenDto;
   }
 
+  private void checkTokenDoesNotAlreadyExists(DbSession dbSession, UserDto user, String name) {
+    UserTokenDto userTokenDto = dbClient.userTokenDao().selectByUserAndName(dbSession, user, name);
+    checkRequest(userTokenDto == null, "A user token for login '%s' and name '%s' already exists", user.getLogin(), name);
+  }
+
   private static GenerateWsResponse buildResponse(UserTokenDto userTokenDto, String token, UserDto user) {
     GenerateWsResponse.Builder responseBuilder = GenerateWsResponse.newBuilder()
       .setLogin(user.getLogin())
@@ -198,6 +245,10 @@ public class GenerateAction implements UserTokensWsAction {
       responseBuilder.setProjectKey(userTokenDto.getProjectKey());
     }
 
+    if (userTokenDto.getExpirationDate() != null) {
+      responseBuilder.setExpirationDate(formatDateTime(userTokenDto.getExpirationDate()));
+    }
+
     return responseBuilder.build();
   }
 
index 118d9014086fc0cb22207dbc26f0268a235d45f0..2e0a64fbca73351301e40f6e5de839a21781472e 100644 (file)
@@ -46,6 +46,7 @@ public class UserTokenSupport {
   static final String PARAM_NAME = "name";
   static final String PARAM_TYPE = "type";
   static final String PARAM_PROJECT_KEY = "projectKey";
+  static final String PARAM_EXPIRATION_DATE = "expirationDate";
 
   private final DbClient dbClient;
   private final UserSession userSession;
index c17d43fbf52c5541215c538525bdd7c80b915355..d32ecf2284dd0900ecba747f39b5c5cd7d961ab5 100644 (file)
@@ -2,6 +2,7 @@
   "login": "grace.hopper",
   "name": "Third Party Application",
   "createdAt": "2018-01-10T14:06:05+0100",
+  "expirationDate": "2022-07-14T00:00:00+0100",
   "token": "123456789",
   "type": "USER_TOKEN"
 }
index a56be8b720c68962df4d2e15ac49b1fc4d3dc7d8..3a0412f87fa4b584eb2aeb27fc51920007112266 100644 (file)
  */
 package org.sonar.server.usertoken.ws;
 
+import java.time.LocalDate;
+import java.time.ZoneId;
+import java.time.ZonedDateTime;
+import java.time.format.DateTimeFormatter;
 import javax.annotation.Nullable;
 import org.junit.Before;
 import org.junit.Rule;
@@ -27,6 +31,7 @@ import org.sonar.api.server.ws.WebService;
 import org.sonar.api.utils.System2;
 import org.sonar.db.DbTester;
 import org.sonar.db.component.ComponentDto;
+import org.sonar.db.user.TokenType;
 import org.sonar.db.user.UserDto;
 import org.sonar.server.exceptions.BadRequestException;
 import org.sonar.server.exceptions.ForbiddenException;
@@ -35,7 +40,6 @@ import org.sonar.server.exceptions.ServerException;
 import org.sonar.server.exceptions.UnauthorizedException;
 import org.sonar.server.tester.UserSessionRule;
 import org.sonar.server.usertoken.TokenGenerator;
-import org.sonar.db.user.TokenType;
 import org.sonar.server.ws.TestRequest;
 import org.sonar.server.ws.WsActionTester;
 import org.sonarqube.ws.MediaTypes;
@@ -46,10 +50,13 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy;
 import static org.mockito.ArgumentMatchers.anyString;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.when;
+import static org.sonar.api.utils.DateUtils.DATETIME_FORMAT;
+import static org.sonar.api.utils.DateUtils.DATE_FORMAT;
 import static org.sonar.db.permission.GlobalPermission.SCAN;
 import static org.sonar.db.user.TokenType.GLOBAL_ANALYSIS_TOKEN;
 import static org.sonar.db.user.TokenType.PROJECT_ANALYSIS_TOKEN;
 import static org.sonar.db.user.TokenType.USER_TOKEN;
+import static org.sonar.server.usertoken.ws.UserTokenSupport.PARAM_EXPIRATION_DATE;
 import static org.sonar.server.usertoken.ws.UserTokenSupport.PARAM_LOGIN;
 import static org.sonar.server.usertoken.ws.UserTokenSupport.PARAM_NAME;
 import static org.sonar.server.usertoken.ws.UserTokenSupport.PARAM_PROJECT_KEY;
@@ -86,13 +93,14 @@ public class GenerateActionTest {
     assertThat(action.since()).isEqualTo("5.3");
     assertThat(action.responseExampleAsString()).isNotEmpty();
     assertThat(action.isPost()).isTrue();
-    assertThat(action.param("login").isRequired()).isFalse();
-    assertThat(action.param("name").isRequired()).isTrue();
-    assertThat(action.param("type").isRequired()).isFalse();
-    assertThat(action.param("type").since()).isEqualTo("9.5");
-    assertThat(action.param("projectKey").isRequired()).isFalse();
-    assertThat(action.param("projectKey").since()).isEqualTo("9.5");
-
+    assertThat(action.param(PARAM_LOGIN).isRequired()).isFalse();
+    assertThat(action.param(PARAM_NAME).isRequired()).isTrue();
+    assertThat(action.param(PARAM_TYPE).isRequired()).isFalse();
+    assertThat(action.param(PARAM_TYPE).since()).isEqualTo("9.5");
+    assertThat(action.param(PARAM_PROJECT_KEY).isRequired()).isFalse();
+    assertThat(action.param(PARAM_PROJECT_KEY).since()).isEqualTo("9.5");
+    assertThat(action.param(PARAM_EXPIRATION_DATE).isRequired()).isFalse();
+    assertThat(action.param(PARAM_EXPIRATION_DATE).since()).isEqualTo("9.6");
   }
 
   @Test
@@ -106,7 +114,7 @@ public class GenerateActionTest {
       .setParam(PARAM_NAME, TOKEN_NAME)
       .execute().getInput();
 
-    assertJson(response).ignoreFields("createdAt").isSimilarTo(getClass().getResource("generate-example.json"));
+    assertJson(response).ignoreFields("createdAt").ignoreFields("expirationDate").isSimilarTo(getClass().getResource("generate-example.json"));
   }
 
   @Test
@@ -178,6 +186,36 @@ public class GenerateActionTest {
     assertThat(responseWithLogin.getCreatedAt()).isNotEmpty();
   }
 
+  @Test
+  public void a_user_can_generate_token_for_himself_with_expiration_date() {
+    UserDto user = db.users().insertUser();
+    userSession.logIn(user);
+
+    // A date 10 days in the future with format yyyy-MM-dd
+    String expirationDateValue = LocalDate.now().plusDays(10).format(DateTimeFormatter.ofPattern(DATE_FORMAT));
+
+    GenerateWsResponse response = newRequest(null, TOKEN_NAME, expirationDateValue);
+
+    assertThat(response.getLogin()).isEqualTo(user.getLogin());
+    assertThat(response.getCreatedAt()).isNotEmpty();
+    assertThat(response.getExpirationDate()).isEqualTo(getFormattedDate(expirationDateValue));
+  }
+
+  @Test
+  public void an_administrator_can_generate_token_for_users_with_expiration_date() {
+    UserDto user = db.users().insertUser();
+    logInAsSystemAdministrator();
+
+    // A date 10 days in the future with format yyyy-MM-dd
+    String expirationDateValue = LocalDate.now().plusDays(10).format(DateTimeFormatter.ofPattern(DATE_FORMAT));
+
+    GenerateWsResponse response = newRequest(user.getLogin(), TOKEN_NAME, expirationDateValue);
+
+    assertThat(response.getLogin()).isEqualTo(user.getLogin());
+    assertThat(response.getCreatedAt()).isNotEmpty();
+    assertThat(response.getExpirationDate()).isEqualTo(getFormattedDate(expirationDateValue));
+  }
+
   @Test
   public void fail_if_login_does_not_exist() {
     logInAsSystemAdministrator();
@@ -216,7 +254,7 @@ public class GenerateActionTest {
     String login = user.getLogin();
     logInAsSystemAdministrator();
 
-    assertThatThrownBy(() -> newRequest(login, "token 1", PROJECT_ANALYSIS_TOKEN, null))
+    assertThatThrownBy(() -> newRequest(login, "token 1", PROJECT_ANALYSIS_TOKEN, "project 1"))
       .isInstanceOf(IllegalArgumentException.class)
       .hasMessage("A Project Analysis Token cannot be generated for another user.");
   }
@@ -283,6 +321,32 @@ public class GenerateActionTest {
       .hasMessage(String.format("A user token for login '%s' and name 'Third Party Application' already exists", user.getLogin()));
   }
 
+  @Test
+  public void fail_if_expirationDate_format_is_wrong() {
+    UserDto user = db.users().insertUser();
+    String login = user.getLogin();
+    logInAsSystemAdministrator();
+
+    assertThatThrownBy(() -> {
+      newRequest(login, TOKEN_NAME, "21/06/2022");
+    })
+      .isInstanceOf(IllegalArgumentException.class)
+      .hasMessage("Supplied date format for parameter expirationDate is wrong. Please supply date in the ISO 8601 date format (YYYY-MM-DD)");
+  }
+
+  @Test
+  public void fail_if_expirationDate_is_not_in_future() {
+    UserDto user = db.users().insertUser();
+    String login = user.getLogin();
+    logInAsSystemAdministrator();
+
+    assertThatThrownBy(() -> {
+      newRequest(login, TOKEN_NAME, "2022-06-29");
+    })
+      .isInstanceOf(IllegalArgumentException.class)
+      .hasMessage(String.format("The minimum value for parameter %s is %s.", PARAM_EXPIRATION_DATE, LocalDate.now().plusDays(1).format(DateTimeFormatter.ISO_DATE)));
+  }
+
   @Test
   public void fail_if_token_hash_already_exists_in_db() {
     UserDto user = db.users().insertUser();
@@ -330,6 +394,17 @@ public class GenerateActionTest {
     return testRequest.executeProtobuf(GenerateWsResponse.class);
   }
 
+  private GenerateWsResponse newRequest(@Nullable String login, String name, String expirationDate) {
+    TestRequest testRequest = ws.newRequest()
+      .setParam(PARAM_NAME, name)
+      .setParam(PARAM_EXPIRATION_DATE, expirationDate);
+    if (login != null) {
+      testRequest.setParam(PARAM_LOGIN, login);
+    }
+
+    return testRequest.executeProtobuf(GenerateWsResponse.class);
+  }
+
   private GenerateWsResponse newRequest(@Nullable String login, String name, TokenType tokenType, @Nullable String projectKey) {
     TestRequest testRequest = ws.newRequest()
       .setParam(PARAM_NAME, name)
@@ -347,4 +422,10 @@ public class GenerateActionTest {
   private void logInAsSystemAdministrator() {
     userSession.logIn().setSystemAdministrator();
   }
+
+  private String getFormattedDate(String expirationDateValue) {
+    return DateTimeFormatter
+      .ofPattern(DATETIME_FORMAT)
+      .format(ZonedDateTime.of(LocalDate.parse(expirationDateValue, DateTimeFormatter.ofPattern(DATE_FORMAT)).atStartOfDay(), ZoneId.systemDefault()));
+  }
 }
index 983659285f6922a67bdb131a8df595ae0512f24c..786a3bfc5ffda3433a23d7a347e17b3e823ea5e0 100644 (file)
@@ -32,6 +32,7 @@ message GenerateWsResponse {
   optional string createdAt = 4;
   optional string type = 5;
   optional string projectKey = 6;
+  optional string expirationDate = 7;
 }
 
 // WS api/user_tokens/search