From a1771496e8535a4fe0197690ebbf048a31970472 Mon Sep 17 00:00:00 2001 From: Belen Pruvost Date: Fri, 15 Oct 2021 09:13:01 +0200 Subject: [PATCH] SONAR-15441 - QG Groups Search Endpoint --- .../QualityGateGroupPermissionsDao.java | 12 + .../QualityGateGroupPermissionsMapper.java | 7 + .../SearchQualityGateGroupsQuery.java | 60 ++++ .../qualityprofile/QProfileEditGroupsDao.java | 5 +- .../QProfileEditGroupsMapper.java | 5 +- .../SearchQualityProfileGroupsQuery.java | 60 ++++ .../SearchGroupMembershipDto.java} | 6 +- .../SearchGroupsQuery.java | 63 ++-- .../QualityGateGroupPermissionsMapper.xml | 54 ++++ .../QProfileEditGroupsMapper.xml | 6 +- .../QualityGateGroupPermissionsDaoTest.java | 132 ++++++++ .../QProfileEditGroupsDaoTest.java | 25 +- .../qualitygate/ws/QualityGateWsModule.java | 3 +- .../ws/QualityGatesWsParameters.java | 1 + .../qualitygate/ws/SearchGroupsAction.java | 146 +++++++++ .../ws/SearchQualityGateUsersRequest.java | 55 ++++ .../qualityprofile/ws/SearchGroupsAction.java | 29 +- ... => SearchQualityProfileUsersRequest.java} | 66 +--- .../qualityprofile/ws/SearchUsersAction.java | 8 +- .../server/user/ws/SearchUsersRequest.java | 95 ++++++ .../qualitygate/ws/search_groups-example.json | 19 ++ .../ws/QualityGateWsModuleTest.java | 2 +- .../ws/SearchGroupsActionTest.java | 306 ++++++++++++++++++ .../src/main/protobuf/ws-qualitygates.proto | 12 + 24 files changed, 1036 insertions(+), 141 deletions(-) create mode 100644 server/sonar-db-dao/src/main/java/org/sonar/db/qualitygate/SearchQualityGateGroupsQuery.java create mode 100644 server/sonar-db-dao/src/main/java/org/sonar/db/qualityprofile/SearchQualityProfileGroupsQuery.java rename server/sonar-db-dao/src/main/java/org/sonar/db/{qualityprofile/GroupMembershipDto.java => user/SearchGroupMembershipDto.java} (89%) rename server/sonar-db-dao/src/main/java/org/sonar/db/{qualityprofile => user}/SearchGroupsQuery.java (56%) create mode 100644 server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualitygate/ws/SearchGroupsAction.java create mode 100644 server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualitygate/ws/SearchQualityGateUsersRequest.java rename server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualityprofile/ws/{SearchUsersRequest.java => SearchQualityProfileUsersRequest.java} (55%) create mode 100644 server/sonar-webserver-webapi/src/main/java/org/sonar/server/user/ws/SearchUsersRequest.java create mode 100644 server/sonar-webserver-webapi/src/main/resources/org/sonar/server/qualitygate/ws/search_groups-example.json create mode 100644 server/sonar-webserver-webapi/src/test/java/org/sonar/server/qualitygate/ws/SearchGroupsActionTest.java diff --git a/server/sonar-db-dao/src/main/java/org/sonar/db/qualitygate/QualityGateGroupPermissionsDao.java b/server/sonar-db-dao/src/main/java/org/sonar/db/qualitygate/QualityGateGroupPermissionsDao.java index eecb9287f33..76b901601a7 100644 --- a/server/sonar-db-dao/src/main/java/org/sonar/db/qualitygate/QualityGateGroupPermissionsDao.java +++ b/server/sonar-db-dao/src/main/java/org/sonar/db/qualitygate/QualityGateGroupPermissionsDao.java @@ -20,10 +20,14 @@ package org.sonar.db.qualitygate; import java.util.Collection; +import java.util.List; import org.sonar.api.utils.System2; import org.sonar.db.Dao; import org.sonar.db.DbSession; +import org.sonar.db.Pagination; import org.sonar.db.user.GroupDto; +import org.sonar.db.user.SearchGroupMembershipDto; +import org.sonar.db.user.SearchGroupsQuery; import static org.sonar.core.util.stream.MoreCollectors.toList; import static org.sonar.db.DatabaseUtils.executeLargeInputs; @@ -56,4 +60,12 @@ public class QualityGateGroupPermissionsDao implements Dao { private static QualityGateGroupPermissionsMapper mapper(DbSession dbSession) { return dbSession.getMapper(QualityGateGroupPermissionsMapper.class); } + + public List selectByQuery(DbSession dbSession, SearchGroupsQuery query, Pagination pagination) { + return mapper(dbSession).selectByQuery(query, pagination); + } + + public int countByQuery(DbSession dbSession, SearchGroupsQuery query) { + return mapper(dbSession).countByQuery(query); + } } diff --git a/server/sonar-db-dao/src/main/java/org/sonar/db/qualitygate/QualityGateGroupPermissionsMapper.java b/server/sonar-db-dao/src/main/java/org/sonar/db/qualitygate/QualityGateGroupPermissionsMapper.java index 1c46b5b1385..b7c23e1fa99 100644 --- a/server/sonar-db-dao/src/main/java/org/sonar/db/qualitygate/QualityGateGroupPermissionsMapper.java +++ b/server/sonar-db-dao/src/main/java/org/sonar/db/qualitygate/QualityGateGroupPermissionsMapper.java @@ -21,6 +21,9 @@ package org.sonar.db.qualitygate; import java.util.List; import org.apache.ibatis.annotations.Param; +import org.sonar.db.Pagination; +import org.sonar.db.user.SearchGroupMembershipDto; +import org.sonar.db.user.SearchGroupsQuery; public interface QualityGateGroupPermissionsMapper { @@ -29,4 +32,8 @@ public interface QualityGateGroupPermissionsMapper { List selectByQualityGateAndGroups(@Param("qualityGateUuid") String qualityGateUuid, @Param("groupUuids") List groupUuids); void insert(@Param("dto") QualityGateGroupPermissionsDto dto, @Param("now") long now); + + List selectByQuery(@Param("query") SearchGroupsQuery query, @Param("pagination") Pagination pagination); + + int countByQuery(@Param("query") SearchGroupsQuery query); } diff --git a/server/sonar-db-dao/src/main/java/org/sonar/db/qualitygate/SearchQualityGateGroupsQuery.java b/server/sonar-db-dao/src/main/java/org/sonar/db/qualitygate/SearchQualityGateGroupsQuery.java new file mode 100644 index 00000000000..7bfeef9bc2a --- /dev/null +++ b/server/sonar-db-dao/src/main/java/org/sonar/db/qualitygate/SearchQualityGateGroupsQuery.java @@ -0,0 +1,60 @@ +/* + * SonarQube + * Copyright (C) 2009-2021 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.qualitygate; + +import java.util.Locale; +import org.sonar.db.user.SearchGroupsQuery; + +import static org.sonar.db.DaoUtils.buildLikeValue; +import static org.sonar.db.WildcardPosition.BEFORE_AND_AFTER; + +public class SearchQualityGateGroupsQuery extends SearchGroupsQuery { + + private final String qualityGateUuid; + + public SearchQualityGateGroupsQuery(Builder builder) { + this.qualityGateUuid = builder.qualityGate.getUuid(); + this.query = builder.getQuery(); + this.membership = builder.getMembership(); + this.querySqlLowercase = query == null ? null : buildLikeValue(query, BEFORE_AND_AFTER).toLowerCase(Locale.ENGLISH); + } + + public String getQualityGateUuid() { + return qualityGateUuid; + } + + public static Builder builder() { + return new Builder(); + } + + public static class Builder extends SearchGroupsQuery.Builder { + private QualityGateDto qualityGate; + + public Builder setQualityGate(QualityGateDto qualityGate) { + this.qualityGate = qualityGate; + return this; + } + + public SearchQualityGateGroupsQuery build() { + initMembership(); + return new SearchQualityGateGroupsQuery(this); + } + } +} diff --git a/server/sonar-db-dao/src/main/java/org/sonar/db/qualityprofile/QProfileEditGroupsDao.java b/server/sonar-db-dao/src/main/java/org/sonar/db/qualityprofile/QProfileEditGroupsDao.java index ce947215e18..8b324e9c898 100644 --- a/server/sonar-db-dao/src/main/java/org/sonar/db/qualityprofile/QProfileEditGroupsDao.java +++ b/server/sonar-db-dao/src/main/java/org/sonar/db/qualityprofile/QProfileEditGroupsDao.java @@ -28,6 +28,7 @@ import org.sonar.db.DatabaseUtils; import org.sonar.db.DbSession; import org.sonar.db.Pagination; import org.sonar.db.user.GroupDto; +import org.sonar.db.user.SearchGroupMembershipDto; import static org.sonar.core.util.stream.MoreCollectors.toList; import static org.sonar.db.DatabaseUtils.executeLargeInputs; @@ -50,11 +51,11 @@ public class QProfileEditGroupsDao implements Dao { .isEmpty(); } - public int countByQuery(DbSession dbSession, SearchGroupsQuery query) { + public int countByQuery(DbSession dbSession, SearchQualityProfileGroupsQuery query) { return mapper(dbSession).countByQuery(query); } - public List selectByQuery(DbSession dbSession, SearchGroupsQuery query, Pagination pagination) { + public List selectByQuery(DbSession dbSession, SearchQualityProfileGroupsQuery query, Pagination pagination) { return mapper(dbSession).selectByQuery(query, pagination); } diff --git a/server/sonar-db-dao/src/main/java/org/sonar/db/qualityprofile/QProfileEditGroupsMapper.java b/server/sonar-db-dao/src/main/java/org/sonar/db/qualityprofile/QProfileEditGroupsMapper.java index c1f536f9e7a..d8cb1e723e6 100644 --- a/server/sonar-db-dao/src/main/java/org/sonar/db/qualityprofile/QProfileEditGroupsMapper.java +++ b/server/sonar-db-dao/src/main/java/org/sonar/db/qualityprofile/QProfileEditGroupsMapper.java @@ -23,14 +23,15 @@ import java.util.Collection; import java.util.List; import org.apache.ibatis.annotations.Param; import org.sonar.db.Pagination; +import org.sonar.db.user.SearchGroupMembershipDto; public interface QProfileEditGroupsMapper { List selectByQProfileAndGroups(@Param("qProfileUuid") String qProfileUuid, @Param("groupUuids") List groupUuids); - int countByQuery(@Param("query") SearchGroupsQuery query); + int countByQuery(@Param("query") SearchQualityProfileGroupsQuery query); - List selectByQuery(@Param("query") SearchGroupsQuery query, @Param("pagination") Pagination pagination); + List selectByQuery(@Param("query") SearchQualityProfileGroupsQuery query, @Param("pagination") Pagination pagination); List selectQProfileUuidsByGroups(@Param("groupUuids") List groupUuids); diff --git a/server/sonar-db-dao/src/main/java/org/sonar/db/qualityprofile/SearchQualityProfileGroupsQuery.java b/server/sonar-db-dao/src/main/java/org/sonar/db/qualityprofile/SearchQualityProfileGroupsQuery.java new file mode 100644 index 00000000000..cdbe85eae89 --- /dev/null +++ b/server/sonar-db-dao/src/main/java/org/sonar/db/qualityprofile/SearchQualityProfileGroupsQuery.java @@ -0,0 +1,60 @@ +/* + * SonarQube + * Copyright (C) 2009-2021 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.qualityprofile; + +import java.util.Locale; +import org.sonar.db.user.SearchGroupsQuery; + +import static org.sonar.db.DaoUtils.buildLikeValue; +import static org.sonar.db.WildcardPosition.BEFORE_AND_AFTER; + +public class SearchQualityProfileGroupsQuery extends SearchGroupsQuery { + + private final String qProfileUuid; + + public SearchQualityProfileGroupsQuery(Builder builder) { + this.qProfileUuid = builder.profile.getKee(); + this.query = builder.getQuery(); + this.membership = builder.getMembership(); + this.querySqlLowercase = query == null ? null : buildLikeValue(query, BEFORE_AND_AFTER).toLowerCase(Locale.ENGLISH); + } + + public String getQProfileUuid() { + return qProfileUuid; + } + + public static Builder builder() { + return new Builder(); + } + + public static class Builder extends SearchGroupsQuery.Builder { + private QProfileDto profile; + + public Builder setProfile(QProfileDto profile) { + this.profile = profile; + return this; + } + + public SearchQualityProfileGroupsQuery build() { + initMembership(); + return new SearchQualityProfileGroupsQuery(this); + } + } +} diff --git a/server/sonar-db-dao/src/main/java/org/sonar/db/qualityprofile/GroupMembershipDto.java b/server/sonar-db-dao/src/main/java/org/sonar/db/user/SearchGroupMembershipDto.java similarity index 89% rename from server/sonar-db-dao/src/main/java/org/sonar/db/qualityprofile/GroupMembershipDto.java rename to server/sonar-db-dao/src/main/java/org/sonar/db/user/SearchGroupMembershipDto.java index db299a20e6f..43a7d4090aa 100644 --- a/server/sonar-db-dao/src/main/java/org/sonar/db/qualityprofile/GroupMembershipDto.java +++ b/server/sonar-db-dao/src/main/java/org/sonar/db/user/SearchGroupMembershipDto.java @@ -17,9 +17,9 @@ * 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.qualityprofile; +package org.sonar.db.user; -public class GroupMembershipDto { +public class SearchGroupMembershipDto { private String groupUuid; // Set by MyBatis @@ -29,7 +29,7 @@ public class GroupMembershipDto { return groupUuid; } - public GroupMembershipDto setGroupUuid(String groupUuid) { + public SearchGroupMembershipDto setGroupUuid(String groupUuid) { this.groupUuid = groupUuid; return this; } diff --git a/server/sonar-db-dao/src/main/java/org/sonar/db/qualityprofile/SearchGroupsQuery.java b/server/sonar-db-dao/src/main/java/org/sonar/db/user/SearchGroupsQuery.java similarity index 56% rename from server/sonar-db-dao/src/main/java/org/sonar/db/qualityprofile/SearchGroupsQuery.java rename to server/sonar-db-dao/src/main/java/org/sonar/db/user/SearchGroupsQuery.java index 1b178156a1b..fd45b7beacd 100644 --- a/server/sonar-db-dao/src/main/java/org/sonar/db/qualityprofile/SearchGroupsQuery.java +++ b/server/sonar-db-dao/src/main/java/org/sonar/db/user/SearchGroupsQuery.java @@ -17,10 +17,8 @@ * 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.qualityprofile; +package org.sonar.db.user; -import com.google.common.collect.ImmutableSet; -import java.util.Locale; import java.util.Set; import javax.annotation.CheckForNull; import javax.annotation.Nullable; @@ -28,33 +26,19 @@ import org.apache.commons.lang.StringUtils; import static com.google.common.base.MoreObjects.firstNonNull; import static com.google.common.base.Preconditions.checkArgument; -import static org.sonar.db.DaoUtils.buildLikeValue; -import static org.sonar.db.WildcardPosition.BEFORE_AND_AFTER; -public class SearchGroupsQuery { +public abstract class SearchGroupsQuery { public static final String ANY = "ANY"; public static final String IN = "IN"; public static final String OUT = "OUT"; - public static final Set AVAILABLE_MEMBERSHIPS = ImmutableSet.of(ANY, IN, OUT); + public static final Set AVAILABLE_MEMBERSHIPS = Set.of(ANY, IN, OUT); - private final String qProfileUuid; - private final String query; - private final String membership; + protected String query; + protected String membership; // for internal use in MyBatis - final String querySqlLowercase; - - private SearchGroupsQuery(Builder builder) { - this.qProfileUuid = builder.profile.getKee(); - this.query = builder.query; - this.membership = builder.membership; - this.querySqlLowercase = query == null ? null : buildLikeValue(query, BEFORE_AND_AFTER).toLowerCase(Locale.ENGLISH); - } - - public String getQProfileUuid() { - return qProfileUuid; - } + protected String querySqlLowercase; public String getMembership() { return membership; @@ -65,42 +49,37 @@ public class SearchGroupsQuery { return query; } - public static Builder builder() { - return new Builder(); - } - - public static class Builder { - private QProfileDto profile; + public abstract static class Builder> { private String query; private String membership; - private Builder() { + public String getQuery(){ + return query; } - public Builder setProfile(QProfileDto profile) { - this.profile = profile; - return this; + public T setQuery(@Nullable String s) { + this.query = StringUtils.defaultIfBlank(s, null); + return self(); } - public Builder setMembership(@Nullable String membership) { - this.membership = membership; - return this; + public String getMembership(){ + return membership; } - public Builder setQuery(@Nullable String s) { - this.query = StringUtils.defaultIfBlank(s, null); - return this; + public T setMembership(@Nullable String membership) { + this.membership = membership; + return self(); } - private void initMembership() { + public void initMembership() { membership = firstNonNull(membership, ANY); checkArgument(AVAILABLE_MEMBERSHIPS.contains(membership), "Membership is not valid (got " + membership + "). Availables values are " + AVAILABLE_MEMBERSHIPS); } - public SearchGroupsQuery build() { - initMembership(); - return new SearchGroupsQuery(this); + @SuppressWarnings("unchecked") + final T self() { + return (T) this; } } } diff --git a/server/sonar-db-dao/src/main/resources/org/sonar/db/qualitygate/QualityGateGroupPermissionsMapper.xml b/server/sonar-db-dao/src/main/resources/org/sonar/db/qualitygate/QualityGateGroupPermissionsMapper.xml index efec92a4183..740123d047f 100644 --- a/server/sonar-db-dao/src/main/resources/org/sonar/db/qualitygate/QualityGateGroupPermissionsMapper.xml +++ b/server/sonar-db-dao/src/main/resources/org/sonar/db/qualitygate/QualityGateGroupPermissionsMapper.xml @@ -29,6 +29,60 @@ and qggp.quality_gate_uuid = #{qualityGateUuid, jdbcType=VARCHAR} + + + + + + + + + + FROM groups g + LEFT JOIN qgate_group_permissions qggp ON qggp.group_uuid=g.uuid AND qggp.quality_gate_uuid=#{query.qualityGateUuid, jdbcType=VARCHAR} + + + + AND qggp.uuid IS NOT NULL + + + AND qggp.uuid IS NULL + + + + AND (LOWER(g.name) LIKE #{query.querySqlLowercase} ESCAPE '/') + + + + insert into qgate_group_permissions( uuid, diff --git a/server/sonar-db-dao/src/main/resources/org/sonar/db/qualityprofile/QProfileEditGroupsMapper.xml b/server/sonar-db-dao/src/main/resources/org/sonar/db/qualityprofile/QProfileEditGroupsMapper.xml index 7539227c211..4314c7ac88a 100644 --- a/server/sonar-db-dao/src/main/resources/org/sonar/db/qualityprofile/QProfileEditGroupsMapper.xml +++ b/server/sonar-db-dao/src/main/resources/org/sonar/db/qualityprofile/QProfileEditGroupsMapper.xml @@ -25,7 +25,7 @@ - SELECT g.uuid as groupUuid, g.name as name, qeg.uuid as uuid ORDER BY g.name ASC @@ -33,7 +33,7 @@ OFFSET #{pagination.offset,jdbcType=INTEGER} - select * from ( select row_number() over(order by g.name asc) as number, g.uuid as groupUuid, g.name as name, qeg.uuid as uuid @@ -44,7 +44,7 @@ order by query.name asc - select * from ( select rownum as rn, t.* from ( select g.uuid as groupUuid, g.name as name, qeg.uuid as uuid diff --git a/server/sonar-db-dao/src/test/java/org/sonar/db/qualitygate/QualityGateGroupPermissionsDaoTest.java b/server/sonar-db-dao/src/test/java/org/sonar/db/qualitygate/QualityGateGroupPermissionsDaoTest.java index b94e8ee3e15..0acdca11c79 100644 --- a/server/sonar-db-dao/src/test/java/org/sonar/db/qualitygate/QualityGateGroupPermissionsDaoTest.java +++ b/server/sonar-db-dao/src/test/java/org/sonar/db/qualitygate/QualityGateGroupPermissionsDaoTest.java @@ -27,11 +27,18 @@ import org.sonar.api.utils.System2; import org.sonar.core.util.Uuids; import org.sonar.db.DbSession; import org.sonar.db.DbTester; +import org.sonar.db.Pagination; import org.sonar.db.user.GroupDto; import org.sonar.db.user.GroupTesting; +import org.sonar.db.user.SearchGroupMembershipDto; import static org.apache.commons.lang.RandomStringUtils.randomAlphabetic; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.tuple; +import static org.sonar.db.qualitygate.SearchQualityGateGroupsQuery.builder; +import static org.sonar.db.user.SearchGroupsQuery.ANY; +import static org.sonar.db.user.SearchGroupsQuery.IN; +import static org.sonar.db.user.SearchGroupsQuery.OUT; public class QualityGateGroupPermissionsDaoTest { @@ -70,6 +77,131 @@ public class QualityGateGroupPermissionsDaoTest { assertThat(underTest.exists(dbSession, randomAlphabetic(5), randomAlphabetic(5))).isFalse(); } + @Test + public void countByQuery() { + QualityGateDto qualityGateDto = insertQualityGate(); + GroupDto group1 = dbTester.users().insertGroup(); + GroupDto group2 = dbTester.users().insertGroup(); + GroupDto group3 = dbTester.users().insertGroup(); + + insertQualityGateGroupPermission(qualityGateDto.getUuid(), group1.getUuid()); + insertQualityGateGroupPermission(qualityGateDto.getUuid(), group2.getUuid()); + + assertThat(underTest.exists(dbSession, qualityGateDto, List.of(group1, group2))).isTrue(); + + assertThat(underTest.countByQuery(dbSession, builder() + .setQualityGate(qualityGateDto) + .setMembership(ANY).build())) + .isEqualTo(3); + + assertThat(underTest.countByQuery(dbSession, builder() + .setQualityGate(qualityGateDto) + .setMembership(IN).build())) + .isEqualTo(2); + + assertThat(underTest.countByQuery(dbSession, builder() + .setQualityGate(qualityGateDto) + .setMembership(OUT).build())) + .isEqualTo(1); + } + + @Test + public void selectByQuery() { + QualityGateDto qualityGateDto = insertQualityGate(); + GroupDto group1 = dbTester.users().insertGroup(); + GroupDto group2 = dbTester.users().insertGroup(); + GroupDto group3 = dbTester.users().insertGroup(); + + insertQualityGateGroupPermission(qualityGateDto.getUuid(), group1.getUuid()); + insertQualityGateGroupPermission(qualityGateDto.getUuid(), group2.getUuid()); + + assertThat(underTest.selectByQuery(dbSession, builder() + .setQualityGate(qualityGateDto) + .setMembership(ANY).build(), Pagination.all())) + .extracting(SearchGroupMembershipDto::getGroupUuid, SearchGroupMembershipDto::isSelected) + .containsExactlyInAnyOrder( + tuple(group1.getUuid(), true), + tuple(group2.getUuid(), true), + tuple(group3.getUuid(), false)); + + assertThat(underTest.selectByQuery(dbSession, builder() + .setQualityGate(qualityGateDto) + .setMembership(IN).build(), + Pagination.all())) + .extracting(SearchGroupMembershipDto::getGroupUuid, SearchGroupMembershipDto::isSelected) + .containsExactlyInAnyOrder(tuple(group1.getUuid(), true), tuple(group2.getUuid(), true)); + + assertThat(underTest.selectByQuery(dbSession, builder() + .setQualityGate(qualityGateDto) + .setMembership(OUT).build(), + Pagination.all())) + .extracting(SearchGroupMembershipDto::getGroupUuid, SearchGroupMembershipDto::isSelected) + .containsExactlyInAnyOrder(tuple(group3.getUuid(), false)); + } + + @Test + public void selectByQuery_search_by_name() { + QualityGateDto qualityGateDto = insertQualityGate(); + GroupDto group1 = dbTester.users().insertGroup("sonar-users-project"); + GroupDto group2 = dbTester.users().insertGroup("sonar-users-qprofile"); + GroupDto group3 = dbTester.users().insertGroup("sonar-admin"); + + insertQualityGateGroupPermission(qualityGateDto.getUuid(), group1.getUuid()); + insertQualityGateGroupPermission(qualityGateDto.getUuid(), group2.getUuid()); + insertQualityGateGroupPermission(qualityGateDto.getUuid(), group3.getUuid()); + + assertThat(underTest.selectByQuery(dbSession, builder() + .setQualityGate(qualityGateDto) + .setMembership(IN) + .setQuery("project").build(), + Pagination.all())) + .extracting(SearchGroupMembershipDto::getGroupUuid) + .containsExactlyInAnyOrder(group1.getUuid()); + + assertThat(underTest.selectByQuery(dbSession, builder() + .setQualityGate(qualityGateDto) + .setMembership(IN) + .setQuery("UserS").build(), + Pagination.all())) + .extracting(SearchGroupMembershipDto::getGroupUuid) + .containsExactlyInAnyOrder(group1.getUuid(), group2.getUuid()); + } + + @Test + public void selectByQuery_with_paging() { + QualityGateDto qualityGateDto = insertQualityGate(); + GroupDto group1 = dbTester.users().insertGroup("group1"); + GroupDto group2 = dbTester.users().insertGroup("group2"); + GroupDto group3 = dbTester.users().insertGroup("group3"); + + insertQualityGateGroupPermission(qualityGateDto.getUuid(), group1.getUuid()); + insertQualityGateGroupPermission(qualityGateDto.getUuid(), group2.getUuid()); + + assertThat(underTest.selectByQuery(dbSession, builder() + .setQualityGate(qualityGateDto) + .setMembership(ANY) + .build(), + Pagination.forPage(1).andSize(1))) + .extracting(SearchGroupMembershipDto::getGroupUuid) + .containsExactly(group1.getUuid()); + + assertThat(underTest.selectByQuery(dbSession, builder() + .setQualityGate(qualityGateDto) + .setMembership(ANY) + .build(), + Pagination.forPage(3).andSize(1))) + .extracting(SearchGroupMembershipDto::getGroupUuid) + .containsExactly(group3.getUuid()); + + assertThat(underTest.selectByQuery(dbSession, builder() + .setQualityGate(qualityGateDto) + .setMembership(ANY) + .build(), + Pagination.forPage(1).andSize(10))) + .extracting(SearchGroupMembershipDto::getGroupUuid) + .containsExactly(group1.getUuid(), group2.getUuid(), group3.getUuid()); + } + private QualityGateDto insertQualityGate() { QualityGateDto qg = new QualityGateDto() .setUuid(randomAlphabetic(5)) diff --git a/server/sonar-db-dao/src/test/java/org/sonar/db/qualityprofile/QProfileEditGroupsDaoTest.java b/server/sonar-db-dao/src/test/java/org/sonar/db/qualityprofile/QProfileEditGroupsDaoTest.java index 2e5948a8fd1..02ef1d812c0 100644 --- a/server/sonar-db-dao/src/test/java/org/sonar/db/qualityprofile/QProfileEditGroupsDaoTest.java +++ b/server/sonar-db-dao/src/test/java/org/sonar/db/qualityprofile/QProfileEditGroupsDaoTest.java @@ -26,6 +26,7 @@ import org.sonar.api.utils.System2; import org.sonar.db.DbTester; import org.sonar.db.Pagination; import org.sonar.db.user.GroupDto; +import org.sonar.db.user.SearchGroupMembershipDto; import static java.util.Arrays.asList; import static java.util.Collections.emptyList; @@ -33,10 +34,10 @@ import static java.util.Collections.singletonList; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.entry; import static org.assertj.core.api.Assertions.tuple; -import static org.sonar.db.qualityprofile.SearchGroupsQuery.ANY; -import static org.sonar.db.qualityprofile.SearchGroupsQuery.IN; -import static org.sonar.db.qualityprofile.SearchGroupsQuery.OUT; -import static org.sonar.db.qualityprofile.SearchGroupsQuery.builder; +import static org.sonar.db.qualityprofile.SearchQualityProfileGroupsQuery.ANY; +import static org.sonar.db.qualityprofile.SearchQualityProfileGroupsQuery.IN; +import static org.sonar.db.qualityprofile.SearchQualityProfileGroupsQuery.OUT; +import static org.sonar.db.qualityprofile.SearchQualityProfileGroupsQuery.builder; public class QProfileEditGroupsDaoTest { @@ -103,7 +104,7 @@ public class QProfileEditGroupsDaoTest { assertThat(underTest.selectByQuery(db.getSession(), builder() .setProfile(profile) .setMembership(ANY).build(), Pagination.all())) - .extracting(GroupMembershipDto::getGroupUuid, GroupMembershipDto::isSelected) + .extracting(SearchGroupMembershipDto::getGroupUuid, SearchGroupMembershipDto::isSelected) .containsExactlyInAnyOrder( tuple(group1.getUuid(), true), tuple(group2.getUuid(), true), @@ -113,14 +114,14 @@ public class QProfileEditGroupsDaoTest { .setProfile(profile) .setMembership(IN).build(), Pagination.all())) - .extracting(GroupMembershipDto::getGroupUuid, GroupMembershipDto::isSelected) + .extracting(SearchGroupMembershipDto::getGroupUuid, SearchGroupMembershipDto::isSelected) .containsExactlyInAnyOrder(tuple(group1.getUuid(), true), tuple(group2.getUuid(), true)); assertThat(underTest.selectByQuery(db.getSession(), builder() .setProfile(profile) .setMembership(OUT).build(), Pagination.all())) - .extracting(GroupMembershipDto::getGroupUuid, GroupMembershipDto::isSelected) + .extracting(SearchGroupMembershipDto::getGroupUuid, SearchGroupMembershipDto::isSelected) .containsExactlyInAnyOrder(tuple(group3.getUuid(), false)); } @@ -139,7 +140,7 @@ public class QProfileEditGroupsDaoTest { .setMembership(IN) .setQuery("project").build(), Pagination.all())) - .extracting(GroupMembershipDto::getGroupUuid) + .extracting(SearchGroupMembershipDto::getGroupUuid) .containsExactlyInAnyOrder(group1.getUuid()); assertThat(underTest.selectByQuery(db.getSession(), builder() @@ -147,7 +148,7 @@ public class QProfileEditGroupsDaoTest { .setMembership(IN) .setQuery("UserS").build(), Pagination.all())) - .extracting(GroupMembershipDto::getGroupUuid) + .extracting(SearchGroupMembershipDto::getGroupUuid) .containsExactlyInAnyOrder(group1.getUuid(), group2.getUuid()); } @@ -165,7 +166,7 @@ public class QProfileEditGroupsDaoTest { .setMembership(ANY) .build(), Pagination.forPage(1).andSize(1))) - .extracting(GroupMembershipDto::getGroupUuid) + .extracting(SearchGroupMembershipDto::getGroupUuid) .containsExactly(group1.getUuid()); assertThat(underTest.selectByQuery(db.getSession(), builder() @@ -173,7 +174,7 @@ public class QProfileEditGroupsDaoTest { .setMembership(ANY) .build(), Pagination.forPage(3).andSize(1))) - .extracting(GroupMembershipDto::getGroupUuid) + .extracting(SearchGroupMembershipDto::getGroupUuid) .containsExactly(group3.getUuid()); assertThat(underTest.selectByQuery(db.getSession(), builder() @@ -181,7 +182,7 @@ public class QProfileEditGroupsDaoTest { .setMembership(ANY) .build(), Pagination.forPage(1).andSize(10))) - .extracting(GroupMembershipDto::getGroupUuid) + .extracting(SearchGroupMembershipDto::getGroupUuid) .containsExactly(group1.getUuid(), group2.getUuid(), group3.getUuid()); } diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualitygate/ws/QualityGateWsModule.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualitygate/ws/QualityGateWsModule.java index b018c949ffb..fcd075d46bd 100644 --- a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualitygate/ws/QualityGateWsModule.java +++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualitygate/ws/QualityGateWsModule.java @@ -43,7 +43,8 @@ public class QualityGateWsModule extends Module { UpdateConditionAction.class, ProjectStatusAction.class, GetByProjectAction.class, - AddGroupAction.class + AddGroupAction.class, + SearchGroupsAction.class ); } } diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualitygate/ws/QualityGatesWsParameters.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualitygate/ws/QualityGatesWsParameters.java index cc2e826581e..d93769833ae 100644 --- a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualitygate/ws/QualityGatesWsParameters.java +++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualitygate/ws/QualityGatesWsParameters.java @@ -31,6 +31,7 @@ public class QualityGatesWsParameters { public static final String ACTION_UPDATE_CONDITION = "update_condition"; public static final String ACTION_ADD_GROUP = "add_group"; public static final String ACTION_ADD_USER = "add_user"; + public static final String ACTION_SEARCH_GROUPS = "search_groups"; public static final String PARAM_ANALYSIS_ID = "analysisId"; public static final String PARAM_BRANCH = "branch"; diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualitygate/ws/SearchGroupsAction.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualitygate/ws/SearchGroupsAction.java new file mode 100644 index 00000000000..2cda1a5d244 --- /dev/null +++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualitygate/ws/SearchGroupsAction.java @@ -0,0 +1,146 @@ +/* + * SonarQube + * Copyright (C) 2009-2021 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.qualitygate.ws; + +import java.util.List; +import java.util.Map; +import org.sonar.api.server.ws.Request; +import org.sonar.api.server.ws.Response; +import org.sonar.api.server.ws.WebService; +import org.sonar.core.util.stream.MoreCollectors; +import org.sonar.db.DbClient; +import org.sonar.db.DbSession; +import org.sonar.db.qualitygate.QualityGateDto; +import org.sonar.db.user.SearchGroupMembershipDto; +import org.sonar.db.user.SearchGroupsQuery; +import org.sonar.db.user.GroupDto; +import org.sonarqube.ws.Common; +import org.sonarqube.ws.Qualitygates; + +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.SELECTED; +import static org.sonar.api.server.ws.WebService.Param.TEXT_QUERY; +import static org.sonar.api.server.ws.WebService.SelectionMode.ALL; +import static org.sonar.api.server.ws.WebService.SelectionMode.DESELECTED; +import static org.sonar.api.server.ws.WebService.SelectionMode.fromParam; +import static org.sonar.core.util.stream.MoreCollectors.toList; +import static org.sonar.db.Pagination.forPage; +import static org.sonar.db.qualitygate.SearchQualityGateGroupsQuery.builder; +import static org.sonar.db.user.SearchGroupsQuery.ANY; +import static org.sonar.db.user.SearchGroupsQuery.IN; +import static org.sonar.db.user.SearchGroupsQuery.OUT; +import static org.sonar.server.qualitygate.ws.QualityGatesWsParameters.ACTION_SEARCH_GROUPS; +import static org.sonar.server.qualitygate.ws.QualityGatesWsParameters.PARAM_GATE_NAME; +import static org.sonar.server.ws.WsUtils.writeProtobuf; + +public class SearchGroupsAction implements QualityGatesWsAction { + + private static final Map MEMBERSHIP = Map.of(WebService.SelectionMode.SELECTED, IN, DESELECTED, OUT, ALL, ANY); + + private final DbClient dbClient; + private final QualityGatesWsSupport wsSupport; + + public SearchGroupsAction(DbClient dbClient, QualityGatesWsSupport wsSupport) { + this.dbClient = dbClient; + this.wsSupport = wsSupport; + } + + @Override + public void define(WebService.NewController context) { + WebService.NewAction action = context + .createAction(ACTION_SEARCH_GROUPS) + .setDescription("List the groups that are allowed to edit a Quality Gate.
" + + "Requires one of the following permissions:" + + "
    " + + "
  • 'Administer Quality Gates'
  • " + + "
  • Edit right on the specified quality gate
  • " + + "
") + .setHandler(this) + .setInternal(true) + .addSelectionModeParam() + .addSearchQuery("sonar", "group names") + .addPagingParams(25) + .setResponseExample(getClass().getResource("search_groups-example.json")) + .setSince("9.2"); + + action.createParam(PARAM_GATE_NAME) + .setDescription("Quality Gate name") + .setRequired(true) + .setExampleValue("SonarSource Way"); + } + + @Override + public void handle(Request request, Response response) throws Exception { + SearchQualityGateUsersRequest wsRequest = buildRequest(request); + try (DbSession dbSession = dbClient.openSession(false)) { + QualityGateDto gate = wsSupport.getByName(dbSession, wsRequest.getQualityGate()); + wsSupport.checkCanLimitedEdit(dbSession, gate); + + SearchGroupsQuery query = builder() + .setQualityGate(gate) + .setQuery(wsRequest.getQuery()) + .setMembership(MEMBERSHIP.get(fromParam(wsRequest.getSelected()))) + .build(); + int total = dbClient.qualityGateGroupPermissionsDao().countByQuery(dbSession, query); + List groupMemberships = dbClient.qualityGateGroupPermissionsDao().selectByQuery(dbSession, query, + forPage(wsRequest.getPage()).andSize(wsRequest.getPageSize())); + Map groupsByUuid = dbClient.groupDao().selectByUuids(dbSession, + groupMemberships.stream().map(SearchGroupMembershipDto::getGroupUuid).collect(MoreCollectors.toList())) + .stream() + .collect(MoreCollectors.uniqueIndex(GroupDto::getUuid)); + writeProtobuf( + Qualitygates.SearchGroupsResponse.newBuilder() + .addAllGroups(groupMemberships.stream() + .map(groupsMembership -> toGroup(groupsByUuid.get(groupsMembership.getGroupUuid()), groupsMembership.isSelected())) + .collect(toList())) + .setPaging(buildPaging(wsRequest, total)).build(), + request, response); + } + } + + private static SearchQualityGateUsersRequest buildRequest(Request request) { + return SearchQualityGateUsersRequest.builder() + .setQualityGate(request.mandatoryParam(PARAM_GATE_NAME)) + .setQuery(request.param(TEXT_QUERY)) + .setSelected(request.mandatoryParam(SELECTED)) + .setPage(request.mandatoryParamAsInt(PAGE)) + .setPageSize(request.mandatoryParamAsInt(PAGE_SIZE)) + .build(); + } + + private static Qualitygates.SearchGroupsResponse.Group toGroup(GroupDto group, boolean isSelected) { + Qualitygates.SearchGroupsResponse.Group.Builder builder = Qualitygates.SearchGroupsResponse.Group.newBuilder() + .setName(group.getName()) + .setSelected(isSelected); + ofNullable(group.getDescription()).ifPresent(builder::setDescription); + return builder.build(); + } + + private static Common.Paging buildPaging(SearchQualityGateUsersRequest wsRequest, int total) { + return Common.Paging.newBuilder() + .setPageIndex(wsRequest.getPage()) + .setPageSize(wsRequest.getPageSize()) + .setTotal(total) + .build(); + } + +} diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualitygate/ws/SearchQualityGateUsersRequest.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualitygate/ws/SearchQualityGateUsersRequest.java new file mode 100644 index 00000000000..58f1637f751 --- /dev/null +++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualitygate/ws/SearchQualityGateUsersRequest.java @@ -0,0 +1,55 @@ +/* + * SonarQube + * Copyright (C) 2009-2021 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.qualitygate.ws; + +import org.sonar.server.user.ws.SearchUsersRequest; + +class SearchQualityGateUsersRequest extends SearchUsersRequest { + private String qualityGate; + + private SearchQualityGateUsersRequest(Builder builder) { + this.qualityGate = builder.qualityGate; + this.selected = builder.getSelected(); + this.query = builder.getQuery(); + this.page = builder.getPage(); + this.pageSize = builder.getPageSize(); + } + + public String getQualityGate() { + return qualityGate; + } + + public static Builder builder() { + return new Builder(); + } + + public static class Builder extends SearchUsersRequest.Builder { + private String qualityGate; + + public Builder setQualityGate(String qualityGate) { + this.qualityGate = qualityGate; + return this; + } + + public SearchQualityGateUsersRequest build() { + return new SearchQualityGateUsersRequest(this); + } + } +} diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualityprofile/ws/SearchGroupsAction.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualityprofile/ws/SearchGroupsAction.java index 79330bbbe91..e71ef37e67e 100644 --- a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualityprofile/ws/SearchGroupsAction.java +++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualityprofile/ws/SearchGroupsAction.java @@ -19,7 +19,6 @@ */ package org.sonar.server.qualityprofile.ws; -import com.google.common.collect.ImmutableMap; import java.util.Arrays; import java.util.List; import java.util.Map; @@ -31,10 +30,10 @@ import org.sonar.api.server.ws.WebService; import org.sonar.core.util.stream.MoreCollectors; import org.sonar.db.DbClient; import org.sonar.db.DbSession; -import org.sonar.db.qualityprofile.GroupMembershipDto; import org.sonar.db.qualityprofile.QProfileDto; -import org.sonar.db.qualityprofile.SearchGroupsQuery; +import org.sonar.db.qualityprofile.SearchQualityProfileGroupsQuery; import org.sonar.db.user.GroupDto; +import org.sonar.db.user.SearchGroupMembershipDto; import org.sonarqube.ws.Common; import org.sonarqube.ws.Qualityprofiles; @@ -49,10 +48,10 @@ import static org.sonar.api.server.ws.WebService.SelectionMode.fromParam; import static org.sonar.core.util.stream.MoreCollectors.toList; import static org.sonar.core.util.stream.MoreCollectors.toSet; import static org.sonar.db.Pagination.forPage; -import static org.sonar.db.qualityprofile.SearchGroupsQuery.ANY; -import static org.sonar.db.qualityprofile.SearchGroupsQuery.IN; -import static org.sonar.db.qualityprofile.SearchGroupsQuery.OUT; -import static org.sonar.db.qualityprofile.SearchGroupsQuery.builder; +import static org.sonar.db.qualityprofile.SearchQualityProfileGroupsQuery.ANY; +import static org.sonar.db.qualityprofile.SearchQualityProfileGroupsQuery.IN; +import static org.sonar.db.qualityprofile.SearchQualityProfileGroupsQuery.OUT; +import static org.sonar.db.qualityprofile.SearchQualityProfileGroupsQuery.builder; import static org.sonar.server.ws.WsUtils.writeProtobuf; import static org.sonarqube.ws.client.qualityprofile.QualityProfileWsParameters.ACTION_SEARCH_GROUPS; import static org.sonarqube.ws.client.qualityprofile.QualityProfileWsParameters.PARAM_LANGUAGE; @@ -60,7 +59,7 @@ import static org.sonarqube.ws.client.qualityprofile.QualityProfileWsParameters. public class SearchGroupsAction implements QProfileWsAction { - private static final Map MEMBERSHIP = ImmutableMap.of(WebService.SelectionMode.SELECTED, IN, DESELECTED, OUT, ALL, ANY); + private static final Map MEMBERSHIP = Map.of(WebService.SelectionMode.SELECTED, IN, DESELECTED, OUT, ALL, ANY); private final DbClient dbClient; private final QProfileWsSupport wsSupport; @@ -104,21 +103,21 @@ public class SearchGroupsAction implements QProfileWsAction { @Override public void handle(Request request, Response response) throws Exception { - SearchUsersRequest wsRequest = buildRequest(request); + SearchQualityProfileUsersRequest wsRequest = buildRequest(request); try (DbSession dbSession = dbClient.openSession(false)) { QProfileDto profile = wsSupport.getProfile(dbSession, wsRequest.getQualityProfile(), wsRequest.getLanguage()); wsSupport.checkCanEdit(dbSession, profile); - SearchGroupsQuery query = builder() + SearchQualityProfileGroupsQuery query = builder() .setProfile(profile) .setQuery(wsRequest.getQuery()) .setMembership(MEMBERSHIP.get(fromParam(wsRequest.getSelected()))) .build(); int total = dbClient.qProfileEditGroupsDao().countByQuery(dbSession, query); - List groupMemberships = dbClient.qProfileEditGroupsDao().selectByQuery(dbSession, query, + List groupMemberships = dbClient.qProfileEditGroupsDao().selectByQuery(dbSession, query, forPage(wsRequest.getPage()).andSize(wsRequest.getPageSize())); Map groupsByUuid = dbClient.groupDao().selectByUuids(dbSession, - groupMemberships.stream().map(GroupMembershipDto::getGroupUuid).collect(MoreCollectors.toList())) + groupMemberships.stream().map(SearchGroupMembershipDto::getGroupUuid).collect(MoreCollectors.toList())) .stream() .collect(MoreCollectors.uniqueIndex(GroupDto::getUuid)); writeProtobuf( @@ -131,8 +130,8 @@ public class SearchGroupsAction implements QProfileWsAction { } } - private static SearchUsersRequest buildRequest(Request request) { - return SearchUsersRequest.builder() + private static SearchQualityProfileUsersRequest buildRequest(Request request) { + return SearchQualityProfileUsersRequest.builder() .setQualityProfile(request.mandatoryParam(PARAM_QUALITY_PROFILE)) .setLanguage(request.mandatoryParam(PARAM_LANGUAGE)) .setQuery(request.param(TEXT_QUERY)) @@ -150,7 +149,7 @@ public class SearchGroupsAction implements QProfileWsAction { return builder.build(); } - private static Common.Paging buildPaging(SearchUsersRequest wsRequest, int total) { + private static Common.Paging buildPaging(SearchQualityProfileUsersRequest wsRequest, int total) { return Common.Paging.newBuilder() .setPageIndex(wsRequest.getPage()) .setPageSize(wsRequest.getPageSize()) diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualityprofile/ws/SearchUsersRequest.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualityprofile/ws/SearchQualityProfileUsersRequest.java similarity index 55% rename from server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualityprofile/ws/SearchUsersRequest.java rename to server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualityprofile/ws/SearchQualityProfileUsersRequest.java index d4ac5a27a0e..bcb2ed72836 100644 --- a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualityprofile/ws/SearchUsersRequest.java +++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualityprofile/ws/SearchQualityProfileUsersRequest.java @@ -19,24 +19,19 @@ */ package org.sonar.server.qualityprofile.ws; -import javax.annotation.CheckForNull; -import javax.annotation.Nullable; +import org.sonar.server.user.ws.SearchUsersRequest; -class SearchUsersRequest { +class SearchQualityProfileUsersRequest extends SearchUsersRequest { private String qualityProfile; private String language; - private String selected; - private String query; - private Integer page; - private Integer pageSize; - private SearchUsersRequest(Builder builder) { + private SearchQualityProfileUsersRequest(Builder builder) { this.qualityProfile = builder.qualityProfile; this.language = builder.language; - this.selected = builder.selected; - this.query = builder.query; - this.page = builder.page; - this.pageSize = builder.pageSize; + this.selected = builder.getSelected(); + this.query = builder.getQuery(); + this.page = builder.getPage(); + this.pageSize = builder.getPageSize(); } public String getQualityProfile() { @@ -47,34 +42,13 @@ class SearchUsersRequest { return language; } - @CheckForNull - public String getQuery() { - return query; - } - - public String getSelected() { - return selected; - } - - public Integer getPage() { - return page; - } - - public Integer getPageSize() { - return pageSize; - } - public static Builder builder() { return new Builder(); } - public static class Builder { + public static class Builder extends SearchUsersRequest.Builder { private String qualityProfile; private String language; - private String selected; - private String query; - private Integer page; - private Integer pageSize; public Builder setQualityProfile(String qualityProfile) { this.qualityProfile = qualityProfile; @@ -86,28 +60,8 @@ class SearchUsersRequest { return this; } - public Builder setSelected(String selected) { - this.selected = selected; - return this; - } - - public Builder setQuery(@Nullable String query) { - this.query = query; - return this; - } - - public Builder setPage(Integer page) { - this.page = page; - return this; - } - - public Builder setPageSize(Integer pageSize) { - this.pageSize = pageSize; - return this; - } - - public SearchUsersRequest build() { - return new SearchUsersRequest(this); + public SearchQualityProfileUsersRequest build() { + return new SearchQualityProfileUsersRequest(this); } } } diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualityprofile/ws/SearchUsersAction.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualityprofile/ws/SearchUsersAction.java index d4456bcdb3d..2b0b5263eb4 100644 --- a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualityprofile/ws/SearchUsersAction.java +++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualityprofile/ws/SearchUsersAction.java @@ -109,7 +109,7 @@ public class SearchUsersAction implements QProfileWsAction { @Override public void handle(Request request, Response response) throws Exception { - SearchUsersRequest wsRequest = buildRequest(request); + SearchQualityProfileUsersRequest wsRequest = buildRequest(request); try (DbSession dbSession = dbClient.openSession(false)) { QProfileDto profile = wsSupport.getProfile(dbSession, wsRequest.getQualityProfile(), wsRequest.getLanguage()); wsSupport.checkCanEdit(dbSession, profile); @@ -134,8 +134,8 @@ public class SearchUsersAction implements QProfileWsAction { } } - private static SearchUsersRequest buildRequest(Request request) { - return SearchUsersRequest.builder() + private static SearchQualityProfileUsersRequest buildRequest(Request request) { + return SearchQualityProfileUsersRequest.builder() .setQualityProfile(request.mandatoryParam(PARAM_QUALITY_PROFILE)) .setLanguage(request.mandatoryParam(PARAM_LANGUAGE)) .setQuery(request.param(TEXT_QUERY)) @@ -155,7 +155,7 @@ public class SearchUsersAction implements QProfileWsAction { .build(); } - private static Common.Paging buildPaging(SearchUsersRequest wsRequest, int total) { + private static Common.Paging buildPaging(SearchQualityProfileUsersRequest wsRequest, int total) { return Common.Paging.newBuilder() .setPageIndex(wsRequest.getPage()) .setPageSize(wsRequest.getPageSize()) diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/user/ws/SearchUsersRequest.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/user/ws/SearchUsersRequest.java new file mode 100644 index 00000000000..3a97bfcd56d --- /dev/null +++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/user/ws/SearchUsersRequest.java @@ -0,0 +1,95 @@ +/* + * SonarQube + * Copyright (C) 2009-2021 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 javax.annotation.CheckForNull; +import javax.annotation.Nullable; + +public abstract class SearchUsersRequest { + protected String selected; + protected String query; + protected Integer page; + protected Integer pageSize; + + @CheckForNull + public String getQuery() { + return query; + } + + public String getSelected() { + return selected; + } + + public Integer getPage() { + return page; + } + + public Integer getPageSize() { + return pageSize; + } + + public abstract static class Builder> { + private String selected; + private String query; + private Integer page; + private Integer pageSize; + + public String getSelected() { + return selected; + } + + public T setSelected(String selected) { + this.selected = selected; + return self(); + } + + public String getQuery() { + return query; + } + + public T setQuery(@Nullable String query) { + this.query = query; + return self(); + } + + public Integer getPage() { + return page; + } + + public T setPage(Integer page) { + this.page = page; + return self(); + } + + public Integer getPageSize() { + return pageSize; + } + + public T setPageSize(Integer pageSize) { + this.pageSize = pageSize; + return self(); + } + + @SuppressWarnings("unchecked") + final T self() { + return (T) this; + } + } +} diff --git a/server/sonar-webserver-webapi/src/main/resources/org/sonar/server/qualitygate/ws/search_groups-example.json b/server/sonar-webserver-webapi/src/main/resources/org/sonar/server/qualitygate/ws/search_groups-example.json new file mode 100644 index 00000000000..a8fdbcf1066 --- /dev/null +++ b/server/sonar-webserver-webapi/src/main/resources/org/sonar/server/qualitygate/ws/search_groups-example.json @@ -0,0 +1,19 @@ +{ + "paging": { + "pageSize": 25, + "total": 2, + "pageIndex": 1 + }, + "groups": [ + { + "name": "users", + "description": "Users", + "selected": true + }, + { + "name": "administrators", + "description": "Administrators", + "selected": false + } + ] +} diff --git a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/qualitygate/ws/QualityGateWsModuleTest.java b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/qualitygate/ws/QualityGateWsModuleTest.java index d7b30969647..9c6ab4fd0d2 100644 --- a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/qualitygate/ws/QualityGateWsModuleTest.java +++ b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/qualitygate/ws/QualityGateWsModuleTest.java @@ -30,7 +30,7 @@ public class QualityGateWsModuleTest { public void verify_count_of_added_components() { ComponentContainer container = new ComponentContainer(); new QualityGateWsModule().configure(container); - assertThat(container.size()).isEqualTo(COMPONENTS_IN_EMPTY_COMPONENT_CONTAINER + 19); + assertThat(container.size()).isEqualTo(COMPONENTS_IN_EMPTY_COMPONENT_CONTAINER + 20); } } diff --git a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/qualitygate/ws/SearchGroupsActionTest.java b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/qualitygate/ws/SearchGroupsActionTest.java new file mode 100644 index 00000000000..b8fcb184430 --- /dev/null +++ b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/qualitygate/ws/SearchGroupsActionTest.java @@ -0,0 +1,306 @@ +/* + * SonarQube + * Copyright (C) 2009-2021 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.qualitygate.ws; + +import org.assertj.core.api.Assertions; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.sonar.api.server.ws.WebService; +import org.sonar.db.DbTester; +import org.sonar.db.permission.GlobalPermission; +import org.sonar.db.qualitygate.QualityGateDto; +import org.sonar.db.user.GroupDto; +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.tester.UserSessionRule; +import org.sonar.server.ws.WsActionTester; +import org.sonarqube.ws.Qualitygates; + +import static org.assertj.core.api.Assertions.assertThat; +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.db.permission.GlobalPermission.ADMINISTER_QUALITY_GATES; +import static org.sonar.db.user.GroupTesting.newGroupDto; +import static org.sonar.server.qualitygate.ws.QualityGatesWsParameters.PARAM_GATE_NAME; +import static org.sonar.test.JsonAssert.assertJson; +import static org.sonarqube.ws.MediaTypes.JSON; + +public class SearchGroupsActionTest { + private static final String XOO = "xoo"; + private static final String FOO = "foo"; + + @Rule + public ExpectedException expectedException = ExpectedException.none(); + @Rule + public UserSessionRule userSession = UserSessionRule.standalone(); + @Rule + public DbTester db = DbTester.create(); + + private final QualityGatesWsSupport wsSupport = new QualityGatesWsSupport(db.getDbClient(), userSession, TestComponentFinder.from(db)); + + private final WsActionTester ws = new WsActionTester(new SearchGroupsAction(db.getDbClient(), wsSupport)); + + @Test + public void test_definition() { + WebService.Action def = ws.getDef(); + assertThat(def.key()).isEqualTo("search_groups"); + assertThat(def.isPost()).isFalse(); + assertThat(def.isInternal()).isTrue(); + assertThat(def.params()).extracting(WebService.Param::key) + .containsExactlyInAnyOrder("gateName", "selected", "q", "p", "ps"); + } + + @Test + public void test_example() { + QualityGateDto gate = db.qualityGates().insertQualityGate(); + GroupDto group1 = db.users().insertGroup(newGroupDto().setName("users").setDescription("Users")); + GroupDto group2 = db.users().insertGroup(newGroupDto().setName("administrators").setDescription("Administrators")); + db.qualityGates().addGroupPermission(gate, group1); + userSession.logIn().addPermission(ADMINISTER_QUALITY_GATES); + + String result = ws.newRequest() + .setParam(PARAM_GATE_NAME, gate.getName()) + .setParam(WebService.Param.SELECTED, "all") + .setMediaType(JSON) + .execute() + .getInput(); + + assertJson(ws.getDef().responseExampleAsString()).isSimilarTo(result); + } + + @Test + public void search_all_groups() { + QualityGateDto gate = db.qualityGates().insertQualityGate(); + GroupDto group1 = db.users().insertGroup(); + GroupDto group2 = db.users().insertGroup(); + db.qualityGates().addGroupPermission(gate, group1); + userSession.logIn().addPermission(ADMINISTER_QUALITY_GATES); + + Qualitygates.SearchGroupsResponse response = ws.newRequest() + .setParam(PARAM_GATE_NAME, gate.getName()) + .setParam(WebService.Param.SELECTED, "all") + .executeProtobuf(Qualitygates.SearchGroupsResponse.class); + + assertThat(response.getGroupsList()) + .extracting(Qualitygates.SearchGroupsResponse.Group::getName, + Qualitygates.SearchGroupsResponse.Group::getDescription, Qualitygates.SearchGroupsResponse.Group::getSelected) + .containsExactlyInAnyOrder( + Assertions.tuple(group1.getName(), group1.getDescription(), true), + Assertions.tuple(group2.getName(), group2.getDescription(), false)); + } + + @Test + public void search_selected_groups() { + QualityGateDto gate = db.qualityGates().insertQualityGate(); + GroupDto group1 = db.users().insertGroup(); + GroupDto group2 = db.users().insertGroup(); + db.qualityGates().addGroupPermission(gate, group1); + userSession.logIn().addPermission(ADMINISTER_QUALITY_GATES); + + Qualitygates.SearchGroupsResponse response = ws.newRequest() + .setParam(PARAM_GATE_NAME, gate.getName()) + .setParam(WebService.Param.SELECTED, "selected") + .executeProtobuf(Qualitygates.SearchGroupsResponse.class); + + assertThat(response.getGroupsList()) + .extracting(Qualitygates.SearchGroupsResponse.Group::getName, + Qualitygates.SearchGroupsResponse.Group::getDescription, Qualitygates.SearchGroupsResponse.Group::getSelected) + .containsExactlyInAnyOrder( + Assertions.tuple(group1.getName(), group1.getDescription(), true)); + } + + @Test + public void search_deselected_groups() { + QualityGateDto gate = db.qualityGates().insertQualityGate(); + GroupDto group1 = db.users().insertGroup(); + GroupDto group2 = db.users().insertGroup(); + db.qualityGates().addGroupPermission(gate, group1); + userSession.logIn().addPermission(ADMINISTER_QUALITY_GATES); + + Qualitygates.SearchGroupsResponse response = ws.newRequest() + .setParam(PARAM_GATE_NAME, gate.getName()) + .setParam(WebService.Param.SELECTED, "deselected") + .executeProtobuf(Qualitygates.SearchGroupsResponse.class); + + assertThat(response.getGroupsList()) + .extracting(Qualitygates.SearchGroupsResponse.Group::getName, + Qualitygates.SearchGroupsResponse.Group::getDescription, Qualitygates.SearchGroupsResponse.Group::getSelected) + .containsExactlyInAnyOrder( + Assertions.tuple(group2.getName(), group2.getDescription(), false)); + } + + @Test + public void search_by_name() { + QualityGateDto gate = db.qualityGates().insertQualityGate(); + GroupDto group1 = db.users().insertGroup("sonar-users-project"); + GroupDto group2 = db.users().insertGroup("sonar-users-qgate"); + GroupDto group3 = db.users().insertGroup("sonar-admin"); + db.qualityGates().addGroupPermission(gate, group1); + db.qualityGates().addGroupPermission(gate, group2); + db.qualityGates().addGroupPermission(gate, group3); + userSession.logIn().addPermission(ADMINISTER_QUALITY_GATES); + + Qualitygates.SearchGroupsResponse response = ws.newRequest() + .setParam(PARAM_GATE_NAME, gate.getName()) + .setParam(TEXT_QUERY, "UsErS") + .setParam(WebService.Param.SELECTED, "all") + .executeProtobuf(Qualitygates.SearchGroupsResponse.class); + + assertThat(response.getGroupsList()).extracting(Qualitygates.SearchGroupsResponse.Group::getName) + .containsExactlyInAnyOrder(group1.getName(), group2.getName()); + } + + @Test + public void group_without_description() { + QualityGateDto gate = db.qualityGates().insertQualityGate(); + GroupDto group = db.users().insertGroup(newGroupDto().setDescription(null)); + db.qualityGates().addGroupPermission(gate, group); + userSession.logIn().addPermission(ADMINISTER_QUALITY_GATES); + + Qualitygates.SearchGroupsResponse response = ws.newRequest() + .setParam(PARAM_GATE_NAME, gate.getName()) + .setParam(WebService.Param.SELECTED, "all") + .executeProtobuf(Qualitygates.SearchGroupsResponse.class); + + assertThat(response.getGroupsList()) + .extracting(Qualitygates.SearchGroupsResponse.Group::getName, Qualitygates.SearchGroupsResponse.Group::hasDescription) + .containsExactlyInAnyOrder(Assertions.tuple(group.getName(), false)); + } + + @Test + public void paging_search() { + QualityGateDto gate = db.qualityGates().insertQualityGate(); + GroupDto group3 = db.users().insertGroup("group3"); + GroupDto group1 = db.users().insertGroup("group1"); + GroupDto group2 = db.users().insertGroup("group2"); + db.qualityGates().addGroupPermission(gate, group1); + db.qualityGates().addGroupPermission(gate, group2); + userSession.logIn().addPermission(ADMINISTER_QUALITY_GATES); + + assertThat(ws.newRequest() + .setParam(PARAM_GATE_NAME, gate.getName()) + .setParam(WebService.Param.SELECTED, "all") + .setParam(PAGE, "1") + .setParam(PAGE_SIZE, "1") + .executeProtobuf(Qualitygates.SearchGroupsResponse.class).getGroupsList()) + .extracting(Qualitygates.SearchGroupsResponse.Group::getName) + .containsExactly(group1.getName()); + + assertThat(ws.newRequest() + .setParam(PARAM_GATE_NAME, gate.getName()) + .setParam(WebService.Param.SELECTED, "all") + .setParam(PAGE, "3") + .setParam(PAGE_SIZE, "1") + .executeProtobuf(Qualitygates.SearchGroupsResponse.class).getGroupsList()) + .extracting(Qualitygates.SearchGroupsResponse.Group::getName) + .containsExactly(group3.getName()); + + assertThat(ws.newRequest() + .setParam(PARAM_GATE_NAME, gate.getName()) + .setParam(WebService.Param.SELECTED, "all") + .setParam(PAGE, "1") + .setParam(PAGE_SIZE, "10") + .executeProtobuf(Qualitygates.SearchGroupsResponse.class).getGroupsList()) + .extracting(Qualitygates.SearchGroupsResponse.Group::getName) + .containsExactly(group1.getName(), group2.getName(), group3.getName()); + } + + @Test + public void uses_global_permission() { + QualityGateDto gate = db.qualityGates().insertQualityGate(); + GroupDto group = db.users().insertGroup(); + db.qualityGates().addGroupPermission(gate, group); + userSession.logIn().addPermission(ADMINISTER_QUALITY_GATES); + + Qualitygates.SearchGroupsResponse response = ws.newRequest() + .setParam(PARAM_GATE_NAME, gate.getName()) + .setParam(WebService.Param.SELECTED, "all") + .executeProtobuf(Qualitygates.SearchGroupsResponse.class); + + assertThat(response.getGroupsList()) + .extracting(Qualitygates.SearchGroupsResponse.Group::getName) + .containsExactlyInAnyOrder(group.getName()); + } + + @Test + public void qg_administers_can_search_groups() { + QualityGateDto gate = db.qualityGates().insertQualityGate(); + GroupDto group = db.users().insertGroup(); + db.qualityGates().addGroupPermission(gate, group); + userSession.logIn().addPermission(GlobalPermission.ADMINISTER_QUALITY_GATES); + + Qualitygates.SearchGroupsResponse response = ws.newRequest() + .setParam(PARAM_GATE_NAME, gate.getName()) + .setParam(WebService.Param.SELECTED, "all") + .executeProtobuf(Qualitygates.SearchGroupsResponse.class); + + assertThat(response.getGroupsList()) + .extracting(Qualitygates.SearchGroupsResponse.Group::getName) + .containsExactlyInAnyOrder(group.getName()); + } + + @Test + public void qg_editors_can_search_groups() { + QualityGateDto gate = db.qualityGates().insertQualityGate(); + GroupDto group = db.users().insertGroup(); + db.qualityGates().addGroupPermission(gate, group); + UserDto userAllowedToEditQualityGate = db.users().insertUser(); + db.qualityGates().addUserPermission(gate, userAllowedToEditQualityGate); + userSession.logIn(userAllowedToEditQualityGate); + + Qualitygates.SearchGroupsResponse response = ws.newRequest() + .setParam(PARAM_GATE_NAME, gate.getName()) + .setParam(WebService.Param.SELECTED, "all") + .executeProtobuf(Qualitygates.SearchGroupsResponse.class); + + assertThat(response.getGroupsList()) + .extracting(Qualitygates.SearchGroupsResponse.Group::getName) + .containsExactlyInAnyOrder(group.getName()); + } + + @Test + public void fail_when_qgate_does_not_exist() { + userSession.logIn().addPermission(GlobalPermission.ADMINISTER_QUALITY_GATES); + + expectedException.expect(NotFoundException.class); + expectedException.expectMessage("No quality gate has been found for name unknown"); + + ws.newRequest() + .setParam(PARAM_GATE_NAME, "unknown") + .execute(); + } + + @Test + public void fail_when_not_enough_permission() { + QualityGateDto gate = db.qualityGates().insertQualityGate(); + userSession.logIn(db.users().insertUser()); + + expectedException.expect(ForbiddenException.class); + + ws.newRequest() + .setParam(PARAM_GATE_NAME, gate.getName()) + .execute(); + } + +} diff --git a/sonar-ws/src/main/protobuf/ws-qualitygates.proto b/sonar-ws/src/main/protobuf/ws-qualitygates.proto index 781e5b2c147..5538d4b5922 100644 --- a/sonar-ws/src/main/protobuf/ws-qualitygates.proto +++ b/sonar-ws/src/main/protobuf/ws-qualitygates.proto @@ -182,5 +182,17 @@ message Actions { optional bool manageConditions = 6; } +// WS api/qualitygates/search_groups +message SearchGroupsResponse { + optional sonarqube.ws.commons.Paging paging = 1; + repeated Group groups = 2; + + message Group { + optional string name = 1; + optional string description = 2; + optional bool selected = 3; + } +} + -- 2.39.5