aboutsummaryrefslogtreecommitdiffstats
path: root/server
diff options
context:
space:
mode:
authorMatteo Mara <matteo.mara@sonarsource.com>2022-06-29 15:31:28 +0200
committersonartech <sonartech@sonarsource.com>2022-07-01 20:03:06 +0000
commit05ebcc134533b03a324a2710c3fe6d711850a5c4 (patch)
tree378093f77e0dc6205a718a8ca089b17b6375c50f /server
parente80f431e31504a51ed67193a6d5e8d9b0d2c0d62 (diff)
downloadsonarqube-05ebcc134533b03a324a2710c3fe6d711850a5c4.tar.gz
sonarqube-05ebcc134533b03a324a2710c3fe6d711850a5c4.zip
SONAR-16565 update the API api/user_tokens/generate for accepting the token expiration date
Diffstat (limited to 'server')
-rw-r--r--server/sonar-db-dao/src/main/java/org/sonar/db/user/UserTokenDto.java3
-rw-r--r--server/sonar-webserver-webapi/src/main/java/org/sonar/server/usertoken/ws/GenerateAction.java107
-rw-r--r--server/sonar-webserver-webapi/src/main/java/org/sonar/server/usertoken/ws/UserTokenSupport.java1
-rw-r--r--server/sonar-webserver-webapi/src/main/resources/org/sonar/server/usertoken/ws/generate-example.json1
-rw-r--r--server/sonar-webserver-webapi/src/test/java/org/sonar/server/usertoken/ws/GenerateActionTest.java101
5 files changed, 174 insertions, 39 deletions
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. <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();
}
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
@@ -179,6 +187,36 @@ public class GenerateActionTest {
}
@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.");
}
@@ -284,6 +322,32 @@ public class GenerateActionTest {
}
@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();
String login = user.getLogin();
@@ -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()));
+ }
}