*/
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;
.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);
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
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);
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) {
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();
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())
responseBuilder.setProjectKey(userTokenDto.getProjectKey());
}
+ if (userTokenDto.getExpirationDate() != null) {
+ responseBuilder.setExpirationDate(formatDateTime(userTokenDto.getExpirationDate()));
+ }
+
return responseBuilder.build();
}
*/
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;
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;
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;
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;
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
.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
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();
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.");
}
.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();
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)
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()));
+ }
}