Browse Source

SONAR-20959 allow to search by external login in /api/users/search

tags/9.9.3.79811
Aurelien Poscia 6 months ago
parent
commit
a6aea6dac4

+ 5
- 0
server/sonar-db-dao/src/main/java/org/sonar/db/user/UserDao.java View File

@@ -184,6 +184,11 @@ public class UserDao implements Dao {
return mapper(dbSession).selectByExternalLoginAndIdentityProvider(externalLogin, externalIdentityProvider);
}

public List<UserDto> selectByExternalLogin(DbSession dbSession, String externalLogin) {
return mapper(dbSession).selectByExternalLogin(externalLogin);
}


public long countSonarlintWeeklyUsers(DbSession dbSession) {
long threshold = system2.now() - WEEK_IN_MS;
return mapper(dbSession).countActiveSonarlintUsers(threshold);

+ 2
- 0
server/sonar-db-dao/src/main/java/org/sonar/db/user/UserMapper.java View File

@@ -64,6 +64,8 @@ public interface UserMapper {
@CheckForNull
UserDto selectByExternalLoginAndIdentityProvider(@Param("externalLogin") String externalLogin, @Param("externalIdentityProvider") String externalExternalIdentityProvider);

List<UserDto> selectByExternalLogin(@Param("externalLogin") String externalLogin);

List<String> selectExternalIdentityProviders();

void scrollAll(ResultHandler<UserDto> handler);

+ 7
- 0
server/sonar-db-dao/src/main/resources/org/sonar/db/user/UserMapper.xml View File

@@ -152,6 +152,13 @@
WHERE u.external_login=#{externalLogin, jdbcType=VARCHAR} AND u.external_identity_provider=#{externalIdentityProvider, jdbcType=VARCHAR}
</select>

<select id="selectByExternalLogin" parameterType="map" resultType="User">
SELECT
<include refid="userColumns"/>
FROM users u
WHERE u.external_login=#{externalLogin, jdbcType=VARCHAR}
</select>

<sql id="deactivateUserUpdatedFields">
active = ${_false},
email = null,

+ 16
- 0
server/sonar-db-dao/src/test/java/org/sonar/db/user/UserDaoTest.java View File

@@ -535,6 +535,22 @@ public class UserDaoTest {
assertThat(underTest.selectByExternalLoginAndIdentityProvider(session, "unknown", "unknown")).isNull();
}

@Test
public void select_by_external_login() {
UserDto activeUser = db.users().insertUser();
UserDto disableUser = db.users().insertUser(u -> u.setActive(false));

findByExternalLoginAndAssertResult(activeUser);
findByExternalLoginAndAssertResult(disableUser);
assertThat(underTest.selectByExternalLogin(session, "unknown")).isEmpty();
}

private void findByExternalLoginAndAssertResult(UserDto activeUser) {
assertThat(underTest.selectByExternalLogin(session, activeUser.getExternalLogin()))
.extracting(UserDto::getLogin)
.containsExactly(activeUser.getLogin());
}

@Test
public void scrollByLUuids() {
UserDto u1 = insertUser(true);

+ 44
- 2
server/sonar-webserver-webapi/src/main/java/org/sonar/server/user/ws/SearchAction.java View File

@@ -24,6 +24,8 @@ import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.TreeSet;
import javax.annotation.CheckForNull;
import javax.annotation.Nullable;
import org.sonar.api.server.ws.Change;
@@ -36,6 +38,7 @@ import org.sonar.db.DbSession;
import org.sonar.db.user.UserDto;
import org.sonar.server.es.SearchOptions;
import org.sonar.server.es.SearchResult;
import org.sonar.server.exceptions.ServerException;
import org.sonar.server.issue.AvatarResolver;
import org.sonar.server.user.UserSession;
import org.sonar.server.user.index.UserDoc;
@@ -48,6 +51,7 @@ 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.util.Optional.ofNullable;
import static java.util.stream.Collectors.toSet;
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;
@@ -62,6 +66,7 @@ import static org.sonarqube.ws.Users.SearchWsResponse.newBuilder;

public class SearchAction implements UsersWsAction {
private static final String DEACTIVATED_PARAM = "deactivated";
static final String EXTERNAL_IDENTITY = "externalIdentity";
private static final int MAX_PAGE_SIZE = 500;

private final UserSession userSession;
@@ -92,6 +97,7 @@ public class SearchAction implements UsersWsAction {
"Field 'lastConnectionDate' is only updated every hour, so it may not be accurate, for instance when a user authenticates many times in less than one hour.")
.setSince("3.6")
.setChangelog(
new Change("9.9.3", "New optional parameters " + EXTERNAL_IDENTITY + " to find a user by its IdP login"),
new Change("9.7", "New parameter 'deactivated' to optionally search for deactivated users"),
new Change("7.7", "New field 'lastConnectionDate' is added to response"),
new Change("7.4", "External identity is only returned to system administrators"),
@@ -123,27 +129,53 @@ public class SearchAction implements UsersWsAction {
.setRequired(false)
.setDefaultValue(false)
.setBooleanPossibleValues();
action.createParam(EXTERNAL_IDENTITY)
.setSince("9.9.3")
.setDescription("""
Find a user by its external identity (ie. its login in the Identity Provider).
This is case sensitive and only available with Administer System permission.
""");
}

@Override
public void handle(Request request, Response response) throws Exception {
throwIfAdminOnlyParametersAreUsed(request);
Users.SearchWsResponse wsResponse = doHandle(toSearchRequest(request));
writeProtobuf(wsResponse, request, response);
}

private void throwIfAdminOnlyParametersAreUsed(Request request) {
if (!userSession.isSystemAdministrator() && (request.param(EXTERNAL_IDENTITY) != null)) {
throw new ServerException(403, "parameter " + EXTERNAL_IDENTITY + " requires Administer System permission.");
}
}

private Users.SearchWsResponse doHandle(SearchRequest request) {
SearchOptions options = new SearchOptions().setPage(request.getPage(), request.getPageSize());
SearchResult<UserDoc> result = userIndex.search(UserQuery.builder().setActive(!request.isDeactivated()).setTextQuery(request.getQuery()).build(), options);
try (DbSession dbSession = dbClient.openSession(false)) {
List<String> logins = result.getDocs().stream().map(UserDoc::login).collect(toList());
Set<String> logins = result.getDocs().stream().map(UserDoc::login).collect(toSet());
Multimap<String, String> groupsByLogin = dbClient.groupMembershipDao().selectGroupsByLogins(dbSession, logins);
List<UserDto> users = dbClient.userDao().selectByOrderedLogins(dbSession, logins);

logins = findUsersWithExternalLoginsIfDefined(dbSession, request.externalLogin, logins);

List<UserDto> users = dbClient.userDao().selectByOrderedLogins(dbSession, new TreeSet<>(logins));
Map<String, Integer> tokenCountsByLogin = dbClient.userTokenDao().countTokensByUsers(dbSession, users);
Paging paging = forPageIndex(request.getPage()).withPageSize(request.getPageSize()).andTotal((int) result.getTotal());
return buildResponse(users, groupsByLogin, tokenCountsByLogin, paging);
}
}

private Set<String> findUsersWithExternalLoginsIfDefined(DbSession dbSession, @Nullable String externalLogin, Set<String> loginsFromSearch) {
if (externalLogin != null) {
List<UserDto> userDtos = dbClient.userDao().selectByExternalLogin(dbSession, externalLogin);
return userDtos.stream().map(UserDto::getLogin)
.filter(loginsFromSearch::contains)
.collect(toSet());
}
return loginsFromSearch;
}

private SearchWsResponse buildResponse(List<UserDto> users, Multimap<String, String> groupsByLogin, Map<String, Integer> tokenCountsByLogin, Paging paging) {
SearchWsResponse.Builder responseBuilder = newBuilder();
users.forEach(user -> responseBuilder.addUsers(towsUser(user, firstNonNull(tokenCountsByLogin.get(user.getUuid()), 0), groupsByLogin.get(user.getLogin()))));
@@ -187,6 +219,7 @@ public class SearchAction implements UsersWsAction {
.setDeactivated(request.mandatoryParamAsBoolean(DEACTIVATED_PARAM))
.setPage(request.mandatoryParamAsInt(PAGE))
.setPageSize(pageSize)
.setExternalLogin(request.param(EXTERNAL_IDENTITY))
.build();
}

@@ -195,12 +228,14 @@ public class SearchAction implements UsersWsAction {
private final Integer pageSize;
private final String query;
private final boolean deactivated;
private final String externalLogin;

private SearchRequest(Builder builder) {
this.page = builder.page;
this.pageSize = builder.pageSize;
this.query = builder.query;
this.deactivated = builder.deactivated;
this.externalLogin = builder.externalLogin;
}

@CheckForNull
@@ -233,6 +268,8 @@ public class SearchAction implements UsersWsAction {
private String query;
private boolean deactivated;

private String externalLogin;

private Builder() {
// enforce factory method use
}
@@ -257,6 +294,11 @@ public class SearchAction implements UsersWsAction {
return this;
}

public Builder setExternalLogin(@Nullable String externalLogin) {
this.externalLogin = externalLogin;
return this;
}

public SearchRequest build() {
return new SearchRequest(this);
}

+ 85
- 1
server/sonar-webserver-webapi/src/test/java/org/sonar/server/user/ws/SearchActionTest.java View File

@@ -20,6 +20,7 @@
package org.sonar.server.user.ws;

import java.util.stream.IntStream;
import org.assertj.core.api.InstanceOfAssertFactories;
import org.junit.Rule;
import org.junit.Test;
import org.sonar.api.server.ws.WebService;
@@ -29,10 +30,12 @@ import org.sonar.db.DbTester;
import org.sonar.db.user.GroupDto;
import org.sonar.db.user.UserDto;
import org.sonar.server.es.EsTester;
import org.sonar.server.exceptions.ServerException;
import org.sonar.server.issue.AvatarResolverImpl;
import org.sonar.server.tester.UserSessionRule;
import org.sonar.server.user.index.UserIndex;
import org.sonar.server.user.index.UserIndexer;
import org.sonar.server.ws.TestRequest;
import org.sonar.server.ws.WsActionTester;
import org.sonarqube.ws.Common.Paging;
import org.sonarqube.ws.Users.SearchWsResponse;
@@ -42,8 +45,10 @@ import static java.util.Arrays.asList;
import static java.util.Collections.emptyList;
import static java.util.Collections.singletonList;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.assertj.core.api.Assertions.tuple;
import static org.sonar.api.utils.DateUtils.formatDateTime;
import static org.sonar.server.user.ws.SearchAction.EXTERNAL_IDENTITY;
import static org.sonar.test.JsonAssert.assertJson;

public class SearchActionTest {
@@ -397,7 +402,86 @@ public class SearchActionTest {
assertThat(action).isNotNull();
assertThat(action.isPost()).isFalse();
assertThat(action.responseExampleAsString()).isNotEmpty();
assertThat(action.params()).hasSize(4);
assertThat(action.params()).hasSize(5);
}

@Test
public void search_whenFilteringOnExternalIdentityAndNotAdmin_shouldThrow() {
userSession.logIn();

TestRequest testRequest = ws.newRequest()
.setParam(EXTERNAL_IDENTITY, "login");

assertForbiddenException(testRequest);
}

private static void assertForbiddenException(TestRequest testRequest) {
assertThatThrownBy(() -> testRequest.executeProtobuf(SearchWsResponse.class))
.asInstanceOf(InstanceOfAssertFactories.type(ServerException.class))
.extracting(ServerException::httpCode)
.isEqualTo(403);
}

@Test
public void search_whenFilteringOnExternalIdentityAndMatch_shouldReturnMatchingUser() {
userSession.logIn().setSystemAdministrator();

prepareUsersWithAndWithoutExternalLogin();

TestRequest testRequest = ws.newRequest()
.setParam(EXTERNAL_IDENTITY, "user1");

assertThat(testRequest.executeProtobuf(SearchWsResponse.class).getUsersList())
.extracting(User::getExternalIdentity)
.containsExactly("user1");
}

@Test
public void search_whenFilteringOnExternalIdentityAndNoMatch_shouldReturnNothing() {
userSession.logIn().setSystemAdministrator();

prepareUsersWithAndWithoutExternalLogin();

TestRequest testRequest = ws.newRequest()
.setParam(EXTERNAL_IDENTITY, "nomatch");

assertThat(testRequest.executeProtobuf(SearchWsResponse.class).getUsersList()).isEmpty();
}

@Test
public void search_whenFilteringOnExternalIdentityAndOtherCriteriaWitoutMatch_shouldReturnNothing() {
userSession.logIn().setSystemAdministrator();

prepareUsersWithAndWithoutExternalLogin();

TestRequest testRequest = ws.newRequest()
.setParam("deactivated", "true")
.setParam(EXTERNAL_IDENTITY, "user1");

assertThat(testRequest.executeProtobuf(SearchWsResponse.class).getUsersList()).isEmpty();
}

@Test
public void search_whenFilteringOnExternalIdentityAndOtherCriteriaWithMatch_shouldReturnMatchingUser() {
userSession.logIn().setSystemAdministrator();

prepareUsersWithAndWithoutExternalLogin();

TestRequest testRequest = ws.newRequest()
.setParam("deactivated", "false")
.setParam(EXTERNAL_IDENTITY, "user1");

assertThat(testRequest.executeProtobuf(SearchWsResponse.class).getUsersList())
.extracting(User::getExternalIdentity)
.containsExactly("user1");
}

private void prepareUsersWithAndWithoutExternalLogin() {
db.users().insertUser();
db.users().insertUser(user -> user.setExternalLogin("user1"));
db.users().insertUser(user -> user.setExternalLogin("USER1"));
db.users().insertUser(user -> user.setExternalLogin("user1-oldaccount"));
userIndexer.indexAll();
}

}

Loading…
Cancel
Save