@@ -409,9 +409,12 @@ subprojects { | |||
dependency ("org.springframework:spring-webmvc:${springVersion}") { | |||
exclude 'commons-logging:commons-logging' | |||
} | |||
dependency 'org.springdoc:springdoc-openapi-ui:1.7.0' | |||
dependency 'org.springdoc:springdoc-openapi-webmvc-core:1.7.0' | |||
dependency 'org.subethamail:subethasmtp:3.1.7' | |||
dependency 'org.yaml:snakeyaml:2.0' | |||
dependency 'org.hibernate:hibernate-validator:6.2.5.Final' | |||
dependency 'javax.el:javax.el-api:3.0.0' | |||
dependency 'org.glassfish:javax.el:3.0.0' | |||
// please keep this list alphabetically ordered | |||
} |
@@ -0,0 +1,29 @@ | |||
sonar { | |||
properties { | |||
property 'sonar.projectName', "${projectTitle} :: WebServer :: Common" | |||
} | |||
} | |||
dependencies { | |||
// please keep the list grouped by configuration and ordered by name | |||
api 'com.google.guava:guava' | |||
api project(':server:sonar-db-dao') | |||
api project(':server:sonar-webserver-ws') | |||
compileOnlyApi 'com.google.code.findbugs:jsr305' | |||
compileOnlyApi 'javax.servlet:javax.servlet-api' | |||
testImplementation 'org.apache.logging.log4j:log4j-api' | |||
testImplementation 'org.apache.logging.log4j:log4j-core' | |||
testImplementation 'com.google.code.findbugs:jsr305' | |||
testImplementation 'com.tngtech.java:junit-dataprovider' | |||
testImplementation 'junit:junit' | |||
testImplementation 'org.assertj:assertj-core' | |||
testImplementation 'org.mockito:mockito-core' | |||
testImplementation project(':sonar-testing-harness') | |||
testImplementation testFixtures(project(':server:sonar-db-dao')) | |||
} |
@@ -0,0 +1,408 @@ | |||
/* | |||
* 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.time.Instant; | |||
import java.time.temporal.ChronoUnit; | |||
import java.util.Optional; | |||
import java.util.Set; | |||
import java.util.function.Function; | |||
import java.util.stream.IntStream; | |||
import org.junit.Rule; | |||
import org.junit.Test; | |||
import org.sonar.api.utils.DateUtils; | |||
import org.sonar.core.util.UuidFactory; | |||
import org.sonar.db.DbTester; | |||
import org.sonar.db.scim.ScimUserDao; | |||
import org.sonar.db.user.GroupDto; | |||
import org.sonar.db.user.UserDto; | |||
import org.sonar.server.common.SearchResults; | |||
import org.sonar.server.common.avatar.AvatarResolverImpl; | |||
import org.sonar.server.management.ManagedInstanceService; | |||
import static java.util.Arrays.asList; | |||
import static java.util.Collections.singletonList; | |||
import static java.util.function.Function.identity; | |||
import static java.util.stream.Collectors.toMap; | |||
import static org.assertj.core.api.Assertions.assertThat; | |||
import static org.assertj.core.api.Assertions.tuple; | |||
import static org.mockito.ArgumentMatchers.any; | |||
import static org.mockito.ArgumentMatchers.anyBoolean; | |||
import static org.mockito.Mockito.mock; | |||
import static org.mockito.Mockito.when; | |||
public class UserServiceIT { | |||
private static final UsersSearchRequest SEARCH_REQUEST = getBuilderWithDefaultsPageSize().build(); | |||
@Rule | |||
public DbTester db = DbTester.create(); | |||
private final ManagedInstanceService managedInstanceService = mock(ManagedInstanceService.class); | |||
private final UserService userService = new UserService(db.getDbClient(), new AvatarResolverImpl(), managedInstanceService); | |||
@Test | |||
public void search_for_all_active_users() { | |||
UserDto user1 = db.users().insertUser(); | |||
UserDto user2 = db.users().insertUser(); | |||
UserDto user3 = db.users().insertUser(u -> u.setActive(false)); | |||
SearchResults<UserSearchResult> users = userService.findUsers(SEARCH_REQUEST); | |||
assertThat(users.searchResults()) | |||
.extracting(r -> r.userDto().getLogin(), r -> r.userDto().getName()) | |||
.containsExactlyInAnyOrder( | |||
tuple(user1.getLogin(), user1.getName()), | |||
tuple(user2.getLogin(), user2.getName())); | |||
} | |||
@Test | |||
public void search_deactivated_users() { | |||
UserDto user1 = db.users().insertUser(u -> u.setActive(false)); | |||
UserDto user2 = db.users().insertUser(u -> u.setActive(true)); | |||
SearchResults<UserSearchResult> users = userService.findUsers(UsersSearchRequest.builder().setPage(1).setPageSize(50).setDeactivated(true).build()); | |||
assertThat(users.searchResults()) | |||
.extracting(r -> r.userDto().getLogin(), r -> r.userDto().getName()) | |||
.containsExactlyInAnyOrder( | |||
tuple(user1.getLogin(), user1.getName())); | |||
} | |||
@Test | |||
public void search_with_query() { | |||
UserDto user = db.users().insertUser(u -> u | |||
.setLogin("user-%_%-login") | |||
.setName("user-name") | |||
.setEmail("user@mail.com") | |||
.setLocal(true) | |||
.setScmAccounts(singletonList("user1"))); | |||
SearchResults<UserSearchResult> users = userService.findUsers(UsersSearchRequest.builder().setPage(1).setPageSize(50).setQuery("user-%_%-").build()); | |||
assertThat(users.searchResults()).extracting(UserSearchResult::userDto).extracting(UserDto::getLogin) | |||
.containsExactly(user.getLogin()); | |||
users = userService.findUsers(UsersSearchRequest.builder().setPage(1).setPageSize(50).setQuery("user@MAIL.com").build()); | |||
assertThat(users.searchResults()).extracting(UserSearchResult::userDto).extracting(UserDto::getLogin) | |||
.containsExactly(user.getLogin()); | |||
users = userService.findUsers(getBuilderWithDefaultsPageSize().setQuery("user-name").build()); | |||
assertThat(users.searchResults()).extracting(UserSearchResult::userDto).extracting(UserDto::getLogin) | |||
.containsExactly(user.getLogin()); | |||
} | |||
@Test | |||
public void return_avatar() { | |||
UserDto user = db.users().insertUser(u -> u.setEmail("john@doe.com")); | |||
SearchResults<UserSearchResult> users = userService.findUsers(SEARCH_REQUEST); | |||
assertThat(users.searchResults()) | |||
.extracting(r -> r.userDto().getLogin(), UserSearchResult::avatar) | |||
.containsExactlyInAnyOrder( | |||
tuple(user.getLogin(), Optional.of("6a6c19fea4a3676970167ce51f39e6ee"))); | |||
} | |||
@Test | |||
public void return_isManagedFlag() { | |||
UserDto nonManagedUser = db.users().insertUser(u -> u.setEmail("john@doe.com")); | |||
UserDto managedUser = db.users().insertUser(u -> u.setEmail("externalUser@doe.com")); | |||
mockUsersAsManaged(managedUser.getUuid()); | |||
SearchResults<UserSearchResult> users = userService.findUsers(SEARCH_REQUEST); | |||
assertThat(users.searchResults()) | |||
.extracting(r -> r.userDto().getLogin(), UserSearchResult::managed) | |||
.containsExactlyInAnyOrder( | |||
tuple(managedUser.getLogin(), true), | |||
tuple(nonManagedUser.getLogin(), false) | |||
); | |||
} | |||
@Test | |||
public void search_whenFilteringByManagedAndInstanceManaged_returnsCorrectResults() { | |||
UserDto nonManagedUser = db.users().insertUser(u -> u.setEmail("john@doe.com")); | |||
UserDto managedUser = db.users().insertUser(u -> u.setEmail("externalUser@doe.com")); | |||
db.users().enableScimForUser(managedUser); | |||
mockUsersAsManaged(managedUser.getUuid()); | |||
mockInstanceExternallyManagedAndFilterForManagedUsers(); | |||
SearchResults<UserSearchResult> users = userService.findUsers(UsersSearchRequest.builder().setPage(1).setPageSize(50).setManaged(true).build()); | |||
assertThat(users.searchResults()) | |||
.extracting(r -> r.userDto().getLogin(), UserSearchResult::managed) | |||
.containsExactlyInAnyOrder( | |||
tuple(managedUser.getLogin(), true) | |||
); | |||
} | |||
@Test | |||
public void search_whenFilteringByNonManagedAndInstanceManaged_returnsCorrectResults() { | |||
UserDto nonManagedUser = db.users().insertUser(u -> u.setEmail("john@doe.com")); | |||
UserDto managedUser = db.users().insertUser(u -> u.setEmail("externalUser@doe.com")); | |||
db.users().enableScimForUser(managedUser); | |||
mockUsersAsManaged(managedUser.getUuid()); | |||
mockInstanceExternallyManagedAndFilterForManagedUsers(); | |||
SearchResults<UserSearchResult> users = userService.findUsers(UsersSearchRequest.builder().setPage(1).setPageSize(50).setManaged(false).build()); | |||
assertThat(users.searchResults()) | |||
.extracting(r -> r.userDto().getLogin(), UserSearchResult::managed) | |||
.containsExactlyInAnyOrder( | |||
tuple(nonManagedUser.getLogin(), false) | |||
); | |||
} | |||
private void mockInstanceExternallyManagedAndFilterForManagedUsers() { | |||
when(managedInstanceService.isInstanceExternallyManaged()).thenReturn(true); | |||
when(managedInstanceService.getManagedUsersSqlFilter(anyBoolean())) | |||
.thenAnswer(invocation -> { | |||
Boolean managed = invocation.getArgument(0, Boolean.class); | |||
return new ScimUserDao(mock(UuidFactory.class)).getManagedUserSqlFilter(managed); | |||
}); | |||
} | |||
@Test | |||
public void return_scm_accounts() { | |||
UserDto user = db.users().insertUser(u -> u.setScmAccounts(asList("john1", "john2"))); | |||
SearchResults<UserSearchResult> users = userService.findUsers(SEARCH_REQUEST); | |||
assertThat(users.searchResults()) | |||
.extracting(r -> r.userDto().getLogin(), userSearchResult -> userSearchResult.userDto().getSortedScmAccounts()) | |||
.containsExactlyInAnyOrder(tuple(user.getLogin(), asList("john1", "john2"))); | |||
} | |||
@Test | |||
public void return_tokens_count_when_system_administer() { | |||
UserDto user = db.users().insertUser(); | |||
db.users().insertToken(user); | |||
db.users().insertToken(user); | |||
SearchResults<UserSearchResult> users = userService.findUsers(SEARCH_REQUEST); | |||
assertThat(users.searchResults()) | |||
.extracting(r -> r.userDto().getLogin(), UserSearchResult::tokensCount) | |||
.containsExactlyInAnyOrder(tuple(user.getLogin(), 2)); | |||
} | |||
@Test | |||
public void return_user_not_having_email() { | |||
UserDto user = db.users().insertUser(u -> u.setEmail(null)); | |||
SearchResults<UserSearchResult> users = userService.findUsers(SEARCH_REQUEST); | |||
assertThat(users.searchResults()) | |||
.extracting(r -> r.userDto().getLogin(), userSearchResult -> userSearchResult.userDto().getEmail()) | |||
.containsExactlyInAnyOrder(tuple(user.getLogin(), null)); | |||
} | |||
@Test | |||
public void return_groups() { | |||
UserDto user = db.users().insertUser(); | |||
GroupDto group1 = db.users().insertGroup("group1"); | |||
GroupDto group2 = db.users().insertGroup("group2"); | |||
GroupDto group3 = db.users().insertGroup("group3"); | |||
db.users().insertMember(group1, user); | |||
db.users().insertMember(group2, user); | |||
SearchResults<UserSearchResult> users = userService.findUsers(SEARCH_REQUEST); | |||
assertThat(users.searchResults()) | |||
.extracting(r -> r.userDto().getLogin(), UserSearchResult::groups) | |||
.containsExactlyInAnyOrder(tuple(user.getLogin(), asList(group1.getName(), group2.getName()))); | |||
} | |||
@Test | |||
public void return_external_information() { | |||
UserDto user = db.users().insertUser(); | |||
SearchResults<UserSearchResult> users = userService.findUsers(SEARCH_REQUEST); | |||
assertThat(users.searchResults()) | |||
.extracting( | |||
r -> r.userDto().getLogin(), | |||
userSearchResult -> userSearchResult.userDto().getExternalLogin(), | |||
userSearchResult -> userSearchResult.userDto().getExternalIdentityProvider() | |||
) | |||
.containsExactlyInAnyOrder(tuple(user.getLogin(), user.getExternalLogin(), user.getExternalIdentityProvider())); | |||
} | |||
@Test | |||
public void return_last_connection_date() { | |||
UserDto userWithLastConnectionDate = db.users().insertUser(); | |||
db.users().updateLastConnectionDate(userWithLastConnectionDate, 10_000_000_000L); | |||
UserDto userWithoutLastConnectionDate = db.users().insertUser(); | |||
SearchResults<UserSearchResult> users = userService.findUsers(SEARCH_REQUEST); | |||
assertThat(users.searchResults()) | |||
.extracting(r -> r.userDto().getLogin(), userSearchResult -> userSearchResult.userDto().getLastConnectionDate()) | |||
.containsExactlyInAnyOrder( | |||
tuple(userWithLastConnectionDate.getLogin(), 10_000_000_000L), | |||
tuple(userWithoutLastConnectionDate.getLogin(), null)); | |||
} | |||
@Test | |||
public void return_all_fields_for_logged_user() { | |||
UserDto user = db.users().insertUser(u -> u.setEmail("aa@bb.com")); | |||
db.users().updateLastConnectionDate(user, 10_000_000_000L); | |||
db.users().insertToken(user); | |||
db.users().insertToken(user); | |||
GroupDto group = db.users().insertGroup(); | |||
db.users().insertMember(group, user); | |||
SearchResults<UserSearchResult> users = userService.findUsers(SEARCH_REQUEST); | |||
assertThat(users.searchResults()) | |||
.extracting(UserSearchResult::userDto) | |||
.extracting(UserDto::getLogin, UserDto::getName, UserDto::getEmail, UserDto::getExternalLogin, UserDto::getExternalIdentityProvider, | |||
userDto -> !userDto.getSortedScmAccounts().isEmpty(), UserDto::getLastConnectionDate) | |||
.containsExactlyInAnyOrder( | |||
tuple(user.getLogin(), user.getName(), user.getEmail(), user.getExternalLogin(), user.getExternalIdentityProvider(), true, 10_000_000_000L)); | |||
assertThat(users.searchResults()) | |||
.extracting(UserSearchResult::avatar, UserSearchResult::tokensCount, userSearchResult -> userSearchResult.groups().size()) | |||
.containsExactly(tuple(Optional.of("5dcdf28d944831f2fb87d48b81500c66"), 2, 1)); | |||
} | |||
@Test | |||
public void search_whenNoPagingInformationProvided_setsDefaultValues() { | |||
IntStream.rangeClosed(0, 9).forEach(i -> db.users().insertUser(u -> u.setLogin("user-" + i).setName("User " + i))); | |||
SearchResults<UserSearchResult> users = userService.findUsers(SEARCH_REQUEST); | |||
assertThat(users.total()).isEqualTo(10); | |||
} | |||
@Test | |||
public void search_with_paging() { | |||
IntStream.rangeClosed(0, 9).forEach(i -> db.users().insertUser(u -> u.setLogin("user-" + i).setName("User " + i))); | |||
SearchResults<UserSearchResult> users = userService.findUsers(UsersSearchRequest.builder().setPage(1).setPageSize(5).build()); | |||
assertThat(users.searchResults()) | |||
.extracting(u -> u.userDto().getLogin()) | |||
.containsExactly("user-0", "user-1", "user-2", "user-3", "user-4"); | |||
assertThat(users.total()).isEqualTo(10); | |||
users = userService.findUsers(UsersSearchRequest.builder().setPage(2).setPageSize(5).build()); | |||
assertThat(users.searchResults()) | |||
.extracting(u -> u.userDto().getLogin()) | |||
.containsExactly("user-5", "user-6", "user-7", "user-8", "user-9"); | |||
assertThat(users.total()).isEqualTo(10); | |||
} | |||
@Test | |||
public void return_empty_result_when_no_user() { | |||
SearchResults<UserSearchResult> users = userService.findUsers(SEARCH_REQUEST); | |||
assertThat(users.searchResults()).isEmpty(); | |||
assertThat(users.total()).isZero(); | |||
} | |||
@Test | |||
public void search_whenFilteringConnectionDate_shouldApplyFilter() { | |||
final Instant lastConnection = Instant.now(); | |||
UserDto user = db.users().insertUser(u -> u | |||
.setLogin("user-%_%-login") | |||
.setName("user-name") | |||
.setEmail("user@mail.com") | |||
.setLocal(true) | |||
.setScmAccounts(singletonList("user1"))); | |||
user = db.users().updateLastConnectionDate(user, lastConnection.toEpochMilli()); | |||
user = db.users().updateSonarLintLastConnectionDate(user, lastConnection.toEpochMilli()); | |||
SearchResults<UserSearchResult> users = userService.findUsers(UsersSearchRequest.builder().setPage(1).setPageSize(50).setQuery("user-%_%-").build()); | |||
assertThat(users.searchResults()) | |||
.extracting(r -> r.userDto().getLogin()) | |||
.containsExactlyInAnyOrder(user.getLogin()); | |||
assertUserWithFilter(b -> b.setLastConnectionDateFrom(DateUtils.formatDateTime(lastConnection.minus(1, ChronoUnit.DAYS).toEpochMilli())), user.getLogin(), true); | |||
assertUserWithFilter(b -> b.setLastConnectionDateFrom(DateUtils.formatDateTime(lastConnection.plus(1, ChronoUnit.DAYS).toEpochMilli())), user.getLogin(), false); | |||
assertUserWithFilter(b -> b.setLastConnectionDateTo(DateUtils.formatDateTime(lastConnection.minus(1, ChronoUnit.DAYS).toEpochMilli())), user.getLogin(), false); | |||
assertUserWithFilter(b -> b.setLastConnectionDateTo(DateUtils.formatDateTime(lastConnection.plus(1, ChronoUnit.DAYS).toEpochMilli())), user.getLogin(), true); | |||
assertUserWithFilter(b -> b.setSonarLintLastConnectionDateFrom(DateUtils.formatDateTime(lastConnection.minus(1, ChronoUnit.DAYS).toEpochMilli())), user.getLogin(), true); | |||
assertUserWithFilter(b -> b.setSonarLintLastConnectionDateFrom(DateUtils.formatDateTime(lastConnection.plus(1, ChronoUnit.DAYS).toEpochMilli())), user.getLogin(), false); | |||
assertUserWithFilter(b -> b.setSonarLintLastConnectionDateTo(DateUtils.formatDateTime(lastConnection.minus(1, ChronoUnit.DAYS).toEpochMilli())), user.getLogin(), false); | |||
assertUserWithFilter(b -> b.setSonarLintLastConnectionDateTo(DateUtils.formatDateTime(lastConnection.plus(1, ChronoUnit.DAYS).toEpochMilli())), user.getLogin(), true); | |||
assertUserWithFilter(b -> b.setSonarLintLastConnectionDateFrom(DateUtils.formatDateTime(lastConnection.toEpochMilli())), user.getLogin(), true); | |||
assertUserWithFilter(b -> b.setSonarLintLastConnectionDateTo(DateUtils.formatDateTime(lastConnection.toEpochMilli())), user.getLogin(), true); | |||
} | |||
@Test | |||
public void search_whenNoLastConnection_shouldReturnForBeforeOnly() { | |||
final Instant lastConnection = Instant.now(); | |||
UserDto user = db.users().insertUser(u -> u | |||
.setLogin("user-%_%-login") | |||
.setName("user-name") | |||
.setEmail("user@mail.com") | |||
.setLocal(true) | |||
.setScmAccounts(singletonList("user1"))); | |||
assertUserWithFilter(b -> b.setLastConnectionDateFrom(DateUtils.formatDateTime(lastConnection.toEpochMilli())), user.getLogin(), false); | |||
assertUserWithFilter(b -> b.setLastConnectionDateTo(DateUtils.formatDateTime(lastConnection.toEpochMilli())), user.getLogin(), true); | |||
assertUserWithFilter(b -> b.setSonarLintLastConnectionDateFrom(DateUtils.formatDateTime(lastConnection.toEpochMilli())), user.getLogin(), false); | |||
assertUserWithFilter(b -> b.setSonarLintLastConnectionDateTo(DateUtils.formatDateTime(lastConnection.toEpochMilli())), user.getLogin(), true); | |||
} | |||
private void assertUserWithFilter(Function<UsersSearchRequest.Builder, UsersSearchRequest.Builder> query, String userLogin, boolean isExpectedToBeThere) { | |||
UsersSearchRequest.Builder builder = getBuilderWithDefaultsPageSize(); | |||
builder = query.apply(builder); | |||
SearchResults<UserSearchResult> users = userService.findUsers(builder.setQuery("user-%_%-").build()); | |||
var assertion = assertThat(users.searchResults()); | |||
if (isExpectedToBeThere) { | |||
assertion | |||
.extracting(r -> r.userDto().getLogin()) | |||
.containsExactlyInAnyOrder(userLogin); | |||
} else { | |||
assertion.isEmpty(); | |||
} | |||
} | |||
private void mockUsersAsManaged(String... userUuids) { | |||
when(managedInstanceService.getUserUuidToManaged(any(), any())).thenAnswer(invocation -> | |||
{ | |||
Set<?> allUsersUuids = invocation.getArgument(1, Set.class); | |||
return allUsersUuids.stream() | |||
.map(userUuid -> (String) userUuid) | |||
.collect(toMap(identity(), userUuid -> Set.of(userUuids).contains(userUuid))); | |||
} | |||
); | |||
} | |||
private static UsersSearchRequest.Builder getBuilderWithDefaultsPageSize() { | |||
return UsersSearchRequest.builder().setPage(1).setPageSize(50); | |||
} | |||
} |
@@ -0,0 +1,25 @@ | |||
/* | |||
* 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; | |||
import java.util.List; | |||
public record SearchResults<T>(List<T> searchResults, int total) { | |||
} |
@@ -17,7 +17,7 @@ | |||
* 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.issue; | |||
package org.sonar.server.common.avatar; | |||
import org.sonar.db.user.UserDto; | |||
@@ -17,7 +17,7 @@ | |||
* 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.issue; | |||
package org.sonar.server.common.avatar; | |||
import com.google.common.hash.Hashing; | |||
import org.sonar.db.user.UserDto; |
@@ -0,0 +1,30 @@ | |||
/* | |||
* 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; | |||
import java.util.List; | |||
import org.sonar.api.utils.Paging; | |||
import org.sonar.server.common.user.service.UserSearchResult; | |||
public interface UsersSearchResponseGenerator<T> { | |||
T toUsersForResponse(List<UserSearchResult> userSearchResults, Paging paging); | |||
} |
@@ -0,0 +1,27 @@ | |||
/* | |||
* 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.Collection; | |||
import java.util.Optional; | |||
import org.sonar.db.user.UserDto; | |||
public record UserSearchResult(UserDto userDto, boolean managed, Optional<String> avatar, Collection<String> groups, int tokensCount) { | |||
} |
@@ -0,0 +1,125 @@ | |||
/* | |||
* 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 com.google.common.collect.Multimap; | |||
import java.util.Collection; | |||
import java.util.List; | |||
import java.util.Map; | |||
import java.util.Optional; | |||
import java.util.Set; | |||
import java.util.stream.Collectors; | |||
import org.sonar.db.DbClient; | |||
import org.sonar.db.DbSession; | |||
import org.sonar.db.user.UserDto; | |||
import org.sonar.db.user.UserQuery; | |||
import org.sonar.server.common.SearchResults; | |||
import org.sonar.server.common.avatar.AvatarResolver; | |||
import org.sonar.server.exceptions.BadRequestException; | |||
import org.sonar.server.management.ManagedInstanceService; | |||
import static java.util.Comparator.comparing; | |||
public class UserService { | |||
private final DbClient dbClient; | |||
private final AvatarResolver avatarResolver; | |||
private final ManagedInstanceService managedInstanceService; | |||
public UserService(DbClient dbClient, AvatarResolver avatarResolver, ManagedInstanceService managedInstanceService) { | |||
this.dbClient = dbClient; | |||
this.avatarResolver = avatarResolver; | |||
this.managedInstanceService = managedInstanceService; | |||
} | |||
public SearchResults<UserSearchResult> findUsers(UsersSearchRequest request) { | |||
UserQuery userQuery = buildUserQuery(request); | |||
try (DbSession dbSession = dbClient.openSession(false)) { | |||
int totalUsers = dbClient.userDao().countUsers(dbSession, userQuery); | |||
List<UserSearchResult> searchResults = performSearch(dbSession, userQuery, request.getPage(), request.getPageSize()); | |||
return new SearchResults<>(searchResults, totalUsers); | |||
} | |||
} | |||
private UserQuery buildUserQuery(UsersSearchRequest request) { | |||
UserQuery.UserQueryBuilder builder = UserQuery.builder(); | |||
request.getLastConnectionDateFrom().ifPresent(builder::lastConnectionDateFrom); | |||
request.getLastConnectionDateTo().ifPresent(builder::lastConnectionDateTo); | |||
request.getSonarLintLastConnectionDateFrom().ifPresent(builder::sonarLintLastConnectionDateFrom); | |||
request.getSonarLintLastConnectionDateTo().ifPresent(builder::sonarLintLastConnectionDateTo); | |||
if (managedInstanceService.isInstanceExternallyManaged()) { | |||
String managedInstanceSql = Optional.ofNullable(request.isManaged()) | |||
.map(managedInstanceService::getManagedUsersSqlFilter) | |||
.orElse(null); | |||
builder.isManagedClause(managedInstanceSql); | |||
} else if (request.isManaged() != null) { | |||
throw BadRequestException.create("The 'managed' parameter is only available for managed instances."); | |||
} | |||
return builder | |||
.isActive(!request.isDeactivated()) | |||
.searchText(request.getQuery()) | |||
.build(); | |||
} | |||
private List<UserSearchResult> performSearch(DbSession dbSession, UserQuery userQuery, int pageIndex, int pageSize) { | |||
List<UserDto> userDtos = findUsersAndSortByLogin(dbSession, userQuery, pageIndex, pageSize); | |||
List<String> logins = userDtos.stream().map(UserDto::getLogin).toList(); | |||
Multimap<String, String> groupsByLogin = dbClient.groupMembershipDao().selectGroupsByLogins(dbSession, logins); | |||
Map<String, Integer> tokenCountsByLogin = dbClient.userTokenDao().countTokensByUsers(dbSession, userDtos); | |||
Map<String, Boolean> userUuidToIsManaged = managedInstanceService.getUserUuidToManaged(dbSession, getUserUuids(userDtos)); | |||
return userDtos.stream() | |||
.map(userDto -> toUserSearchResult( | |||
groupsByLogin.get(userDto.getLogin()), | |||
tokenCountsByLogin.getOrDefault(userDto.getUuid(), 0), | |||
userUuidToIsManaged.getOrDefault(userDto.getUuid(), false), | |||
userDto | |||
) | |||
).toList(); | |||
} | |||
private UserSearchResult toUserSearchResult(Collection<String> groups, int tokenCount, boolean managed, UserDto userDto) { | |||
return new UserSearchResult( | |||
userDto, | |||
managed, | |||
findAvatar(userDto), | |||
groups, | |||
tokenCount | |||
); | |||
} | |||
private List<UserDto> findUsersAndSortByLogin(DbSession dbSession, UserQuery userQuery, int page, int pageSize) { | |||
return dbClient.userDao().selectUsers(dbSession, userQuery, page, pageSize) | |||
.stream() | |||
.sorted(comparing(UserDto::getLogin)) | |||
.toList(); | |||
} | |||
private Optional<String> findAvatar(UserDto userDto) { | |||
return Optional.ofNullable(userDto.getEmail()).map(email -> avatarResolver.create(userDto)); | |||
} | |||
private static Set<String> getUserUuids(List<UserDto> users) { | |||
return users.stream().map(UserDto::getUuid).collect(Collectors.toSet()); | |||
} | |||
} |
@@ -0,0 +1,163 @@ | |||
/* | |||
* 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.time.OffsetDateTime; | |||
import java.util.Optional; | |||
import javax.annotation.CheckForNull; | |||
import javax.annotation.Nullable; | |||
import org.sonar.api.utils.DateUtils; | |||
import org.sonar.api.utils.MessageException; | |||
import org.sonar.server.exceptions.ServerException; | |||
public class UsersSearchRequest { | |||
private final Integer page; | |||
private final Integer pageSize; | |||
private final String query; | |||
private final boolean deactivated; | |||
private final Boolean managed; | |||
private final OffsetDateTime lastConnectionDateFrom; | |||
private final OffsetDateTime lastConnectionDateTo; | |||
private final OffsetDateTime sonarLintLastConnectionDateFrom; | |||
private final OffsetDateTime sonarLintLastConnectionDateTo; | |||
private UsersSearchRequest(Builder builder) { | |||
this.page = builder.page; | |||
this.pageSize = builder.pageSize; | |||
this.query = builder.query; | |||
this.deactivated = builder.deactivated; | |||
this.managed = builder.managed; | |||
try { | |||
this.lastConnectionDateFrom = Optional.ofNullable(builder.lastConnectionDateFrom).map(DateUtils::parseOffsetDateTime).orElse(null); | |||
this.lastConnectionDateTo = Optional.ofNullable(builder.lastConnectionDateTo).map(DateUtils::parseOffsetDateTime).orElse(null); | |||
this.sonarLintLastConnectionDateFrom = Optional.ofNullable(builder.sonarLintLastConnectionDateFrom).map(DateUtils::parseOffsetDateTime).orElse(null); | |||
this.sonarLintLastConnectionDateTo = Optional.ofNullable(builder.sonarLintLastConnectionDateTo).map(DateUtils::parseOffsetDateTime).orElse(null); | |||
} catch (MessageException me) { | |||
throw new ServerException(400, me.getMessage()); | |||
} | |||
} | |||
public Integer getPage() { | |||
return page; | |||
} | |||
public Integer getPageSize() { | |||
return pageSize; | |||
} | |||
@CheckForNull | |||
public String getQuery() { | |||
return query; | |||
} | |||
public boolean isDeactivated() { | |||
return deactivated; | |||
} | |||
@CheckForNull | |||
public Boolean isManaged() { | |||
return managed; | |||
} | |||
public Optional<OffsetDateTime> getLastConnectionDateFrom() { | |||
return Optional.ofNullable(lastConnectionDateFrom); | |||
} | |||
public Optional<OffsetDateTime> getLastConnectionDateTo() { | |||
return Optional.ofNullable(lastConnectionDateTo); | |||
} | |||
public Optional<OffsetDateTime> getSonarLintLastConnectionDateFrom() { | |||
return Optional.ofNullable(sonarLintLastConnectionDateFrom); | |||
} | |||
public Optional<OffsetDateTime> getSonarLintLastConnectionDateTo() { | |||
return Optional.ofNullable(sonarLintLastConnectionDateTo); | |||
} | |||
public static Builder builder() { | |||
return new Builder(); | |||
} | |||
public static class Builder { | |||
private Integer page; | |||
private Integer pageSize; | |||
private String query; | |||
private boolean deactivated; | |||
private Boolean managed; | |||
private String lastConnectionDateFrom; | |||
private String lastConnectionDateTo; | |||
private String sonarLintLastConnectionDateFrom; | |||
private String sonarLintLastConnectionDateTo; | |||
private Builder() { | |||
// enforce factory method use | |||
} | |||
public Builder setPage(Integer page) { | |||
this.page = page; | |||
return this; | |||
} | |||
public Builder setPageSize(Integer pageSize) { | |||
this.pageSize = pageSize; | |||
return this; | |||
} | |||
public Builder setQuery(@Nullable String query) { | |||
this.query = query; | |||
return this; | |||
} | |||
public Builder setDeactivated(boolean deactivated) { | |||
this.deactivated = deactivated; | |||
return this; | |||
} | |||
public Builder setManaged(@Nullable Boolean managed) { | |||
this.managed = managed; | |||
return this; | |||
} | |||
public Builder setLastConnectionDateFrom(@Nullable String lastConnectionDateFrom) { | |||
this.lastConnectionDateFrom = lastConnectionDateFrom; | |||
return this; | |||
} | |||
public Builder setLastConnectionDateTo(@Nullable String lastConnectionDateTo) { | |||
this.lastConnectionDateTo = lastConnectionDateTo; | |||
return this; | |||
} | |||
public Builder setSonarLintLastConnectionDateFrom(@Nullable String sonarLintLastConnectionDateFrom) { | |||
this.sonarLintLastConnectionDateFrom = sonarLintLastConnectionDateFrom; | |||
return this; | |||
} | |||
public Builder setSonarLintLastConnectionDateTo(@Nullable String sonarLintLastConnectionDateTo) { | |||
this.sonarLintLastConnectionDateTo = sonarLintLastConnectionDateTo; | |||
return this; | |||
} | |||
public UsersSearchRequest build() { | |||
return new UsersSearchRequest(this); | |||
} | |||
} | |||
} |
@@ -17,29 +17,28 @@ | |||
* 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.issue; | |||
package org.sonar.server.common.avatar; | |||
import org.junit.Test; | |||
import org.sonar.db.user.UserTesting; | |||
import static org.assertj.core.api.Assertions.assertThat; | |||
import static org.assertj.core.api.Assertions.assertThatThrownBy; | |||
import static org.sonar.db.user.UserTesting.newUserDto; | |||
public class AvatarResolverImplTest { | |||
private AvatarResolverImpl underTest = new AvatarResolverImpl(); | |||
@Test | |||
public void create() { | |||
String avatar = underTest.create(newUserDto("john", "John", "john@doo.com")); | |||
String avatar = underTest.create(UserTesting.newUserDto("john", "John", "john@doo.com")); | |||
assertThat(avatar).isEqualTo("9297bfb538f650da6143b604e82a355d"); | |||
} | |||
@Test | |||
public void create_is_case_insensitive() { | |||
assertThat(underTest.create(newUserDto("john", "John", "john@doo.com"))).isEqualTo(underTest.create(newUserDto("john", "John", "John@Doo.com"))); | |||
assertThat(underTest.create(UserTesting.newUserDto("john", "John", "john@doo.com"))).isEqualTo(underTest.create(UserTesting.newUserDto("john", "John", "John@Doo.com"))); | |||
} | |||
@Test | |||
@@ -51,14 +50,14 @@ public class AvatarResolverImplTest { | |||
@Test | |||
public void fail_with_NP_when_email_is_null() { | |||
assertThatThrownBy(() -> underTest.create(newUserDto("john", "John", null))) | |||
assertThatThrownBy(() -> underTest.create(UserTesting.newUserDto("john", "John", null))) | |||
.isInstanceOf(NullPointerException.class) | |||
.hasMessage("Email cannot be null"); | |||
} | |||
@Test | |||
public void fail_when_email_is_empty() { | |||
assertThatThrownBy(() -> underTest.create(newUserDto("john", "John", ""))) | |||
assertThatThrownBy(() -> underTest.create(UserTesting.newUserDto("john", "John", ""))) | |||
.isInstanceOf(NullPointerException.class) | |||
.hasMessage("Email cannot be null"); | |||
} |
@@ -6,11 +6,15 @@ sonarqube { | |||
dependencies { | |||
// please keep the list grouped by configuration and ordered by name | |||
api 'org.springdoc:springdoc-openapi-ui' | |||
api 'org.springdoc:springdoc-openapi-webmvc-core' | |||
api 'org.springframework:spring-webmvc' | |||
api 'org.hibernate:hibernate-validator' | |||
api 'javax.el:javax.el-api' | |||
api 'org.glassfish:javax.el' | |||
api project(':server:sonar-db-dao') | |||
// We are not suppose to have a v1 dependency. The ideal would be to have another common module between webapi and webapi-v2 but that needs a lot of refactoring. | |||
api project(':server:sonar-webserver-common') | |||
// We are not supposed to have a v1 dependency. The ideal would be to have another common module between webapi and webapi-v2 but that needs a lot of refactoring. | |||
api project(':server:sonar-webserver-webapi') | |||
testImplementation 'org.mockito:mockito-core' |
@@ -0,0 +1,28 @@ | |||
/* | |||
* 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.controller; | |||
import static org.assertj.core.api.Assertions.assertThat; | |||
import static org.mockito.Mockito.mock; | |||
import static org.mockito.Mockito.when; | |||
public class DefaultUserControllerTest { | |||
} |
@@ -20,6 +20,7 @@ | |||
package org.sonar.server.v2.config; | |||
import org.sonar.db.DbClient; | |||
import org.sonar.server.common.user.service.UserService; | |||
import org.sonar.server.health.CeStatusNodeCheck; | |||
import org.sonar.server.health.DbConnectionNodeCheck; | |||
import org.sonar.server.health.EsStatusNodeCheck; | |||
@@ -98,4 +99,9 @@ public class MockConfigForControllers { | |||
ManagedInstanceChecker managedInstanceChecker() { | |||
return mock(ManagedInstanceChecker.class); | |||
} | |||
@Bean | |||
UserService userService() { | |||
return mock(UserService.class); | |||
} | |||
} |
@@ -73,7 +73,7 @@ public class DefaultLivenessControllerIT extends ControllerIT { | |||
mockMvc.perform(get(LIVENESS_ENDPOINT)) | |||
.andExpectAll( | |||
status().isForbidden(), | |||
content().string("Insufficient privileges")); | |||
content().json("{\"message\":\"Insufficient privileges\"}")); | |||
} | |||
@Test | |||
@@ -84,7 +84,7 @@ public class DefaultLivenessControllerIT extends ControllerIT { | |||
mockMvc.perform(get(LIVENESS_ENDPOINT).header(PASSCODE_HTTP_HEADER, INVALID_PASSCODE)) | |||
.andExpectAll( | |||
status().isForbidden(), | |||
content().string("Insufficient privileges")); | |||
content().json("{\"message\":\"Insufficient privileges\"}")); | |||
} | |||
@Test | |||
@@ -95,6 +95,6 @@ public class DefaultLivenessControllerIT extends ControllerIT { | |||
mockMvc.perform(get(LIVENESS_ENDPOINT).header(PASSCODE_HTTP_HEADER, VALID_PASSCODE)) | |||
.andExpectAll( | |||
status().isInternalServerError(), | |||
content().string("Liveness check failed")); | |||
content().json("{\"message\":\"Liveness check failed\"}")); | |||
} | |||
} |
@@ -101,7 +101,7 @@ public class HealthControllerIT extends ControllerIT { | |||
mockMvc.perform(get(HEALTH_ENDPOINT)) | |||
.andExpectAll( | |||
status().isForbidden(), | |||
content().string("Insufficient privileges")); | |||
content().json("{\"message\":\"Insufficient privileges\"}")); | |||
} | |||
@Test | |||
@@ -112,7 +112,7 @@ public class HealthControllerIT extends ControllerIT { | |||
mockMvc.perform(get(HEALTH_ENDPOINT).header(PASSCODE_HTTP_HEADER, INVALID_PASSCODE)) | |||
.andExpectAll( | |||
status().isForbidden(), | |||
content().string("Insufficient privileges")); | |||
content().json("{\"message\":\"Insufficient privileges\"}")); | |||
} | |||
@Test | |||
@@ -122,7 +122,7 @@ public class HealthControllerIT extends ControllerIT { | |||
mockMvc.perform(get(HEALTH_ENDPOINT)) | |||
.andExpectAll( | |||
status().isUnauthorized(), | |||
content().string("UnauthorizedException")); | |||
content().json("{\"message\":\"UnauthorizedException\"}")); | |||
} | |||
@Test | |||
@@ -133,6 +133,6 @@ public class HealthControllerIT extends ControllerIT { | |||
mockMvc.perform(get(HEALTH_ENDPOINT).header(PASSCODE_HTTP_HEADER, VALID_PASSCODE)) | |||
.andExpectAll( | |||
status().is(HTTP_NOT_IMPLEMENTED), | |||
content().string("Unsupported in cluster mode")); | |||
content().json("{\"message\":\"Unsupported in cluster mode\"}")); | |||
} | |||
} |
@@ -25,6 +25,7 @@ public class WebApiEndpoints { | |||
public static final String LIVENESS_ENDPOINT = SYSTEM_ENDPOINTS + "/liveness"; | |||
public static final String HEALTH_ENDPOINT = SYSTEM_ENDPOINTS + "/health"; | |||
public static final String USER_ENDPOINT = "/users"; | |||
private WebApiEndpoints() { | |||
} |
@@ -0,0 +1,22 @@ | |||
/* | |||
* 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.model; | |||
public record RestError(String message) {} |
@@ -0,0 +1,54 @@ | |||
/* | |||
* 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.model; | |||
import com.google.common.annotations.VisibleForTesting; | |||
import io.swagger.v3.oas.annotations.Parameter; | |||
import io.swagger.v3.oas.annotations.media.Schema; | |||
import javax.validation.constraints.Max; | |||
import javax.validation.constraints.Min; | |||
import javax.validation.constraints.Positive; | |||
import org.jetbrains.annotations.Nullable; | |||
public record RestPage( | |||
@Min(1) | |||
@Max(500) | |||
@Parameter( | |||
description = "Number of results per page", | |||
schema = @Schema(defaultValue = DEFAULT_PAGE_SIZE, implementation = Integer.class)) | |||
Integer pageSize, | |||
@Positive | |||
@Parameter( | |||
description = "1-based page number", | |||
schema = @Schema(defaultValue = DEFAULT_PAGE_INDEX, implementation = Integer.class)) | |||
Integer pageIndex | |||
) { | |||
@VisibleForTesting | |||
static final String DEFAULT_PAGE_SIZE = "50"; | |||
@VisibleForTesting | |||
static final String DEFAULT_PAGE_INDEX = "1"; | |||
public RestPage(@Nullable Integer pageSize, @Nullable Integer pageIndex) { | |||
this.pageSize = pageSize == null ? Integer.valueOf(DEFAULT_PAGE_SIZE) : pageSize; | |||
this.pageIndex = pageIndex == null ? Integer.valueOf(DEFAULT_PAGE_INDEX) : pageIndex; | |||
} | |||
} |
@@ -0,0 +1,23 @@ | |||
/* | |||
* 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. | |||
*/ | |||
@ParametersAreNonnullByDefault | |||
package org.sonar.server.v2.api.model; | |||
import javax.annotation.ParametersAreNonnullByDefault; |
@@ -0,0 +1,23 @@ | |||
/* | |||
* 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.response; | |||
public record PageRestResponse(int pageIndex, int pageSize, int total) { | |||
} |
@@ -0,0 +1,23 @@ | |||
/* | |||
* 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. | |||
*/ | |||
@ParametersAreNonnullByDefault | |||
package org.sonar.server.v2.api.response; | |||
import javax.annotation.ParametersAreNonnullByDefault; |
@@ -0,0 +1,90 @@ | |||
/* | |||
* 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.controller; | |||
import java.util.Optional; | |||
import javax.annotation.Nullable; | |||
import org.sonar.api.utils.Paging; | |||
import org.sonar.server.common.SearchResults; | |||
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.exceptions.ForbiddenException; | |||
import org.sonar.server.user.UserSession; | |||
import org.sonar.server.v2.api.model.RestPage; | |||
import org.sonar.server.v2.api.user.converter.UsersSearchRestResponseGenerator; | |||
import org.sonar.server.v2.api.user.request.UsersSearchRestRequest; | |||
import org.sonar.server.v2.api.user.response.UsersSearchRestResponse; | |||
import static org.sonar.api.utils.Paging.forPageIndex; | |||
public class DefaultUserController implements UserController { | |||
private final UsersSearchRestResponseGenerator usersSearchResponseGenerator; | |||
private final UserService userService; | |||
private final UserSession userSession; | |||
public DefaultUserController(UserSession userSession, UserService userService, UsersSearchRestResponseGenerator usersSearchResponseGenerator) { | |||
this.userSession = userSession; | |||
this.usersSearchResponseGenerator = usersSearchResponseGenerator; | |||
this.userService = userService; | |||
} | |||
@Override | |||
public UsersSearchRestResponse search(UsersSearchRestRequest usersSearchRestRequest, RestPage page) { | |||
throwIfAdminOnlyParametersAreUsed(usersSearchRestRequest); | |||
SearchResults<UserSearchResult> userSearchResults = userService.findUsers(toUserSearchRequest(usersSearchRestRequest, page)); | |||
Paging paging = forPageIndex(page.pageIndex()).withPageSize(page.pageSize()).andTotal(userSearchResults.total()); | |||
return usersSearchResponseGenerator.toUsersForResponse(userSearchResults.searchResults(), paging); | |||
} | |||
private void throwIfAdminOnlyParametersAreUsed(UsersSearchRestRequest usersSearchRestRequest) { | |||
if (!userSession.isSystemAdministrator()) { | |||
throwIfValuePresent("sonarLintLastConnectionDateFrom", usersSearchRestRequest.sonarLintLastConnectionDateFrom()); | |||
throwIfValuePresent("sonarLintLastConnectionDateTo", usersSearchRestRequest.sonarLintLastConnectionDateTo()); | |||
throwIfValuePresent("sonarQubeLastConnectionDateFrom", usersSearchRestRequest.sonarQubeLastConnectionDateFrom()); | |||
throwIfValuePresent("sonarQubeLastConnectionDateTo", usersSearchRestRequest.sonarQubeLastConnectionDateTo()); | |||
} | |||
} | |||
private static void throwIfValuePresent(String parameter, @Nullable Object value) { | |||
Optional.ofNullable(value).ifPresent(v -> throwForbiddenFor(parameter)); | |||
} | |||
private static void throwForbiddenFor(String parameterName) { | |||
throw new ForbiddenException("parameter " + parameterName + " requires Administer System permission."); | |||
} | |||
private static UsersSearchRequest toUserSearchRequest(UsersSearchRestRequest usersSearchRestRequest, RestPage page) { | |||
return UsersSearchRequest.builder() | |||
.setDeactivated(Optional.ofNullable(usersSearchRestRequest.active()).map(active -> !active).orElse(false)) | |||
.setManaged(usersSearchRestRequest.managed()) | |||
.setQuery(usersSearchRestRequest.q()) | |||
.setLastConnectionDateFrom(usersSearchRestRequest.sonarQubeLastConnectionDateFrom()) | |||
.setLastConnectionDateTo(usersSearchRestRequest.sonarQubeLastConnectionDateTo()) | |||
.setSonarLintLastConnectionDateFrom(usersSearchRestRequest.sonarLintLastConnectionDateFrom()) | |||
.setSonarLintLastConnectionDateTo(usersSearchRestRequest.sonarLintLastConnectionDateTo()) | |||
.setPage(page.pageIndex()) | |||
.setPageSize(page.pageSize()) | |||
.build(); | |||
} | |||
} |
@@ -0,0 +1,56 @@ | |||
/* | |||
* 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.controller; | |||
import io.swagger.v3.oas.annotations.Operation; | |||
import javax.validation.Valid; | |||
import org.sonar.server.v2.api.model.RestPage; | |||
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.http.HttpStatus; | |||
import org.springframework.http.MediaType; | |||
import org.springframework.web.bind.annotation.GetMapping; | |||
import org.springframework.web.bind.annotation.RequestMapping; | |||
import org.springframework.web.bind.annotation.ResponseStatus; | |||
import org.springframework.web.bind.annotation.RestController; | |||
import static org.sonar.server.v2.WebApiEndpoints.USER_ENDPOINT; | |||
@RequestMapping(USER_ENDPOINT) | |||
@RestController | |||
public interface UserController { | |||
@GetMapping(produces = MediaType.APPLICATION_JSON_VALUE) | |||
@ResponseStatus(HttpStatus.OK) | |||
@Operation(summary = "Users search", description = """ | |||
Get a list of users. By default, only active users are returned. | |||
The following fields are only returned when user has Administer System permission or for logged-in in user : | |||
'email' | |||
'externalIdentity' | |||
'externalProvider' | |||
'groups' | |||
'lastConnectionDate' | |||
'sonarLintLastConnectionDate' | |||
'tokensCount' | |||
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. | |||
""") | |||
UsersSearchRestResponse search(@ParameterObject UsersSearchRestRequest usersSearchRestRequest, @Valid @ParameterObject RestPage restPage); | |||
} |
@@ -0,0 +1,23 @@ | |||
/* | |||
* 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. | |||
*/ | |||
@ParametersAreNonnullByDefault | |||
package org.sonar.server.v2.api.user.controller; | |||
import javax.annotation.ParametersAreNonnullByDefault; |
@@ -0,0 +1,111 @@ | |||
/* | |||
* 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.converter; | |||
import java.util.List; | |||
import java.util.Objects; | |||
import java.util.Optional; | |||
import org.jetbrains.annotations.Nullable; | |||
import org.sonar.api.utils.DateUtils; | |||
import org.sonar.api.utils.Paging; | |||
import org.sonar.db.user.UserDto; | |||
import org.sonar.server.common.user.UsersSearchResponseGenerator; | |||
import org.sonar.server.common.user.service.UserSearchResult; | |||
import org.sonar.server.user.UserSession; | |||
import org.sonar.server.v2.api.response.PageRestResponse; | |||
import org.sonar.server.v2.api.user.model.RestUser; | |||
import org.sonar.server.v2.api.user.response.UsersSearchRestResponse; | |||
public class UsersSearchRestResponseGenerator implements UsersSearchResponseGenerator<UsersSearchRestResponse> { | |||
private final UserSession userSession; | |||
public UsersSearchRestResponseGenerator(UserSession userSession) { | |||
this.userSession = userSession; | |||
} | |||
@Override | |||
public UsersSearchRestResponse toUsersForResponse(List<UserSearchResult> userSearchResults, Paging paging) { | |||
List<RestUser> usersForResponse = toUsersForResponse(userSearchResults); | |||
PageRestResponse pageRestResponse = new PageRestResponse(paging.pageIndex(), paging.pageSize(), paging.total()); | |||
return new UsersSearchRestResponse(usersForResponse, pageRestResponse); | |||
} | |||
private List<RestUser> toUsersForResponse(List<UserSearchResult> userSearchResults) { | |||
return userSearchResults.stream() | |||
.map(this::toUser) | |||
.toList(); | |||
} | |||
private RestUser toUser(UserSearchResult userSearchResult) { | |||
UserDto userDto = userSearchResult.userDto(); | |||
String login = userDto.getLogin(); | |||
String name = userDto.getName(); | |||
String avatar = null; | |||
Boolean active = null; | |||
Boolean local = null; | |||
String email = null; | |||
String externalIdentityProvider = null; | |||
String externalLogin = null; | |||
Boolean managed = null; | |||
String sqLastConnectionDate = null; | |||
String slLastConnectionDate = null; | |||
Integer groupSize = null; | |||
Integer tokensCount = null; | |||
if (userSession.isLoggedIn()) { | |||
avatar = userSearchResult.avatar().orElse(null); | |||
active = userDto.isActive(); | |||
local = userDto.isLocal(); | |||
email = userDto.getEmail(); | |||
externalIdentityProvider = userDto.getExternalIdentityProvider(); | |||
} | |||
if (userSession.isSystemAdministrator() || Objects.equals(userSession.getUuid(), userDto.getUuid())) { | |||
externalLogin = userDto.getExternalLogin(); | |||
managed = userSearchResult.managed(); | |||
sqLastConnectionDate = toDateTime(userDto.getLastConnectionDate()); | |||
slLastConnectionDate = toDateTime(userDto.getLastSonarlintConnectionDate()); | |||
groupSize = userSearchResult.groups().size(); | |||
tokensCount = userSearchResult.tokensCount(); | |||
} | |||
return new RestUser( | |||
login, | |||
login, | |||
name, | |||
email, | |||
active, | |||
local, | |||
managed, | |||
externalLogin, | |||
externalIdentityProvider, | |||
avatar, | |||
sqLastConnectionDate, | |||
slLastConnectionDate, | |||
groupSize, | |||
tokensCount | |||
); | |||
} | |||
private static String toDateTime(@Nullable Long dateTimeMs) { | |||
return Optional.ofNullable(dateTimeMs).map(DateUtils::formatDateTime).orElse(null); | |||
} | |||
} |
@@ -0,0 +1,23 @@ | |||
/* | |||
* 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. | |||
*/ | |||
@ParametersAreNonnullByDefault | |||
package org.sonar.server.v2.api.user.converter; | |||
import javax.annotation.ParametersAreNonnullByDefault; |
@@ -0,0 +1,51 @@ | |||
/* | |||
* 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.model; | |||
import javax.annotation.Nullable; | |||
public record RestUser( | |||
String id, | |||
String login, | |||
String name, | |||
@Nullable | |||
String email, | |||
@Nullable | |||
Boolean active, | |||
@Nullable | |||
Boolean local, | |||
@Nullable | |||
Boolean managed, | |||
@Nullable | |||
String externalLogin, | |||
@Nullable | |||
String externalProvider, | |||
@Nullable | |||
String avatar, | |||
@Nullable | |||
String sonarQubeLastConnectionDate, | |||
@Nullable | |||
String sonarLintLastConnectionDate, | |||
@Nullable | |||
Integer groupsCount, | |||
@Nullable | |||
Integer tokensCount | |||
) { | |||
} |
@@ -0,0 +1,23 @@ | |||
/* | |||
* 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. | |||
*/ | |||
@ParametersAreNonnullByDefault | |||
package org.sonar.server.v2.api.user.model; | |||
import javax.annotation.ParametersAreNonnullByDefault; |
@@ -0,0 +1,61 @@ | |||
/* | |||
* 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.Parameter; | |||
import io.swagger.v3.oas.annotations.media.Schema; | |||
import javax.annotation.Nullable; | |||
public record UsersSearchRestRequest( | |||
@Parameter( | |||
description = "Return active/inactive users", | |||
schema = @Schema(defaultValue = "true", implementation = Boolean.class)) | |||
Boolean active, | |||
@Nullable | |||
@Parameter(description = "Return managed or non-managed users. Only available for managed instances, throws for non-managed instances") | |||
Boolean managed, | |||
@Nullable | |||
@Parameter(description = "Filter on login, name and email.\n" | |||
+ "This parameter can either perform an exact match, or a partial match (contains), it is case insensitive.") | |||
String q, | |||
@Nullable | |||
@Parameter(description = "Filter the users based on the last connection date field. Only users who interacted with this instance at or after the date will be returned. " | |||
+ "The format must be ISO 8601 datetime format (YYYY-MM-DDThh:mm:ss±hhmm)", | |||
example = "2020-01-01T00:00:00+0100") | |||
String sonarQubeLastConnectionDateFrom, | |||
@Nullable | |||
@Parameter(description = "Filter the users based on the last connection date field. Only users that never connected or who interacted with this instance at " | |||
+ "or before the date will be returned. The format must be ISO 8601 datetime format (YYYY-MM-DDThh:mm:ss±hhmm)", | |||
example = "2020-01-01T00:00:00+0100") | |||
String sonarQubeLastConnectionDateTo, | |||
@Nullable | |||
@Parameter(description = "Filter the users based on the sonar lint last connection date field Only users who interacted with this instance using SonarLint at or after " | |||
+ "the date will be returned. The format must be ISO 8601 datetime format (YYYY-MM-DDThh:mm:ss±hhmm)", | |||
example = "2020-01-01T00:00:00+0100") | |||
String sonarLintLastConnectionDateFrom, | |||
@Nullable | |||
@Parameter(description = "Filter the users based on the sonar lint last connection date field. Only users that never connected or who interacted with this instance " | |||
+ "using SonarLint at or before the date will be returned. The format must be ISO 8601 datetime format (YYYY-MM-DDThh:mm:ss±hhmm)", | |||
example = "2020-01-01T00:00:00+0100") | |||
String sonarLintLastConnectionDateTo | |||
) { | |||
} |
@@ -0,0 +1,23 @@ | |||
/* | |||
* 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. | |||
*/ | |||
@ParametersAreNonnullByDefault | |||
package org.sonar.server.v2.api.user.request; | |||
import javax.annotation.ParametersAreNonnullByDefault; |
@@ -0,0 +1,27 @@ | |||
/* | |||
* 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.response; | |||
import java.util.List; | |||
import org.sonar.server.v2.api.response.PageRestResponse; | |||
import org.sonar.server.v2.api.user.model.RestUser; | |||
public record UsersSearchRestResponse(List<RestUser> users, PageRestResponse pageRestResponse) { | |||
} |
@@ -0,0 +1,23 @@ | |||
/* | |||
* 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. | |||
*/ | |||
@ParametersAreNonnullByDefault | |||
package org.sonar.server.v2.api.user.response; | |||
import javax.annotation.ParametersAreNonnullByDefault; |
@@ -20,11 +20,16 @@ | |||
package org.sonar.server.v2.common; | |||
import java.util.Optional; | |||
import java.util.stream.Collectors; | |||
import org.sonar.server.exceptions.BadRequestException; | |||
import org.sonar.server.exceptions.ForbiddenException; | |||
import org.sonar.server.exceptions.ServerException; | |||
import org.sonar.server.exceptions.UnauthorizedException; | |||
import org.sonar.server.v2.api.model.RestError; | |||
import org.springframework.http.HttpStatus; | |||
import org.springframework.http.ResponseEntity; | |||
import org.springframework.validation.BindException; | |||
import org.springframework.validation.FieldError; | |||
import org.springframework.web.bind.annotation.ExceptionHandler; | |||
import org.springframework.web.bind.annotation.ResponseStatus; | |||
import org.springframework.web.bind.annotation.RestControllerAdvice; | |||
@@ -34,25 +39,29 @@ public class RestResponseEntityExceptionHandler { | |||
@ExceptionHandler(IllegalStateException.class) | |||
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) | |||
protected ResponseEntity<Object> handleIllegalStateException(IllegalStateException illegalStateException) { | |||
return new ResponseEntity<>(illegalStateException.getMessage(), HttpStatus.INTERNAL_SERVER_ERROR); | |||
protected ResponseEntity<RestError> handleIllegalStateException(IllegalStateException illegalStateException) { | |||
return new ResponseEntity<>(new RestError(illegalStateException.getMessage()), HttpStatus.INTERNAL_SERVER_ERROR); | |||
} | |||
@ExceptionHandler(ForbiddenException.class) | |||
@ResponseStatus(HttpStatus.FORBIDDEN) | |||
protected ResponseEntity<Object> handleForbiddenException(ForbiddenException forbiddenException) { | |||
return handleServerException(forbiddenException); | |||
@ExceptionHandler(BindException.class) | |||
@ResponseStatus(HttpStatus.BAD_REQUEST) | |||
protected ResponseEntity<RestError> handleBindException(BindException bindException) { | |||
String validationErrors = bindException.getFieldErrors().stream() | |||
.map(RestResponseEntityExceptionHandler::handleFieldError) | |||
.collect(Collectors.joining()); | |||
return new ResponseEntity<>(new RestError(validationErrors), HttpStatus.BAD_REQUEST); | |||
} | |||
@ExceptionHandler(UnauthorizedException.class) | |||
@ResponseStatus(HttpStatus.UNAUTHORIZED) | |||
protected ResponseEntity<Object> handleUnauthorizedException(UnauthorizedException unauthorizedException) { | |||
return handleServerException(unauthorizedException); | |||
private static String handleFieldError(FieldError fieldError) { | |||
String fieldName = fieldError.getField(); | |||
String rejectedValueAsString = Optional.ofNullable(fieldError.getRejectedValue()).map(Object::toString).orElse("{}"); | |||
String defaultMessage = fieldError.getDefaultMessage(); | |||
return String.format("Value %s for field %s was rejected. Error: %s", rejectedValueAsString, fieldName, defaultMessage); | |||
} | |||
@ExceptionHandler(ServerException.class) | |||
protected ResponseEntity<Object> handleServerException(ServerException serverException) { | |||
return new ResponseEntity<>(serverException.getMessage(), Optional.ofNullable(HttpStatus.resolve(serverException.httpCode())).orElse(HttpStatus.INTERNAL_SERVER_ERROR)); | |||
@ExceptionHandler({ServerException.class, ForbiddenException.class, UnauthorizedException.class, BadRequestException.class}) | |||
protected ResponseEntity<RestError> handleServerException(ServerException serverException) { | |||
return new ResponseEntity<>(new RestError(serverException.getMessage()), | |||
Optional.ofNullable(HttpStatus.resolve(serverException.httpCode())).orElse(HttpStatus.INTERNAL_SERVER_ERROR)); | |||
} | |||
} |
@@ -22,10 +22,14 @@ package org.sonar.server.v2.config; | |||
import io.swagger.v3.oas.models.OpenAPI; | |||
import io.swagger.v3.oas.models.info.Info; | |||
import org.sonar.server.v2.common.RestResponseEntityExceptionHandler; | |||
import org.springdoc.core.SpringDocConfigProperties; | |||
import org.springframework.beans.factory.config.BeanFactoryPostProcessor; | |||
import org.springframework.context.annotation.Bean; | |||
import org.springframework.context.annotation.ComponentScan; | |||
import org.springframework.context.annotation.Configuration; | |||
import org.springframework.context.annotation.PropertySource; | |||
import org.springframework.http.MediaType; | |||
import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean; | |||
import org.springframework.web.servlet.config.annotation.EnableWebMvc; | |||
@Configuration | |||
@@ -33,6 +37,10 @@ import org.springframework.web.servlet.config.annotation.EnableWebMvc; | |||
@ComponentScan(basePackages = {"org.springdoc"}) | |||
@PropertySource("classpath:springdoc.properties") | |||
public class CommonWebConfig { | |||
@Bean | |||
public LocalValidatorFactoryBean validator() { | |||
return new LocalValidatorFactoryBean(); | |||
} | |||
@Bean | |||
public RestResponseEntityExceptionHandler restResponseEntityExceptionHandler() { | |||
@@ -45,9 +53,14 @@ public class CommonWebConfig { | |||
.info( | |||
new Info() | |||
.title("SonarQube Web API") | |||
.version("0.0.1 alpha") | |||
.version("1.0.0 beta") | |||
.description("Documentation of SonarQube Web API") | |||
); | |||
} | |||
@Bean | |||
public BeanFactoryPostProcessor beanFactoryPostProcessor1(SpringDocConfigProperties springDocConfigProperties) { | |||
return beanFactory -> springDocConfigProperties.setDefaultProducesMediaType(MediaType.APPLICATION_JSON_VALUE); | |||
} | |||
} |
@@ -20,6 +20,7 @@ | |||
package org.sonar.server.v2.config; | |||
import javax.annotation.Nullable; | |||
import org.sonar.server.common.user.service.UserService; | |||
import org.sonar.server.health.CeStatusNodeCheck; | |||
import org.sonar.server.health.DbConnectionNodeCheck; | |||
import org.sonar.server.health.EsStatusNodeCheck; | |||
@@ -30,6 +31,9 @@ import org.sonar.server.platform.ws.LivenessChecker; | |||
import org.sonar.server.platform.ws.LivenessCheckerImpl; | |||
import org.sonar.server.user.SystemPasscode; | |||
import org.sonar.server.user.UserSession; | |||
import org.sonar.server.v2.api.user.controller.DefaultUserController; | |||
import org.sonar.server.v2.api.user.controller.UserController; | |||
import org.sonar.server.v2.api.user.converter.UsersSearchRestResponseGenerator; | |||
import org.sonar.server.v2.controller.DefautLivenessController; | |||
import org.sonar.server.v2.controller.HealthController; | |||
import org.sonar.server.v2.controller.LivenessController; | |||
@@ -57,4 +61,15 @@ public class PlatformLevel4WebConfig { | |||
UserSession userSession) { | |||
return new HealthController(healthChecker, systemPasscode, nodeInformation, userSession); | |||
} | |||
@Bean | |||
public UsersSearchRestResponseGenerator usersSearchResponseGenerator(UserSession userSession) { | |||
return new UsersSearchRestResponseGenerator(userSession); | |||
} | |||
@Bean | |||
public UserController userController(UserSession userSession, UsersSearchRestResponseGenerator usersSearchResponseGenerator, UserService userService) { | |||
return new DefaultUserController(userSession, userService, usersSearchResponseGenerator); | |||
} | |||
} |
@@ -20,15 +20,16 @@ | |||
package org.sonar.server.v2.controller; | |||
import javax.annotation.Nullable; | |||
import org.slf4j.Logger; | |||
import org.slf4j.LoggerFactory; | |||
import org.sonar.server.exceptions.ForbiddenException; | |||
import org.sonar.server.platform.ws.LivenessChecker; | |||
import org.sonar.server.user.SystemPasscode; | |||
import org.sonar.server.user.UserSession; | |||
import org.springframework.web.bind.annotation.RestController; | |||
@RestController | |||
public class DefautLivenessController implements LivenessController { | |||
private static final Logger LOGGER = LoggerFactory.getLogger(DefautLivenessController.class); | |||
private final LivenessChecker livenessChecker; | |||
private final UserSession userSession; | |||
private final SystemPasscode systemPasscode; |
@@ -28,10 +28,12 @@ import org.springframework.web.bind.annotation.GetMapping; | |||
import org.springframework.web.bind.annotation.RequestHeader; | |||
import org.springframework.web.bind.annotation.RequestMapping; | |||
import org.springframework.web.bind.annotation.ResponseStatus; | |||
import org.springframework.web.bind.annotation.RestController; | |||
import static org.sonar.server.v2.WebApiEndpoints.LIVENESS_ENDPOINT; | |||
@RequestMapping(LIVENESS_ENDPOINT) | |||
@RestController | |||
public interface LivenessController { | |||
@GetMapping |
@@ -0,0 +1,44 @@ | |||
/* | |||
* 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.model; | |||
import org.junit.Test; | |||
import static org.assertj.core.api.Assertions.assertThat; | |||
import static org.mockito.Mockito.mock; | |||
import static org.mockito.Mockito.when; | |||
public class RestPageTest { | |||
@Test | |||
public void constructor_whenNoValueProvided_setsDefaultValues() { | |||
RestPage restPage = new RestPage(null, null); | |||
assertThat(restPage.pageIndex()).asString().isEqualTo(RestPage.DEFAULT_PAGE_INDEX); | |||
assertThat(restPage.pageSize()).asString().isEqualTo(RestPage.DEFAULT_PAGE_SIZE); | |||
} | |||
@Test | |||
public void constructor_whenValuesProvided_useThem() { | |||
RestPage restPage = new RestPage(10, 100); | |||
assertThat(restPage.pageIndex()).isEqualTo(100); | |||
assertThat(restPage.pageSize()).isEqualTo(10); | |||
} | |||
} |
@@ -0,0 +1,205 @@ | |||
/* | |||
* 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.converter; | |||
import java.util.List; | |||
import java.util.Optional; | |||
import org.jetbrains.annotations.Nullable; | |||
import org.junit.Test; | |||
import org.junit.runner.RunWith; | |||
import org.mockito.InjectMocks; | |||
import org.mockito.Mock; | |||
import org.mockito.junit.MockitoJUnitRunner; | |||
import org.sonar.api.utils.DateUtils; | |||
import org.sonar.api.utils.Paging; | |||
import org.sonar.db.user.UserDto; | |||
import org.sonar.server.common.user.service.UserSearchResult; | |||
import org.sonar.server.user.UserSession; | |||
import org.sonar.server.v2.api.response.PageRestResponse; | |||
import org.sonar.server.v2.api.user.model.RestUser; | |||
import org.sonar.server.v2.api.user.response.UsersSearchRestResponse; | |||
import static org.assertj.core.api.Assertions.assertThat; | |||
import static org.mockito.Mockito.RETURNS_DEEP_STUBS; | |||
import static org.mockito.Mockito.mock; | |||
import static org.mockito.Mockito.when; | |||
@RunWith(MockitoJUnitRunner.class) | |||
public class UsersSearchRestResponseGeneratorTest { | |||
@Mock | |||
private UserSession userSession; | |||
@InjectMocks | |||
private UsersSearchRestResponseGenerator usersSearchRestResponseGenerator; | |||
@Test | |||
public void toUsersForResponse_whenNoResults_mapsCorrectly() { | |||
Paging paging = Paging.forPageIndex(1).withPageSize(2).andTotal(3); | |||
UsersSearchRestResponse usersForResponse = usersSearchRestResponseGenerator.toUsersForResponse(List.of(), paging); | |||
assertThat(usersForResponse.users()).isEmpty(); | |||
assertPaginationInformationAreCorrect(paging, usersForResponse.pageRestResponse()); | |||
} | |||
@Test | |||
public void toUsersForResponse_whenAdmin_mapsAllFields() { | |||
when(userSession.isLoggedIn()).thenReturn(true); | |||
when(userSession.isSystemAdministrator()).thenReturn(true); | |||
Paging paging = Paging.forPageIndex(1).withPageSize(2).andTotal(3); | |||
UserSearchResult userSearchResult1 = mockSearchResult(1, true); | |||
UserSearchResult userSearchResult2 = mockSearchResult(2, false); | |||
UsersSearchRestResponse usersForResponse = usersSearchRestResponseGenerator.toUsersForResponse(List.of(userSearchResult1, userSearchResult2), paging); | |||
RestUser expectUser1 = buildExpectedResponseForAdmin(userSearchResult1); | |||
RestUser expectUser2 = buildExpectedResponseForAdmin(userSearchResult2); | |||
assertThat(usersForResponse.users()).containsExactly(expectUser1, expectUser2); | |||
assertPaginationInformationAreCorrect(paging, usersForResponse.pageRestResponse()); | |||
} | |||
private static RestUser buildExpectedResponseForAdmin(UserSearchResult userSearchResult) { | |||
UserDto userDto = userSearchResult.userDto(); | |||
return new RestUser( | |||
userDto.getLogin(), | |||
userDto.getLogin(), | |||
userDto.getName(), | |||
userDto.getEmail(), | |||
userDto.isActive(), | |||
userDto.isLocal(), | |||
userSearchResult.managed(), | |||
userDto.getExternalLogin(), | |||
userDto.getExternalIdentityProvider(), | |||
userSearchResult.avatar().orElse(null), | |||
toDateTime(userDto.getLastConnectionDate()), | |||
toDateTime(userDto.getLastSonarlintConnectionDate()), | |||
userSearchResult.groups().size(), | |||
userSearchResult.tokensCount() | |||
); | |||
} | |||
@Test | |||
public void toUsersForResponse_whenNonAdmin_mapsNonAdminFields() { | |||
when(userSession.isLoggedIn()).thenReturn(true); | |||
Paging paging = Paging.forPageIndex(1).withPageSize(2).andTotal(3); | |||
UserSearchResult userSearchResult1 = mockSearchResult(1, true); | |||
UserSearchResult userSearchResult2 = mockSearchResult(2, false); | |||
UsersSearchRestResponse usersForResponse = usersSearchRestResponseGenerator.toUsersForResponse(List.of(userSearchResult1, userSearchResult2), paging); | |||
RestUser expectUser1 = buildExpectedResponseForUser(userSearchResult1); | |||
RestUser expectUser2 = buildExpectedResponseForUser(userSearchResult2); | |||
assertThat(usersForResponse.users()).containsExactly(expectUser1, expectUser2); | |||
assertPaginationInformationAreCorrect(paging, usersForResponse.pageRestResponse()); | |||
} | |||
private static RestUser buildExpectedResponseForUser(UserSearchResult userSearchResult) { | |||
UserDto userDto = userSearchResult.userDto(); | |||
return new RestUser( | |||
userDto.getLogin(), | |||
userDto.getLogin(), | |||
userDto.getName(), | |||
userDto.getEmail(), | |||
userDto.isActive(), | |||
userDto.isLocal(), | |||
null, | |||
null, | |||
userDto.getExternalIdentityProvider(), | |||
userSearchResult.avatar().orElse(null), | |||
null, | |||
null, | |||
null, | |||
null | |||
); | |||
} | |||
@Test | |||
public void toUsersForResponse_whenAnonymous_returnsOnlyNameAndLogin() { | |||
Paging paging = Paging.forPageIndex(1).withPageSize(2).andTotal(3); | |||
UserSearchResult userSearchResult1 = mockSearchResult(1, true); | |||
UserSearchResult userSearchResult2 = mockSearchResult(2, false); | |||
UsersSearchRestResponse usersForResponse = usersSearchRestResponseGenerator.toUsersForResponse(List.of(userSearchResult1, userSearchResult2), paging); | |||
RestUser expectUser1 = buildExpectedResponseForAnonymous(userSearchResult1); | |||
RestUser expectUser2 = buildExpectedResponseForAnonymous(userSearchResult2); | |||
assertThat(usersForResponse.users()).containsExactly(expectUser1, expectUser2); | |||
assertPaginationInformationAreCorrect(paging, usersForResponse.pageRestResponse()); | |||
} | |||
private static RestUser buildExpectedResponseForAnonymous(UserSearchResult userSearchResult) { | |||
UserDto userDto = userSearchResult.userDto(); | |||
return new RestUser( | |||
userDto.getLogin(), | |||
userDto.getLogin(), | |||
userDto.getName(), | |||
null, | |||
null, | |||
null, | |||
null, | |||
null, | |||
null, | |||
null, | |||
null, | |||
null, | |||
null, | |||
null | |||
); | |||
} | |||
private static String toDateTime(@Nullable Long dateTimeMs) { | |||
return Optional.ofNullable(dateTimeMs).map(DateUtils::formatDateTime).orElse(null); | |||
} | |||
private static UserSearchResult mockSearchResult(int i, boolean booleanFlagsValue) { | |||
UserSearchResult userSearchResult = mock(UserSearchResult.class, RETURNS_DEEP_STUBS); | |||
UserDto user1 = new UserDto() | |||
.setUuid("uuid_" + i) | |||
.setLogin("login_" + i) | |||
.setName("name_" + i) | |||
.setEmail("email@" + i) | |||
.setExternalId("externalId" + i) | |||
.setExternalLogin("externalLogin" + 1) | |||
.setExternalIdentityProvider("exernalIdp_" + i) | |||
.setLastConnectionDate(100L + i) | |||
.setLastSonarlintConnectionDate(200L + i) | |||
.setLocal(booleanFlagsValue) | |||
.setActive(booleanFlagsValue); | |||
when(userSearchResult.userDto()).thenReturn(user1); | |||
when(userSearchResult.managed()).thenReturn(booleanFlagsValue); | |||
when(userSearchResult.tokensCount()).thenReturn(i); | |||
when(userSearchResult.groups().size()).thenReturn(i * 100); | |||
return userSearchResult; | |||
} | |||
private static void assertPaginationInformationAreCorrect(Paging paging, PageRestResponse pageRestResponse) { | |||
assertThat(pageRestResponse.pageIndex()).isEqualTo(paging.pageIndex()); | |||
assertThat(pageRestResponse.pageSize()).isEqualTo(paging.pageSize()); | |||
assertThat(pageRestResponse.total()).isEqualTo(paging.total()); | |||
} | |||
} |
@@ -18,6 +18,7 @@ dependencies { | |||
api project(':server:sonar-db-dao') | |||
api project(':server:sonar-process') | |||
api project(':server:sonar-webserver-auth') | |||
api project(':server:sonar-webserver-common') | |||
api project(':server:sonar-webserver-es') | |||
api project(':server:sonar-webserver-ws') | |||
api project(':server:sonar-webserver-pushapi') |
@@ -67,8 +67,8 @@ import org.sonar.db.user.UserTesting; | |||
import org.sonar.server.es.EsTester; | |||
import org.sonar.server.exceptions.ForbiddenException; | |||
import org.sonar.server.exceptions.NotFoundException; | |||
import org.sonar.server.issue.AvatarResolver; | |||
import org.sonar.server.issue.AvatarResolverImpl; | |||
import org.sonar.server.common.avatar.AvatarResolver; | |||
import org.sonar.server.common.avatar.AvatarResolverImpl; | |||
import org.sonar.server.issue.IssueChangeWSSupport; | |||
import org.sonar.server.issue.IssueChangeWSSupport.FormattingContext; | |||
import org.sonar.server.issue.IssueChangeWSSupport.Load; |
@@ -48,6 +48,7 @@ import org.sonar.db.issue.IssueDto; | |||
import org.sonar.db.issue.IssueTesting; | |||
import org.sonar.db.user.UserDto; | |||
import org.sonar.markdown.Markdown; | |||
import org.sonar.server.common.avatar.AvatarResolverImpl; | |||
import org.sonar.server.issue.IssueChangeWSSupport.FormattingContext; | |||
import org.sonar.server.issue.IssueChangeWSSupport.Load; | |||
import org.sonar.server.tester.UserSessionRule; |
@@ -36,7 +36,7 @@ import org.sonar.db.user.UserDto; | |||
import org.sonar.db.user.UserTesting; | |||
import org.sonar.server.exceptions.ForbiddenException; | |||
import org.sonar.server.exceptions.NotFoundException; | |||
import org.sonar.server.issue.AvatarResolverImpl; | |||
import org.sonar.server.common.avatar.AvatarResolverImpl; | |||
import org.sonar.server.issue.IssueChangeWSSupport; | |||
import org.sonar.server.issue.IssueFinder; | |||
import org.sonar.server.tester.UserSessionRule; |
@@ -45,10 +45,10 @@ import org.sonar.db.metric.MetricDto; | |||
import org.sonar.db.protobuf.DbIssues; | |||
import org.sonar.db.rule.RuleDto; | |||
import org.sonar.db.user.UserDto; | |||
import org.sonar.server.common.avatar.AvatarResolverImpl; | |||
import org.sonar.server.component.ComponentFinder; | |||
import org.sonar.server.component.TestComponentFinder; | |||
import org.sonar.server.exceptions.ForbiddenException; | |||
import org.sonar.server.issue.AvatarResolverImpl; | |||
import org.sonar.server.issue.IssueFieldsSetter; | |||
import org.sonar.server.issue.NewCodePeriodResolver; | |||
import org.sonar.server.issue.TextRangeResponseFormatter; |
@@ -38,7 +38,7 @@ import org.sonar.db.issue.IssueDto; | |||
import org.sonar.db.project.ProjectDto; | |||
import org.sonar.db.rule.RuleDto; | |||
import org.sonar.server.es.EsTester; | |||
import org.sonar.server.issue.AvatarResolverImpl; | |||
import org.sonar.server.common.avatar.AvatarResolverImpl; | |||
import org.sonar.server.issue.IssueFieldsSetter; | |||
import org.sonar.server.issue.TextRangeResponseFormatter; | |||
import org.sonar.server.issue.TransitionService; |
@@ -37,7 +37,7 @@ import org.sonar.db.component.ComponentDto; | |||
import org.sonar.db.rule.RuleDto; | |||
import org.sonar.db.user.UserDto; | |||
import org.sonar.server.es.EsTester; | |||
import org.sonar.server.issue.AvatarResolverImpl; | |||
import org.sonar.server.common.avatar.AvatarResolverImpl; | |||
import org.sonar.server.issue.TextRangeResponseFormatter; | |||
import org.sonar.server.issue.TransitionService; | |||
import org.sonar.server.issue.index.IssueIndex; |
@@ -62,7 +62,7 @@ import org.sonar.db.rule.RuleTesting; | |||
import org.sonar.db.user.UserDto; | |||
import org.sonar.server.es.EsTester; | |||
import org.sonar.server.es.SearchOptions; | |||
import org.sonar.server.issue.AvatarResolverImpl; | |||
import org.sonar.server.common.avatar.AvatarResolverImpl; | |||
import org.sonar.server.issue.IssueFieldsSetter; | |||
import org.sonar.server.issue.TextRangeResponseFormatter; | |||
import org.sonar.server.issue.TransitionService; |
@@ -35,7 +35,7 @@ import org.sonar.server.exceptions.BadRequestException; | |||
import org.sonar.server.exceptions.ForbiddenException; | |||
import org.sonar.server.exceptions.NotFoundException; | |||
import org.sonar.server.exceptions.UnauthorizedException; | |||
import org.sonar.server.issue.AvatarResolverImpl; | |||
import org.sonar.server.common.avatar.AvatarResolverImpl; | |||
import org.sonar.server.management.ManagedInstanceService; | |||
import org.sonar.server.permission.PermissionService; | |||
import org.sonar.server.permission.PermissionServiceImpl; |
@@ -35,7 +35,7 @@ import org.sonar.server.exceptions.BadRequestException; | |||
import org.sonar.server.exceptions.ForbiddenException; | |||
import org.sonar.server.exceptions.NotFoundException; | |||
import org.sonar.server.exceptions.UnauthorizedException; | |||
import org.sonar.server.issue.AvatarResolverImpl; | |||
import org.sonar.server.common.avatar.AvatarResolverImpl; | |||
import org.sonar.server.permission.PermissionService; | |||
import org.sonar.server.permission.PermissionServiceImpl; | |||
import org.sonar.server.permission.RequestValidator; |
@@ -28,8 +28,8 @@ import org.sonar.db.user.UserDto; | |||
import org.sonar.server.component.TestComponentFinder; | |||
import org.sonar.server.exceptions.ForbiddenException; | |||
import org.sonar.server.exceptions.NotFoundException; | |||
import org.sonar.server.issue.AvatarResolver; | |||
import org.sonar.server.issue.AvatarResolverImpl; | |||
import org.sonar.server.common.avatar.AvatarResolver; | |||
import org.sonar.server.common.avatar.AvatarResolverImpl; | |||
import org.sonar.server.issue.FakeAvatarResolver; | |||
import org.sonar.server.tester.UserSessionRule; | |||
import org.sonar.server.ws.TestRequest; |
@@ -29,8 +29,8 @@ import org.sonar.db.qualityprofile.QProfileDto; | |||
import org.sonar.db.user.UserDto; | |||
import org.sonar.server.exceptions.ForbiddenException; | |||
import org.sonar.server.exceptions.NotFoundException; | |||
import org.sonar.server.issue.AvatarResolver; | |||
import org.sonar.server.issue.AvatarResolverImpl; | |||
import org.sonar.server.common.avatar.AvatarResolver; | |||
import org.sonar.server.common.avatar.AvatarResolverImpl; | |||
import org.sonar.server.issue.FakeAvatarResolver; | |||
import org.sonar.server.language.LanguageTesting; | |||
import org.sonar.server.tester.UserSessionRule; |
@@ -39,7 +39,7 @@ import org.sonar.db.DbTester; | |||
import org.sonar.db.component.ComponentDto; | |||
import org.sonar.db.component.ProjectData; | |||
import org.sonar.db.user.UserDto; | |||
import org.sonar.server.issue.AvatarResolverImpl; | |||
import org.sonar.server.common.avatar.AvatarResolverImpl; | |||
import org.sonar.server.permission.PermissionService; | |||
import org.sonar.server.permission.PermissionServiceImpl; | |||
import org.sonar.server.tester.UserSessionRule; |
@@ -34,7 +34,7 @@ import org.sonar.db.DbTester; | |||
import org.sonar.db.component.ComponentDto; | |||
import org.sonar.db.property.PropertyDto; | |||
import org.sonar.db.user.UserDto; | |||
import org.sonar.server.issue.AvatarResolverImpl; | |||
import org.sonar.server.common.avatar.AvatarResolverImpl; | |||
import org.sonar.server.permission.PermissionService; | |||
import org.sonar.server.permission.PermissionServiceImpl; | |||
import org.sonar.server.tester.UserSessionRule; |
@@ -36,9 +36,10 @@ import org.sonar.db.DbTester; | |||
import org.sonar.db.scim.ScimUserDao; | |||
import org.sonar.db.user.GroupDto; | |||
import org.sonar.db.user.UserDto; | |||
import org.sonar.server.common.avatar.AvatarResolverImpl; | |||
import org.sonar.server.common.user.service.UserService; | |||
import org.sonar.server.exceptions.BadRequestException; | |||
import org.sonar.server.exceptions.ServerException; | |||
import org.sonar.server.issue.AvatarResolverImpl; | |||
import org.sonar.server.management.ManagedInstanceService; | |||
import org.sonar.server.tester.UserSessionRule; | |||
import org.sonar.server.ws.TestRequest; | |||
@@ -71,8 +72,13 @@ public class SearchActionIT { | |||
@Rule | |||
public DbTester db = DbTester.create(); | |||
private ManagedInstanceService managedInstanceService = mock(ManagedInstanceService.class); | |||
private WsActionTester ws = new WsActionTester(new SearchAction(userSession, db.getDbClient(), new AvatarResolverImpl(), managedInstanceService)); | |||
private final ManagedInstanceService managedInstanceService = mock(ManagedInstanceService.class); | |||
private final UserService userService = new UserService(db.getDbClient(), new AvatarResolverImpl(), managedInstanceService); | |||
private final SearchWsReponseGenerator searchWsReponseGenerator = new SearchWsReponseGenerator(userSession); | |||
private final WsActionTester ws = new WsActionTester(new SearchAction(userSession, userService, searchWsReponseGenerator)); | |||
@Test | |||
public void search_for_all_active_users() { | |||
@@ -529,7 +535,6 @@ public class SearchActionIT { | |||
assertUserWithFilter(SearchAction.SONAR_LINT_LAST_CONNECTION_DATE_TO, lastConnection, user.getLogin(), true); | |||
} | |||
@Test | |||
public void search_whenNoLastConnection_shouldReturnForBeforeOnly() { | |||
userSession.logIn().setSystemAdministrator(); |
@@ -47,6 +47,7 @@ import org.sonar.db.issue.IssueChangeDto; | |||
import org.sonar.db.issue.IssueDto; | |||
import org.sonar.db.user.UserDto; | |||
import org.sonar.markdown.Markdown; | |||
import org.sonar.server.common.avatar.AvatarResolver; | |||
import org.sonar.server.user.UserSession; | |||
import org.sonarqube.ws.Common; | |||
@@ -20,7 +20,7 @@ | |||
package org.sonar.server.issue.ws; | |||
import org.sonar.core.platform.Module; | |||
import org.sonar.server.issue.AvatarResolverImpl; | |||
import org.sonar.server.common.avatar.AvatarResolverImpl; | |||
import org.sonar.server.issue.IssueChangeWSSupport; | |||
import org.sonar.server.issue.IssueFieldsSetter; | |||
import org.sonar.server.issue.IssueFinder; |
@@ -20,7 +20,7 @@ | |||
package org.sonar.server.issue.ws; | |||
import org.sonar.db.user.UserDto; | |||
import org.sonar.server.issue.AvatarResolver; | |||
import org.sonar.server.common.avatar.AvatarResolver; | |||
import org.sonarqube.ws.Common; | |||
import static com.google.common.base.Strings.emptyToNull; |
@@ -38,7 +38,7 @@ import org.sonar.db.entity.EntityDto; | |||
import org.sonar.db.permission.PermissionQuery; | |||
import org.sonar.db.permission.UserPermissionDto; | |||
import org.sonar.db.user.UserDto; | |||
import org.sonar.server.issue.AvatarResolver; | |||
import org.sonar.server.common.avatar.AvatarResolver; | |||
import org.sonar.server.management.ManagedInstanceService; | |||
import org.sonar.server.permission.RequestValidator; | |||
import org.sonar.server.user.UserSession; |
@@ -34,7 +34,7 @@ import org.sonar.db.permission.PermissionQuery; | |||
import org.sonar.db.permission.template.PermissionTemplateDto; | |||
import org.sonar.db.permission.template.PermissionTemplateUserDto; | |||
import org.sonar.db.user.UserDto; | |||
import org.sonar.server.issue.AvatarResolver; | |||
import org.sonar.server.common.avatar.AvatarResolver; | |||
import org.sonar.server.permission.RequestValidator; | |||
import org.sonar.server.permission.ws.PermissionWsSupport; | |||
import org.sonar.server.permission.ws.PermissionsWsAction; |
@@ -33,7 +33,7 @@ import org.sonar.db.qualitygate.QualityGateDto; | |||
import org.sonar.db.user.SearchPermissionQuery; | |||
import org.sonar.db.user.SearchUserMembershipDto; | |||
import org.sonar.db.user.UserDto; | |||
import org.sonar.server.issue.AvatarResolver; | |||
import org.sonar.server.common.avatar.AvatarResolver; | |||
import org.sonarqube.ws.Common; | |||
import org.sonarqube.ws.Qualitygates.SearchUsersResponse; | |||
@@ -37,7 +37,7 @@ import org.sonar.db.qualityprofile.QProfileDto; | |||
import org.sonar.db.qualityprofile.SearchQualityProfilePermissionQuery; | |||
import org.sonar.db.user.SearchUserMembershipDto; | |||
import org.sonar.db.user.UserDto; | |||
import org.sonar.server.issue.AvatarResolver; | |||
import org.sonar.server.common.avatar.AvatarResolver; | |||
import org.sonarqube.ws.Common; | |||
import org.sonarqube.ws.Qualityprofiles.SearchUsersResponse; | |||
@@ -36,7 +36,7 @@ import org.sonar.db.permission.GlobalPermission; | |||
import org.sonar.db.project.ProjectDto; | |||
import org.sonar.db.property.PropertyQuery; | |||
import org.sonar.db.user.UserDto; | |||
import org.sonar.server.issue.AvatarResolver; | |||
import org.sonar.server.common.avatar.AvatarResolver; | |||
import org.sonar.server.permission.PermissionService; | |||
import org.sonar.server.user.UserSession; | |||
import org.sonarqube.ws.Users.CurrentWsResponse; |
@@ -19,74 +19,47 @@ | |||
*/ | |||
package org.sonar.server.user.ws; | |||
import com.google.common.collect.Multimap; | |||
import java.time.OffsetDateTime; | |||
import java.util.Collection; | |||
import java.util.List; | |||
import java.util.Map; | |||
import java.util.Objects; | |||
import java.util.Optional; | |||
import java.util.Set; | |||
import java.util.stream.Collectors; | |||
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.api.utils.DateUtils; | |||
import org.sonar.api.utils.MessageException; | |||
import org.sonar.api.utils.Paging; | |||
import org.sonar.db.DbClient; | |||
import org.sonar.db.DbSession; | |||
import org.sonar.db.user.UserDto; | |||
import org.sonar.db.user.UserQuery; | |||
import org.sonar.server.common.SearchResults; | |||
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.es.SearchOptions; | |||
import org.sonar.server.exceptions.BadRequestException; | |||
import org.sonar.server.exceptions.ServerException; | |||
import org.sonar.server.issue.AvatarResolver; | |||
import org.sonar.server.management.ManagedInstanceService; | |||
import org.sonar.server.user.UserSession; | |||
import org.sonarqube.ws.Users; | |||
import org.sonarqube.ws.Users.SearchWsResponse; | |||
import static com.google.common.base.MoreObjects.firstNonNull; | |||
import static com.google.common.base.Preconditions.checkArgument; | |||
import static com.google.common.base.Strings.emptyToNull; | |||
import static java.lang.Boolean.TRUE; | |||
import static java.util.Comparator.comparing; | |||
import static java.util.Optional.ofNullable; | |||
import static org.sonar.api.server.ws.WebService.Param.PAGE; | |||
import static org.sonar.api.server.ws.WebService.Param.PAGE_SIZE; | |||
import static org.sonar.api.server.ws.WebService.Param.TEXT_QUERY; | |||
import static org.sonar.api.utils.Paging.forPageIndex; | |||
import static org.sonar.server.ws.WsUtils.writeProtobuf; | |||
import static org.sonarqube.ws.Users.SearchWsResponse.Groups; | |||
import static org.sonarqube.ws.Users.SearchWsResponse.ScmAccounts; | |||
import static org.sonarqube.ws.Users.SearchWsResponse.User; | |||
import static org.sonarqube.ws.Users.SearchWsResponse.newBuilder; | |||
public class SearchAction implements UsersWsAction { | |||
private static final String DEACTIVATED_PARAM = "deactivated"; | |||
private static final String MANAGED_PARAM = "managed"; | |||
private static final int MAX_PAGE_SIZE = 500; | |||
static final String LAST_CONNECTION_DATE_FROM = "lastConnectedAfter"; | |||
static final String LAST_CONNECTION_DATE_TO = "lastConnectedBefore"; | |||
static final String SONAR_LINT_LAST_CONNECTION_DATE_FROM = "slLastConnectedAfter"; | |||
static final String SONAR_LINT_LAST_CONNECTION_DATE_TO = "slLastConnectedBefore"; | |||
private final UserSession userSession; | |||
private final DbClient dbClient; | |||
private final AvatarResolver avatarResolver; | |||
private final ManagedInstanceService managedInstanceService; | |||
public SearchAction(UserSession userSession, DbClient dbClient, AvatarResolver avatarResolver, | |||
ManagedInstanceService managedInstanceService) { | |||
private final UserService userService; | |||
private final SearchWsReponseGenerator searchWsReponseGenerator; | |||
public SearchAction(UserSession userSession, | |||
UserService userService, SearchWsReponseGenerator searchWsReponseGenerator) { | |||
this.userSession = userSession; | |||
this.dbClient = dbClient; | |||
this.avatarResolver = avatarResolver; | |||
this.managedInstanceService = managedInstanceService; | |||
this.userService = userService; | |||
this.searchWsReponseGenerator = searchWsReponseGenerator; | |||
} | |||
@Override | |||
@@ -182,113 +155,31 @@ public class SearchAction implements UsersWsAction { | |||
@Override | |||
public void handle(Request request, Response response) throws Exception { | |||
throwIfAdminOnlyParametersAreUsed(request); | |||
Users.SearchWsResponse wsResponse = doHandle(toSearchRequest(request)); | |||
writeProtobuf(wsResponse, request, response); | |||
} | |||
private Users.SearchWsResponse doHandle(SearchRequest request) { | |||
UserQuery userQuery = buildUserQuery(request); | |||
try (DbSession dbSession = dbClient.openSession(false)) { | |||
List<UserDto> users = findUsersAndSortByLogin(request, dbSession, userQuery); | |||
int totalUsers = dbClient.userDao().countUsers(dbSession, userQuery); | |||
List<String> logins = users.stream().map(UserDto::getLogin).toList(); | |||
Multimap<String, String> groupsByLogin = dbClient.groupMembershipDao().selectGroupsByLogins(dbSession, logins); | |||
Map<String, Integer> tokenCountsByLogin = dbClient.userTokenDao().countTokensByUsers(dbSession, users); | |||
Map<String, Boolean> userUuidToIsManaged = managedInstanceService.getUserUuidToManaged(dbSession, getUserUuids(users)); | |||
Paging paging = forPageIndex(request.getPage()).withPageSize(request.getPageSize()).andTotal(totalUsers); | |||
return buildResponse(users, groupsByLogin, tokenCountsByLogin, userUuidToIsManaged, paging); | |||
} | |||
} | |||
private static Set<String> getUserUuids(List<UserDto> users) { | |||
return users.stream().map(UserDto::getUuid).collect(Collectors.toSet()); | |||
} | |||
private UserQuery buildUserQuery(SearchRequest request) { | |||
UserQuery.UserQueryBuilder builder = UserQuery.builder(); | |||
if(!userSession.isSystemAdministrator()) { | |||
request.getLastConnectionDateFrom().ifPresent(v -> throwForbiddenFor(LAST_CONNECTION_DATE_FROM)); | |||
request.getLastConnectionDateTo().ifPresent(v -> throwForbiddenFor(LAST_CONNECTION_DATE_TO)); | |||
request.getSonarLintLastConnectionDateFrom().ifPresent(v -> throwForbiddenFor(SONAR_LINT_LAST_CONNECTION_DATE_FROM)); | |||
request.getSonarLintLastConnectionDateTo().ifPresent(v -> throwForbiddenFor(SONAR_LINT_LAST_CONNECTION_DATE_TO)); | |||
} | |||
request.getLastConnectionDateFrom().ifPresent(builder::lastConnectionDateFrom); | |||
request.getLastConnectionDateTo().ifPresent(builder::lastConnectionDateTo); | |||
request.getSonarLintLastConnectionDateFrom().ifPresent(builder::sonarLintLastConnectionDateFrom); | |||
request.getSonarLintLastConnectionDateTo().ifPresent(builder::sonarLintLastConnectionDateTo); | |||
if (managedInstanceService.isInstanceExternallyManaged()) { | |||
String managedInstanceSql = Optional.ofNullable(request.isManaged()) | |||
.map(managedInstanceService::getManagedUsersSqlFilter) | |||
.orElse(null); | |||
builder.isManagedClause(managedInstanceSql); | |||
} else if (request.isManaged() != null) { | |||
throw BadRequestException.create("The 'managed' parameter is only available for managed instances."); | |||
private void throwIfAdminOnlyParametersAreUsed(Request request) { | |||
if (!userSession.isSystemAdministrator()) { | |||
throwIfParameterValuePresent(request, LAST_CONNECTION_DATE_FROM); | |||
throwIfParameterValuePresent(request, LAST_CONNECTION_DATE_TO); | |||
throwIfParameterValuePresent(request, SONAR_LINT_LAST_CONNECTION_DATE_FROM); | |||
throwIfParameterValuePresent(request, SONAR_LINT_LAST_CONNECTION_DATE_TO); | |||
} | |||
return builder | |||
.isActive(!request.isDeactivated()) | |||
.searchText(request.getQuery()) | |||
.build(); | |||
} | |||
private static void throwForbiddenFor(String parameterName) { | |||
throw new ServerException(403, "parameter " + parameterName + " requires Administer System permission."); | |||
} | |||
private List<UserDto> findUsersAndSortByLogin(SearchRequest request, DbSession dbSession, UserQuery userQuery) { | |||
return dbClient.userDao().selectUsers(dbSession, userQuery, request.getPage(), request.getPageSize()) | |||
.stream() | |||
.sorted(comparing(UserDto::getLogin)) | |||
.toList(); | |||
} | |||
private Users.SearchWsResponse doHandle(UsersSearchRequest request) { | |||
SearchResults<UserSearchResult> userSearchResults = userService.findUsers(request); | |||
Paging paging = forPageIndex(request.getPage()).withPageSize(request.getPageSize()).andTotal(userSearchResults.total()); | |||
private SearchWsResponse buildResponse(List<UserDto> users, Multimap<String, String> groupsByLogin, Map<String, Integer> tokenCountsByLogin, | |||
Map<String, Boolean> userUuidToIsManaged, Paging paging) { | |||
SearchWsResponse.Builder responseBuilder = newBuilder(); | |||
users.forEach(user -> responseBuilder.addUsers( | |||
towsUser(user, firstNonNull(tokenCountsByLogin.get(user.getUuid()), 0), groupsByLogin.get(user.getLogin()), userUuidToIsManaged.get(user.getUuid())) | |||
)); | |||
responseBuilder.getPagingBuilder() | |||
.setPageIndex(paging.pageIndex()) | |||
.setPageSize(paging.pageSize()) | |||
.setTotal(paging.total()) | |||
.build(); | |||
return responseBuilder.build(); | |||
return searchWsReponseGenerator.toUsersForResponse(userSearchResults.searchResults(), paging); | |||
} | |||
private User towsUser(UserDto user, @Nullable Integer tokensCount, Collection<String> groups, Boolean managed) { | |||
User.Builder userBuilder = User.newBuilder().setLogin(user.getLogin()); | |||
ofNullable(user.getName()).ifPresent(userBuilder::setName); | |||
if (userSession.isLoggedIn()) { | |||
ofNullable(emptyToNull(user.getEmail())).ifPresent(u -> userBuilder.setAvatar(avatarResolver.create(user))); | |||
userBuilder.setActive(user.isActive()); | |||
userBuilder.setLocal(user.isLocal()); | |||
ofNullable(user.getExternalIdentityProvider()).ifPresent(userBuilder::setExternalProvider); | |||
if (!user.getSortedScmAccounts().isEmpty()) { | |||
userBuilder.setScmAccounts(ScmAccounts.newBuilder().addAllScmAccounts(user.getSortedScmAccounts())); | |||
} | |||
} | |||
if (userSession.isSystemAdministrator() || Objects.equals(userSession.getUuid(), user.getUuid())) { | |||
ofNullable(user.getEmail()).ifPresent(userBuilder::setEmail); | |||
if (!groups.isEmpty()) { | |||
userBuilder.setGroups(Groups.newBuilder().addAllGroups(groups)); | |||
} | |||
ofNullable(user.getExternalLogin()).ifPresent(userBuilder::setExternalIdentity); | |||
ofNullable(tokensCount).ifPresent(userBuilder::setTokensCount); | |||
ofNullable(user.getLastConnectionDate()).map(DateUtils::formatDateTime).ifPresent(userBuilder::setLastConnectionDate); | |||
ofNullable(user.getLastSonarlintConnectionDate()) | |||
.map(DateUtils::formatDateTime).ifPresent(userBuilder::setSonarLintLastConnectionDate); | |||
userBuilder.setManaged(TRUE.equals(managed)); | |||
} | |||
return userBuilder.build(); | |||
} | |||
private static SearchRequest toSearchRequest(Request request) { | |||
private UsersSearchRequest toSearchRequest(Request request) { | |||
int pageSize = request.mandatoryParamAsInt(PAGE_SIZE); | |||
checkArgument(pageSize <= MAX_PAGE_SIZE, "The '%s' parameter must be less than %s", PAGE_SIZE, MAX_PAGE_SIZE); | |||
return SearchRequest.builder() | |||
return UsersSearchRequest.builder() | |||
.setQuery(request.param(TEXT_QUERY)) | |||
.setDeactivated(request.mandatoryParamAsBoolean(DEACTIVATED_PARAM)) | |||
.setManaged(request.paramAsBoolean(MANAGED_PARAM)) | |||
@@ -301,139 +192,12 @@ public class SearchAction implements UsersWsAction { | |||
.build(); | |||
} | |||
private static class SearchRequest { | |||
private final Integer page; | |||
private final Integer pageSize; | |||
private final String query; | |||
private final boolean deactivated; | |||
private final Boolean managed; | |||
private final OffsetDateTime lastConnectionDateFrom; | |||
private final OffsetDateTime lastConnectionDateTo; | |||
private final OffsetDateTime sonarLintLastConnectionDateFrom; | |||
private final OffsetDateTime sonarLintLastConnectionDateTo; | |||
private SearchRequest(Builder builder) { | |||
this.page = builder.page; | |||
this.pageSize = builder.pageSize; | |||
this.query = builder.query; | |||
this.deactivated = builder.deactivated; | |||
this.managed = builder.managed; | |||
try { | |||
this.lastConnectionDateFrom = Optional.ofNullable(builder.lastConnectionDateFrom).map(DateUtils::parseOffsetDateTime).orElse(null); | |||
this.lastConnectionDateTo = Optional.ofNullable(builder.lastConnectionDateTo).map(DateUtils::parseOffsetDateTime).orElse(null); | |||
this.sonarLintLastConnectionDateFrom = Optional.ofNullable(builder.sonarLintLastConnectionDateFrom).map(DateUtils::parseOffsetDateTime).orElse(null); | |||
this.sonarLintLastConnectionDateTo = Optional.ofNullable(builder.sonarLintLastConnectionDateTo).map(DateUtils::parseOffsetDateTime).orElse(null); | |||
} catch (MessageException me) { | |||
throw new ServerException(400, me.getMessage()); | |||
} | |||
} | |||
public Integer getPage() { | |||
return page; | |||
} | |||
public Integer getPageSize() { | |||
return pageSize; | |||
} | |||
@CheckForNull | |||
public String getQuery() { | |||
return query; | |||
} | |||
public boolean isDeactivated() { | |||
return deactivated; | |||
} | |||
@CheckForNull | |||
private Boolean isManaged() { | |||
return managed; | |||
} | |||
public Optional<OffsetDateTime> getLastConnectionDateFrom() { | |||
return Optional.ofNullable(lastConnectionDateFrom); | |||
} | |||
public Optional<OffsetDateTime> getLastConnectionDateTo() { | |||
return Optional.ofNullable(lastConnectionDateTo); | |||
} | |||
public Optional<OffsetDateTime> getSonarLintLastConnectionDateFrom() { | |||
return Optional.ofNullable(sonarLintLastConnectionDateFrom); | |||
} | |||
public Optional<OffsetDateTime> getSonarLintLastConnectionDateTo() { | |||
return Optional.ofNullable(sonarLintLastConnectionDateTo); | |||
} | |||
public static Builder builder() { | |||
return new Builder(); | |||
} | |||
private static void throwIfParameterValuePresent(Request request, String parameter) { | |||
Optional.ofNullable(request.param(parameter)).ifPresent(v -> throwForbiddenFor(parameter)); | |||
} | |||
private static class Builder { | |||
private Integer page; | |||
private Integer pageSize; | |||
private String query; | |||
private boolean deactivated; | |||
private Boolean managed; | |||
private String lastConnectionDateFrom; | |||
private String lastConnectionDateTo; | |||
private String sonarLintLastConnectionDateFrom; | |||
private String sonarLintLastConnectionDateTo; | |||
private Builder() { | |||
// enforce factory method use | |||
} | |||
public Builder setPage(Integer page) { | |||
this.page = page; | |||
return this; | |||
} | |||
public Builder setPageSize(Integer pageSize) { | |||
this.pageSize = pageSize; | |||
return this; | |||
} | |||
public Builder setQuery(@Nullable String query) { | |||
this.query = query; | |||
return this; | |||
} | |||
public Builder setDeactivated(boolean deactivated) { | |||
this.deactivated = deactivated; | |||
return this; | |||
} | |||
public Builder setManaged(@Nullable Boolean managed) { | |||
this.managed = managed; | |||
return this; | |||
} | |||
public Builder setLastConnectionDateFrom(@Nullable String lastConnectionDateFrom) { | |||
this.lastConnectionDateFrom = lastConnectionDateFrom; | |||
return this; | |||
} | |||
public Builder setLastConnectionDateTo(@Nullable String lastConnectionDateTo) { | |||
this.lastConnectionDateTo = lastConnectionDateTo; | |||
return this; | |||
} | |||
public Builder setSonarLintLastConnectionDateFrom(@Nullable String sonarLintLastConnectionDateFrom) { | |||
this.sonarLintLastConnectionDateFrom = sonarLintLastConnectionDateFrom; | |||
return this; | |||
} | |||
public Builder setSonarLintLastConnectionDateTo(@Nullable String sonarLintLastConnectionDateTo) { | |||
this.sonarLintLastConnectionDateTo = sonarLintLastConnectionDateTo; | |||
return this; | |||
} | |||
public SearchRequest build() { | |||
return new SearchRequest(this); | |||
} | |||
private static void throwForbiddenFor(String parameterName) { | |||
throw new ServerException(403, "parameter " + parameterName + " requires Administer System permission."); | |||
} | |||
} |
@@ -0,0 +1,83 @@ | |||
/* | |||
* 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.user.ws; | |||
import java.util.List; | |||
import java.util.Objects; | |||
import org.sonar.api.utils.DateUtils; | |||
import org.sonar.api.utils.Paging; | |||
import org.sonar.db.user.UserDto; | |||
import org.sonar.server.common.user.UsersSearchResponseGenerator; | |||
import org.sonar.server.common.user.service.UserSearchResult; | |||
import org.sonar.server.user.UserSession; | |||
import org.sonarqube.ws.Users; | |||
import static java.lang.Boolean.TRUE; | |||
import static java.util.Optional.ofNullable; | |||
import static org.sonarqube.ws.Users.SearchWsResponse.newBuilder; | |||
public class SearchWsReponseGenerator implements UsersSearchResponseGenerator<Users.SearchWsResponse> { | |||
private final UserSession userSession; | |||
public SearchWsReponseGenerator(UserSession userSession) { | |||
this.userSession = userSession; | |||
} | |||
@Override | |||
public Users.SearchWsResponse toUsersForResponse(List<UserSearchResult> userSearchResults, Paging paging) { | |||
Users.SearchWsResponse.Builder responseBuilder = newBuilder(); | |||
userSearchResults.forEach(user -> responseBuilder.addUsers(toSearchResponsUser(user))); | |||
responseBuilder.getPagingBuilder() | |||
.setPageIndex(paging.pageIndex()) | |||
.setPageSize(paging.pageSize()) | |||
.setTotal(paging.total()) | |||
.build(); | |||
return responseBuilder.build(); | |||
} | |||
private Users.SearchWsResponse.User toSearchResponsUser(UserSearchResult userSearchResult) { | |||
UserDto userDto = userSearchResult.userDto(); | |||
Users.SearchWsResponse.User.Builder userBuilder = Users.SearchWsResponse.User.newBuilder().setLogin(userDto.getLogin()); | |||
ofNullable(userDto.getName()).ifPresent(userBuilder::setName); | |||
if (userSession.isLoggedIn()) { | |||
userSearchResult.avatar().ifPresent(userBuilder::setAvatar); | |||
userBuilder.setActive(userDto.isActive()); | |||
userBuilder.setLocal(userDto.isLocal()); | |||
ofNullable(userDto.getExternalIdentityProvider()).ifPresent(userBuilder::setExternalProvider); | |||
if (!userDto.getSortedScmAccounts().isEmpty()) { | |||
userBuilder.setScmAccounts(Users.SearchWsResponse.ScmAccounts.newBuilder().addAllScmAccounts(userDto.getSortedScmAccounts())); | |||
} | |||
} | |||
if (userSession.isSystemAdministrator() || Objects.equals(userSession.getUuid(), userDto.getUuid())) { | |||
ofNullable(userDto.getEmail()).ifPresent(userBuilder::setEmail); | |||
if (!userSearchResult.groups().isEmpty()) { | |||
userBuilder.setGroups(Users.SearchWsResponse.Groups.newBuilder().addAllGroups(userSearchResult.groups())); | |||
} | |||
ofNullable(userDto.getExternalLogin()).ifPresent(userBuilder::setExternalIdentity); | |||
userBuilder.setTokensCount(userSearchResult.tokensCount()); | |||
ofNullable(userDto.getLastConnectionDate()).map(DateUtils::formatDateTime).ifPresent(userBuilder::setLastConnectionDate); | |||
ofNullable(userDto.getLastSonarlintConnectionDate()) | |||
.map(DateUtils::formatDateTime).ifPresent(userBuilder::setSonarLintLastConnectionDate); | |||
userBuilder.setManaged(TRUE.equals(userSearchResult.managed())); | |||
} | |||
return userBuilder.build(); | |||
} | |||
} |
@@ -20,6 +20,7 @@ | |||
package org.sonar.server.user.ws; | |||
import org.sonar.core.platform.Module; | |||
import org.sonar.server.common.user.service.UserService; | |||
public class UsersWsModule extends Module { | |||
@@ -36,6 +37,8 @@ public class UsersWsModule extends Module { | |||
ChangePasswordAction.class, | |||
CurrentAction.class, | |||
SearchAction.class, | |||
UserService.class, | |||
SearchWsReponseGenerator.class, | |||
GroupsAction.class, | |||
IdentityProvidersAction.class, | |||
UserJsonWriter.class, |
@@ -20,6 +20,7 @@ | |||
package org.sonar.server.issue; | |||
import org.sonar.db.user.UserDto; | |||
import org.sonar.server.common.avatar.AvatarResolver; | |||
public class FakeAvatarResolver implements AvatarResolver { | |||
@@ -36,6 +36,7 @@ include 'server:sonar-web:design-system' | |||
include 'server:sonar-webserver' | |||
include 'server:sonar-webserver-api' | |||
include 'server:sonar-webserver-auth' | |||
include 'server:sonar-webserver-common' | |||
include 'server:sonar-webserver-core' | |||
include 'server:sonar-webserver-es' | |||
include 'server:sonar-webserver-webapi' |