From: Matteo Mara Date: Wed, 29 Jun 2022 13:31:28 +0000 (+0200) Subject: SONAR-16565 update the API api/user_tokens/generate for accepting the token expiratio... X-Git-Tag: 9.6.0.59041~296 X-Git-Url: https://source.dussan.org/?a=commitdiff_plain;h=05ebcc134533b03a324a2710c3fe6d711850a5c4;p=sonarqube.git SONAR-16565 update the API api/user_tokens/generate for accepting the token expiration date --- diff --git a/server/sonar-db-dao/src/main/java/org/sonar/db/user/UserTokenDto.java b/server/sonar-db-dao/src/main/java/org/sonar/db/user/UserTokenDto.java index 8c7cd8476e4..bcd9572efee 100644 --- a/server/sonar-db-dao/src/main/java/org/sonar/db/user/UserTokenDto.java +++ b/server/sonar-db-dao/src/main/java/org/sonar/db/user/UserTokenDto.java @@ -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; } } diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/usertoken/ws/GenerateAction.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/usertoken/ws/GenerateAction.java index ccecb44f153..cfb002233e9 100644 --- a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/usertoken/ws/GenerateAction.java +++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/usertoken/ws/GenerateAction.java @@ -19,30 +19,36 @@ */ 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.
" + "Please keep your tokens secret. They enable to authenticate and analyze projects.
" + "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 getProjecKeyFromRequest(Request request) { + private static Optional 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(); } diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/usertoken/ws/UserTokenSupport.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/usertoken/ws/UserTokenSupport.java index 118d9014086..2e0a64fbca7 100644 --- a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/usertoken/ws/UserTokenSupport.java +++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/usertoken/ws/UserTokenSupport.java @@ -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; diff --git a/server/sonar-webserver-webapi/src/main/resources/org/sonar/server/usertoken/ws/generate-example.json b/server/sonar-webserver-webapi/src/main/resources/org/sonar/server/usertoken/ws/generate-example.json index c17d43fbf52..d32ecf2284d 100644 --- a/server/sonar-webserver-webapi/src/main/resources/org/sonar/server/usertoken/ws/generate-example.json +++ b/server/sonar-webserver-webapi/src/main/resources/org/sonar/server/usertoken/ws/generate-example.json @@ -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" } diff --git a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/usertoken/ws/GenerateActionTest.java b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/usertoken/ws/GenerateActionTest.java index a56be8b720c..3a0412f87fa 100644 --- a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/usertoken/ws/GenerateActionTest.java +++ b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/usertoken/ws/GenerateActionTest.java @@ -19,6 +19,10 @@ */ 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())); + } } diff --git a/sonar-ws/src/main/protobuf/ws-user_tokens.proto b/sonar-ws/src/main/protobuf/ws-user_tokens.proto index 983659285f6..786a3bfc5ff 100644 --- a/sonar-ws/src/main/protobuf/ws-user_tokens.proto +++ b/sonar-ws/src/main/protobuf/ws-user_tokens.proto @@ -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