From c2607c6af4c068c3bb4acbb5b6d200a3f4908a30 Mon Sep 17 00:00:00 2001 From: Wojtek Wajerowicz <115081248+wojciech-wajerowicz-sonarsource@users.noreply.github.com> Date: Fri, 10 Mar 2023 11:02:58 +0100 Subject: [PATCH] SONAR-18657 allow filtering on isManaged for /api/user_groups/search --- .../java/org/sonar/db/scim/ScimUserDaoIT.java | 12 +++ .../it/java/org/sonar/db/user/GroupDaoIT.java | 41 ++++--- .../java/org/sonar/db/scim/ScimGroupDao.java | 4 + .../main/java/org/sonar/db/user/GroupDao.java | 23 +--- .../java/org/sonar/db/user/GroupMapper.java | 5 +- .../java/org/sonar/db/user/GroupQuery.java | 83 +++++++++++++++ .../java/org/sonar/db/user/UserQuery.java | 1 - .../org/sonar/db/user/GroupMapper.xml | 20 ++-- .../org/sonar/db/scim/ScimGroupDaoTest.java | 13 +++ .../DelegatingManagedInstanceService.java | 10 +- .../management/ManagedInstanceService.java | 2 + .../DelegatingManagedInstanceServiceTest.java | 28 +++++ .../server/usergroups/ws/SearchActionIT.java | 100 ++++++++++++++++-- .../server/usergroups/ws/SearchAction.java | 49 ++++++++- 14 files changed, 333 insertions(+), 58 deletions(-) create mode 100644 server/sonar-db-dao/src/main/java/org/sonar/db/user/GroupQuery.java diff --git a/server/sonar-db-dao/src/it/java/org/sonar/db/scim/ScimUserDaoIT.java b/server/sonar-db-dao/src/it/java/org/sonar/db/scim/ScimUserDaoIT.java index 6e5a31221c4..701f99684ef 100644 --- a/server/sonar-db-dao/src/it/java/org/sonar/db/scim/ScimUserDaoIT.java +++ b/server/sonar-db-dao/src/it/java/org/sonar/db/scim/ScimUserDaoIT.java @@ -334,6 +334,18 @@ public class ScimUserDaoIT { assertThat(filterManagedUser).isNotEqualTo(filterNonManagedUser); } + @Test + public void getManagedGroupsSqlFilter_whenFilterByManagedIsTrue_returnsCorrectQuery() { + String filterManagedUser = scimUserDao.getManagedUserSqlFilter(true); + assertThat(filterManagedUser).isEqualTo(" exists (select user_uuid from scim_users su where su.user_uuid = uuid)"); + } + + @Test + public void getManagedGroupsSqlFilter_whenFilterByManagedIsFalse_returnsCorrectQuery() { + String filterNonManagedUser = scimUserDao.getManagedUserSqlFilter(false); + assertThat(filterNonManagedUser).isEqualTo("not exists (select user_uuid from scim_users su where su.user_uuid = uuid)"); + } + private static class ScimUserTestData { private final String scimUserUuid; diff --git a/server/sonar-db-dao/src/it/java/org/sonar/db/user/GroupDaoIT.java b/server/sonar-db-dao/src/it/java/org/sonar/db/user/GroupDaoIT.java index 01cee91dfc3..a904793fe6a 100644 --- a/server/sonar-db-dao/src/it/java/org/sonar/db/user/GroupDaoIT.java +++ b/server/sonar-db-dao/src/it/java/org/sonar/db/user/GroupDaoIT.java @@ -41,6 +41,8 @@ public class GroupDaoIT { private static final long NOW = 1_500_000L; private static final String MISSING_UUID = "unknown"; + private static final GroupQuery EMPTY_QUERY = GroupQuery.builder().build(); + private System2 system2 = mock(System2.class); @Rule @@ -153,29 +155,42 @@ public class GroupDaoIT { */ // Null query - assertThat(underTest.selectByQuery(dbSession, null, 0, 10)) + assertThat(underTest.selectByQuery(dbSession, EMPTY_QUERY, 0, 10)) .hasSize(5) .extracting("name").containsOnly("customers-group1", "customers-group2", "customers-group3", "SONAR-ADMINS", "sonar-users"); // Empty query - assertThat(underTest.selectByQuery(dbSession, "", 0, 10)) + assertThat(underTest.selectByQuery(dbSession, textSearchQuery(""), 0, 10)) .hasSize(5) .extracting("name").containsOnly("customers-group1", "customers-group2", "customers-group3", "SONAR-ADMINS", "sonar-users"); // Filter on name - assertThat(underTest.selectByQuery(dbSession, "sonar", 0, 10)) + assertThat(underTest.selectByQuery(dbSession, textSearchQuery("sonar"), 0, 10)) .hasSize(2) .extracting("name").containsOnly("SONAR-ADMINS", "sonar-users"); + //Filter on name and additionalClause + assertThat(underTest.selectByQuery(dbSession, textSearchAndManagedClauseQuery("sonar", " name = 'SONAR-ADMINS'"), 0, 10)) + .hasSize(1) + .extracting("name").containsOnly("SONAR-ADMINS"); + // Pagination - assertThat(underTest.selectByQuery(dbSession, null, 0, 3)) + assertThat(underTest.selectByQuery(dbSession, EMPTY_QUERY, 0, 3)) .hasSize(3); - assertThat(underTest.selectByQuery(dbSession, null, 3, 3)) + assertThat(underTest.selectByQuery(dbSession, EMPTY_QUERY, 3, 3)) .hasSize(2); - assertThat(underTest.selectByQuery(dbSession, null, 6, 3)).isEmpty(); - assertThat(underTest.selectByQuery(dbSession, null, 0, 5)) + assertThat(underTest.selectByQuery(dbSession, EMPTY_QUERY, 6, 3)).isEmpty(); + assertThat(underTest.selectByQuery(dbSession, EMPTY_QUERY, 0, 5)) .hasSize(5); - assertThat(underTest.selectByQuery(dbSession, null, 5, 5)).isEmpty(); + assertThat(underTest.selectByQuery(dbSession, EMPTY_QUERY, 5, 5)).isEmpty(); + } + + private static GroupQuery textSearchQuery(String query) { + return GroupQuery.builder().searchText(query).build(); + } + + private static GroupQuery textSearchAndManagedClauseQuery(String query, String managedClause) { + return GroupQuery.builder().searchText(query).isManagedClause(managedClause).build(); } @Test @@ -184,8 +199,8 @@ public class GroupDaoIT { underTest.insert(dbSession, newGroupDto().setName(groupNameWithSpecialCharacters)); db.commit(); - List result = underTest.selectByQuery(dbSession, "roup%_%/nam", 0, 10); - int resultCount = underTest.countByQuery(dbSession, "roup%_%/nam"); + List result = underTest.selectByQuery(dbSession, textSearchQuery("roup%_%/nam"), 0, 10); + int resultCount = underTest.countByQuery(dbSession, textSearchQuery("roup%_%/nam")); assertThat(result).hasSize(1); assertThat(result.get(0).getName()).isEqualTo(groupNameWithSpecialCharacters); @@ -201,13 +216,13 @@ public class GroupDaoIT { db.users().insertGroup("customers-group3"); // Null query - assertThat(underTest.countByQuery(dbSession, null)).isEqualTo(5); + assertThat(underTest.countByQuery(dbSession, EMPTY_QUERY)).isEqualTo(5); // Empty query - assertThat(underTest.countByQuery(dbSession, "")).isEqualTo(5); + assertThat(underTest.countByQuery(dbSession, textSearchQuery(""))).isEqualTo(5); // Filter on name - assertThat(underTest.countByQuery(dbSession, "sonar")).isEqualTo(2); + assertThat(underTest.countByQuery(dbSession, textSearchQuery("sonar"))).isEqualTo(2); } @Test diff --git a/server/sonar-db-dao/src/main/java/org/sonar/db/scim/ScimGroupDao.java b/server/sonar-db-dao/src/main/java/org/sonar/db/scim/ScimGroupDao.java index 36e336cff48..83c37d5a5d0 100644 --- a/server/sonar-db-dao/src/main/java/org/sonar/db/scim/ScimGroupDao.java +++ b/server/sonar-db-dao/src/main/java/org/sonar/db/scim/ScimGroupDao.java @@ -70,4 +70,8 @@ public class ScimGroupDao implements Dao { private static ScimGroupMapper mapper(DbSession session) { return session.getMapper(ScimGroupMapper.class); } + + public String getManagedGroupSqlFilter(boolean filterByManaged) { + return String.format("%s exists (select group_uuid from scim_groups sg where sg.group_uuid = uuid)", filterByManaged ? "" : "not"); + } } diff --git a/server/sonar-db-dao/src/main/java/org/sonar/db/user/GroupDao.java b/server/sonar-db-dao/src/main/java/org/sonar/db/user/GroupDao.java index b26e6bda0c9..5bd431d723c 100644 --- a/server/sonar-db-dao/src/main/java/org/sonar/db/user/GroupDao.java +++ b/server/sonar-db-dao/src/main/java/org/sonar/db/user/GroupDao.java @@ -22,17 +22,12 @@ package org.sonar.db.user; import java.util.Collection; import java.util.Date; import java.util.List; -import java.util.Locale; import java.util.Optional; import javax.annotation.CheckForNull; -import javax.annotation.Nullable; -import org.apache.commons.lang.StringUtils; import org.apache.ibatis.session.RowBounds; import org.sonar.api.utils.System2; import org.sonar.db.Dao; -import org.sonar.db.DaoUtils; import org.sonar.db.DbSession; -import org.sonar.db.WildcardPosition; import org.sonar.db.audit.AuditPersister; import org.sonar.db.audit.model.UserGroupNewValue; @@ -78,12 +73,12 @@ public class GroupDao implements Dao { } } - public int countByQuery(DbSession session, @Nullable String query) { - return mapper(session).countByQuery(groupSearchToSql(query)); + public int countByQuery(DbSession session, GroupQuery query) { + return mapper(session).countByQuery(query); } - public List selectByQuery(DbSession session, @Nullable String query, int offset, int limit) { - return mapper(session).selectByQuery(groupSearchToSql(query), new RowBounds(offset, limit)); + public List selectByQuery(DbSession session, GroupQuery query, int offset, int limit) { + return mapper(session).selectByQuery(query, new RowBounds(offset, limit)); } public GroupDto insert(DbSession session, GroupDto item) { @@ -106,16 +101,6 @@ public class GroupDao implements Dao { return mapper(session).selectByUserLogin(login); } - @CheckForNull - private static String groupSearchToSql(@Nullable String query) { - if (query == null) { - return null; - } - - String upperCasedNameQuery = StringUtils.upperCase(query, Locale.ENGLISH); - return DaoUtils.buildLikeValue(upperCasedNameQuery, WildcardPosition.BEFORE_AND_AFTER); - } - private static GroupMapper mapper(DbSession session) { return session.getMapper(GroupMapper.class); } diff --git a/server/sonar-db-dao/src/main/java/org/sonar/db/user/GroupMapper.java b/server/sonar-db-dao/src/main/java/org/sonar/db/user/GroupMapper.java index 4d4eafcc0f9..ef1879ba8ca 100644 --- a/server/sonar-db-dao/src/main/java/org/sonar/db/user/GroupMapper.java +++ b/server/sonar-db-dao/src/main/java/org/sonar/db/user/GroupMapper.java @@ -21,7 +21,6 @@ package org.sonar.db.user; import java.util.List; import javax.annotation.CheckForNull; -import javax.annotation.Nullable; import org.apache.ibatis.annotations.Param; import org.apache.ibatis.session.RowBounds; @@ -38,9 +37,9 @@ public interface GroupMapper { void update(GroupDto item); - List selectByQuery(@Nullable @Param("query") String query, RowBounds rowBounds); + List selectByQuery(@Param("query") GroupQuery query, RowBounds rowBounds); - int countByQuery(@Nullable @Param("query") String query); + int countByQuery(@Param("query") GroupQuery query); int deleteByUuid(String groupUuid); diff --git a/server/sonar-db-dao/src/main/java/org/sonar/db/user/GroupQuery.java b/server/sonar-db-dao/src/main/java/org/sonar/db/user/GroupQuery.java new file mode 100644 index 00000000000..a8cf01456dc --- /dev/null +++ b/server/sonar-db-dao/src/main/java/org/sonar/db/user/GroupQuery.java @@ -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.db.user; + +import java.util.Locale; +import javax.annotation.CheckForNull; +import javax.annotation.Nullable; +import org.apache.commons.lang.StringUtils; +import org.sonar.db.DaoUtils; +import org.sonar.db.WildcardPosition; + +public class GroupQuery { + private final String searchText; + private final String isManagedSqlClause; + + private GroupQuery(@Nullable String searchText, @Nullable String isManagedSqlClause) { + this.searchText = searchTextToSearchTextSql(searchText); + this.isManagedSqlClause = isManagedSqlClause; + } + + private static String searchTextToSearchTextSql(@Nullable String text) { + if (text == null) { + return null; + } + + String upperCasedNameQuery = StringUtils.upperCase(text, Locale.ENGLISH); + return DaoUtils.buildLikeValue(upperCasedNameQuery, WildcardPosition.BEFORE_AND_AFTER); + } + + @CheckForNull + public String getSearchText() { + return searchText; + } + + @CheckForNull + public String getIsManagedSqlClause() { + return isManagedSqlClause; + } + + public static GroupQueryBuilder builder() { + return new GroupQueryBuilder(); + } + + public static final class GroupQueryBuilder { + private String searchText = null; + private String isManagedSqlClause = null; + + private GroupQueryBuilder() { + } + + public GroupQuery.GroupQueryBuilder searchText(@Nullable String searchText) { + this.searchText = searchText; + return this; + } + + + public GroupQuery.GroupQueryBuilder isManagedClause(@Nullable String isManagedSqlClause) { + this.isManagedSqlClause = isManagedSqlClause; + return this; + } + + public GroupQuery build() { + return new GroupQuery(searchText, isManagedSqlClause); + } + } +} diff --git a/server/sonar-db-dao/src/main/java/org/sonar/db/user/UserQuery.java b/server/sonar-db-dao/src/main/java/org/sonar/db/user/UserQuery.java index e512deed795..43d326ec43c 100644 --- a/server/sonar-db-dao/src/main/java/org/sonar/db/user/UserQuery.java +++ b/server/sonar-db-dao/src/main/java/org/sonar/db/user/UserQuery.java @@ -24,7 +24,6 @@ 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; diff --git a/server/sonar-db-dao/src/main/resources/org/sonar/db/user/GroupMapper.xml b/server/sonar-db-dao/src/main/resources/org/sonar/db/user/GroupMapper.xml index 75b607bff32..77ed6c345c5 100644 --- a/server/sonar-db-dao/src/main/resources/org/sonar/db/user/GroupMapper.xml +++ b/server/sonar-db-dao/src/main/resources/org/sonar/db/user/GroupMapper.xml @@ -93,17 +93,25 @@ select from groups g - - where upper(g.name) like #{query,jdbcType=VARCHAR} escape '/' - + order by upper(g.name) + + + + 1=1 + + AND upper(g.name) like #{query.searchText,jdbcType=VARCHAR} escape '/' + + + AND ${query.isManagedSqlClause} + + + diff --git a/server/sonar-db-dao/src/test/java/org/sonar/db/scim/ScimGroupDaoTest.java b/server/sonar-db-dao/src/test/java/org/sonar/db/scim/ScimGroupDaoTest.java index 47ccb73f1bb..6b59f414501 100644 --- a/server/sonar-db-dao/src/test/java/org/sonar/db/scim/ScimGroupDaoTest.java +++ b/server/sonar-db-dao/src/test/java/org/sonar/db/scim/ScimGroupDaoTest.java @@ -100,6 +100,19 @@ public class ScimGroupDaoTest { assertThat(scimGroupsUuids).containsExactlyElementsOf(expectedScimGroupUuids); } + @Test + public void getManagedGroupsSqlFilter_whenFilterByManagedIsTrue_returnsCorrectQuery() { + String filterManagedUser = scimGroupDao.getManagedGroupSqlFilter(true); + assertThat(filterManagedUser).isEqualTo(" exists (select group_uuid from scim_groups sg where sg.group_uuid = uuid)"); + } + + @Test + public void getManagedGroupsSqlFilter_whenFilterByManagedIsFalse_returnsCorrectQuery() { + String filterNonManagedUser = scimGroupDao.getManagedGroupSqlFilter(false); + assertThat(filterNonManagedUser).isEqualTo("not exists (select group_uuid from scim_groups sg where sg.group_uuid = uuid)"); + + } + private void generateScimGroups(int totalScimGroups) { List allScimGroups = Stream.iterate(1, i -> i + 1) .map(i -> insertScimGroup(i.toString())) diff --git a/server/sonar-server-common/src/main/java/org/sonar/server/management/DelegatingManagedInstanceService.java b/server/sonar-server-common/src/main/java/org/sonar/server/management/DelegatingManagedInstanceService.java index 8c58caf15f5..4ff988baab3 100644 --- a/server/sonar-server-common/src/main/java/org/sonar/server/management/DelegatingManagedInstanceService.java +++ b/server/sonar-server-common/src/main/java/org/sonar/server/management/DelegatingManagedInstanceService.java @@ -36,6 +36,7 @@ import static org.sonar.api.utils.Preconditions.checkState; @Priority(ManagedInstanceService.DELEGATING_INSTANCE_PRIORITY) public class DelegatingManagedInstanceService implements ManagedInstanceService { + private static final IllegalStateException NOT_MANAGED_INSTANCE_EXCEPTION = new IllegalStateException("This instance is not managed."); private final Set delegates; public DelegatingManagedInstanceService(Set delegates) { @@ -64,7 +65,14 @@ public class DelegatingManagedInstanceService implements ManagedInstanceService public String getManagedUsersSqlFilter(boolean filterByManaged) { return findManagedInstanceService() .map(managedInstanceService -> managedInstanceService.getManagedUsersSqlFilter(filterByManaged)) - .orElseThrow(() -> new IllegalStateException("This instance is not managed.")); + .orElseThrow(() -> NOT_MANAGED_INSTANCE_EXCEPTION); + } + + @Override + public String getManagedGroupsSqlFilter(boolean filterByManaged) { + return findManagedInstanceService() + .map(managedInstanceService -> managedInstanceService.getManagedGroupsSqlFilter(filterByManaged)) + .orElseThrow(() -> NOT_MANAGED_INSTANCE_EXCEPTION); } private Optional findManagedInstanceService() { diff --git a/server/sonar-server-common/src/main/java/org/sonar/server/management/ManagedInstanceService.java b/server/sonar-server-common/src/main/java/org/sonar/server/management/ManagedInstanceService.java index 942d3b5f9e4..1a615555d92 100644 --- a/server/sonar-server-common/src/main/java/org/sonar/server/management/ManagedInstanceService.java +++ b/server/sonar-server-common/src/main/java/org/sonar/server/management/ManagedInstanceService.java @@ -34,4 +34,6 @@ public interface ManagedInstanceService { Map getGroupUuidToManaged(DbSession dbSession, Set groupUuids); String getManagedUsersSqlFilter(boolean filterByManaged); + + String getManagedGroupsSqlFilter(boolean filterByManaged); } diff --git a/server/sonar-server-common/src/test/java/org/sonar/server/management/DelegatingManagedInstanceServiceTest.java b/server/sonar-server-common/src/test/java/org/sonar/server/management/DelegatingManagedInstanceServiceTest.java index e201461ec52..aee38302901 100644 --- a/server/sonar-server-common/src/test/java/org/sonar/server/management/DelegatingManagedInstanceServiceTest.java +++ b/server/sonar-server-common/src/test/java/org/sonar/server/management/DelegatingManagedInstanceServiceTest.java @@ -143,6 +143,24 @@ public class DelegatingManagedInstanceServiceTest { true)); } + @Test + public void getManagedGroupsSqlFilter_whenNoDelegates_throws() { + Set managedInstanceServices = emptySet(); + DelegatingManagedInstanceService delegatingManagedInstanceService = new DelegatingManagedInstanceService(managedInstanceServices); + assertThatIllegalStateException() + .isThrownBy(() -> delegatingManagedInstanceService.getManagedGroupsSqlFilter(true)) + .withMessage("This instance is not managed."); + } + + @Test + public void getManagedGroupsSqlFilter_delegatesToRightService_andPropagateAnswer() { + AlwaysManagedInstanceService alwaysManagedInstanceService = new AlwaysManagedInstanceService(); + DelegatingManagedInstanceService managedInstanceService = new DelegatingManagedInstanceService(Set.of(new NeverManagedInstanceService(), alwaysManagedInstanceService)); + + assertThat(managedInstanceService.getManagedGroupsSqlFilter(true)).isNotNull().isEqualTo(alwaysManagedInstanceService.getManagedGroupsSqlFilter( + true)); + } + private ManagedInstanceService getManagedInstanceService(Set userUuids, Map uuidToManaged) { ManagedInstanceService anotherManagedInstanceService = mock(ManagedInstanceService.class); when(anotherManagedInstanceService.isInstanceExternallyManaged()).thenReturn(true); @@ -172,6 +190,11 @@ public class DelegatingManagedInstanceServiceTest { public String getManagedUsersSqlFilter(boolean filterByManaged) { return null; } + + @Override + public String getManagedGroupsSqlFilter(boolean filterByManaged) { + return null; + } } private static class AlwaysManagedInstanceService implements ManagedInstanceService { @@ -195,6 +218,11 @@ public class DelegatingManagedInstanceServiceTest { public String getManagedUsersSqlFilter(boolean filterByManaged) { return "any filter"; } + + @Override + public String getManagedGroupsSqlFilter(boolean filterByManaged) { + return "any filter"; + } } } diff --git a/server/sonar-webserver-webapi/src/it/java/org/sonar/server/usergroups/ws/SearchActionIT.java b/server/sonar-webserver-webapi/src/it/java/org/sonar/server/usergroups/ws/SearchActionIT.java index b3d06a4113a..93ebda9a07c 100644 --- a/server/sonar-webserver-webapi/src/it/java/org/sonar/server/usergroups/ws/SearchActionIT.java +++ b/server/sonar-webserver-webapi/src/it/java/org/sonar/server/usergroups/ws/SearchActionIT.java @@ -25,9 +25,13 @@ import org.junit.Test; import org.sonar.api.server.ws.Change; import org.sonar.api.server.ws.WebService; import org.sonar.api.utils.System2; +import org.sonar.core.util.UuidFactory; +import org.sonar.db.DbSession; import org.sonar.db.DbTester; +import org.sonar.db.scim.ScimGroupDao; import org.sonar.db.user.GroupDto; import org.sonar.db.user.UserDto; +import org.sonar.server.exceptions.BadRequestException; import org.sonar.server.exceptions.UnauthorizedException; import org.sonar.server.management.ManagedInstanceService; import org.sonar.server.tester.UserSessionRule; @@ -41,9 +45,11 @@ import static java.util.function.Function.identity; import static java.util.stream.Collectors.toMap; import static org.apache.commons.lang.StringUtils.capitalize; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.assertj.core.api.Assertions.assertThatThrownBy; 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.server.ws.WebService.Param.FIELDS; @@ -75,8 +81,9 @@ public class SearchActionIT { assertThat(action).isNotNull(); assertThat(action.key()).isEqualTo("search"); assertThat(action.responseExampleAsString()).isNotEmpty(); - assertThat(action.params()).hasSize(4); + assertThat(action.params()).hasSize(5); assertThat(action.changelog()).extracting(Change::getVersion, Change::getDescription).containsOnly( + tuple("10.0", "New parameter 'managed' to optionally search by managed status"), tuple("10.0", "Response includes 'managed' field."), tuple("8.4", "Field 'id' in the response is deprecated. Format changes from integer to string."), tuple("6.4", "Paging response fields moved to a Paging object"), @@ -141,6 +148,54 @@ public class SearchActionIT { tuple("sonar-users", "Users", 5)); } + @Test + public void search_whenFilteringByManagedAndInstanceManaged_returnsCorrectResults() { + insertGroupsAndMockExternallyManaged(); + + SearchWsResponse managedResponse = call(ws.newRequest().setParam("managed", "true")); + + assertThat(managedResponse.getGroupsList()).extracting(Group::getName, Group::getManaged).containsOnly( + tuple("group1", true), + tuple("group3", true)); + assertThat(managedResponse.getPaging().getTotal()).isEqualTo(2); + } + + @Test + public void search_whenFilteringByManagedNonAndInstanceManaged_returnsCorrectResults() { + insertGroupsAndMockExternallyManaged(); + + SearchWsResponse notManagedResponse = call(ws.newRequest().setParam("managed", "false")); + + assertThat(notManagedResponse.getGroupsList()).extracting(Group::getName, Group::getManaged).containsOnly( + tuple("group2", false), + tuple("group4", false), + tuple("sonar-users", false)); + assertThat(notManagedResponse.getPaging().getTotal()).isEqualTo(3); + } + + @Test + public void search_whenFilteringByManagedNonAndInstanceManagedAndTextParameter_returnsCorrectResults() { + insertGroupsAndMockExternallyManaged(); + + SearchWsResponse notManagedResponse = call(ws.newRequest().setParam("managed", "false").setParam("q", "sonar")); + + assertThat(notManagedResponse.getGroupsList()).extracting(Group::getName, Group::getManaged).containsOnly( + tuple("sonar-users", false)); + assertThat(notManagedResponse.getPaging().getTotal()).isEqualTo(1); + } + + @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_with_query() { insertDefaultGroup(0); @@ -251,7 +306,7 @@ public class SearchActionIT { assertThat(action.isInternal()).isFalse(); assertThat(action.responseExampleAsString()).isNotEmpty(); - assertThat(action.params()).extracting(WebService.Param::key).containsOnly("p", "q", "ps", "f"); + assertThat(action.params()).extracting(WebService.Param::key).containsOnly("p", "q", "ps", "f", "managed"); assertThat(action.param("f").possibleValues()).containsOnly("name", "description", "membersCount", "managed"); } @@ -272,15 +327,40 @@ public class SearchActionIT { return group; } + private void insertGroupsAndMockExternallyManaged() { + insertDefaultGroup(0); + GroupDto group1 = insertGroup("group1", 0); + insertGroup("group2", 0); + GroupDto group3 = insertGroup("group3", 0); + insertGroup("group4", 0); + loginAsAdmin(); + + mockGroupAsManaged(group1.getUuid(), group3.getUuid()); + mockInstanceExternallyManagedAndFilterForManagedGroups(); + } + + private void mockInstanceExternallyManagedAndFilterForManagedGroups() { + when(managedInstanceService.isInstanceExternallyManaged()).thenReturn(true); + when(managedInstanceService.getManagedGroupsSqlFilter(anyBoolean())) + .thenAnswer(invocation -> { + Boolean managed = invocation.getArgument(0, Boolean.class); + return new ScimGroupDao(mock(UuidFactory.class)).getManagedGroupSqlFilter(managed); + }); + } + private void mockGroupAsManaged(String... groupUuids) { - when(managedInstanceService.getGroupUuidToManaged(any(), any())).thenAnswer(invocation -> - { - @SuppressWarnings("unchecked") - Set allGroupUuids = (Set) invocation.getArgument(1, Set.class); - return allGroupUuids.stream() - .collect(toMap(identity(), userUuid -> Set.of(groupUuids).contains(userUuid))); - } - ); + when(managedInstanceService.getGroupUuidToManaged(any(), any())).thenAnswer(invocation -> { + @SuppressWarnings("unchecked") + Set allGroupUuids = (Set) invocation.getArgument(1, Set.class); + return allGroupUuids.stream() + .collect(toMap(identity(), userUuid -> Set.of(groupUuids).contains(userUuid))); + }); + DbSession session = db.getSession(); + for (String groupUuid : groupUuids) { + db.getDbClient().scimGroupDao().enableScimForGroup(session, groupUuid); + } + session.commit(); + } private void addMembers(GroupDto group, int numberOfMembers) { diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/usergroups/ws/SearchAction.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/usergroups/ws/SearchAction.java index 19db3bd741f..f34b123314a 100644 --- a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/usergroups/ws/SearchAction.java +++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/usergroups/ws/SearchAction.java @@ -23,25 +23,28 @@ import java.util.Arrays; import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.Set; +import org.jetbrains.annotations.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.server.ws.WebService.NewController; import org.sonar.api.server.ws.WebService.Param; import org.sonar.api.utils.Paging; -import org.sonar.core.util.stream.MoreCollectors; import org.sonar.db.DbClient; import org.sonar.db.DbSession; import org.sonar.db.user.GroupDto; +import org.sonar.db.user.GroupQuery; import org.sonar.server.es.SearchOptions; +import org.sonar.server.exceptions.BadRequestException; import org.sonar.server.management.ManagedInstanceService; import org.sonar.server.user.UserSession; import org.sonar.server.usergroups.DefaultGroupFinder; import static java.lang.Boolean.TRUE; import static java.util.Optional.ofNullable; -import static org.apache.commons.lang.StringUtils.defaultIfBlank; import static org.sonar.api.utils.Paging.forPageIndex; import static org.sonar.db.permission.GlobalPermission.ADMINISTER; import static org.sonar.server.es.SearchOptions.MAX_PAGE_SIZE; @@ -55,6 +58,7 @@ public class SearchAction implements UserGroupsWsAction { private static final String FIELD_DESCRIPTION = "description"; private static final String FIELD_MEMBERS_COUNT = "membersCount"; private static final String FIELD_IS_MANAGED = "managed"; + private static final String MANAGED_PARAM = "managed"; private static final List ALL_FIELDS = Arrays.asList(FIELD_NAME, FIELD_DESCRIPTION, FIELD_MEMBERS_COUNT, FIELD_IS_MANAGED); private final DbClient dbClient; @@ -71,7 +75,7 @@ public class SearchAction implements UserGroupsWsAction { @Override public void define(NewController context) { - context.createAction("search") + WebService.NewAction action = context.createAction("search") .setDescription("Search for user groups.
" + "Requires the following permission: 'Administer System'.") .setHandler(this) @@ -81,10 +85,17 @@ public class SearchAction implements UserGroupsWsAction { .addPagingParams(100, MAX_PAGE_SIZE) .addSearchQuery("sonar-users", "names") .setChangelog( + new Change("10.0", "New parameter 'managed' to optionally search by managed status"), new Change("10.0", "Response includes 'managed' field."), new Change("8.4", "Field 'id' in the response is deprecated. Format changes from integer to string."), new Change("6.4", "Paging response fields moved to a Paging object"), new Change("6.4", "'default' response field has been added")); + + action.createParam(MANAGED_PARAM) + .setSince("10.0") + .setDescription("Return managed or non-managed groups. Only available for managed instances, throws for non-managed instances.") + .setRequired(false) + .setBooleanPossibleValues(); } @Override @@ -94,7 +105,7 @@ public class SearchAction implements UserGroupsWsAction { SearchOptions options = new SearchOptions() .setPage(page, pageSize); - String query = defaultIfBlank(request.param(Param.TEXT_QUERY), ""); + GroupQuery query = buildGroupQuery(request); Set fields = neededFields(request); try (DbSession dbSession = dbClient.openSession(false)) { @@ -104,13 +115,41 @@ public class SearchAction implements UserGroupsWsAction { int limit = dbClient.groupDao().countByQuery(dbSession, query); Paging paging = forPageIndex(page).withPageSize(pageSize).andTotal(limit); List groups = dbClient.groupDao().selectByQuery(dbSession, query, options.getOffset(), pageSize); - List groupUuids = groups.stream().map(GroupDto::getUuid).collect(MoreCollectors.toList(groups.size())); + List groupUuids = extractGroupUuids(groups); Map groupUuidToIsManaged = managedInstanceService.getGroupUuidToManaged(dbSession, new HashSet<>(groupUuids)); Map userCountByGroup = dbClient.groupMembershipDao().countUsersByGroups(dbSession, groupUuids); writeProtobuf(buildResponse(groups, userCountByGroup, groupUuidToIsManaged, fields, paging, defaultGroup), request, response); } } + private GroupQuery buildGroupQuery(Request request) { + String textQuery = request.param(Param.TEXT_QUERY); + Optional managed = Optional.ofNullable(request.paramAsBoolean(MANAGED_PARAM)); + + GroupQuery.GroupQueryBuilder queryBuilder = GroupQuery.builder() + .searchText(textQuery); + + if (managedInstanceService.isInstanceExternallyManaged()) { + String managedInstanceSql = getManagedInstanceSql(managed); + queryBuilder.isManagedClause(managedInstanceSql); + } else if (managed.isPresent()) { + throw BadRequestException.create("The 'managed' parameter is only available for managed instances."); + } + return queryBuilder.build(); + + } + + @Nullable + private String getManagedInstanceSql(Optional managed) { + return managed + .map(managedInstanceService::getManagedGroupsSqlFilter) + .orElse(null); + } + + private static List extractGroupUuids(List groups) { + return groups.stream().map(GroupDto::getUuid).toList(); + } + private static Set neededFields(Request request) { Set fields = new HashSet<>(); List fieldsFromRequest = request.paramAsStrings(Param.FIELDS); -- 2.39.5