]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-18689 add filtering by managed in /api/users/search
authorAurelien Poscia <aurelien.poscia@sonarsource.com>
Fri, 10 Mar 2023 15:23:31 +0000 (16:23 +0100)
committersonartech <sonartech@sonarsource.com>
Wed, 22 Mar 2023 20:04:07 +0000 (20:04 +0000)
server/sonar-db-dao/src/it/java/org/sonar/db/scim/ScimUserDaoIT.java
server/sonar-db-dao/src/main/java/org/sonar/db/scim/ScimUserDao.java
server/sonar-db-dao/src/main/java/org/sonar/db/user/UserQuery.java
server/sonar-db-dao/src/main/resources/org/sonar/db/user/UserMapper.xml
server/sonar-server-common/src/main/java/org/sonar/server/management/DelegatingManagedInstanceService.java
server/sonar-server-common/src/main/java/org/sonar/server/management/ManagedInstanceService.java
server/sonar-server-common/src/test/java/org/sonar/server/management/DelegatingManagedInstanceServiceTest.java
server/sonar-webserver-webapi/src/it/java/org/sonar/server/user/ws/SearchActionIT.java
server/sonar-webserver-webapi/src/main/java/org/sonar/server/user/ws/SearchAction.java

index 6f90e777a365cc8bfdee7dece3eba7186cfb4608..6e5a31221c4bd4f2e0a5bb873d105cc83b48fbf7 100644 (file)
@@ -324,6 +324,16 @@ public class ScimUserDaoIT {
     return scimUserTestData;
   }
 
