--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.server.common.user.service;
+
+import org.junit.Test;
+
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+public class UserCreateRequestTest {
+ @Test
+ public void build_whenNoPasswordAndUserLocal_shouldThrow() {
+ UserCreateRequest.Builder requestBuilder = UserCreateRequest.builder()
+ .setPassword(null)
+ .setLocal(true);
+
+ assertThatThrownBy(requestBuilder::build)
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessage("Password is mandatory and must not be empty");
+ }
+
+ @Test
+ public void build_whenPasswordSetButUserNonLocal_shouldThrow() {
+ UserCreateRequest.Builder requestBuilder = UserCreateRequest.builder()
+ .setPassword("password")
+ .setLocal(false);
+
+ assertThatThrownBy(requestBuilder::build)
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessage("Password should only be set on local user");
+ }
+
+}
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.Collection;
+import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.IntStream;
+import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
+import org.sonar.api.config.internal.MapSettings;
import org.sonar.api.utils.DateUtils;
import org.sonar.core.util.UuidFactory;
import org.sonar.db.DbTester;
+import org.sonar.db.audit.NoOpAuditPersister;
import org.sonar.db.scim.ScimUserDao;
import org.sonar.db.user.GroupDto;
import org.sonar.db.user.UserDto;
+import org.sonar.server.authentication.CredentialsLocalAuthentication;
import org.sonar.server.common.SearchResults;
import org.sonar.server.common.avatar.AvatarResolverImpl;
import org.sonar.server.common.management.ManagedInstanceChecker;
import org.sonar.server.exceptions.BadRequestException;
import org.sonar.server.exceptions.NotFoundException;
import org.sonar.server.management.ManagedInstanceService;
+import org.sonar.server.user.NewUserNotifier;
+import org.sonar.server.user.UserUpdater;
+import org.sonar.server.usergroups.DefaultGroupFinder;
+import static java.lang.String.format;
import static java.util.Arrays.asList;
import static java.util.Collections.singletonList;
import static java.util.function.Function.identity;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
+import static org.sonar.db.user.UserTesting.newUserDto;
public class UserServiceIT {
private static final UsersSearchRequest SEARCH_REQUEST = getBuilderWithDefaultsPageSize().build();
+ private GroupDto defaultGroup;
+
@Rule
public DbTester db = DbTester.create();
-
private final ManagedInstanceService managedInstanceService = mock(ManagedInstanceService.class);
-
private final ManagedInstanceChecker managedInstanceChecker = mock(ManagedInstanceChecker.class);
-
private final UserDeactivator userDeactivator = mock(UserDeactivator.class);
+ private final MapSettings settings = new MapSettings().setProperty("sonar.internal.pbkdf2.iterations", "1");
+ private final CredentialsLocalAuthentication localAuthentication = new CredentialsLocalAuthentication(db.getDbClient(), settings.asConfig());
+ private final UserUpdater userUpdater = new UserUpdater(mock(NewUserNotifier.class), db.getDbClient(), new DefaultGroupFinder(db.getDbClient()),
+ settings.asConfig(), new NoOpAuditPersister(), localAuthentication);
- private final UserService userService = new UserService(db.getDbClient(), new AvatarResolverImpl(), managedInstanceService, managedInstanceChecker, userDeactivator);
+ private final UserService userService = new UserService(db.getDbClient(), new AvatarResolverImpl(), managedInstanceService, managedInstanceChecker, userDeactivator, userUpdater);
+
+ @Before
+ public void setUp() {
+ defaultGroup = db.users().insertDefaultGroup();
+ }
@Test
public void search_for_all_active_users() {
private static UsersSearchRequest.Builder getBuilderWithDefaultsPageSize() {
return UsersSearchRequest.builder().setPage(1).setPageSize(50);
}
+
+ @Test
+ public void createUser_shouldCreateLocalUser() {
+ UserCreateRequest userCreateRequest = UserCreateRequest.builder()
+ .setLogin("john")
+ .setName("John")
+ .setEmail("john@email.com")
+ .setScmAccounts(singletonList("jn"))
+ .setPassword("1234")
+ .setLocal(true)
+ .build();
+
+ UserSearchResult user = userService.createUser(userCreateRequest);
+
+ assertThat(user.userDto())
+ .extracting(UserDto::getLogin, UserDto::getName, UserDto::getEmail, UserDto::getSortedScmAccounts, UserDto::isLocal)
+ .containsOnly("john", "John", "john@email.com", singletonList("jn"), true);
+
+ Optional<UserDto> dbUser = db.users().selectUserByLogin("john");
+ assertThat(dbUser).isPresent();
+
+ assertThat(db.users().selectGroupUuidsOfUser(dbUser.get())).containsOnly(defaultGroup.getUuid());
+ }
+
+ @Test
+ public void createUser_shouldCreateNonLocalUser() {
+ UserCreateRequest userCreateRequest = UserCreateRequest.builder()
+ .setLogin("john")
+ .setName("John")
+ .setLocal(false)
+ .build();
+
+ userService.createUser(userCreateRequest);
+
+ assertThat(db.users().selectUserByLogin("john").get())
+ .extracting(UserDto::isLocal, UserDto::getExternalIdentityProvider, UserDto::getExternalLogin)
+ .containsOnly(false, "sonarqube", "john");
+ }
+
+ @Test
+ public void createUser_shouldHandleCommasInScmAccounts() {
+ UserCreateRequest userCreateRequest = UserCreateRequest.builder()
+ .setLogin("john")
+ .setName("John")
+ .setEmail("john@email.com")
+ .setScmAccounts(singletonList("j,n"))
+ .setPassword("1234")
+ .setLocal(true)
+ .build();
+
+ UserSearchResult user = userService.createUser(userCreateRequest);
+
+ assertThat(user.userDto().getSortedScmAccounts()).containsOnly("j,n");
+ }
+
+ @Test
+ public void createUser_whenWhitespaceInScmAccounts_shouldFail() {
+ UserCreateRequest userCreateRequest = UserCreateRequest.builder()
+ .setLogin("john")
+ .setName("John")
+ .setEmail("john@email.com")
+ .setScmAccounts(List.of("admin", " admin "))
+ .setPassword("1234")
+ .setLocal(true)
+ .build();
+
+ assertThatThrownBy(() -> userService.createUser(userCreateRequest))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessage("SCM account cannot start or end with whitespace: ' admin '");
+ }
+
+ @Test
+ public void createUser_whenDuplicatesInScmAccounts_shouldFail() {
+ UserCreateRequest userCreateRequest = UserCreateRequest.builder()
+ .setLogin("john")
+ .setName("John")
+ .setEmail("john@email.com")
+ .setScmAccounts(List.of("admin", "admin"))
+ .setPassword("1234")
+ .setLocal(true)
+ .build();
+
+ assertThatThrownBy(() -> userService.createUser(userCreateRequest))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessage("Duplicate SCM account: 'admin'");
+ }
+
+ @Test
+ public void createUser_whenEmptyEmail_shouldCreateUser() {
+ UserCreateRequest userCreateRequest = UserCreateRequest.builder()
+ .setLogin("john")
+ .setName("John")
+ .setPassword("1234")
+ .setEmail("")
+ .setLocal(true)
+ .build();
+
+ userService.createUser(userCreateRequest);
+
+ assertThat(db.users().selectUserByLogin("john").get())
+ .extracting(UserDto::getExternalLogin)
+ .isEqualTo("john");
+ }
+
+ @Test
+ public void createUser_whenDeactivatedUserExists_shouldReactivate() {
+ db.users().insertUser(newUserDto("john", "John", "john@email.com").setActive(false));
+
+ UserCreateRequest userCreateRequest = UserCreateRequest.builder()
+ .setLogin("john")
+ .setName("John")
+ .setEmail("john@email.com")
+ .setScmAccounts(singletonList("jn"))
+ .setPassword("1234")
+ .setLocal(true)
+ .build();
+
+ userService.createUser(userCreateRequest);
+
+ assertThat(db.users().selectUserByLogin("john").get().isActive()).isTrue();
+ }
+
+ @Test
+ public void createUser_whenActiveUserExists_shouldThrow() {
+ UserDto user = db.users().insertUser();
+
+ UserCreateRequest userCreateRequest = UserCreateRequest.builder()
+ .setLogin(user.getLogin())
+ .setName("John")
+ .setPassword("1234")
+ .setLocal(true)
+ .build();
+
+ assertThatThrownBy(() -> userService.createUser(userCreateRequest))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessage(format("An active user with login '%s' already exists", user.getLogin()));
+ }
+
+ @Test
+ public void createUser_whenInstanceManaged_shouldThrow() {
+ BadRequestException badRequestException = BadRequestException.create("message");
+ doThrow(badRequestException).when(managedInstanceChecker).throwIfInstanceIsManaged();
+
+ UserCreateRequest userCreateRequest = UserCreateRequest.builder()
+ .setLogin("john")
+ .setName("John")
+ .setLocal(false)
+ .build();
+
+ assertThatThrownBy(() -> userService.createUser(userCreateRequest))
+ .isEqualTo(badRequestException);
+ }
+
}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.server.common.user.service;
+
+import java.util.List;
+import java.util.Optional;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Strings.isNullOrEmpty;
+
+public class UserCreateRequest {
+ private final String email;
+ private final Boolean local;
+ private final String login;
+ private final String name;
+ private final String password;
+ private final List<String> scmAccounts;
+
+ private UserCreateRequest(Builder builder) {
+ this.email = builder.email;
+ this.local = builder.local;
+ this.login = builder.login;
+ this.name = builder.name;
+ this.password = builder.password;
+ this.scmAccounts = builder.scmAccounts;
+ }
+
+ public Optional<String> getEmail() {
+ return Optional.ofNullable(email);
+ }
+
+ public Boolean isLocal() {
+ return local;
+ }
+
+ public String getLogin() {
+ return login;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public Optional<String> getPassword() {
+ return Optional.ofNullable(password);
+ }
+
+ public Optional<List<String>> getScmAccounts() {
+ return Optional.ofNullable(scmAccounts);
+ }
+
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ public static class Builder {
+ private String email;
+ private Boolean local;
+ private String login;
+ private String name;
+ private String password;
+ private List<String> scmAccounts;
+
+ private Builder() {
+ // enforce factory method use
+ }
+
+ public Builder setEmail(String email) {
+ this.email = email;
+ return this;
+ }
+
+ public Builder setLocal(Boolean local) {
+ this.local = local;
+ return this;
+ }
+
+ public Builder setLogin(String login) {
+ this.login = login;
+ return this;
+ }
+
+ public Builder setName(String name) {
+ this.name = name;
+ return this;
+ }
+
+ public Builder setPassword(String password) {
+ this.password = password;
+ return this;
+ }
+
+ public Builder setScmAccounts(List<String> scmAccounts) {
+ this.scmAccounts = scmAccounts;
+ return this;
+ }
+
+ public UserCreateRequest build() {
+ checkArgument(!local || !isNullOrEmpty(password), "Password is mandatory and must not be empty");
+ checkArgument(local || isNullOrEmpty(password), "Password should only be set on local user");
+ return new UserCreateRequest(this);
+ }
+ }
+
+}
package org.sonar.server.common.user.service;
import com.google.common.collect.Multimap;
+import java.util.ArrayList;
import java.util.Collection;
+import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import org.sonar.server.common.user.UserDeactivator;
import org.sonar.server.exceptions.BadRequestException;
import org.sonar.server.management.ManagedInstanceService;
+import org.sonar.server.user.ExternalIdentity;
+import org.sonar.server.user.NewUser;
+import org.sonar.server.user.UserUpdater;
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Strings.emptyToNull;
import static java.util.Comparator.comparing;
import static org.sonar.server.exceptions.NotFoundException.checkFound;
+import static org.sonar.server.user.ExternalIdentity.SQ_AUTHORITY;
public class UserService {
private final ManagedInstanceService managedInstanceService;
private final ManagedInstanceChecker managedInstanceChecker;
private final UserDeactivator userDeactivator;
+ private final UserUpdater userUpdater;
public UserService(
DbClient dbClient,
AvatarResolver avatarResolver,
ManagedInstanceService managedInstanceService,
ManagedInstanceChecker managedInstanceChecker,
- UserDeactivator userDeactivator) {
+ UserDeactivator userDeactivator,
+ UserUpdater userUpdater) {
this.dbClient = dbClient;
this.avatarResolver = avatarResolver;
this.managedInstanceService = managedInstanceService;
this.managedInstanceChecker = managedInstanceChecker;
this.userDeactivator = userDeactivator;
+ this.userUpdater = userUpdater;
}
public SearchResults<UserSearchResult> findUsers(UsersSearchRequest request) {
}
private Optional<String> findAvatar(UserDto userDto) {
- return Optional.ofNullable(userDto.getEmail()).map(email -> avatarResolver.create(userDto));
+ return Optional.ofNullable(emptyToNull(userDto.getEmail())).map(email -> avatarResolver.create(userDto));
}
private static Set<String> getUserUuids(List<UserDto> users) {
groups,
tokenCount);
}
+
+ public UserSearchResult createUser(UserCreateRequest userCreateRequest) {
+ managedInstanceChecker.throwIfInstanceIsManaged();
+ List<String> scmAccounts = userCreateRequest.getScmAccounts().orElse(new ArrayList<>());
+ validateScmAccounts(scmAccounts);
+ try (DbSession dbSession = dbClient.openSession(false)) {
+ String login = userCreateRequest.getLogin();
+ NewUser.Builder newUserBuilder = NewUser.builder()
+ .setLogin(login)
+ .setName(userCreateRequest.getName())
+ .setEmail(userCreateRequest.getEmail().orElse(null))
+ .setScmAccounts(scmAccounts)
+ .setPassword(userCreateRequest.getPassword().orElse(null));
+ if (Boolean.FALSE.equals(userCreateRequest.isLocal())) {
+ newUserBuilder.setExternalIdentity(new ExternalIdentity(SQ_AUTHORITY, login, login));
+ }
+ return registerUser(dbSession, login, newUserBuilder);
+ }
+ }
+
+ private UserSearchResult registerUser(DbSession dbSession, String login, NewUser.Builder newUserBuilder) {
+ UserDto user = dbClient.userDao().selectByLogin(dbSession, login);
+ if (user == null) {
+ user = userUpdater.createAndCommit(dbSession, newUserBuilder.build(), u -> {});
+ } else {
+ checkArgument(!user.isActive(), "An active user with login '%s' already exists", login);
+ user = userUpdater.reactivateAndCommit(dbSession, user, newUserBuilder.build(), u -> {});
+ }
+ return fetchUser(user.getLogin());
+ }
+
+ public static void validateScmAccounts(List<String> scmAccounts) {
+ scmAccounts.forEach(UserService::validateScmAccountFormat);
+ validateNoDuplicates(scmAccounts);
+ }
+
+ private static void validateScmAccountFormat(String scmAccount) {
+ checkArgument(scmAccount.equals(scmAccount.strip()), "SCM account cannot start or end with whitespace: '%s'", scmAccount);
+ }
+
+ private static void validateNoDuplicates(List<String> scmAccounts) {
+ Set<String> duplicateCheck = new HashSet<>();
+ for (String account : scmAccounts) {
+ checkArgument(duplicateCheck.add(account), "Duplicate SCM account: '%s'", account);
+ }
+ }
+
}
description = "Number of results per page",
schema = @Schema(defaultValue = DEFAULT_PAGE_SIZE, implementation = Integer.class))
Integer pageSize,
+
@Positive
@Parameter(
description = "1-based page number",
import javax.annotation.Nullable;
import org.sonar.api.utils.Paging;
import org.sonar.server.common.SearchResults;
+import org.sonar.server.common.user.service.UserCreateRequest;
import org.sonar.server.common.user.service.UserSearchResult;
import org.sonar.server.common.user.service.UserService;
import org.sonar.server.common.user.service.UsersSearchRequest;
import org.sonar.server.v2.api.model.RestPage;
import org.sonar.server.v2.api.user.converter.UsersSearchRestResponseGenerator;
import org.sonar.server.v2.api.user.model.RestUser;
+import org.sonar.server.v2.api.user.request.UserCreateRestRequest;
import org.sonar.server.v2.api.user.request.UsersSearchRestRequest;
import org.sonar.server.v2.api.user.response.UsersSearchRestResponse;
public RestUser fetchUser(String login) {
return usersSearchResponseGenerator.toRestUser(userService.fetchUser(login));
}
+
+ @Override
+ public RestUser create(UserCreateRestRequest userCreateRestRequest) {
+ userSession.checkLoggedIn().checkIsSystemAdministrator();
+ UserCreateRequest userCreateRequest = toUserCreateRequest(userCreateRestRequest);
+ return usersSearchResponseGenerator.toRestUser(userService.createUser(userCreateRequest));
+ }
+
+ private static UserCreateRequest toUserCreateRequest(UserCreateRestRequest userCreateRestRequest) {
+ return UserCreateRequest.builder()
+ .setEmail(userCreateRestRequest.email())
+ .setLocal(userCreateRestRequest.local())
+ .setLogin(userCreateRestRequest.login())
+ .setName(userCreateRestRequest.name())
+ .setPassword(userCreateRestRequest.password())
+ .setScmAccounts(userCreateRestRequest.scmAccounts())
+ .build();
+ }
+
}
import javax.validation.Valid;
import org.sonar.server.v2.api.model.RestPage;
import org.sonar.server.v2.api.user.model.RestUser;
+import org.sonar.server.v2.api.user.request.UserCreateRestRequest;
import org.sonar.server.v2.api.user.request.UsersSearchRestRequest;
import org.sonar.server.v2.api.user.response.UsersSearchRestResponse;
import org.springdoc.api.annotations.ParameterObject;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseStatus;
Field 'sonarqubeLastConnectionDate' is only updated every hour, so it may not be accurate, for instance when a user authenticates many times in less than one hour.
""")
RestUser fetchUser(@PathVariable("login") @Parameter(description = "The login of the user to fetch.", required = true, in = ParameterIn.PATH) String login);
+
+ @PostMapping(consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
+ @ResponseStatus(HttpStatus.OK)
+ @Operation(summary = "User creation", description = """
+ Create a user.
+ If a deactivated user account exists with the given login, it will be reactivated.
+ Requires Administer System permission
+ """)
+ RestUser create(@Valid @RequestBody(required = true) UserCreateRestRequest userCreateRestRequest);
+
}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.server.v2.api.user.request;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import java.util.List;
+import javax.annotation.Nullable;
+import javax.validation.constraints.Email;
+import javax.validation.constraints.NotNull;
+import javax.validation.constraints.Size;
+
+public record UserCreateRestRequest(
+ @Nullable
+ @Email
+ @Size(max = 100)
+ @Schema(description = "User email")
+ String email,
+
+ @Nullable
+ @Schema(description = "Specify if the user should be authenticated from SonarQube server or from an external authentication system. " +
+ "Password should not be set when local is set to false.",
+ defaultValue = "true")
+ Boolean local,
+
+ @NotNull
+ @Size(min = 2, max = 100)
+ @Schema(description = "User login")
+ String login,
+
+ @NotNull
+ @Size(max = 200)
+ @Schema(description = "User name")
+ String name,
+
+ @Nullable
+ @Schema(description = "User password. Only mandatory when creating local user, otherwise it should not be set")
+ String password,
+
+ @Nullable
+ @Schema(description = "List of SCM accounts.")
+ List<String> scmAccounts) {
+
+ public UserCreateRestRequest(@Nullable String email, @Nullable Boolean local, String login, String name, @Nullable String password, @Nullable List<String> scmAccounts) {
+ this.email = email;
+ this.local = local == null ? Boolean.TRUE : local;
+ this.login = login;
+ this.name = name;
+ this.password = password;
+ this.scmAccounts = scmAccounts;
+ }
+
+}
import java.util.stream.Collectors;
import org.sonar.server.exceptions.BadRequestException;
import org.sonar.server.exceptions.ForbiddenException;
+import org.sonar.server.exceptions.NotFoundException;
import org.sonar.server.exceptions.ServerException;
import org.sonar.server.exceptions.UnauthorizedException;
import org.sonar.server.v2.api.model.RestError;
@RestControllerAdvice
public class RestResponseEntityExceptionHandler {
- @ExceptionHandler(IllegalStateException.class)
+ @ExceptionHandler({IllegalStateException.class})
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
protected ResponseEntity<RestError> handleIllegalStateException(IllegalStateException illegalStateException) {
return new ResponseEntity<>(new RestError(illegalStateException.getMessage()), HttpStatus.INTERNAL_SERVER_ERROR);
}
+ @ExceptionHandler({IllegalArgumentException.class})
+ @ResponseStatus(HttpStatus.BAD_REQUEST)
+ protected ResponseEntity<RestError> handleIllegalArgumentException(IllegalArgumentException illegalArgumentException) {
+ return new ResponseEntity<>(new RestError(illegalArgumentException.getMessage()), HttpStatus.BAD_REQUEST);
+ }
+
@ExceptionHandler(BindException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
protected ResponseEntity<RestError> handleBindException(BindException bindException) {
return new ResponseEntity<>(new RestError(serverException.getMessage()),
Optional.ofNullable(HttpStatus.resolve(serverException.httpCode())).orElse(HttpStatus.INTERNAL_SERVER_ERROR));
}
+
+ @ExceptionHandler({NotFoundException.class})
+ @ResponseStatus(HttpStatus.NOT_FOUND)
+ protected ResponseEntity<RestError> handleNotFoundException(NotFoundException notFoundException) {
+ return new ResponseEntity<>(new RestError(notFoundException.getMessage()), HttpStatus.NOT_FOUND);
+ }
+
}
import org.sonar.server.v2.api.response.PageRestResponse;
import org.sonar.server.v2.api.user.converter.UsersSearchRestResponseGenerator;
import org.sonar.server.v2.api.user.model.RestUser;
+import org.sonar.server.v2.api.user.request.UserCreateRestRequest;
import org.sonar.server.v2.api.user.response.UsersSearchRestResponse;
+import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult;
import static org.sonar.server.v2.api.model.RestPage.DEFAULT_PAGE_SIZE;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
public void deactivate_whenAnonymizeParameterIsNotBoolean_shouldReturnBadRequest() throws Exception {
userSession.logIn().setSystemAdministrator();
- mockMvc.perform(delete(USER_ENDPOINT + "/userToDelete?anonymize=maybe"))
+ mockMvc.perform(delete(USER_ENDPOINT + "/userToDelete").param("anonymize", "maybe"))
.andExpect(
status().isBadRequest());
}
.andReturn();
RestUser responseUser = gson.fromJson(mvcResult.getResponse().getContentAsString(), RestUser.class);
assertThat(responseUser).isEqualTo(restUser);
+ }
+
+ @Test
+ public void create_whenNotAnAdmin_shouldReturnForbidden() throws Exception {
+ userSession.logIn().setNonSystemAdministrator();
+
+ mockMvc.perform(
+ post(USER_ENDPOINT)
+ .contentType(MediaType.APPLICATION_JSON_VALUE)
+ .content(gson.toJson(new UserCreateRestRequest(null, null, "login", "name", null, null))))
+ .andExpectAll(
+ status().isForbidden(),
+ content().json("{\"message\":\"Insufficient privileges\"}"));
+ }
+
+ @Test
+ public void create_whenNoLogin_shouldReturnBadRequest() throws Exception {
+ userSession.logIn().setSystemAdministrator();
+
+ mockMvc.perform(
+ post(USER_ENDPOINT)
+ .contentType(MediaType.APPLICATION_JSON_VALUE)
+ .content(gson.toJson(new UserCreateRestRequest(null, null, null, "name", null, null))))
+ .andExpectAll(
+ status().isBadRequest(),
+ content().json("{\"message\":\"Value {} for field login was rejected. Error: must not be null\"}"));
+ }
+
+ @Test
+ public void create_whenNoName_shouldReturnBadRequest() throws Exception {
+ userSession.logIn().setSystemAdministrator();
+
+ mockMvc.perform(
+ post(USER_ENDPOINT)
+ .contentType(MediaType.APPLICATION_JSON_VALUE)
+ .content(gson.toJson(new UserCreateRestRequest(null, null, "login", null, null, null))))
+ .andExpectAll(
+ status().isBadRequest(),
+ content().json("{\"message\":\"Value {} for field name was rejected. Error: must not be null\"}"));
+ }
+
+ @Test
+ public void create_whenUserServiceThrow_shouldReturnServerError() throws Exception {
+ userSession.logIn().setSystemAdministrator();
+ when(userService.createUser(any())).thenThrow(new IllegalArgumentException("IllegalArgumentException"));
+
+ mockMvc.perform(
+ post(USER_ENDPOINT)
+ .contentType(MediaType.APPLICATION_JSON_VALUE)
+ .content(gson.toJson(new UserCreateRestRequest("e@mail.com", true, "login", "name", "password", List.of("scm")))))
+ .andExpectAll(
+ status().isBadRequest(),
+ content().json("{\"message\":\"IllegalArgumentException\"}"));
+ }
+ @Test
+ public void create_whenUserServiceReturnUser_shouldReturnIt() throws Exception {
+ userSession.logIn().setSystemAdministrator();
+ UserSearchResult userSearchResult = generateUserSearchResult("1", true, true, false, 1, 2);
+ UserDto userDto = userSearchResult.userDto();
+ when(userService.createUser(any())).thenReturn(userSearchResult);
+ when(responseGenerator.toRestUser(userSearchResult)).thenReturn(toRestUser(userSearchResult));
+
+ MvcResult mvcResult = mockMvc.perform(
+ post(USER_ENDPOINT)
+ .contentType(MediaType.APPLICATION_JSON_VALUE)
+ .content(gson.toJson(new UserCreateRestRequest(
+ userDto.getEmail(), userDto.isLocal(), userDto.getLogin(), userDto.getName(), "password", userDto.getSortedScmAccounts()))))
+ .andExpect(status().isOk())
+ .andReturn();
+ RestUser responseUser = gson.fromJson(mvcResult.getResponse().getContentAsString(), RestUser.class);
+ assertThat(responseUser).isEqualTo(toRestUser(userSearchResult));
}
+
}
import org.sonar.db.user.GroupDto;
import org.sonar.db.user.UserDto;
import org.sonar.server.authentication.CredentialsLocalAuthentication;
+import org.sonar.server.common.avatar.AvatarResolverImpl;
+import org.sonar.server.common.management.ManagedInstanceChecker;
+import org.sonar.server.common.user.UserDeactivator;
+import org.sonar.server.common.user.service.UserService;
import org.sonar.server.exceptions.BadRequestException;
import org.sonar.server.exceptions.ForbiddenException;
import org.sonar.server.exceptions.UnauthorizedException;
-import org.sonar.server.common.management.ManagedInstanceChecker;
+import org.sonar.server.management.ManagedInstanceService;
import org.sonar.server.tester.UserSessionRule;
import org.sonar.server.user.NewUserNotifier;
import org.sonar.server.user.UserUpdater;
private final CredentialsLocalAuthentication localAuthentication = new CredentialsLocalAuthentication(db.getDbClient(), settings.asConfig());
private final ManagedInstanceChecker managedInstanceChecker = mock(ManagedInstanceChecker.class);
- private final WsActionTester tester = new WsActionTester(new CreateAction(db.getDbClient(), new UserUpdater(mock(NewUserNotifier.class),
- db.getDbClient(), new DefaultGroupFinder(db.getDbClient()), settings.asConfig(), new NoOpAuditPersister(), localAuthentication), userSessionRule, managedInstanceChecker));
+ private final ManagedInstanceService managedInstanceService = mock(ManagedInstanceService.class);
+ private final UserService userService = new UserService(db.getDbClient(), new AvatarResolverImpl(), managedInstanceService, managedInstanceChecker, mock(UserDeactivator.class),
+ new UserUpdater(mock(NewUserNotifier.class), db.getDbClient(), new DefaultGroupFinder(db.getDbClient()), settings.asConfig(), new NoOpAuditPersister(), localAuthentication));
+ private final WsActionTester tester = new WsActionTester(new CreateAction(userSessionRule, managedInstanceChecker, userService));
@Before
public void setUp() {
import org.sonar.server.management.ManagedInstanceService;
import org.sonar.server.tester.UserSessionRule;
import org.sonar.server.user.ExternalIdentity;
+import org.sonar.server.user.UserUpdater;
import org.sonar.server.ws.TestRequest;
import org.sonar.server.ws.TestResponse;
import org.sonar.server.ws.WsActionTester;
private final UserDeactivator userDeactivator = new UserDeactivator(dbClient, userAnonymizer);
private final ManagedInstanceChecker managedInstanceChecker = mock(ManagedInstanceChecker.class);
- private final UserService userService = new UserService(dbClient, mock(AvatarResolver.class), mock(ManagedInstanceService.class), managedInstanceChecker, userDeactivator);
+ private final UserService userService = new UserService(dbClient, mock(AvatarResolver.class), mock(ManagedInstanceService.class), managedInstanceChecker, userDeactivator,
+ mock(UserUpdater.class));
private final WsActionTester ws = new WsActionTester(new DeactivateAction(dbClient, userSession, new UserJsonWriter(userSession), userService));
@Test
import org.sonar.server.exceptions.ServerException;
import org.sonar.server.management.ManagedInstanceService;
import org.sonar.server.tester.UserSessionRule;
+import org.sonar.server.user.UserUpdater;
import org.sonar.server.ws.TestRequest;
import org.sonar.server.ws.WsActionTester;
import org.sonarqube.ws.Common.Paging;
new AvatarResolverImpl(),
managedInstanceService,
mock(ManagedInstanceChecker.class),
- mock(UserDeactivator.class));
+ mock(UserDeactivator.class),
+ mock(UserUpdater.class));
private final SearchWsReponseGenerator searchWsReponseGenerator = new SearchWsReponseGenerator(userSession);
*/
package org.sonar.server.user.ws;
-import java.util.HashSet;
import java.util.List;
-import java.util.Set;
import javax.annotation.CheckForNull;
import javax.annotation.Nullable;
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.db.DbClient;
-import org.sonar.db.DbSession;
import org.sonar.db.user.UserDto;
import org.sonar.server.common.management.ManagedInstanceChecker;
-import org.sonar.server.user.ExternalIdentity;
-import org.sonar.server.user.NewUser;
+import org.sonar.server.common.user.service.UserCreateRequest;
+import org.sonar.server.common.user.service.UserSearchResult;
+import org.sonar.server.common.user.service.UserService;
import org.sonar.server.user.UserSession;
-import org.sonar.server.user.UserUpdater;
import org.sonarqube.ws.Users.CreateWsResponse;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Strings.isNullOrEmpty;
import static java.util.Collections.emptyList;
import static java.util.Optional.ofNullable;
-import static org.sonar.server.user.ExternalIdentity.SQ_AUTHORITY;
import static org.sonar.server.user.UserUpdater.EMAIL_MAX_LENGTH;
import static org.sonar.server.user.UserUpdater.LOGIN_MAX_LENGTH;
import static org.sonar.server.user.UserUpdater.LOGIN_MIN_LENGTH;
public class CreateAction implements UsersWsAction {
- private final DbClient dbClient;
- private final UserUpdater userUpdater;
private final UserSession userSession;
private final ManagedInstanceChecker managedInstanceChecker;
+ private final UserService userService;
- public CreateAction(DbClient dbClient, UserUpdater userUpdater, UserSession userSession, ManagedInstanceChecker managedInstanceService) {
- this.dbClient = dbClient;
- this.userUpdater = userUpdater;
+ public CreateAction(UserSession userSession, ManagedInstanceChecker managedInstanceService, UserService userService) {
this.userSession = userSession;
this.managedInstanceChecker = managedInstanceService;
+ this.userService = userService;
}
@Override
public void handle(Request request, Response response) throws Exception {
userSession.checkLoggedIn().checkIsSystemAdministrator();
managedInstanceChecker.throwIfInstanceIsManaged();
- CreateRequest createRequest = toWsRequest(request);
- checkArgument(isValidIfPresent(createRequest.getEmail()), "Email '%s' is not valid", createRequest.getEmail());
- writeProtobuf(doHandle(createRequest), request, response);
+
+ UserCreateRequest userCreateRequest = toUserCreateRequest(request);
+ String email = userCreateRequest.getEmail().orElse(null);
+ checkArgument(isValidIfPresent(email), "Email '%s' is not valid", email);
+
+ writeProtobuf(doHandle(userCreateRequest), request, response);
}
- private CreateWsResponse doHandle(CreateRequest request) {
- try (DbSession dbSession = dbClient.openSession(false)) {
- String login = request.getLogin();
- NewUser.Builder newUser = NewUser.builder()
- .setLogin(login)
- .setName(request.getName())
- .setEmail(request.getEmail())
- .setScmAccounts(request.getScmAccounts())
- .setPassword(request.getPassword());
- if (!request.isLocal()) {
- newUser.setExternalIdentity(new ExternalIdentity(SQ_AUTHORITY, login, login));
- }
- UserDto existingUser = dbClient.userDao().selectByLogin(dbSession, login);
- if (existingUser == null) {
- return buildResponse(userUpdater.createAndCommit(dbSession, newUser.build(), u -> {
- }));
- }
- checkArgument(!existingUser.isActive(), "An active user with login '%s' already exists", login);
- return buildResponse(userUpdater.reactivateAndCommit(dbSession, existingUser, newUser.build(), u -> {
- }));
- }
+ private CreateWsResponse doHandle(UserCreateRequest userCreateRequest) {
+ UserSearchResult userSearchResult = userService.createUser(userCreateRequest);
+ return buildResponse(userSearchResult.userDto());
}
private static CreateWsResponse buildResponse(UserDto userDto) {
return CreateWsResponse.newBuilder().setUser(userBuilder).build();
}
- private static CreateRequest toWsRequest(Request request) {
- return CreateRequest.builder()
+ private static UserCreateRequest toUserCreateRequest(Request request) {
+ return UserCreateRequest.builder()
+ .setEmail(request.param(PARAM_EMAIL))
+ .setLocal(request.mandatoryParamAsBoolean(PARAM_LOCAL))
.setLogin(request.mandatoryParam(PARAM_LOGIN))
.setName(request.mandatoryParam(PARAM_NAME))
.setPassword(request.param(PARAM_PASSWORD))
- .setEmail(request.param(PARAM_EMAIL))
.setScmAccounts(parseScmAccounts(request))
- .setLocal(request.mandatoryParamAsBoolean(PARAM_LOCAL))
.build();
}
public static List<String> parseScmAccounts(Request request) {
if (request.hasParam(PARAM_SCM_ACCOUNT)) {
- List<String> scmAccounts = request.multiParam(PARAM_SCM_ACCOUNT);
- validateScmAccounts(scmAccounts);
- return scmAccounts;
+ return request.multiParam(PARAM_SCM_ACCOUNT);
}
return emptyList();
}
- private static void validateScmAccounts(List<String> scmAccounts) {
- scmAccounts.forEach(CreateAction::validateScmAccountFormat);
- validateNoDuplicates(scmAccounts);
- }
-
- private static void validateScmAccountFormat(String scmAccount) {
- checkArgument(scmAccount.equals(scmAccount.strip()), "SCM account cannot start or end with whitespace: '%s'", scmAccount);
- }
-
- private static void validateNoDuplicates(List<String> scmAccounts) {
- Set<String> duplicateCheck = new HashSet<>();
- for (String account : scmAccounts) {
- checkArgument(duplicateCheck.add(account), "Duplicate SCM account: '%s'", account);
- }
- }
-
static class CreateRequest {
private final String login;
import org.sonar.db.DbClient;
import org.sonar.db.DbSession;
import org.sonar.db.user.UserDto;
-import org.sonar.server.exceptions.NotFoundException;
import org.sonar.server.common.management.ManagedInstanceChecker;
+import org.sonar.server.common.user.service.UserService;
+import org.sonar.server.exceptions.NotFoundException;
import org.sonar.server.user.UpdateUser;
import org.sonar.server.user.UserSession;
import org.sonar.server.user.UserUpdater;
}
private static UpdateRequest toWsRequest(Request request) {
+ List<String> scmAccounts = parseScmAccounts(request);
+ UserService.validateScmAccounts(scmAccounts);
return UpdateRequest.builder()
.setLogin(request.mandatoryParam(PARAM_LOGIN))
.setName(request.param(PARAM_NAME))
.setEmail(request.param(PARAM_EMAIL))
- .setScmAccounts(parseScmAccounts(request))
+ .setScmAccounts(scmAccounts)
.build();
}