+  @Test
+  public void getManagedUserSqlFilter_isNotEmpty() {
+    String filterManagedUser = scimUserDao.getManagedUserSqlFilter(true);
+    assertThat(filterManagedUser).isNotEmpty();
+    String filterNonManagedUser = scimUserDao.getManagedUserSqlFilter(false);
+    assertThat(filterNonManagedUser).isNotEmpty();
+
+    assertThat(filterManagedUser).isNotEqualTo(filterNonManagedUser);
+  }
+
   private static class ScimUserTestData {
 
     private final String scimUserUuid;
index d2a9fb25da19a26e375fc89bccde5c31fbc20381..f428a62590215c42ddc8b12ee1d9b696387d302e 100644 (file)
@@ -50,7 +50,6 @@ public class ScimUserDao implements Dao {
     return Optional.ofNullable(mapper(dbSession).findByUserUuid(userUuid));
   }
 
-
   public ScimUserDto enableScimForUser(DbSession dbSession, String userUuid) {
     ScimUserDto scimUserDto = new ScimUserDto(uuidFactory.create(), userUuid);
     mapper(dbSession).insert(scimUserDto);
@@ -104,4 +103,8 @@ public class ScimUserDao implements Dao {
   public void deleteByScimUuid(DbSession dbSession, String scimUuid) {
     mapper(dbSession).deleteByScimUuid(scimUuid);
   }
+
+  public String getManagedUserSqlFilter(boolean filterByManaged) {
+    return String.format("%s exists (select user_uuid from scim_users su where su.user_uuid = uuid)", filterByManaged ? "" : "not");
+  }
 }
index b23037260e01c18144da9c1370f6bee403cab4a0..e512deed795daf76ee606b793b60363983ceaca1 100644 (file)
@@ -24,12 +24,15 @@ import javax.annotation.Nullable;
 import org.apache.commons.lang.StringUtils;
 
 public class UserQuery {
+  private static final String MATCH_NOTHING = "1=2";
   private final String searchText;
   private final Boolean isActive;
+  private final String isManagedSqlClause;
 
-  public UserQuery(@Nullable String searchText, @Nullable Boolean isActive) {
+  public UserQuery(@Nullable String searchText, @Nullable Boolean isActive, @Nullable String isManagedSqlClause) {
     this.searchText = searchTextToSearchTextSql(searchText);
     this.isActive = isActive;
+    this.isManagedSqlClause = isManagedSqlClause;
   }
 
   private static String searchTextToSearchTextSql(@Nullable String text) {
@@ -43,22 +46,28 @@ public class UserQuery {
   }
 
   @CheckForNull
-  private String getSearchText() {
+  public String getSearchText() {
     return searchText;
   }
 
   @CheckForNull
-  private Boolean isActive() {
+  public Boolean isActive() {
     return isActive;
   }
 
+  @CheckForNull
+  public String getIsManagedSqlClause() {
+    return isManagedSqlClause;
+  }
+
   public static UserQueryBuilder builder() {
     return new UserQueryBuilder();
   }
 
   public static final class UserQueryBuilder {
-    private String searchText;
-    private Boolean isActive;
+    private String searchText = null;
+    private Boolean isActive = null;
+    private String isManagedSqlClause = null;
 
     private UserQueryBuilder() {
     }
@@ -73,8 +82,13 @@ public class UserQuery {
       return this;
     }
 
+    public UserQueryBuilder isManagedClause(@Nullable String isManagedSqlClause) {
+      this.isManagedSqlClause = isManagedSqlClause;
+      return this;
+    }
+
     public UserQuery build() {
-      return new UserQuery(searchText, isActive);
+      return new UserQuery(searchText, isActive, isManagedSqlClause);
     }
   }
 }
index bf5145c8791e100b58a3f7187ab6508c6ec8ee57..78930597d295888388cfbe7b5a7d6532f2138273 100644 (file)
             <include refid="userColumns"/>
         FROM
             (SELECT
-                    u.uuid,
-                    u.login,
-                    u.name,
-                    u.email,
-                    u.active,
-                    u.scm_accounts,
-                    u.salt,
-                    u.crypted_password,
-                    u.hash_method,
-                    u.external_id,
-                    u.external_login,
-                    u.external_identity_provider,
-                    u.user_local,
-                    u.reset_password,
-                    u.homepage_type,
-                    u.homepage_parameter,
-                    u.last_connection_date,
-                    u.last_sonarlint_connection,
-                    u.created_at,
-                    u.updated_at
+                <include refid="searchByQueryInnerQueryColumns"/>
             FROM users u
             <include refid="searchByQueryWhereClause"/>
             ORDER BY u.login
         FROM
             (SELECT rownum as rn, t.* from (
                 SELECT
-                    u.uuid,
-                    u.login,
-                    u.name,
-                    u.email,
-                    u.active,
-                    u.scm_accounts,
-                    u.salt,
-                    u.crypted_password,
-                    u.hash_method,
-                    u.external_id,
-                    u.external_login,
-                    u.external_identity_provider,
-                    u.user_local,
-                    u.reset_password,
-                    u.homepage_type,
-                    u.homepage_parameter,
-                    u.last_connection_date,
-                    u.last_sonarlint_connection,
-                    u.created_at,
-                    u.updated_at
+                <include refid="searchByQueryInnerQueryColumns"/>
                 FROM users u
                 <include refid="searchByQueryWhereClause"/>
                 ORDER BY u.login ASC
         <include refid="searchByQueryWhereClause"/>
     </select>
 
+    <sql id="searchByQueryInnerQueryColumns">
+        u.uuid,
+        u.login,
+        u.name,
+        u.email,
+        u.active,
+        u.scm_accounts,
+        u.salt,
+        u.crypted_password,
+        u.hash_method,
+        u.external_id,
+        u.external_login,
+        u.external_identity_provider,
+        u.user_local,
+        u.reset_password,
+        u.homepage_type,
+        u.homepage_parameter,
+        u.last_connection_date,
+        u.last_sonarlint_connection,
+        u.created_at,
+        u.updated_at
+    </sql>
+
     <sql id="searchByQueryWhereClause">
-             <where>
+         <where>
+             1=1
             <if test="query.isActive != null">
-                u.active=#{query.isActive, jdbcType=BOOLEAN}
+                AND u.active=#{query.isActive, jdbcType=BOOLEAN}
             </if>
             <if test="query.searchText != null">
                 AND (
                     OR (lower(u.email) LIKE lower(#{query.searchText, jdbcType=VARCHAR}) ESCAPE '/')
                 )
             </if>
+            <if test="query.isManagedSqlClause != null">
+                AND ${query.isManagedSqlClause}
+            </if>
         </where>
-
     </sql>
 
     <select id="selectUsersForTelemetry" parameterType="map" resultType="UserTelemetry">
index 9bb07da8b31a8abdaa84f4f783b86fd696030239..8c58caf15f53a0831dee06a6ec6e4d7e38d9c3fc 100644 (file)
@@ -60,6 +60,13 @@ public class DelegatingManagedInstanceService implements ManagedInstanceService
       .orElse(returnNonManagedForAllGroups(groupUuids));
   }
 
+  @Override
+  public String getManagedUsersSqlFilter(boolean filterByManaged) {
+    return findManagedInstanceService()
+      .map(managedInstanceService -> managedInstanceService.getManagedUsersSqlFilter(filterByManaged))
+      .orElseThrow(() -> new IllegalStateException("This instance is not managed."));
+  }
+
   private Optional<ManagedInstanceService> findManagedInstanceService() {
     Set<ManagedInstanceService> managedInstanceServices = delegates.stream()
       .filter(ManagedInstanceService::isInstanceExternallyManaged)
index 581fe04e34f6f2e9a0e0be12263fc502f932ff39..942d3b5f9e44c4cc92e7d28bd35c5a34192860f9 100644 (file)
@@ -32,4 +32,6 @@ public interface ManagedInstanceService {
   Map<String, Boolean> getUserUuidToManaged(DbSession dbSession, Set<String> userUuids);
 
   Map<String, Boolean> getGroupUuidToManaged(DbSession dbSession, Set<String> groupUuids);
+
+  String getManagedUsersSqlFilter(boolean filterByManaged);
 }
index 542074300b574d19c7a159f7325339eb8b74ae39..e201461ec52b5c01e5c2aa113f73ec048e32cf9a 100644 (file)
@@ -125,6 +125,24 @@ public class DelegatingManagedInstanceServiceTest {
       .withMessage("The instance can't be managed by more than one identity provider and 2 were found.");
   }
 
+  @Test
+  public void getManagedUsersSqlFilter_whenNoDelegates_throws() {
+    Set<ManagedInstanceService> managedInstanceServices = emptySet();
+    DelegatingManagedInstanceService delegatingManagedInstanceService = new DelegatingManagedInstanceService(managedInstanceServices);
+    assertThatIllegalStateException()
+      .isThrownBy(() -> delegatingManagedInstanceService.getManagedUsersSqlFilter(true))
+      .withMessage("This instance is not managed.");
+  }
+
+  @Test
+  public void getManagedUsersSqlFilter_delegatesToRightService_andPropagateAnswer() {
+    AlwaysManagedInstanceService alwaysManagedInstanceService = new AlwaysManagedInstanceService();
+    DelegatingManagedInstanceService managedInstanceService = new DelegatingManagedInstanceService(Set.of(new NeverManagedInstanceService(), alwaysManagedInstanceService));
+
+    assertThat(managedInstanceService.getManagedUsersSqlFilter(true)).isNotNull().isEqualTo(alwaysManagedInstanceService.getManagedUsersSqlFilter(
+      true));
+  }
+
   private ManagedInstanceService getManagedInstanceService(Set<String> userUuids, Map<String, Boolean> uuidToManaged) {
     ManagedInstanceService anotherManagedInstanceService = mock(ManagedInstanceService.class);
     when(anotherManagedInstanceService.isInstanceExternallyManaged()).thenReturn(true);
@@ -149,6 +167,11 @@ public class DelegatingManagedInstanceServiceTest {
     public Map<String, Boolean> getGroupUuidToManaged(DbSession dbSession, Set<String> groupUuids) {
       return null;
     }
+
+    @Override
+    public String getManagedUsersSqlFilter(boolean filterByManaged) {
+      return null;
+    }
   }
 
   private static class AlwaysManagedInstanceService implements ManagedInstanceService {
@@ -167,6 +190,11 @@ public class DelegatingManagedInstanceServiceTest {
     public Map<String, Boolean> getGroupUuidToManaged(DbSession dbSession, Set<String> groupUuids) {
       return null;
     }
+
+    @Override
+    public String getManagedUsersSqlFilter(boolean filterByManaged) {
+      return "any filter";
+    }
   }
 
 }
index 5fe1512d720a8b369132fbcb28fd5e46e3a22a24..6d3bb32da1bb81e4e707299dc7e753c3fca0af9c 100644 (file)
@@ -25,12 +25,16 @@ import org.junit.Rule;
 import org.junit.Test;
 import org.sonar.api.server.ws.WebService;
 import org.sonar.api.server.ws.WebService.Param;
+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.exceptions.BadRequestException;
 import org.sonar.server.issue.AvatarResolverImpl;
 import org.sonar.server.management.ManagedInstanceService;
 import org.sonar.server.tester.UserSessionRule;
+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 +46,10 @@ 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.assertThatExceptionOfType;
 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;
 import static org.sonar.api.utils.DateUtils.formatDateTime;
@@ -135,7 +141,7 @@ public class SearchActionIT {
   }
 
   @Test
-  public void return_isManaged() {
+  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());
@@ -152,6 +158,67 @@ public class SearchActionIT {
       );
   }
 
+  @Test
+  public void search_whenFilteringByManagedAndInstanceNotManaged_throws() {
+    userSession.logIn().setSystemAdministrator();
+
+    TestRequest testRequest = ws.newRequest()
+      .setParam("managed", "true");
+
+    assertThatExceptionOfType(BadRequestException.class)
+      .isThrownBy(() -> testRequest.executeProtobuf(SearchWsResponse.class))
+      .withMessage("The 'managed' parameter is only available for managed instances.");
+  }
+
+  @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();
+    userSession.logIn().setSystemAdministrator();
+
+    SearchWsResponse response = ws.newRequest()
+      .setParam("managed", "true")
+      .executeProtobuf(SearchWsResponse.class);
+
+    assertThat(response.getUsersList())
+      .extracting(User::getLogin, User::getManaged)
+      .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();
+    userSession.logIn().setSystemAdministrator();
+
+    SearchWsResponse response = ws.newRequest()
+      .setParam("managed", "false")
+      .executeProtobuf(SearchWsResponse.class);
+
+    assertThat(response.getUsersList())
+      .extracting(User::getLogin, User::getManaged)
+      .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")));
@@ -414,7 +481,7 @@ public class SearchActionIT {
     assertThat(action).isNotNull();
     assertThat(action.isPost()).isFalse();
     assertThat(action.responseExampleAsString()).isNotEmpty();
-    assertThat(action.params()).hasSize(4);
+    assertThat(action.params()).hasSize(5);
   }
 
   private void mockUsersAsManaged(String... userUuids) {
index dc463de3d68a0c4c103344c90c48067fd00c8d1f..8f9628c9bf83d0c96aae6f37f0a5839ab91eabd8 100644 (file)
@@ -24,6 +24,7 @@ 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;
@@ -38,6 +39,7 @@ import org.sonar.db.DbSession;
 import org.sonar.db.user.UserDto;
 import org.sonar.db.user.UserQuery;
 import org.sonar.server.es.SearchOptions;
+import org.sonar.server.exceptions.BadRequestException;
 import org.sonar.server.issue.AvatarResolver;
 import org.sonar.server.management.ManagedInstanceService;
 import org.sonar.server.user.UserSession;
@@ -64,6 +66,7 @@ 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;
 
   private final UserSession userSession;
@@ -96,6 +99,7 @@ public class SearchAction implements UsersWsAction {
       .setSince("3.6")
       .setChangelog(
         new Change("10.0", "'q' parameter values is now always performing a case insensitive match"),
+        new Change("10.0", "New parameter 'managed' to optionally search by managed status"),
         new Change("10.0", "Response includes 'managed' field."),
         new Change("9.7", "New parameter 'deactivated' to optionally search for deactivated users"),
         new Change("7.7", "New field 'lastConnectionDate' is added to response"),
@@ -118,6 +122,12 @@ public class SearchAction implements UsersWsAction {
       .setRequired(false)
       .setDefaultValue(false)
       .setBooleanPossibleValues();
+    action.createParam(MANAGED_PARAM)
+      .setSince("10.0")
+      .setDescription("Return managed or non-managed users. Only available for managed instances, throws for non-managed instances.")
+      .setRequired(false)
+      .setDefaultValue(null)
+      .setBooleanPossibleValues();
   }
 
   @Override
@@ -129,7 +139,7 @@ public class SearchAction implements UsersWsAction {
   private Users.SearchWsResponse doHandle(SearchRequest request) {
     UserQuery userQuery = buildUserQuery(request);
     try (DbSession dbSession = dbClient.openSession(false)) {
-      List<UserDto> users = fetchUsersAndSortByLogin(request, dbSession, userQuery);
+      List<UserDto> users = findUsersAndSortByLogin(request, dbSession, userQuery);
       int totalUsers = dbClient.userDao().countUsers(dbSession, userQuery);
 
       List<String> logins = users.stream().map(UserDto::getLogin).collect(toList());
@@ -145,14 +155,27 @@ public class SearchAction implements UsersWsAction {
     return users.stream().map(UserDto::getUuid).collect(Collectors.toSet());
   }
 
-  private static UserQuery buildUserQuery(SearchRequest request) {
+  private UserQuery buildUserQuery(SearchRequest request) {
+    if (managedInstanceService.isInstanceExternallyManaged()) {
+      String managedInstanceSql = Optional.ofNullable(request.isManaged())
+        .map(managedInstanceService::getManagedUsersSqlFilter)
+        .orElse(null);
+      return UserQuery.builder()
+        .isActive(!request.isDeactivated())
+        .searchText(request.getQuery())
+        .isManagedClause(managedInstanceSql)
+        .build();
+    }
+    if (request.isManaged() != null) {
+      throw BadRequestException.create("The 'managed' parameter is only available for managed instances.");
+    }
     return UserQuery.builder()
       .isActive(!request.isDeactivated())
       .searchText(request.getQuery())
       .build();
   }
 
-  private List<UserDto> fetchUsersAndSortByLogin(SearchRequest request, DbSession dbSession, UserQuery userQuery) {
+  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))
@@ -173,7 +196,7 @@ public class SearchAction implements UsersWsAction {
     return responseBuilder.build();
   }
 
-  private User towsUser(UserDto user, @Nullable Integer tokensCount, Collection<String> groups, Boolean isManaged) {
+  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()) {
@@ -193,7 +216,7 @@ public class SearchAction implements UsersWsAction {
       ofNullable(user.getExternalLogin()).ifPresent(userBuilder::setExternalIdentity);
       ofNullable(tokensCount).ifPresent(userBuilder::setTokensCount);
       ofNullable(user.getLastConnectionDate()).ifPresent(date -> userBuilder.setLastConnectionDate(formatDateTime(date)));
-      userBuilder.setManaged(TRUE.equals(isManaged));
+      userBuilder.setManaged(TRUE.equals(managed));
     }
     return userBuilder.build();
   }
@@ -204,6 +227,7 @@ public class SearchAction implements UsersWsAction {
     return SearchRequest.builder()
       .setQuery(request.param(TEXT_QUERY))
       .setDeactivated(request.mandatoryParamAsBoolean(DEACTIVATED_PARAM))
+      .setManaged(request.paramAsBoolean(MANAGED_PARAM))
       .setPage(request.mandatoryParamAsInt(PAGE))
       .setPageSize(pageSize)
       .build();
@@ -214,12 +238,14 @@ public class SearchAction implements UsersWsAction {
     private final Integer pageSize;
     private final String query;
     private final boolean deactivated;
+    private final Boolean managed;
 
     private SearchRequest(Builder builder) {
       this.page = builder.page;
       this.pageSize = builder.pageSize;
       this.query = builder.query;
       this.deactivated = builder.deactivated;
+      this.managed = builder.managed;
     }
 
     public Integer getPage() {
@@ -239,6 +265,11 @@ public class SearchAction implements UsersWsAction {
       return deactivated;
     }
 
+    @CheckForNull
+    private Boolean isManaged() {
+      return managed;
+    }
+
     public static Builder builder() {
       return new Builder();
     }
@@ -249,6 +280,7 @@ public class SearchAction implements UsersWsAction {
     private Integer pageSize;
     private String query;
     private boolean deactivated;
+    private Boolean managed;
 
     private Builder() {
       // enforce factory method use
@@ -274,6 +306,11 @@ public class SearchAction implements UsersWsAction {
       return this;
     }
 
+    public Builder setManaged(@Nullable Boolean managed) {
+      this.managed = managed;
+      return this;
+    }
+
     public SearchRequest build() {
       return new SearchRequest(this);
     }