aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--server/sonar-db-dao/src/it/java/org/sonar/db/user/UserDaoIT.java34
-rw-r--r--server/sonar-db-dao/src/it/java/org/sonar/db/user/UserGroupDaoIT.java72
-rw-r--r--server/sonar-db-dao/src/main/java/org/sonar/db/user/UserGroupDao.java16
-rw-r--r--server/sonar-db-dao/src/main/java/org/sonar/db/user/UserGroupDto.java34
-rw-r--r--server/sonar-db-dao/src/main/java/org/sonar/db/user/UserGroupMapper.java4
-rw-r--r--server/sonar-db-dao/src/main/java/org/sonar/db/user/UserGroupQuery.java25
-rw-r--r--server/sonar-db-dao/src/main/java/org/sonar/db/user/UserQuery.java32
-rw-r--r--server/sonar-db-dao/src/main/resources/org/sonar/db/user/UserGroupMapper.xml34
-rw-r--r--server/sonar-db-dao/src/main/resources/org/sonar/db/user/UserMapper.xml6
-rw-r--r--server/sonar-db-dao/src/schema/schema-sq.ddl4
-rw-r--r--server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v104/AddUuidColumnToGroupsUsersIT.java51
-rw-r--r--server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v104/CreatePrimaryKeyOnGroupsUsersTableIT.java52
-rw-r--r--server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v104/MakeUuidInGroupsUsersNotNullableIT.java51
-rw-r--r--server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v104/PopulateGroupsUsersUuidIT.java107
-rw-r--r--server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/step/MassUpdate.java2
-rw-r--r--server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v104/AddUuidColumnToGroupsUsers.java55
-rw-r--r--server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v104/CreatePrimaryKeyOnGroupsUsersTable.java52
-rw-r--r--server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v104/DbVersion104.java6
-rw-r--r--server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v104/MakeUuidInGroupsUsersNotNullable.java48
-rw-r--r--server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v104/PopulateGroupsUsersUuid.java69
-rw-r--r--server/sonar-webserver-common/src/it/java/org/sonar/server/common/user/service/UserServiceIT.java32
-rw-r--r--server/sonar-webserver-common/src/main/java/org/sonar/server/common/group/service/GroupMembershipSearchRequest.java29
-rw-r--r--server/sonar-webserver-common/src/main/java/org/sonar/server/common/group/service/GroupMembershipService.java128
-rw-r--r--server/sonar-webserver-common/src/main/java/org/sonar/server/common/group/service/GroupService.java6
-rw-r--r--server/sonar-webserver-common/src/main/java/org/sonar/server/common/user/service/UserService.java3
-rw-r--r--server/sonar-webserver-common/src/main/java/org/sonar/server/common/user/service/UsersSearchRequest.java24
-rw-r--r--server/sonar-webserver-common/src/test/java/org/sonar/server/common/group/service/GroupMembershipServiceTest.java222
-rw-r--r--server/sonar-webserver-common/src/test/java/org/sonar/server/common/group/service/GroupServiceTest.java10
-rw-r--r--server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/WebApiEndpoints.java1
-rw-r--r--server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/group/controller/DefaultGroupController.java2
-rw-r--r--server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/group/controller/GroupController.java7
-rw-r--r--server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/membership/controller/DefaultGroupMembershipController.java92
-rw-r--r--server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/membership/controller/GroupMembershipController.java69
-rw-r--r--server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/membership/controller/package-info.java23
-rw-r--r--server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/membership/request/GroupMembershipCreateRestRequest.java32
-rw-r--r--server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/membership/request/GroupsMembershipSearchRestRequest.java36
-rw-r--r--server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/membership/request/package-info.java23
-rw-r--r--server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/membership/response/GroupMembershipRestResponse.java31
-rw-r--r--server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/membership/response/GroupsMembershipSearchRestResponse.java26
-rw-r--r--server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/membership/response/package-info.java23
-rw-r--r--server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/user/controller/DefaultUserController.java14
-rw-r--r--server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/user/controller/UserController.java6
-rw-r--r--server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/user/request/UsersSearchRestRequest.java9
-rw-r--r--server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/config/PlatformLevel4WebConfig.java15
-rw-r--r--server/sonar-webserver-webapi-v2/src/test/java/org/sonar/server/v2/api/group/controller/DefaultGroupControllerTest.java27
-rw-r--r--server/sonar-webserver-webapi-v2/src/test/java/org/sonar/server/v2/api/membership/controller/DefaultGroupMembershipControllerTest.java246
-rw-r--r--server/sonar-webserver-webapi-v2/src/test/java/org/sonar/server/v2/api/user/controller/DefaultUserControllerTest.java16
-rw-r--r--server/sonar-webserver-webapi/src/it/java/org/sonar/server/usergroups/ws/AddUserActionIT.java43
-rw-r--r--server/sonar-webserver-webapi/src/it/java/org/sonar/server/usergroups/ws/RemoveUserActionIT.java9
-rw-r--r--server/sonar-webserver-webapi/src/main/java/org/sonar/server/usergroups/ws/AddUserAction.java26
-rw-r--r--server/sonar-webserver-webapi/src/main/java/org/sonar/server/usergroups/ws/RemoveUserAction.java40
-rw-r--r--server/sonar-webserver-webapi/src/main/java/org/sonar/server/usergroups/ws/UserGroupsModule.java2
-rw-r--r--server/sonar-webserver/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel4.java6
53 files changed, 1925 insertions, 107 deletions
diff --git a/server/sonar-db-dao/src/it/java/org/sonar/db/user/UserDaoIT.java b/server/sonar-db-dao/src/it/java/org/sonar/db/user/UserDaoIT.java
index ab8afc81fde..cbfc3cfefbe 100644
--- a/server/sonar-db-dao/src/it/java/org/sonar/db/user/UserDaoIT.java
+++ b/server/sonar-db-dao/src/it/java/org/sonar/db/user/UserDaoIT.java
@@ -267,6 +267,40 @@ public class UserDaoIT {
}
@Test
+ public void selectUsersByQuery_whenSearchingByGroupUuid_findsTheRightResults() {
+ db.users().insertUser();
+ UserDto userToFind1 = db.users().insertUser(u -> u.setLogin("z"));
+ UserDto userToFind2 = db.users().insertUser(u -> u.setLogin("a"));
+
+ GroupDto groupDto = db.users().insertGroup();
+ db.users().insertMember(groupDto, userToFind2);
+ db.users().insertMember(groupDto, userToFind1);
+
+ UserQuery query = UserQuery.builder().groupUuid(groupDto.getUuid()).build();
+ List<UserDto> users = underTest.selectUsers(session, query);
+
+ assertThat(users).usingRecursiveFieldByFieldElementComparator().containsExactly(userToFind2, userToFind1);
+ assertThat(underTest.countUsers(session, query)).isEqualTo(2);
+ }
+
+ @Test
+ public void selectUsersByQuery_whenExcludingGroupUuid_findsTheRightResults() {
+ UserDto userToFind1 = db.users().insertUser(u -> u.setLogin("z"));
+ UserDto userToFind2 = db.users().insertUser(u -> u.setLogin("a"));
+ UserDto userToFind3 = db.users().insertUser(u -> u.setLogin("b"));
+
+ GroupDto groupDto = db.users().insertGroup();
+ db.users().insertMember(groupDto, userToFind2);
+ db.users().insertMember(groupDto, userToFind1);
+
+ UserQuery query = UserQuery.builder().excludedGroupUuid(groupDto.getUuid()).build();
+ List<UserDto> users = underTest.selectUsers(session, query);
+
+ assertThat(users).usingRecursiveFieldByFieldElementComparator().containsExactly(userToFind3);
+ assertThat(underTest.countUsers(session, query)).isEqualTo(1);
+ }
+
+ @Test
public void selectUsersByQuery_whenSearchingByUuidsWithLongRange_shouldReturnTheExpectedUsers() {
db.users().insertUser();
List<UserDto> users = generateAndInsertUsers(3200);
diff --git a/server/sonar-db-dao/src/it/java/org/sonar/db/user/UserGroupDaoIT.java b/server/sonar-db-dao/src/it/java/org/sonar/db/user/UserGroupDaoIT.java
index 79e5f6b45e8..6f504776284 100644
--- a/server/sonar-db-dao/src/it/java/org/sonar/db/user/UserGroupDaoIT.java
+++ b/server/sonar-db-dao/src/it/java/org/sonar/db/user/UserGroupDaoIT.java
@@ -19,15 +19,21 @@
*/
package org.sonar.db.user;
+import com.tngtech.java.junit.dataprovider.DataProvider;
+import com.tngtech.java.junit.dataprovider.DataProviderRunner;
+import com.tngtech.java.junit.dataprovider.UseDataProvider;
+import java.util.List;
import java.util.Set;
import org.junit.Rule;
import org.junit.Test;
+import org.junit.runner.RunWith;
import org.sonar.api.utils.System2;
import org.sonar.db.DbSession;
import org.sonar.db.DbTester;
import static org.assertj.core.api.Assertions.assertThat;
+@RunWith(DataProviderRunner.class)
public class UserGroupDaoIT {
@Rule
@@ -118,4 +124,70 @@ public class UserGroupDaoIT {
assertThat(dbTester.getDbClient().groupMembershipDao().selectGroupUuidsByUserUuid(dbTester.getSession(), user2.getUuid())).containsOnly(group1.getUuid(), group2.getUuid());
}
+ @DataProvider
+ public static Object[][] userQueryAndExpectedValues() {
+ return new Object[][] {
+ {new UserGroupQuery(null, null, null),
+ List.of(
+ new UserGroupDto().setUuid("3").setGroupUuid("group_a").setUserUuid("1"),
+ new UserGroupDto().setUuid("4").setGroupUuid("group_a").setUserUuid("2"),
+ new UserGroupDto().setUuid("5").setGroupUuid("group_b").setUserUuid("1"),
+ new UserGroupDto().setUuid("6").setGroupUuid("group_b").setUserUuid("2")
+ )},
+ {new UserGroupQuery("3", null, null),
+ List.of(
+ new UserGroupDto().setUuid("3").setGroupUuid("group_a").setUserUuid("1")
+ )},
+ {new UserGroupQuery("3", "group_a", "1"),
+ List.of(
+ new UserGroupDto().setUuid("3").setGroupUuid("group_a").setUserUuid("1")
+ )},
+ {new UserGroupQuery("3", "group_b", "1"),
+ List.of()},
+ {new UserGroupQuery(null,"group_b", null),
+ List.of(
+ new UserGroupDto().setUuid("5").setGroupUuid("group_b").setUserUuid("1"),
+ new UserGroupDto().setUuid("6").setGroupUuid("group_b").setUserUuid("2")
+ )},
+ {new UserGroupQuery(null,null, "2"),
+ List.of(
+ new UserGroupDto().setUuid("4").setGroupUuid("group_a").setUserUuid("2"),
+ new UserGroupDto().setUuid("6").setGroupUuid("group_b").setUserUuid("2")
+ )},
+ {new UserGroupQuery(null,"group_a", "2"),
+ List.of(
+ new UserGroupDto().setUuid("4").setGroupUuid("group_a").setUserUuid("2")
+ )},
+ {new UserGroupQuery(null,"group_c", null),
+ List.of()},
+ {new UserGroupQuery(null,"group_c", "2"),
+ List.of()},
+ {new UserGroupQuery(null,"group_a", "3"),
+ List.of()}
+ };
+ }
+
+ @Test
+ @UseDataProvider("userQueryAndExpectedValues")
+ public void selectByQuery_returnsExpectedResults(UserGroupQuery userQuery, List<UserGroupDto> expectedUserGroupDtos) {
+ insertUsersGroupsAndMembership();
+
+ List<UserGroupDto> userGroupDtos = underTest.selectByQuery(dbTester.getSession(), userQuery, 1, 100);
+
+ assertThat(userGroupDtos).usingRecursiveFieldByFieldElementComparator().isEqualTo(expectedUserGroupDtos);
+ assertThat(underTest.countByQuery(dbTester.getSession(), userQuery)).isEqualTo(expectedUserGroupDtos.size());
+ }
+
+
+ private void insertUsersGroupsAndMembership() {
+ UserDto user1 = dbTester.users().insertUser();
+ UserDto user2 = dbTester.users().insertUser();
+ GroupDto group1 = dbTester.users().insertGroup(g -> g.setUuid("group_a"));
+ GroupDto group2 = dbTester.users().insertGroup(g -> g.setUuid("group_b"));
+ dbTester.users().insertMember(group1, user1);
+ dbTester.users().insertMember(group1, user2);
+ dbTester.users().insertMember(group2, user1);
+ dbTester.users().insertMember(group2, user2);
+ }
+
}
diff --git a/server/sonar-db-dao/src/main/java/org/sonar/db/user/UserGroupDao.java b/server/sonar-db-dao/src/main/java/org/sonar/db/user/UserGroupDao.java
index 68ca217b4ea..e11a01fd91a 100644
--- a/server/sonar-db-dao/src/main/java/org/sonar/db/user/UserGroupDao.java
+++ b/server/sonar-db-dao/src/main/java/org/sonar/db/user/UserGroupDao.java
@@ -19,20 +19,26 @@
*/
package org.sonar.db.user;
+import java.util.List;
import java.util.Set;
+import org.sonar.core.util.UuidFactory;
import org.sonar.db.Dao;
import org.sonar.db.DbSession;
+import org.sonar.db.Pagination;
import org.sonar.db.audit.AuditPersister;
import org.sonar.db.audit.model.UserGroupNewValue;
public class UserGroupDao implements Dao {
private final AuditPersister auditPersister;
+ private final UuidFactory uuidFactory;
- public UserGroupDao(AuditPersister auditPersister) {
+ public UserGroupDao(AuditPersister auditPersister, UuidFactory uuidFactory) {
this.auditPersister = auditPersister;
+ this.uuidFactory = uuidFactory;
}
public UserGroupDto insert(DbSession session, UserGroupDto dto, String groupName, String login) {
+ dto.setUuid(uuidFactory.create());
mapper(session).insert(dto);
auditPersister.addUserToGroup(session, new UserGroupNewValue(dto, groupName, login));
return dto;
@@ -42,6 +48,14 @@ public class UserGroupDao implements Dao {
return mapper(session).selectUserUuidsInGroup(groupUuid);
}
+ public List<UserGroupDto> selectByQuery(DbSession session, UserGroupQuery query, int page, int pageSize) {
+ return mapper(session).selectByQuery(query, Pagination.forPage(page).andSize(pageSize));
+ }
+
+ public int countByQuery(DbSession session, UserGroupQuery query) {
+ return mapper(session).countByQuery(query);
+ }
+
public void delete(DbSession session, GroupDto group, UserDto user) {
int deletedRows = mapper(session).delete(group.getUuid(), user.getUuid());
diff --git a/server/sonar-db-dao/src/main/java/org/sonar/db/user/UserGroupDto.java b/server/sonar-db-dao/src/main/java/org/sonar/db/user/UserGroupDto.java
index 648db5c1ebb..cd69c3f222a 100644
--- a/server/sonar-db-dao/src/main/java/org/sonar/db/user/UserGroupDto.java
+++ b/server/sonar-db-dao/src/main/java/org/sonar/db/user/UserGroupDto.java
@@ -19,11 +19,26 @@
*/
package org.sonar.db.user;
-public class UserGroupDto {
+import java.util.Objects;
+public class UserGroupDto {
+ private String uuid;
private String userUuid;
private String groupUuid;
+ public UserGroupDto() {
+ //
+ }
+
+ public String getUuid() {
+ return uuid;
+ }
+
+ public UserGroupDto setUuid(String uuid) {
+ this.uuid = uuid;
+ return this;
+ }
+
public String getUserUuid() {
return userUuid;
}
@@ -41,4 +56,21 @@ public class UserGroupDto {
this.groupUuid = groupUuid;
return this;
}
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ UserGroupDto that = (UserGroupDto) o;
+ return Objects.equals(userUuid, that.userUuid) && Objects.equals(groupUuid, that.groupUuid);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(userUuid, groupUuid);
+ }
}
diff --git a/server/sonar-db-dao/src/main/java/org/sonar/db/user/UserGroupMapper.java b/server/sonar-db-dao/src/main/java/org/sonar/db/user/UserGroupMapper.java
index ed89e480477..7ec5235cb4e 100644
--- a/server/sonar-db-dao/src/main/java/org/sonar/db/user/UserGroupMapper.java
+++ b/server/sonar-db-dao/src/main/java/org/sonar/db/user/UserGroupMapper.java
@@ -19,8 +19,10 @@
*/
package org.sonar.db.user;
+import java.util.List;
import java.util.Set;
import org.apache.ibatis.annotations.Param;
+import org.sonar.db.Pagination;
public interface UserGroupMapper {
@@ -34,4 +36,6 @@ public interface UserGroupMapper {
int deleteByUserUuid(@Param("userUuid") String userUuid);
+ List<UserGroupDto> selectByQuery(@Param("query") UserGroupQuery query, @Param("pagination") Pagination pagination);
+ int countByQuery(@Param("query") UserGroupQuery query);
}
diff --git a/server/sonar-db-dao/src/main/java/org/sonar/db/user/UserGroupQuery.java b/server/sonar-db-dao/src/main/java/org/sonar/db/user/UserGroupQuery.java
new file mode 100644
index 00000000000..425f4020df3
--- /dev/null
+++ b/server/sonar-db-dao/src/main/java/org/sonar/db/user/UserGroupQuery.java
@@ -0,0 +1,25 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.db.user;
+
+import javax.annotation.Nullable;
+
+public record UserGroupQuery(@Nullable String uuid, @Nullable String groupUuid, @Nullable String userUuid) {
+}
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 6b624e26cd7..6cc169bc81c 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
@@ -37,6 +37,8 @@ public class UserQuery {
private final Long sonarLintLastConnectionDateFrom;
private final Long sonarLintLastConnectionDateTo;
private final String externalLogin;
+ private final String groupUuid;
+ private final String excludedGroupUuid;
private final Set<String> userUuids;
private UserQuery(UserQuery userQuery, Collection<String> userUuids) {
@@ -48,13 +50,15 @@ public class UserQuery {
this.sonarLintLastConnectionDateTo = userQuery.getSonarLintLastConnectionDateTo();
this.sonarLintLastConnectionDateFrom = userQuery.getSonarLintLastConnectionDateFrom();
this.externalLogin = userQuery.externalLogin;
+ this.groupUuid = userQuery.groupUuid;
+ this.excludedGroupUuid = userQuery.excludedGroupUuid;
this.userUuids = new HashSet<>(userUuids);
}
private UserQuery(@Nullable String searchText, @Nullable Boolean isActive, @Nullable String isManagedSqlClause,
@Nullable OffsetDateTime lastConnectionDateFrom, @Nullable OffsetDateTime lastConnectionDateTo,
@Nullable OffsetDateTime sonarLintLastConnectionDateFrom, @Nullable OffsetDateTime sonarLintLastConnectionDateTo, @Nullable String externalLogin,
- @Nullable Set<String> userUuids) {
+ @Nullable String groupUuid, @Nullable String excludedGroupUuid, @Nullable Set<String> userUuids) {
this.searchText = searchTextToSearchTextSql(searchText);
this.isActive = isActive;
this.isManagedSqlClause = isManagedSqlClause;
@@ -63,6 +67,8 @@ public class UserQuery {
this.sonarLintLastConnectionDateFrom = parseDateToLong(sonarLintLastConnectionDateFrom);
this.sonarLintLastConnectionDateTo = formatDateToInput(sonarLintLastConnectionDateTo);
this.externalLogin = externalLogin;
+ this.groupUuid = groupUuid;
+ this.excludedGroupUuid = excludedGroupUuid;
this.userUuids = userUuids;
}
@@ -142,6 +148,16 @@ public class UserQuery {
return userUuids;
}
+ @CheckForNull
+ private String getGroupUuid() {
+ return groupUuid;
+ }
+
+ @CheckForNull
+ private String getExcludedGroupUuid() {
+ return excludedGroupUuid;
+ }
+
public static UserQueryBuilder builder() {
return new UserQueryBuilder();
}
@@ -155,6 +171,8 @@ public class UserQuery {
private OffsetDateTime sonarLintLastConnectionDateFrom = null;
private OffsetDateTime sonarLintLastConnectionDateTo = null;
private String externalLogin = null;
+ private String groupUuid = null;
+ private String excludedGroupUuid;
private Set<String> userUuids = null;
private UserQueryBuilder() {
@@ -200,6 +218,16 @@ public class UserQuery {
return this;
}
+ public UserQueryBuilder groupUuid(@Nullable String groupUuid) {
+ this.groupUuid = groupUuid;
+ return this;
+ }
+
+ public UserQueryBuilder excludedGroupUuid(@Nullable String excludedGroupUuid) {
+ this.excludedGroupUuid = excludedGroupUuid;
+ return this;
+ }
+
public UserQueryBuilder userUuids(@Nullable Set<String> userUuids) {
this.userUuids = userUuids;
return this;
@@ -208,7 +236,7 @@ public class UserQuery {
public UserQuery build() {
return new UserQuery(
searchText, isActive, isManagedSqlClause, lastConnectionDateFrom, lastConnectionDateTo,
- sonarLintLastConnectionDateFrom, sonarLintLastConnectionDateTo, externalLogin, userUuids);
+ sonarLintLastConnectionDateFrom, sonarLintLastConnectionDateTo, externalLogin, groupUuid, excludedGroupUuid, userUuids);
}
}
}
diff --git a/server/sonar-db-dao/src/main/resources/org/sonar/db/user/UserGroupMapper.xml b/server/sonar-db-dao/src/main/resources/org/sonar/db/user/UserGroupMapper.xml
index 8151f29ea83..85961adcd0d 100644
--- a/server/sonar-db-dao/src/main/resources/org/sonar/db/user/UserGroupMapper.xml
+++ b/server/sonar-db-dao/src/main/resources/org/sonar/db/user/UserGroupMapper.xml
@@ -5,9 +5,11 @@
<insert id="insert" parameterType="UserGroup" useGeneratedKeys="false">
insert into groups_users (
+ uuid,
user_uuid,
group_uuid
) values (
+ #{uuid,jdbcType=VARCHAR},
#{userUuid,jdbcType=VARCHAR},
#{groupUuid,jdbcType=VARCHAR}
)
@@ -19,6 +21,38 @@
where gu.group_uuid=#{groupUuid,jdbcType=VARCHAR}
</select>
+ <select id="selectByQuery" resultType="org.sonar.db.user.UserGroupDto">
+ select
+ gu.uuid as uuid,
+ gu.user_uuid as userUuid,
+ gu.group_uuid as groupUuid
+ from groups_users gu
+ <include refid="searchByQueryWhereClause"/>
+ order by gu.group_uuid, gu.user_uuid
+ <include refid="org.sonar.db.common.Common.pagination"/>
+ </select>
+
+ <select id="countByQuery" parameterType="map" resultType="int">
+ select count(1)
+ from groups_users gu
+ <include refid="searchByQueryWhereClause"/>
+ </select>
+
+ <sql id="searchByQueryWhereClause">
+ <where>
+ 1=1
+ <if test="query.uuid != null">
+ AND gu.uuid='${query.uuid}'
+ </if>
+ <if test="query.userUuid != null">
+ AND gu.user_uuid='${query.userUuid}'
+ </if>
+ <if test="query.groupUuid != null">
+ AND gu.group_uuid='${query.groupUuid}'
+ </if>
+ </where>
+ </sql>
+
<delete id="delete" parameterType="map">
delete from groups_users
where user_uuid = #{userUuid,jdbcType=VARCHAR} and
diff --git a/server/sonar-db-dao/src/main/resources/org/sonar/db/user/UserMapper.xml b/server/sonar-db-dao/src/main/resources/org/sonar/db/user/UserMapper.xml
index d3e234b2749..dae72afd8ce 100644
--- a/server/sonar-db-dao/src/main/resources/org/sonar/db/user/UserMapper.xml
+++ b/server/sonar-db-dao/src/main/resources/org/sonar/db/user/UserMapper.xml
@@ -172,6 +172,12 @@
<if test="query.externalLogin != null">
AND (u.external_login = #{query.externalLogin, jdbcType=VARCHAR})
</if>
+ <if test="query.groupUuid != null">
+ AND exists (select 1 from groups_users ug where ug.user_uuid = u.uuid AND ug.group_uuid=#{query.groupUuid, jdbcType=VARCHAR})
+ </if>
+ <if test="query.excludedGroupUuid != null">
+ AND NOT exists (select 1 from groups_users ug where ug.user_uuid = u.uuid AND ug.group_uuid=#{query.excludedGroupUuid, jdbcType=VARCHAR})
+ </if>
</where>
</sql>
diff --git a/server/sonar-db-dao/src/schema/schema-sq.ddl b/server/sonar-db-dao/src/schema/schema-sq.ddl
index 9bd6370e171..272f0e0cdf2 100644
--- a/server/sonar-db-dao/src/schema/schema-sq.ddl
+++ b/server/sonar-db-dao/src/schema/schema-sq.ddl
@@ -387,8 +387,10 @@ CREATE UNIQUE NULLS NOT DISTINCT INDEX "UNIQ_GROUPS_NAME" ON "GROUPS"("NAME" NUL
CREATE TABLE "GROUPS_USERS"(
"GROUP_UUID" CHARACTER VARYING(40) NOT NULL,
- "USER_UUID" CHARACTER VARYING(255) NOT NULL
+ "USER_UUID" CHARACTER VARYING(255) NOT NULL,
+ "UUID" CHARACTER VARYING(40) NOT NULL
);
+ALTER TABLE "GROUPS_USERS" ADD CONSTRAINT "PK_GROUPS_USERS" PRIMARY KEY("UUID");
CREATE INDEX "INDEX_GROUPS_USERS_GROUP_UUID" ON "GROUPS_USERS"("GROUP_UUID" NULLS FIRST);
CREATE INDEX "INDEX_GROUPS_USERS_USER_UUID" ON "GROUPS_USERS"("USER_UUID" NULLS FIRST);
CREATE UNIQUE NULLS NOT DISTINCT INDEX "GROUPS_USERS_UNIQUE" ON "GROUPS_USERS"("USER_UUID" NULLS FIRST, "GROUP_UUID" NULLS FIRST);
diff --git a/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v104/AddUuidColumnToGroupsUsersIT.java b/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v104/AddUuidColumnToGroupsUsersIT.java
new file mode 100644
index 00000000000..d6a1bfd39c0
--- /dev/null
+++ b/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v104/AddUuidColumnToGroupsUsersIT.java
@@ -0,0 +1,51 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.server.platform.db.migration.version.v104;
+
+import java.sql.SQLException;
+import java.sql.Types;
+import org.junit.Rule;
+import org.junit.Test;
+import org.sonar.db.MigrationDbTester;
+
+import static org.assertj.core.api.Assertions.assertThatCode;
+
+public class AddUuidColumnToGroupsUsersIT {
+
+ private static final String TABLE_NAME = "groups_users";
+ private static final String COLUMN_NAME = "uuid";
+
+ @Rule
+ public final MigrationDbTester db = MigrationDbTester.createForMigrationStep(AddUuidColumnToGroupsUsers.class);
+ private final AddUuidColumnToGroupsUsers underTest = new AddUuidColumnToGroupsUsers(db.database());
+
+ @Test
+ public void execute_whenColumnDoesNotExist_shouldCreateColumn() throws SQLException {
+ db.assertColumnDoesNotExist(TABLE_NAME, COLUMN_NAME);
+ underTest.execute();
+ db.assertColumnDefinition(TABLE_NAME, COLUMN_NAME, Types.VARCHAR, 40, true);
+ }
+
+ @Test
+ public void execute_whenColumnsAlreadyExists_shouldNotFail() throws SQLException {
+ underTest.execute();
+ assertThatCode(underTest::execute).doesNotThrowAnyException();
+ }
+}
diff --git a/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v104/CreatePrimaryKeyOnGroupsUsersTableIT.java b/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v104/CreatePrimaryKeyOnGroupsUsersTableIT.java
new file mode 100644
index 00000000000..85b9a4534b8
--- /dev/null
+++ b/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v104/CreatePrimaryKeyOnGroupsUsersTableIT.java
@@ -0,0 +1,52 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.server.platform.db.migration.version.v104;
+
+import java.sql.SQLException;
+import org.junit.Rule;
+import org.junit.Test;
+import org.sonar.db.MigrationDbTester;
+
+import static org.sonar.server.platform.db.migration.version.v104.AddUuidColumnToGroupsUsers.GROUPS_USERS_TABLE_NAME;
+import static org.sonar.server.platform.db.migration.version.v104.AddUuidColumnToGroupsUsers.GROUPS_USERS_UUID_COLUMN_NAME;
+import static org.sonar.server.platform.db.migration.version.v104.CreatePrimaryKeyOnGroupsUsersTable.PK_NAME;
+
+public class CreatePrimaryKeyOnGroupsUsersTableIT {
+ @Rule
+ public final MigrationDbTester db = MigrationDbTester.createForMigrationStep(CreatePrimaryKeyOnGroupsUsersTable.class);
+ private final CreatePrimaryKeyOnGroupsUsersTable createIndex = new CreatePrimaryKeyOnGroupsUsersTable(db.database());
+
+ @Test
+ public void execute_whenPrimaryKeyDoesntExist_shouldCreatePrimaryKey() throws SQLException {
+ db.assertNoPrimaryKey(GROUPS_USERS_TABLE_NAME);
+
+ createIndex.execute();
+ db.assertPrimaryKey(GROUPS_USERS_TABLE_NAME, PK_NAME, GROUPS_USERS_UUID_COLUMN_NAME);
+ }
+
+ @Test
+ public void execute_whenPrimaryKeyAlreadyExist_shouldKeepThePrimaryKeyAndNotFail() throws SQLException {
+ createIndex.execute();
+ createIndex.execute();
+
+ db.assertPrimaryKey(GROUPS_USERS_TABLE_NAME, PK_NAME, GROUPS_USERS_UUID_COLUMN_NAME);
+ }
+
+}
diff --git a/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v104/MakeUuidInGroupsUsersNotNullableIT.java b/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v104/MakeUuidInGroupsUsersNotNullableIT.java
new file mode 100644
index 00000000000..88c3f6d1005
--- /dev/null
+++ b/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v104/MakeUuidInGroupsUsersNotNullableIT.java
@@ -0,0 +1,51 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.server.platform.db.migration.version.v104;
+
+import java.sql.SQLException;
+import org.junit.Rule;
+import org.junit.Test;
+import org.sonar.db.MigrationDbTester;
+
+import static java.sql.Types.VARCHAR;
+import static org.sonar.server.platform.db.migration.version.v104.AddUuidColumnToGroupsUsers.GROUPS_USERS_TABLE_NAME;
+import static org.sonar.server.platform.db.migration.version.v104.AddUuidColumnToGroupsUsers.GROUPS_USERS_UUID_COLUMN_NAME;
+
+public class MakeUuidInGroupsUsersNotNullableIT {
+
+ @Rule
+ public final MigrationDbTester db = MigrationDbTester.createForMigrationStep( MakeUuidInGroupsUsersNotNullable.class);
+ private final MakeUuidInGroupsUsersNotNullable underTest = new MakeUuidInGroupsUsersNotNullable(db.database());
+
+ @Test
+ public void execute_whenUuidColumnIsNullable_shouldMakeItNonNullable() throws SQLException {
+ db.assertColumnDefinition(GROUPS_USERS_TABLE_NAME, GROUPS_USERS_UUID_COLUMN_NAME, VARCHAR, null, true);
+ underTest.execute();
+ db.assertColumnDefinition(GROUPS_USERS_TABLE_NAME, GROUPS_USERS_UUID_COLUMN_NAME, VARCHAR, null, false);
+ }
+
+ @Test
+ public void execute_whenUuidColumnIsNullable_shouldKeepItNullableAndNotFail() throws SQLException {
+ db.assertColumnDefinition(GROUPS_USERS_TABLE_NAME, GROUPS_USERS_UUID_COLUMN_NAME, VARCHAR, null, true);
+ underTest.execute();
+ underTest.execute();
+ db.assertColumnDefinition(GROUPS_USERS_TABLE_NAME, GROUPS_USERS_UUID_COLUMN_NAME, VARCHAR, null, false);
+ }
+}
diff --git a/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v104/PopulateGroupsUsersUuidIT.java b/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v104/PopulateGroupsUsersUuidIT.java
new file mode 100644
index 00000000000..ab8060739a3
--- /dev/null
+++ b/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v104/PopulateGroupsUsersUuidIT.java
@@ -0,0 +1,107 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.server.platform.db.migration.version.v104;
+
+import java.sql.SQLException;
+import java.util.List;
+import java.util.Map;
+import org.assertj.core.groups.Tuple;
+import org.junit.Rule;
+import org.junit.Test;
+import org.sonar.core.util.UuidFactoryFast;
+import org.sonar.db.MigrationDbTester;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.tuple;
+
+public class PopulateGroupsUsersUuidIT {
+
+ private static final String GROUPS_USERS_TABLE_NAME = "groups_users";
+ @Rule
+ public final MigrationDbTester db = MigrationDbTester.createForMigrationStep(PopulateGroupsUsersUuid.class);
+
+ private final PopulateGroupsUsersUuid migration = new PopulateGroupsUsersUuid(db.database(), UuidFactoryFast.getInstance());
+
+ @Test
+ public void execute_whenTableIsEmpty_shouldPopulate() throws SQLException {
+ insertRowsWithoutUuid();
+
+ migration.execute();
+
+ verifyUuidPresentAndUnique();
+ }
+
+
+
+ @Test
+ public void execute_isReentrant() throws SQLException {
+ insertRowsWithoutUuid();
+ migration.execute();
+ List<Tuple> existingUuids = getExistingUuids();
+
+ migration.execute();
+ verifyUuidsNotChanged(existingUuids);
+
+ migration.execute();
+ verifyUuidsNotChanged(existingUuids);
+ }
+
+ private void insertRowsWithoutUuid() {
+ db.executeInsert(GROUPS_USERS_TABLE_NAME,
+ "uuid", null,
+ "group_uuid", "group1_uuid",
+ "user_uuid", "user1_uuid");
+
+ db.executeInsert(GROUPS_USERS_TABLE_NAME,
+ "uuid", null,
+ "group_uuid", "group2_uuid",
+ "user_uuid", "user2_uuid");
+
+ db.executeInsert(GROUPS_USERS_TABLE_NAME,
+ "uuid", null,
+ "group_uuid", "group3_uuid",
+ "user_uuid", "user3_uuid");
+ }
+
+ private void verifyUuidPresentAndUnique() {
+ List<Map<String, Object>> rows = db.select("select uuid, group_uuid, user_uuid from groups_users");
+ rows
+ .forEach(stringObjectMap -> assertThat(stringObjectMap.get("UUID")).isNotNull());
+ long uniqueCount = rows.stream().map(row -> row.get("UUID")).distinct().count();
+ assertThat(uniqueCount).isEqualTo(rows.size());
+
+ }
+
+ private List<Tuple> getExistingUuids() {
+ return db.select("select uuid, group_uuid, user_uuid from groups_users")
+ .stream()
+ .map(stringObjectMap -> tuple(stringObjectMap.get("UUID"), stringObjectMap.get("GROUP_UUID"), stringObjectMap.get("USER_UUID")))
+ .toList();
+ }
+
+ private void verifyUuidsNotChanged(List<Tuple> existingUuids) {
+ assertThat(db.select("select uuid, group_uuid, user_uuid from groups_users"))
+ .extracting(stringObjectMap -> tuple(stringObjectMap.get("UUID"), stringObjectMap.get("GROUP_UUID"), stringObjectMap.get("USER_UUID")))
+ .containsExactlyInAnyOrderElementsOf(existingUuids);
+ }
+
+
+
+}
diff --git a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/step/MassUpdate.java b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/step/MassUpdate.java
index fc08adfe0a4..117ca3271e6 100644
--- a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/step/MassUpdate.java
+++ b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/step/MassUpdate.java
@@ -67,7 +67,7 @@ public class MassUpdate {
this.writeConnection = writeConnection;
}
- public SqlStatement select(String sql) throws SQLException {
+ public SqlStatement<Select> select(String sql) throws SQLException {
this.select = SelectImpl.create(db, readConnection, sql);
return this.select;
}
diff --git a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v104/AddUuidColumnToGroupsUsers.java b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v104/AddUuidColumnToGroupsUsers.java
new file mode 100644
index 00000000000..8e3772b1164
--- /dev/null
+++ b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v104/AddUuidColumnToGroupsUsers.java
@@ -0,0 +1,55 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.server.platform.db.migration.version.v104;
+
+import java.sql.Connection;
+import java.sql.SQLException;
+import org.sonar.db.Database;
+import org.sonar.db.DatabaseUtils;
+import org.sonar.server.platform.db.migration.def.ColumnDef;
+import org.sonar.server.platform.db.migration.def.VarcharColumnDef;
+import org.sonar.server.platform.db.migration.sql.AddColumnsBuilder;
+import org.sonar.server.platform.db.migration.step.DdlChange;
+
+import static org.sonar.server.platform.db.migration.def.VarcharColumnDef.UUID_SIZE;
+
+public class AddUuidColumnToGroupsUsers extends DdlChange {
+
+ public static final String GROUPS_USERS_TABLE_NAME = "groups_users";
+ public static final String GROUPS_USERS_UUID_COLUMN_NAME = "uuid";
+
+ public AddUuidColumnToGroupsUsers(Database db) {
+ super(db);
+ }
+
+ @Override
+ public void execute(Context context) throws SQLException {
+ try (Connection connection = getDatabase().getDataSource().getConnection()) {
+ if (!DatabaseUtils.tableColumnExists(connection, GROUPS_USERS_TABLE_NAME, GROUPS_USERS_UUID_COLUMN_NAME)) {
+ ColumnDef columnDef = VarcharColumnDef.newVarcharColumnDefBuilder()
+ .setColumnName(GROUPS_USERS_UUID_COLUMN_NAME)
+ .setLimit(UUID_SIZE)
+ .setIsNullable(true)
+ .build();
+ context.execute(new AddColumnsBuilder(getDialect(), GROUPS_USERS_TABLE_NAME).addColumn(columnDef).build());
+ }
+ }
+ }
+}
diff --git a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v104/CreatePrimaryKeyOnGroupsUsersTable.java b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v104/CreatePrimaryKeyOnGroupsUsersTable.java
new file mode 100644
index 00000000000..549e6a3bed8
--- /dev/null
+++ b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v104/CreatePrimaryKeyOnGroupsUsersTable.java
@@ -0,0 +1,52 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.server.platform.db.migration.version.v104;
+
+import com.google.common.annotations.VisibleForTesting;
+import java.sql.SQLException;
+import org.sonar.db.Database;
+import org.sonar.server.platform.db.migration.sql.AddPrimaryKeyBuilder;
+import org.sonar.server.platform.db.migration.sql.DbPrimaryKeyConstraintFinder;
+import org.sonar.server.platform.db.migration.step.DdlChange;
+
+import static org.sonar.server.platform.db.migration.version.v104.AddUuidColumnToGroupsUsers.GROUPS_USERS_TABLE_NAME;
+import static org.sonar.server.platform.db.migration.version.v104.AddUuidColumnToGroupsUsers.GROUPS_USERS_UUID_COLUMN_NAME;
+
+public class CreatePrimaryKeyOnGroupsUsersTable extends DdlChange {
+
+ @VisibleForTesting
+ static final String PK_NAME = "pk_groups_users";
+
+ public CreatePrimaryKeyOnGroupsUsersTable(Database db) {
+ super(db);
+ }
+
+ @Override
+ public void execute(Context context) throws SQLException {
+ createPrimaryKey(context);
+ }
+
+ private void createPrimaryKey(Context context) throws SQLException {
+ boolean pkExists = new DbPrimaryKeyConstraintFinder(getDatabase()).findConstraintName(GROUPS_USERS_TABLE_NAME).isPresent();
+ if (!pkExists) {
+ context.execute(new AddPrimaryKeyBuilder(GROUPS_USERS_TABLE_NAME, GROUPS_USERS_UUID_COLUMN_NAME).build());
+ }
+ }
+}
diff --git a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v104/DbVersion104.java b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v104/DbVersion104.java
index 6a7136534e3..73f7742f355 100644
--- a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v104/DbVersion104.java
+++ b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v104/DbVersion104.java
@@ -46,6 +46,10 @@ public class DbVersion104 implements DbVersion {
.add(10_4_002, "Create table 'rules_tags'", CreateRuleTagsTable.class)
.add(10_4_003, "Populate 'rule_tags' table", PopulateRuleTagsTable.class)
.add(10_4_004, "Drop column 'tags' in the 'rules' table", DropTagsInRules.class)
- .add(10_4_005, "Drop column 'system_tags' in the 'rules' table", DropSystemTagsInRules.class);
+ .add(10_4_005, "Drop column 'system_tags' in the 'rules' table", DropSystemTagsInRules.class)
+ .add(10_4_006, "Add 'uuid' column to 'groups_users'", AddUuidColumnToGroupsUsers.class)
+ .add(10_4_007, "Populate 'uuid' column in 'groups_users'", PopulateGroupsUsersUuid.class)
+ .add(10_4_008, "Make 'uuid' column in 'groups_users' table non-nullable", MakeUuidInGroupsUsersNotNullable.class)
+ .add(10_4_009, "Create primary key on 'groups_users.uuid'", CreatePrimaryKeyOnGroupsUsersTable.class);
}
}
diff --git a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v104/MakeUuidInGroupsUsersNotNullable.java b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v104/MakeUuidInGroupsUsersNotNullable.java
new file mode 100644
index 00000000000..f39d13f173e
--- /dev/null
+++ b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v104/MakeUuidInGroupsUsersNotNullable.java
@@ -0,0 +1,48 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.server.platform.db.migration.version.v104;
+
+import java.sql.SQLException;
+import org.sonar.db.Database;
+import org.sonar.server.platform.db.migration.def.VarcharColumnDef;
+import org.sonar.server.platform.db.migration.sql.AlterColumnsBuilder;
+import org.sonar.server.platform.db.migration.step.DdlChange;
+
+import static org.sonar.server.platform.db.migration.def.VarcharColumnDef.UUID_SIZE;
+import static org.sonar.server.platform.db.migration.version.v104.AddUuidColumnToGroupsUsers.GROUPS_USERS_TABLE_NAME;
+import static org.sonar.server.platform.db.migration.version.v104.AddUuidColumnToGroupsUsers.GROUPS_USERS_UUID_COLUMN_NAME;
+
+public class MakeUuidInGroupsUsersNotNullable extends DdlChange {
+
+ private static final VarcharColumnDef UUID_COLUMN_DEF = VarcharColumnDef.newVarcharColumnDefBuilder(GROUPS_USERS_UUID_COLUMN_NAME)
+ .setIsNullable(false)
+ .setLimit(UUID_SIZE)
+ .build();
+
+ public MakeUuidInGroupsUsersNotNullable(Database db) {
+ super(db);
+ }
+
+ @Override
+ public void execute(Context context) throws SQLException {
+ context.execute(new AlterColumnsBuilder(getDialect(), GROUPS_USERS_TABLE_NAME).updateColumn(UUID_COLUMN_DEF).build());
+
+ }
+}
diff --git a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v104/PopulateGroupsUsersUuid.java b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v104/PopulateGroupsUsersUuid.java
new file mode 100644
index 00000000000..4bf8d11ad55
--- /dev/null
+++ b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v104/PopulateGroupsUsersUuid.java
@@ -0,0 +1,69 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.server.platform.db.migration.version.v104;
+
+import java.sql.SQLException;
+import org.sonar.core.util.UuidFactory;
+import org.sonar.db.Database;
+import org.sonar.server.platform.db.migration.step.DataChange;
+import org.sonar.server.platform.db.migration.step.MassUpdate;
+import org.sonar.server.platform.db.migration.step.Select;
+import org.sonar.server.platform.db.migration.step.SqlStatement;
+import org.sonar.server.platform.db.migration.step.Upsert;
+
+public class PopulateGroupsUsersUuid extends DataChange {
+
+ private static final String SELECT_QUERY = """
+ SELECT group_uuid, user_uuid
+ FROM groups_users
+ WHERE uuid IS NULL
+ """;
+
+ private static final String SET_UUID_STATEMENT = """
+ UPDATE groups_users
+ SET uuid=?
+ WHERE group_uuid=? AND user_uuid=?
+ """;
+
+ private final UuidFactory uuidFactory;
+
+ public PopulateGroupsUsersUuid(Database db, UuidFactory uuidFactory) {
+ super(db);
+ this.uuidFactory = uuidFactory;
+ }
+
+ @Override
+ protected void execute(Context context) throws SQLException {
+ MassUpdate massUpdate = context.prepareMassUpdate();
+ SqlStatement<Select> select = massUpdate.select(SELECT_QUERY);
+ Upsert setUuid = massUpdate.update(SET_UUID_STATEMENT);
+ try (select; setUuid) {
+ massUpdate.execute((row, update, index) -> {
+ String groupUuid = row.getString(1);
+ String userUuid = row.getString(2);
+ String uuid = uuidFactory.create();
+ update.setString(1, uuid);
+ update.setString(2, groupUuid);
+ update.setString(3, userUuid);
+ return true;
+ });
+ }
+ }
+}
diff --git a/server/sonar-webserver-common/src/it/java/org/sonar/server/common/user/service/UserServiceIT.java b/server/sonar-webserver-common/src/it/java/org/sonar/server/common/user/service/UserServiceIT.java
index 96aaa999c3c..721a095931d 100644
--- a/server/sonar-webserver-common/src/it/java/org/sonar/server/common/user/service/UserServiceIT.java
+++ b/server/sonar-webserver-common/src/it/java/org/sonar/server/common/user/service/UserServiceIT.java
@@ -163,6 +163,36 @@ public class UserServiceIT {
}
@Test
+ public void findUsers_whenFilteringByGroup_returnsCorrectUsers() {
+ GroupDto groupDto = db.users().insertGroup();
+ UserDto user3 = db.users().insertUser(u -> u.setLogin("user3"));
+ UserDto user2 = db.users().insertUser(u -> u.setLogin("user2"));
+ db.users().insertUser(u -> u.setLogin("user1"));
+
+ db.users().insertMember(groupDto, user3);
+ db.users().insertMember(groupDto, user2);
+
+ SearchResults<UserInformation> users = userService.findUsers(UsersSearchRequest.builder().setGroupUuid(groupDto.getUuid()).setPageSize(10).setPage(1).build());
+ assertThat(users.searchResults()).extracting(UserInformation::userDto).extracting(UserDto::getLogin)
+ .containsExactly(user2.getLogin(), user3.getLogin());
+ }
+
+ @Test
+ public void findUsers_whenGroupIsExcluded_returnsCorrectUsers() {
+ GroupDto groupDto = db.users().insertGroup();
+ UserDto user3 = db.users().insertUser(u -> u.setLogin("user3"));
+ UserDto user2 = db.users().insertUser(u -> u.setLogin("user2"));
+ UserDto user1 = db.users().insertUser(u -> u.setLogin("user1"));
+
+ db.users().insertMember(groupDto, user3);
+ db.users().insertMember(groupDto, user2);
+
+ SearchResults<UserInformation> users = userService.findUsers(UsersSearchRequest.builder().setExcludedGroupUuid(groupDto.getUuid()).setPageSize(10).setPage(1).build());
+ assertThat(users.searchResults()).extracting(UserInformation::userDto).extracting(UserDto::getLogin)
+ .containsOnly(user1.getLogin());
+ }
+
+ @Test
public void return_avatar() {
UserDto user = db.users().insertUser(u -> u.setEmail("john@doe.com"));
@@ -510,7 +540,7 @@ public class UserServiceIT {
db.users().insertToken(user);
db.commit();
- userService.deactivate(user.getUuid(),false);
+ userService.deactivate(user.getUuid(), false);
assertThat(db.getDbClient().userTokenDao().selectByUser(dbSession, user)).isEmpty();
}
diff --git a/server/sonar-webserver-common/src/main/java/org/sonar/server/common/group/service/GroupMembershipSearchRequest.java b/server/sonar-webserver-common/src/main/java/org/sonar/server/common/group/service/GroupMembershipSearchRequest.java
new file mode 100644
index 00000000000..2fb41f7b6d3
--- /dev/null
+++ b/server/sonar-webserver-common/src/main/java/org/sonar/server/common/group/service/GroupMembershipSearchRequest.java
@@ -0,0 +1,29 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.server.common.group.service;
+
+import javax.annotation.Nullable;
+
+public record GroupMembershipSearchRequest(
+ @Nullable String groupUuid,
+ @Nullable String userUuid
+) {
+
+}
diff --git a/server/sonar-webserver-common/src/main/java/org/sonar/server/common/group/service/GroupMembershipService.java b/server/sonar-webserver-common/src/main/java/org/sonar/server/common/group/service/GroupMembershipService.java
new file mode 100644
index 00000000000..843bbc82efb
--- /dev/null
+++ b/server/sonar-webserver-common/src/main/java/org/sonar/server/common/group/service/GroupMembershipService.java
@@ -0,0 +1,128 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.server.common.group.service;
+
+import java.util.List;
+import java.util.Optional;
+import org.sonar.api.security.DefaultGroups;
+import org.sonar.api.server.ServerSide;
+import org.sonar.db.DbClient;
+import org.sonar.db.DbSession;
+import org.sonar.db.permission.GlobalPermission;
+import org.sonar.db.user.GroupDao;
+import org.sonar.db.user.GroupDto;
+import org.sonar.db.user.UserDao;
+import org.sonar.db.user.UserDto;
+import org.sonar.db.user.UserGroupDao;
+import org.sonar.db.user.UserGroupDto;
+import org.sonar.db.user.UserGroupQuery;
+import org.sonar.server.common.SearchResults;
+import org.sonar.server.exceptions.NotFoundException;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static java.lang.String.format;
+import static org.sonar.server.exceptions.BadRequestException.checkRequest;
+import static org.sonar.server.exceptions.NotFoundException.checkFound;
+
+@ServerSide
+public class GroupMembershipService {
+ private final DbClient dbClient;
+ private final UserGroupDao userGroupDao;
+ private final UserDao userDao;
+ private final GroupDao groupDao;
+
+ public GroupMembershipService(DbClient dbClient, UserGroupDao userGroupDao, UserDao userDao, GroupDao groupDao) {
+ this.dbClient = dbClient;
+ this.userGroupDao = userGroupDao;
+ this.userDao = userDao;
+ this.groupDao = groupDao;
+ }
+
+ public SearchResults<UserGroupDto> searchMembers(GroupMembershipSearchRequest groupMembershipSearchRequest, int pageIndex, int pageSize) {
+ try (DbSession dbSession = dbClient.openSession(false)) {
+ UserGroupQuery query = new UserGroupQuery(null, groupMembershipSearchRequest.groupUuid(), groupMembershipSearchRequest.userUuid());
+ int total = userGroupDao.countByQuery(dbSession, query);
+ if (pageSize == 0) {
+ return new SearchResults<>(List.of(), total);
+ }
+ List<UserGroupDto> userGroupDtos = userGroupDao.selectByQuery(dbSession, query, pageIndex, pageSize);
+ return new SearchResults<>(userGroupDtos, total);
+ }
+ }
+
+ public UserGroupDto addMembership(String groupUuid, String userUuid) {
+ try (DbSession dbSession = dbClient.openSession(false)) {
+ UserDto userDto = findUserOrThrow(userUuid, dbSession);
+ GroupDto groupDto = findNonDefaultGroupOrThrow(groupUuid, dbSession);
+ UserGroupDto userGroupDto = new UserGroupDto().setGroupUuid(groupUuid).setUserUuid(userUuid);
+ checkArgument(isNotInGroup(dbSession, groupUuid, userUuid), "User '%s' is already a member of group '%s'", userDto.getLogin(), groupDto.getName());
+ userGroupDao.insert(dbSession, userGroupDto, groupDto.getName(), userDto.getLogin());
+ dbSession.commit();
+ return userGroupDto;
+ }
+ }
+
+ private boolean isNotInGroup(DbSession dbSession, String groupUuid, String userUuid) {
+ return userGroupDao.selectByQuery(dbSession, new UserGroupQuery(null, groupUuid, userUuid), 1, 1).isEmpty();
+ }
+
+ public void removeMembership(String groupMembershipUuid) {
+ try (DbSession dbSession = dbClient.openSession(false)) {
+ UserGroupDto userGroupDto = findMembershipOrThrow(groupMembershipUuid, dbSession);
+ removeMembership(userGroupDto.getGroupUuid(), userGroupDto.getUserUuid());
+ }
+ }
+
+ private UserGroupDto findMembershipOrThrow(String groupMembershipUuid, DbSession dbSession) {
+ return userGroupDao.selectByQuery(dbSession, new UserGroupQuery(groupMembershipUuid, null, null), 1, 1).stream()
+ .findFirst()
+ .orElseThrow(() -> new NotFoundException(format("Group membership '%s' not found", groupMembershipUuid)));
+ }
+
+ public void removeMembership(String groupUuid, String userUuid) {
+ try (DbSession dbSession = dbClient.openSession(false)) {
+ UserDto userDto = findUserOrThrow(userUuid, dbSession);
+ GroupDto groupDto = findNonDefaultGroupOrThrow(groupUuid, dbSession);
+ ensureLastAdminIsNotRemoved(dbSession, groupUuid, userUuid);
+ userGroupDao.delete(dbSession, groupDto, userDto);
+ dbSession.commit();
+ }
+ }
+
+ private GroupDto findNonDefaultGroupOrThrow(String groupUuid, DbSession dbSession) {
+ GroupDto groupDto = groupDao.selectByUuid(dbSession, groupUuid);
+ checkFound(groupDto, "Group '%s' not found", groupUuid);
+ checkArgument(!groupDto.getName().equals(DefaultGroups.USERS), "Default group '%s' cannot be used to perform this action", groupDto.getName());
+ return groupDto;
+ }
+
+ private UserDto findUserOrThrow(String userUuid, DbSession dbSession) {
+ return Optional.ofNullable(userDao.selectByUuid(dbSession, userUuid))
+ .filter(UserDto::isActive)
+ .orElseThrow(() -> new NotFoundException(format("User '%s' not found", userUuid)));
+ }
+
+ private void ensureLastAdminIsNotRemoved(DbSession dbSession, String groupUuids, String userUuid) {
+ int remainingAdmins = dbClient.authorizationDao().countUsersWithGlobalPermissionExcludingGroupMember(dbSession,
+ GlobalPermission.ADMINISTER.getKey(), groupUuids, userUuid);
+ checkRequest(remainingAdmins > 0, "The last administrator user cannot be removed");
+ }
+
+}
diff --git a/server/sonar-webserver-common/src/main/java/org/sonar/server/common/group/service/GroupService.java b/server/sonar-webserver-common/src/main/java/org/sonar/server/common/group/service/GroupService.java
index bf4dee25832..8479634cb3e 100644
--- a/server/sonar-webserver-common/src/main/java/org/sonar/server/common/group/service/GroupService.java
+++ b/server/sonar-webserver-common/src/main/java/org/sonar/server/common/group/service/GroupService.java
@@ -73,6 +73,11 @@ public class GroupService {
GroupDto defaultGroup = defaultGroupFinder.findDefaultGroup(dbSession);
GroupQuery query = toGroupQuery(groupSearchRequest);
+ int limit = dbClient.groupDao().countByQuery(dbSession, query);
+ if (groupSearchRequest.page() == 0) {
+ return new SearchResults<>(List.of(), limit);
+ }
+
List<GroupDto> groups = dbClient.groupDao().selectByQuery(dbSession, query, groupSearchRequest.page(), groupSearchRequest.pageSize());
List<String> groupUuids = extractGroupUuids(groups);
Map<String, Boolean> groupUuidToIsManaged = managedInstanceService.getGroupUuidToManaged(dbSession, new HashSet<>(groupUuids));
@@ -81,7 +86,6 @@ public class GroupService {
.map(groupDto -> toGroupInformation(groupDto, defaultGroup.getUuid(), groupUuidToIsManaged))
.toList();
- int limit = dbClient.groupDao().countByQuery(dbSession, query);
return new SearchResults<>(results, limit);
}
diff --git a/server/sonar-webserver-common/src/main/java/org/sonar/server/common/user/service/UserService.java b/server/sonar-webserver-common/src/main/java/org/sonar/server/common/user/service/UserService.java
index 32e091e401c..47cab5bd065 100644
--- a/server/sonar-webserver-common/src/main/java/org/sonar/server/common/user/service/UserService.java
+++ b/server/sonar-webserver-common/src/main/java/org/sonar/server/common/user/service/UserService.java
@@ -94,7 +94,8 @@ public class UserService {
request.getSonarLintLastConnectionDateFrom().ifPresent(builder::sonarLintLastConnectionDateFrom);
request.getSonarLintLastConnectionDateTo().ifPresent(builder::sonarLintLastConnectionDateTo);
request.getExternalLogin().ifPresent(builder::externalLogin);
-
+ request.getGroupUuid().ifPresent(builder::groupUuid);
+ request.getExcludedGroupUuid().ifPresent(builder::excludedGroupUuid);
if (managedInstanceService.isInstanceExternallyManaged()) {
String managedInstanceSql = Optional.ofNullable(request.isManaged())
.map(managedInstanceService::getManagedUsersSqlFilter)
diff --git a/server/sonar-webserver-common/src/main/java/org/sonar/server/common/user/service/UsersSearchRequest.java b/server/sonar-webserver-common/src/main/java/org/sonar/server/common/user/service/UsersSearchRequest.java
index 419e78dae61..3f1b3e8f47c 100644
--- a/server/sonar-webserver-common/src/main/java/org/sonar/server/common/user/service/UsersSearchRequest.java
+++ b/server/sonar-webserver-common/src/main/java/org/sonar/server/common/user/service/UsersSearchRequest.java
@@ -38,6 +38,8 @@ public class UsersSearchRequest {
private final OffsetDateTime sonarLintLastConnectionDateFrom;
private final OffsetDateTime sonarLintLastConnectionDateTo;
private final String externalLogin;
+ private final String groupUuid;
+ private final String excludedGroupUuid;
private UsersSearchRequest(Builder builder) {
this.page = builder.page;
@@ -46,6 +48,8 @@ public class UsersSearchRequest {
this.deactivated = builder.deactivated;
this.managed = builder.managed;
this.externalLogin = builder.externalLogin;
+ this.groupUuid = builder.groupUuid;
+ this.excludedGroupUuid = builder.excludedGroupUuid;
try {
this.lastConnectionDateFrom = Optional.ofNullable(builder.lastConnectionDateFrom).map(DateUtils::parseOffsetDateTime).orElse(null);
this.lastConnectionDateTo = Optional.ofNullable(builder.lastConnectionDateTo).map(DateUtils::parseOffsetDateTime).orElse(null);
@@ -98,10 +102,18 @@ public class UsersSearchRequest {
return Optional.ofNullable(externalLogin);
}
+ public Optional<String> getGroupUuid() {
+ return Optional.ofNullable(groupUuid);
+ }
+
public static Builder builder() {
return new Builder();
}
+ public Optional<String> getExcludedGroupUuid() {
+ return Optional.ofNullable(excludedGroupUuid);
+ }
+
public static class Builder {
private Integer page;
private Integer pageSize;
@@ -113,6 +125,8 @@ public class UsersSearchRequest {
private String sonarLintLastConnectionDateFrom;
private String sonarLintLastConnectionDateTo;
private String externalLogin;
+ private String groupUuid;
+ private String excludedGroupUuid;
private Builder() {
// enforce factory method use
@@ -168,6 +182,16 @@ public class UsersSearchRequest {
return this;
}
+ public Builder setGroupUuid(@Nullable String groupUuid) {
+ this.groupUuid = groupUuid;
+ return this;
+ }
+
+ public Builder setExcludedGroupUuid(@Nullable String excludedGroupUuid) {
+ this.excludedGroupUuid = excludedGroupUuid;
+ return this;
+ }
+
public UsersSearchRequest build() {
return new UsersSearchRequest(this);
}
diff --git a/server/sonar-webserver-common/src/test/java/org/sonar/server/common/group/service/GroupMembershipServiceTest.java b/server/sonar-webserver-common/src/test/java/org/sonar/server/common/group/service/GroupMembershipServiceTest.java
new file mode 100644
index 00000000000..bde5921acb9
--- /dev/null
+++ b/server/sonar-webserver-common/src/test/java/org/sonar/server/common/group/service/GroupMembershipServiceTest.java
@@ -0,0 +1,222 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.server.common.group.service;
+
+import java.util.List;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Answers;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnitRunner;
+import org.sonar.db.DbClient;
+import org.sonar.db.DbSession;
+import org.sonar.db.permission.GlobalPermission;
+import org.sonar.db.user.GroupDao;
+import org.sonar.db.user.GroupDto;
+import org.sonar.db.user.UserDao;
+import org.sonar.db.user.UserDto;
+import org.sonar.db.user.UserGroupDao;
+import org.sonar.db.user.UserGroupDto;
+import org.sonar.db.user.UserGroupQuery;
+import org.sonar.server.common.SearchResults;
+import org.sonar.server.exceptions.BadRequestException;
+import org.sonar.server.exceptions.NotFoundException;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+@RunWith(MockitoJUnitRunner.class)
+public class GroupMembershipServiceTest {
+ private static final String GROUP_A = "group_a";
+ private static final String USER_1 = "user_1";
+ private static final String UUID = "1";
+ @Mock(answer = Answers.RETURNS_DEEP_STUBS)
+ private DbClient dbClient;
+ @Mock
+ private UserGroupDao userGroupDao;
+ @Mock
+ private UserDao userDao;
+ @Mock
+ private GroupDao groupDao;
+ @Mock
+ private DbSession dbSession;
+ @InjectMocks
+ private GroupMembershipService groupMembershipService;
+
+ @Before
+ public void setup() {
+ when(dbClient.openSession(false)).thenReturn(dbSession);
+ }
+
+ @Test
+ public void addMembership_ifGroupAndUserNotFound_shouldThrow() {
+ assertThatExceptionOfType(NotFoundException.class)
+ .isThrownBy(() -> groupMembershipService.addMembership(GROUP_A, USER_1))
+ .withMessage("User 'user_1' not found");
+ }
+
+ @Test
+ public void addMembership_ifGroupNotFound_shouldThrow() {
+ mockUserDto();
+
+ assertThatExceptionOfType(NotFoundException.class)
+ .isThrownBy(() -> groupMembershipService.addMembership(GROUP_A, USER_1))
+ .withMessage("Group 'group_a' not found");
+ }
+
+ @Test
+ public void addMembership_ifGroupAndUserFound_shouldAddMemberToGroup() {
+ GroupDto groupDto = mockGroupDto();
+ UserDto userDto = mockUserDto();
+
+ UserGroupDto userGroupDto = groupMembershipService.addMembership(GROUP_A, USER_1);
+
+ assertThat(userGroupDto.getGroupUuid()).isEqualTo(groupDto.getUuid());
+ assertThat(userGroupDto.getUserUuid()).isEqualTo(userDto.getUuid());
+
+ verify(userGroupDao).insert(dbSession, new UserGroupDto().setGroupUuid(GROUP_A).setUserUuid(USER_1), groupDto.getName(), userDto.getLogin());
+ verify(dbSession).commit();
+ }
+
+ @Test
+ public void removeMembership_ifGroupAndUserNotFound_shouldThrow() {
+ assertThatExceptionOfType(NotFoundException.class)
+ .isThrownBy(() -> groupMembershipService.removeMembership(GROUP_A, USER_1))
+ .withMessage("User 'user_1' not found");
+ }
+
+ @Test
+ public void removeMembership_ifGroupNotFound_shouldThrow() {
+ mockUserDto();
+
+ assertThatExceptionOfType(NotFoundException.class)
+ .isThrownBy(() -> groupMembershipService.removeMembership(GROUP_A, USER_1))
+ .withMessage("Group 'group_a' not found");
+ }
+
+ @Test
+ public void removeMembership_ifLastAdmin_shouldThrow() {
+ mockUserDto();
+ mockGroupDto();
+
+ assertThatExceptionOfType(BadRequestException.class)
+ .isThrownBy(() -> groupMembershipService.removeMembership(GROUP_A, USER_1))
+ .withMessage("The last administrator user cannot be removed");
+ }
+
+ @Test
+ public void removeMembership_ifGroupAndUserFound_shouldRemoveMemberFromGroup() {
+ mockAdminInGroup(GROUP_A, USER_1);
+ GroupDto groupDto = mockGroupDto();
+ UserDto userDto = mockUserDto();
+
+ groupMembershipService.removeMembership(GROUP_A, USER_1);
+
+ verify(userGroupDao).delete(dbSession, groupDto, userDto);
+ verify(dbSession).commit();
+ }
+
+ @Test
+ public void removeMemberByMembershipUuid_ifMembershipNotFound_shouldThrow() {
+ assertThatExceptionOfType(NotFoundException.class)
+ .isThrownBy(() -> groupMembershipService.removeMembership(UUID))
+ .withMessage("Group membership '1' not found");
+ }
+
+ @Test
+ public void removeMemberByMembershipUuid_ifFound_shouldRemoveMemberFromGroup() {
+ mockAdminInGroup(GROUP_A, USER_1);
+
+ GroupDto groupDto = mockGroupDto();
+ UserDto userDto = mockUserDto();
+ UserGroupDto userGroupDto = new UserGroupDto().setUuid(UUID).setUserUuid(USER_1).setGroupUuid(GROUP_A);
+ when(userGroupDao.selectByQuery(any(), any(), anyInt(), anyInt())).thenReturn(List.of(userGroupDto));
+
+ groupMembershipService.removeMembership(UUID);
+
+ verify(userGroupDao).selectByQuery(dbSession, new UserGroupQuery(UUID, null, null), 1, 1);
+ verify(userGroupDao).delete(dbSession, groupDto, userDto);
+ verify(dbSession).commit();
+ }
+
+ @Test
+ public void searchMembers_shouldReturnMembers() {
+ GroupDto groupDto = mockGroupDto();
+ UserDto userDto = mockUserDto();
+
+ UserGroupDto result = mock(UserGroupDto.class);
+ when(userGroupDao.selectByQuery(any(), any(), anyInt(), anyInt())).thenReturn(List.of(result));
+ when(userGroupDao.countByQuery(any(), any())).thenReturn(10);
+
+ GroupMembershipSearchRequest searchRequest = new GroupMembershipSearchRequest(groupDto.getUuid(), userDto.getUuid());
+ SearchResults<UserGroupDto> userGroupDtoSearchResults = groupMembershipService.searchMembers(searchRequest, 1, 10);
+
+ assertThat(userGroupDtoSearchResults.searchResults()).containsOnly(result);
+ assertThat(userGroupDtoSearchResults.total()).isEqualTo(10);
+
+ verify(userGroupDao).selectByQuery(dbSession, new UserGroupQuery(null, groupDto.getUuid(), userDto.getUuid()), 1, 10);
+ verify(userGroupDao).countByQuery(dbSession, new UserGroupQuery(null, groupDto.getUuid(), userDto.getUuid()));
+ }
+
+ @Test
+ public void searchMembers_withPageSizeEquals0_shouldOnlyComputeTotal() {
+ when(userGroupDao.countByQuery(any(), any())).thenReturn(10);
+
+ GroupMembershipSearchRequest searchRequest = new GroupMembershipSearchRequest(GROUP_A, USER_1);
+ SearchResults<UserGroupDto> userGroupDtoSearchResults = groupMembershipService.searchMembers(searchRequest, 1, 0);
+
+ assertThat(userGroupDtoSearchResults.searchResults()).isEmpty();
+ assertThat(userGroupDtoSearchResults.total()).isEqualTo(10);
+
+ verify(userGroupDao, never()).selectByQuery(any(), any(), anyInt(), anyInt());
+ verify(userGroupDao).countByQuery(dbSession, new UserGroupQuery(null, GROUP_A, USER_1));
+ }
+
+ private UserDto mockUserDto() {
+ UserDto userDto = mock(UserDto.class);
+ when(userDto.getUuid()).thenReturn(USER_1);
+ when(userDto.getLogin()).thenReturn("loginA");
+ when(userDto.isActive()).thenReturn(true);
+ when(userDao.selectByUuid(dbSession, USER_1)).thenReturn(userDto);
+ return userDto;
+ }
+
+ private GroupDto mockGroupDto() {
+ GroupDto groupDto = mock(GroupDto.class);
+ when(groupDto.getUuid()).thenReturn(GROUP_A);
+ when(groupDto.getName()).thenReturn("name_" + GROUP_A);
+ when(groupDao.selectByUuid(dbSession, GROUP_A)).thenReturn(groupDto);
+ return groupDto;
+ }
+
+ private void mockAdminInGroup(String groupUuid, String userUuid) {
+ when(dbClient.authorizationDao().countUsersWithGlobalPermissionExcludingGroupMember(dbSession,
+ GlobalPermission.ADMINISTER.getKey(), groupUuid, userUuid)).thenReturn(1);
+ }
+
+}
diff --git a/server/sonar-webserver-common/src/test/java/org/sonar/server/common/group/service/GroupServiceTest.java b/server/sonar-webserver-common/src/test/java/org/sonar/server/common/group/service/GroupServiceTest.java
index 485ff5fd16e..12c1e87a291 100644
--- a/server/sonar-webserver-common/src/test/java/org/sonar/server/common/group/service/GroupServiceTest.java
+++ b/server/sonar-webserver-common/src/test/java/org/sonar/server/common/group/service/GroupServiceTest.java
@@ -390,6 +390,16 @@ public class GroupServiceTest {
assertThat(queryCaptor.getValue().getSearchText()).isEqualTo("%QUERY%");
assertThat(queryCaptor.getValue().getIsManagedSqlClause()).isNull();
}
+ @Test
+ public void search_whenPageSizeEquals0_returnsOnlyTotal() {
+ when(dbClient.groupDao().countByQuery(eq(dbSession), any())).thenReturn(10);
+
+ SearchResults<GroupInformation> searchResults = groupService.search(dbSession, new GroupSearchRequest("query", null, 0, 24));
+ assertThat(searchResults.total()).isEqualTo(10);
+ assertThat(searchResults.searchResults()).isEmpty();
+
+ verify(dbClient.groupDao(), never()).selectByQuery(eq(dbSession), any(), anyInt(), anyInt());
+ }
@Test
public void search_whenInstanceManagedAndManagedIsTrue_addsManagedClause() {
diff --git a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/WebApiEndpoints.java b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/WebApiEndpoints.java
index 7c99e450da1..5e2449c8c6f 100644
--- a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/WebApiEndpoints.java
+++ b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/WebApiEndpoints.java
@@ -30,6 +30,7 @@ public class WebApiEndpoints {
public static final String AUTHORIZATIONS_DOMAIN = "/authorizations";
public static final String GROUPS_ENDPOINT = AUTHORIZATIONS_DOMAIN + "/groups";
+ public static final String GROUP_MEMBERSHIPS_ENDPOINT = AUTHORIZATIONS_DOMAIN + "/group-memberships";
private WebApiEndpoints() {
}
diff --git a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/group/controller/DefaultGroupController.java b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/group/controller/DefaultGroupController.java
index 4eafac5232d..55df54b8967 100644
--- a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/group/controller/DefaultGroupController.java
+++ b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/group/controller/DefaultGroupController.java
@@ -45,7 +45,7 @@ public class DefaultGroupController implements GroupController {
private final UserSession userSession;
private final ManagedInstanceChecker managedInstanceChecker;
- public DefaultGroupController(GroupService groupService, DbClient dbClient, ManagedInstanceChecker managedInstanceChecker, UserSession userSession) {
+ public DefaultGroupController(UserSession userSession, DbClient dbClient, GroupService groupService, ManagedInstanceChecker managedInstanceChecker) {
this.groupService = groupService;
this.dbClient = dbClient;
this.userSession = userSession;
diff --git a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/group/controller/GroupController.java b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/group/controller/GroupController.java
index 2b84b0defeb..d34538470dd 100644
--- a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/group/controller/GroupController.java
+++ b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/group/controller/GroupController.java
@@ -26,8 +26,8 @@ import javax.validation.Valid;
import org.sonar.server.v2.api.group.request.GroupCreateRestRequest;
import org.sonar.server.v2.api.group.request.GroupUpdateRestRequest;
import org.sonar.server.v2.api.group.request.GroupsSearchRestRequest;
-import org.sonar.server.v2.api.group.response.GroupsSearchRestResponse;
import org.sonar.server.v2.api.group.response.GroupRestResponse;
+import org.sonar.server.v2.api.group.response.GroupsSearchRestResponse;
import org.sonar.server.v2.api.model.RestPage;
import org.springdoc.api.annotations.ParameterObject;
import org.springframework.http.HttpStatus;
@@ -76,9 +76,8 @@ public interface GroupController {
@PatchMapping(path = "/{id}", consumes = JSON_MERGE_PATCH_CONTENT_TYPE, produces = MediaType.APPLICATION_JSON_VALUE)
@ResponseStatus(HttpStatus.OK)
- @Operation(summary = "Update a user", description = """
- Update a user.
- Allows updating user's name, email and SCM accounts.
+ @Operation(summary = "Update a group", description = """
+ Update a group name or description.
""")
GroupRestResponse updateGroup(@PathVariable("id") String id, @Valid @RequestBody GroupUpdateRestRequest updateRequest);
}
diff --git a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/membership/controller/DefaultGroupMembershipController.java b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/membership/controller/DefaultGroupMembershipController.java
new file mode 100644
index 00000000000..cd919a506a5
--- /dev/null
+++ b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/membership/controller/DefaultGroupMembershipController.java
@@ -0,0 +1,92 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.server.v2.api.membership.controller;
+
+import java.util.List;
+import org.sonar.db.user.UserGroupDto;
+import org.sonar.server.common.SearchResults;
+import org.sonar.server.common.group.service.GroupMembershipSearchRequest;
+import org.sonar.server.common.group.service.GroupMembershipService;
+import org.sonar.server.common.management.ManagedInstanceChecker;
+import org.sonar.server.user.UserSession;
+import org.sonar.server.v2.api.membership.request.GroupMembershipCreateRestRequest;
+import org.sonar.server.v2.api.membership.request.GroupsMembershipSearchRestRequest;
+import org.sonar.server.v2.api.membership.response.GroupsMembershipSearchRestResponse;
+import org.sonar.server.v2.api.membership.response.GroupMembershipRestResponse;
+import org.sonar.server.v2.api.model.RestPage;
+import org.sonar.server.v2.api.response.PageRestResponse;
+
+public class DefaultGroupMembershipController implements GroupMembershipController {
+
+ private final GroupMembershipService groupMembershipService;
+ private final UserSession userSession;
+ private final ManagedInstanceChecker managedInstanceChecker;
+
+ public DefaultGroupMembershipController(UserSession userSession, GroupMembershipService groupMembershipService, ManagedInstanceChecker managedInstanceChecker) {
+ this.groupMembershipService = groupMembershipService;
+ this.userSession = userSession;
+ this.managedInstanceChecker = managedInstanceChecker;
+ }
+
+ @Override
+ public GroupsMembershipSearchRestResponse search(GroupsMembershipSearchRestRequest groupsSearchRestRequest, RestPage restPage) {
+ userSession.checkLoggedIn().checkIsSystemAdministrator();
+ SearchResults<UserGroupDto> groupMembershipSearchResults = searchMembership(groupsSearchRestRequest, restPage);
+
+ List<GroupMembershipRestResponse> groupMembershipRestRespons = toRestGroupMembershipResponse(groupMembershipSearchResults);
+ return new GroupsMembershipSearchRestResponse(groupMembershipRestRespons,
+ new PageRestResponse(restPage.pageIndex(), restPage.pageSize(), groupMembershipSearchResults.total())
+ );
+ }
+
+ private SearchResults<UserGroupDto> searchMembership(GroupsMembershipSearchRestRequest groupsSearchRestRequest, RestPage restPage) {
+ GroupMembershipSearchRequest groupMembershipSearchRequest = new GroupMembershipSearchRequest(groupsSearchRestRequest.groupId(), groupsSearchRestRequest.userId());
+ return groupMembershipService.searchMembers(groupMembershipSearchRequest, restPage.pageIndex(), restPage.pageSize());
+ }
+
+ @Override
+ public void delete(String id) {
+ throwIfNotAllowedToModifyGroups();
+ groupMembershipService.removeMembership(id);
+ }
+
+ @Override
+ public GroupMembershipRestResponse create(GroupMembershipCreateRestRequest request) {
+ throwIfNotAllowedToModifyGroups();
+ UserGroupDto userGroupDto = groupMembershipService.addMembership(request.groupId(), request.userId());
+ return toRestGroupMembershipResponse(userGroupDto);
+ }
+
+ private static List<GroupMembershipRestResponse> toRestGroupMembershipResponse(SearchResults<UserGroupDto> groupMembershipSearchResults) {
+ return groupMembershipSearchResults.searchResults().stream()
+ .map(DefaultGroupMembershipController::toRestGroupMembershipResponse)
+ .toList();
+ }
+
+ private void throwIfNotAllowedToModifyGroups() {
+ userSession.checkIsSystemAdministrator();
+ managedInstanceChecker.throwIfInstanceIsManaged();
+ }
+
+ private static GroupMembershipRestResponse toRestGroupMembershipResponse(UserGroupDto group) {
+ return new GroupMembershipRestResponse(group.getUuid(), group.getGroupUuid(), group.getUserUuid());
+ }
+
+}
diff --git a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/membership/controller/GroupMembershipController.java b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/membership/controller/GroupMembershipController.java
new file mode 100644
index 00000000000..a3bf27856ab
--- /dev/null
+++ b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/membership/controller/GroupMembershipController.java
@@ -0,0 +1,69 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.server.v2.api.membership.controller;
+
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.enums.ParameterIn;
+import javax.validation.Valid;
+import org.sonar.server.v2.api.membership.request.GroupMembershipCreateRestRequest;
+import org.sonar.server.v2.api.membership.request.GroupsMembershipSearchRestRequest;
+import org.sonar.server.v2.api.membership.response.GroupsMembershipSearchRestResponse;
+import org.sonar.server.v2.api.membership.response.GroupMembershipRestResponse;
+import org.sonar.server.v2.api.model.RestPage;
+import org.springdoc.api.annotations.ParameterObject;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.MediaType;
+import org.springframework.web.bind.annotation.DeleteMapping;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.ResponseStatus;
+import org.springframework.web.bind.annotation.RestController;
+
+import static org.sonar.server.v2.WebApiEndpoints.GROUP_MEMBERSHIPS_ENDPOINT;
+
+@RequestMapping(GROUP_MEMBERSHIPS_ENDPOINT)
+@RestController
+public interface GroupMembershipController {
+
+ @GetMapping(produces = MediaType.APPLICATION_JSON_VALUE)
+ @ResponseStatus(HttpStatus.OK)
+ @Operation(summary = "Search across group memberships", description = """
+ Get the list of groups and members matching the query.
+ """)
+ GroupsMembershipSearchRestResponse search(
+ @Valid @ParameterObject GroupsMembershipSearchRestRequest groupsSearchRestRequest,
+ @Valid @ParameterObject RestPage restPage);
+
+ @PostMapping(consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
+ @ResponseStatus(HttpStatus.CREATED)
+ @Operation(summary = "Add a group membership", description = "Add a user to a group.")
+ GroupMembershipRestResponse create(@Valid @RequestBody GroupMembershipCreateRestRequest request);
+
+ @DeleteMapping(path = "/{id}")
+ @ResponseStatus(HttpStatus.NO_CONTENT)
+ @Operation(summary = "Remove a group membership", description = "Remove a user from a group")
+ void delete(@PathVariable("id") @Parameter(description = "The ID of the group membership to delete.", required = true, in = ParameterIn.PATH) String id);
+
+
+}
diff --git a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/membership/controller/package-info.java b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/membership/controller/package-info.java
new file mode 100644
index 00000000000..2b1f2f923f7
--- /dev/null
+++ b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/membership/controller/package-info.java
@@ -0,0 +1,23 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+@ParametersAreNonnullByDefault
+package org.sonar.server.v2.api.membership.controller;
+
+import javax.annotation.ParametersAreNonnullByDefault;
diff --git a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/membership/request/GroupMembershipCreateRestRequest.java b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/membership/request/GroupMembershipCreateRestRequest.java
new file mode 100644
index 00000000000..1e9f072bcb7
--- /dev/null
+++ b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/membership/request/GroupMembershipCreateRestRequest.java
@@ -0,0 +1,32 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.server.v2.api.membership.request;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+
+public record GroupMembershipCreateRestRequest(
+
+ @Schema(description = "ID of the user to add to group.")
+ String userId,
+
+ @Schema(description = "ID of the group where a member needs to be added.")
+ String groupId
+
+) {}
diff --git a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/membership/request/GroupsMembershipSearchRestRequest.java b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/membership/request/GroupsMembershipSearchRestRequest.java
new file mode 100644
index 00000000000..efa0b20331d
--- /dev/null
+++ b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/membership/request/GroupsMembershipSearchRestRequest.java
@@ -0,0 +1,36 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.server.v2.api.membership.request;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import javax.annotation.Nullable;
+
+public record GroupsMembershipSearchRestRequest(
+ @Nullable
+ @Schema(description = "ID of the user for which to search groups. If not set, all groups are returned.")
+ String userId,
+
+ @Nullable
+ @Schema(description = "ID of the group for which to search members. If not set, all groups are returned.")
+ String groupId
+
+) {
+
+}
diff --git a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/membership/request/package-info.java b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/membership/request/package-info.java
new file mode 100644
index 00000000000..bfb39bf56de
--- /dev/null
+++ b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/membership/request/package-info.java
@@ -0,0 +1,23 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+@ParametersAreNonnullByDefault
+package org.sonar.server.v2.api.membership.request;
+
+import javax.annotation.ParametersAreNonnullByDefault;
diff --git a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/membership/response/GroupMembershipRestResponse.java b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/membership/response/GroupMembershipRestResponse.java
new file mode 100644
index 00000000000..1a04e37224d
--- /dev/null
+++ b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/membership/response/GroupMembershipRestResponse.java
@@ -0,0 +1,31 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.server.v2.api.membership.response;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+
+public record GroupMembershipRestResponse(
+ @Schema(accessMode = Schema.AccessMode.READ_ONLY)
+ String id,
+ @Schema(accessMode = Schema.AccessMode.READ_ONLY)
+ String groupId,
+ @Schema(accessMode = Schema.AccessMode.READ_ONLY)
+ String userId) {
+}
diff --git a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/membership/response/GroupsMembershipSearchRestResponse.java b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/membership/response/GroupsMembershipSearchRestResponse.java
new file mode 100644
index 00000000000..71a12ea0d1e
--- /dev/null
+++ b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/membership/response/GroupsMembershipSearchRestResponse.java
@@ -0,0 +1,26 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.server.v2.api.membership.response;
+
+import java.util.List;
+import org.sonar.server.v2.api.response.PageRestResponse;
+
+public record GroupsMembershipSearchRestResponse(List<GroupMembershipRestResponse> groupMemberships, PageRestResponse page) {
+}
diff --git a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/membership/response/package-info.java b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/membership/response/package-info.java
new file mode 100644
index 00000000000..648538c4197
--- /dev/null
+++ b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/membership/response/package-info.java
@@ -0,0 +1,23 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+@ParametersAreNonnullByDefault
+package org.sonar.server.v2.api.membership.response;
+
+import javax.annotation.ParametersAreNonnullByDefault;
diff --git a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/user/controller/DefaultUserController.java b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/user/controller/DefaultUserController.java
index 6c0fbb5e947..bf9d8c358b5 100644
--- a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/user/controller/DefaultUserController.java
+++ b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/user/controller/DefaultUserController.java
@@ -56,17 +56,19 @@ public class DefaultUserController implements UserController {
}
@Override
- public UsersSearchRestResponse search(UsersSearchRestRequest usersSearchRestRequest, RestPage page) {
- throwIfAdminOnlyParametersAreUsed(usersSearchRestRequest);
+ public UsersSearchRestResponse search(UsersSearchRestRequest usersSearchRestRequest, @Nullable String excludedGroupId, RestPage page) {
+ throwIfAdminOnlyParametersAreUsed(usersSearchRestRequest, excludedGroupId);
- SearchResults<UserInformation> userSearchResults = userService.findUsers(toUserSearchRequest(usersSearchRestRequest, page));
+ SearchResults<UserInformation> userSearchResults = userService.findUsers(toUserSearchRequest(usersSearchRestRequest, excludedGroupId, page));
PaginationInformation paging = forPageIndex(page.pageIndex()).withPageSize(page.pageSize()).andTotal(userSearchResults.total());
return usersSearchResponseGenerator.toUsersForResponse(userSearchResults.searchResults(), paging);
}
- private void throwIfAdminOnlyParametersAreUsed(UsersSearchRestRequest usersSearchRestRequest) {
+ private void throwIfAdminOnlyParametersAreUsed(UsersSearchRestRequest usersSearchRestRequest, @Nullable String excludedGroupId) {
if (!userSession.isSystemAdministrator()) {
+ throwIfValuePresent("groupId", usersSearchRestRequest.groupId());
+ throwIfValuePresent("groupId!", excludedGroupId);
throwIfValuePresent("externalIdentity", usersSearchRestRequest.externalIdentity());
throwIfValuePresent("sonarLintLastConnectionDateFrom", usersSearchRestRequest.sonarLintLastConnectionDateFrom());
throwIfValuePresent("sonarLintLastConnectionDateTo", usersSearchRestRequest.sonarLintLastConnectionDateTo());
@@ -83,7 +85,7 @@ public class DefaultUserController implements UserController {
throw new ForbiddenException("Parameter " + parameterName + " requires Administer System permission.");
}
- private static UsersSearchRequest toUserSearchRequest(UsersSearchRestRequest usersSearchRestRequest, RestPage page) {
+ private static UsersSearchRequest toUserSearchRequest(UsersSearchRestRequest usersSearchRestRequest, @Nullable String excludedGroupId, RestPage page) {
return UsersSearchRequest.builder()
.setDeactivated(Optional.ofNullable(usersSearchRestRequest.active()).map(active -> !active).orElse(false))
.setManaged(usersSearchRestRequest.managed())
@@ -93,6 +95,8 @@ public class DefaultUserController implements UserController {
.setLastConnectionDateTo(usersSearchRestRequest.sonarQubeLastConnectionDateTo())
.setSonarLintLastConnectionDateFrom(usersSearchRestRequest.sonarLintLastConnectionDateFrom())
.setSonarLintLastConnectionDateTo(usersSearchRestRequest.sonarLintLastConnectionDateTo())
+ .setGroupUuid(usersSearchRestRequest.groupId())
+ .setExcludedGroupUuid(excludedGroupId)
.setPage(page.pageIndex())
.setPageSize(page.pageSize())
.build();
diff --git a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/user/controller/UserController.java b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/user/controller/UserController.java
index 9a02a37a1b9..9d703ee6aca 100644
--- a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/user/controller/UserController.java
+++ b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/user/controller/UserController.java
@@ -22,6 +22,10 @@ package org.sonar.server.v2.api.user.controller;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.enums.ParameterIn;
+import io.swagger.v3.oas.annotations.extensions.Extension;
+import io.swagger.v3.oas.annotations.extensions.ExtensionProperty;
+import io.swagger.v3.oas.annotations.media.Schema;
+import javax.annotation.Nullable;
import javax.validation.Valid;
import org.sonar.server.v2.api.model.RestPage;
import org.sonar.server.v2.api.user.response.UserRestResponse;
@@ -67,6 +71,8 @@ public interface UserController {
""")
UsersSearchRestResponse search(
@Valid @ParameterObject UsersSearchRestRequest usersSearchRestRequest,
+ @RequestParam(name = "groupId!") @Nullable @Schema(description = "Filter users not belonging to group. Only available for system administrators.",
+ extensions = @Extension(properties = {@ExtensionProperty(name = "internal", value = "true")}), hidden = true) String excludedGroupId,
@Valid @ParameterObject RestPage restPage);
@DeleteMapping(path = "/{id}")
diff --git a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/user/request/UsersSearchRestRequest.java b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/user/request/UsersSearchRestRequest.java
index 7cf37e83c41..d607fe4f6b8 100644
--- a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/user/request/UsersSearchRestRequest.java
+++ b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/user/request/UsersSearchRestRequest.java
@@ -19,6 +19,8 @@
*/
package org.sonar.server.v2.api.user.request;
+import io.swagger.v3.oas.annotations.extensions.Extension;
+import io.swagger.v3.oas.annotations.extensions.ExtensionProperty;
import io.swagger.v3.oas.annotations.media.Schema;
import javax.annotation.Nullable;
@@ -62,7 +64,12 @@ public record UsersSearchRestRequest(
@Schema(description = "Filter users based on the SonarLint last connection date field. Only users that never connected or who interacted with this instance "
+ "using SonarLint at or before the date will be returned. The format must be ISO 8601 datetime format (YYYY-MM-DDThh:mm:ss±hhmm)",
example = "2020-01-01T00:00:00+0100")
- String sonarLintLastConnectionDateTo
+ String sonarLintLastConnectionDateTo,
+
+ @Nullable
+ @Schema(description = "Filter users belonging to group. Only available for system administrators. Using != operator will exclude users from this group.",
+ extensions = @Extension(properties = {@ExtensionProperty(name = "internal", value = "true")}))
+ String groupId
) {
diff --git a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/config/PlatformLevel4WebConfig.java b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/config/PlatformLevel4WebConfig.java
index 95530e69891..42b057f357c 100644
--- a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/config/PlatformLevel4WebConfig.java
+++ b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/config/PlatformLevel4WebConfig.java
@@ -21,6 +21,7 @@ package org.sonar.server.v2.config;
import javax.annotation.Nullable;
import org.sonar.db.DbClient;
+import org.sonar.server.common.group.service.GroupMembershipService;
import org.sonar.server.common.group.service.GroupService;
import org.sonar.server.common.health.CeStatusNodeCheck;
import org.sonar.server.common.health.DbConnectionNodeCheck;
@@ -36,6 +37,8 @@ import org.sonar.server.user.SystemPasscode;
import org.sonar.server.user.UserSession;
import org.sonar.server.v2.api.group.controller.DefaultGroupController;
import org.sonar.server.v2.api.group.controller.GroupController;
+import org.sonar.server.v2.api.membership.controller.DefaultGroupMembershipController;
+import org.sonar.server.v2.api.membership.controller.GroupMembershipController;
import org.sonar.server.v2.api.system.controller.DefaultLivenessController;
import org.sonar.server.v2.api.system.controller.HealthController;
import org.sonar.server.v2.api.system.controller.LivenessController;
@@ -81,8 +84,16 @@ public class PlatformLevel4WebConfig {
}
@Bean
- public GroupController groupController(GroupService groupService, DbClient dbClient, ManagedInstanceChecker managedInstanceChecker, UserSession userSession) {
- return new DefaultGroupController(groupService, dbClient, managedInstanceChecker, userSession);
+ public GroupController groupController(UserSession userSession, DbClient dbClient, GroupService groupService, ManagedInstanceChecker managedInstanceChecker) {
+ return new DefaultGroupController(userSession, dbClient, groupService, managedInstanceChecker);
}
+
+ @Bean
+ public GroupMembershipController groupMembershipsController(UserSession userSession,
+ GroupMembershipService groupMembershipService, ManagedInstanceChecker managedInstanceChecker) {
+ return new DefaultGroupMembershipController(userSession, groupMembershipService, managedInstanceChecker);
+ }
+
+
}
diff --git a/server/sonar-webserver-webapi-v2/src/test/java/org/sonar/server/v2/api/group/controller/DefaultGroupControllerTest.java b/server/sonar-webserver-webapi-v2/src/test/java/org/sonar/server/v2/api/group/controller/DefaultGroupControllerTest.java
index 5ea432b0f79..c736d0236b5 100644
--- a/server/sonar-webserver-webapi-v2/src/test/java/org/sonar/server/v2/api/group/controller/DefaultGroupControllerTest.java
+++ b/server/sonar-webserver-webapi-v2/src/test/java/org/sonar/server/v2/api/group/controller/DefaultGroupControllerTest.java
@@ -80,7 +80,7 @@ public class DefaultGroupControllerTest {
private final DbSession dbSession = mock();
private final ManagedInstanceChecker managedInstanceChecker = mock();
private final MockMvc mockMvc = ControllerTester
- .getMockMvc(new DefaultGroupController(groupService, dbClient, managedInstanceChecker, userSession));
+ .getMockMvc(new DefaultGroupController(userSession, dbClient, groupService, managedInstanceChecker));
@Before
public void setUp() {
@@ -88,6 +88,15 @@ public class DefaultGroupControllerTest {
}
@Test
+ public void fetchGroup_whenNotAnAdmin_shouldThrow() throws Exception {
+ userSession.logIn();
+ mockMvc.perform(get(GROUPS_ENDPOINT + "/" + GROUP_UUID))
+ .andExpectAll(
+ status().isForbidden(),
+ content().json("{\"message\":\"Insufficient privileges\"}"));
+ }
+
+ @Test
public void fetchGroup_whenGroupExists_returnsTheGroup() throws Exception {
GroupDto groupDto = new GroupDto().setUuid(GROUP_UUID).setName("name").setDescription("description");
@@ -262,13 +271,13 @@ public class DefaultGroupControllerTest {
when(groupService.updateGroup(dbSession, groupDto, newName, newDescription)).thenReturn(newGroupInformation);
MvcResult mvcResult = mockMvc.perform(
- patch(GROUPS_ENDPOINT + "/" + GROUP_UUID).contentType(JSON_MERGE_PATCH_CONTENT_TYPE).content(
- """
- {
- "name": "%s",
- "description": %s
- }
- """.formatted(newName, newDescription == null ? "null" : "\"" + newDescription + "\"")))
+ patch(GROUPS_ENDPOINT + "/" + GROUP_UUID).contentType(JSON_MERGE_PATCH_CONTENT_TYPE).content(
+ """
+ {
+ "name": "%s",
+ "description": %s
+ }
+ """.formatted(newName, newDescription == null ? "null" : "\"" + newDescription + "\"")))
.andExpect(status().isOk())
.andReturn();
@@ -384,7 +393,7 @@ public class DefaultGroupControllerTest {
public void search_whenCallerIsNotAdmin_shouldReturnForbidden() throws Exception {
userSession.logIn().setNonSystemAdministrator();
mockMvc.perform(
- get(GROUPS_ENDPOINT + "/" + GROUP_UUID))
+ get(GROUPS_ENDPOINT))
.andExpectAll(
status().isForbidden(),
content().json("{\"message\":\"Insufficient privileges\"}"));
diff --git a/server/sonar-webserver-webapi-v2/src/test/java/org/sonar/server/v2/api/membership/controller/DefaultGroupMembershipControllerTest.java b/server/sonar-webserver-webapi-v2/src/test/java/org/sonar/server/v2/api/membership/controller/DefaultGroupMembershipControllerTest.java
new file mode 100644
index 00000000000..7dfa7b9ccc0
--- /dev/null
+++ b/server/sonar-webserver-webapi-v2/src/test/java/org/sonar/server/v2/api/membership/controller/DefaultGroupMembershipControllerTest.java
@@ -0,0 +1,246 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.server.v2.api.membership.controller;
+
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import java.util.List;
+import java.util.Map;
+import org.junit.Rule;
+import org.junit.Test;
+import org.sonar.db.user.UserGroupDto;
+import org.sonar.server.common.SearchResults;
+import org.sonar.server.common.group.service.GroupMembershipSearchRequest;
+import org.sonar.server.common.group.service.GroupMembershipService;
+import org.sonar.server.common.management.ManagedInstanceChecker;
+import org.sonar.server.exceptions.BadRequestException;
+import org.sonar.server.tester.UserSessionRule;
+import org.sonar.server.v2.api.ControllerTester;
+import org.sonar.server.v2.api.membership.response.GroupsMembershipSearchRestResponse;
+import org.sonar.server.v2.api.membership.response.GroupMembershipRestResponse;
+import org.springframework.http.MediaType;
+import org.springframework.test.web.servlet.MockMvc;
+import org.springframework.test.web.servlet.MvcResult;
+
+import static java.util.function.Function.identity;
+import static java.util.stream.Collectors.toMap;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+import static org.sonar.server.v2.WebApiEndpoints.GROUP_MEMBERSHIPS_ENDPOINT;
+import static org.sonar.server.v2.api.model.RestPage.DEFAULT_PAGE_INDEX;
+import static org.sonar.server.v2.api.model.RestPage.DEFAULT_PAGE_SIZE;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+public class DefaultGroupMembershipControllerTest {
+ private static final String GROUP_UUID = "1234";
+ private static final String GROUP_MEMBERSHIP_UUID = "1234";
+ private static final String USER_UUID = "abcd";
+ private static final String CREATE_PAYLOAD = """
+ {
+ "userId": "%s",
+ "groupId": "%s"
+ }
+ """.formatted(USER_UUID, GROUP_UUID);
+
+ private static final Gson GSON = new GsonBuilder().create();
+
+ @Rule
+ public UserSessionRule userSession = UserSessionRule.standalone();
+ private final GroupMembershipService groupMembershipService = mock();
+ private final ManagedInstanceChecker managedInstanceChecker = mock();
+
+ private final MockMvc mockMvc = ControllerTester
+ .getMockMvc(new DefaultGroupMembershipController(userSession, groupMembershipService, managedInstanceChecker));
+
+ @Test
+ public void create_whenCallersIsNotAdmin_shouldReturnForbidden() throws Exception {
+ userSession.logIn().setNonSystemAdministrator();
+ mockMvc.perform(
+ post(GROUP_MEMBERSHIPS_ENDPOINT)
+ .contentType(MediaType.APPLICATION_JSON_VALUE)
+ .content(CREATE_PAYLOAD))
+ .andExpectAll(
+ status().isForbidden(),
+ content().json("{\"message\":\"Insufficient privileges\"}"));
+ }
+
+ @Test
+ public void create_whenInstanceIsManaged_shouldReturnException() throws Exception {
+ userSession.logIn().setSystemAdministrator();
+ doThrow(BadRequestException.create("the instance is managed")).when(managedInstanceChecker).throwIfInstanceIsManaged();
+ mockMvc.perform(
+ post(GROUP_MEMBERSHIPS_ENDPOINT)
+ .contentType(MediaType.APPLICATION_JSON_VALUE)
+ .content(CREATE_PAYLOAD))
+ .andExpectAll(
+ status().isBadRequest(),
+ content().json("{\"message\":\"the instance is managed\"}"));
+ }
+
+ @Test
+ public void create_whenUserIsAnAdmin_shouldReturnCreatedGroup() throws Exception {
+ userSession.logIn().setSystemAdministrator();
+
+ UserGroupDto userGroupDto = new UserGroupDto().setUuid(GROUP_MEMBERSHIP_UUID).setGroupUuid(GROUP_UUID).setUserUuid(USER_UUID);
+
+ when(groupMembershipService.addMembership(GROUP_UUID, USER_UUID)).thenReturn(userGroupDto);
+
+ mockMvc.perform(
+ post(GROUP_MEMBERSHIPS_ENDPOINT)
+ .contentType(MediaType.APPLICATION_JSON_VALUE)
+ .content(CREATE_PAYLOAD))
+ .andExpectAll(
+ status().isCreated(),
+ content().json("""
+ {
+ "id": "%s",
+ "userId": "%s",
+ "groupId": "%s"
+ }
+
+ """.formatted(GROUP_MEMBERSHIP_UUID, USER_UUID, GROUP_UUID)));
+ }
+
+ @Test
+ public void delete_whenCallersIsNotAdmin_shouldReturnForbidden() throws Exception {
+ userSession.logIn().setNonSystemAdministrator();
+ mockMvc.perform(
+ delete(GROUP_MEMBERSHIPS_ENDPOINT + "/" + GROUP_MEMBERSHIP_UUID)
+ .contentType(MediaType.APPLICATION_JSON_VALUE)
+ .content(CREATE_PAYLOAD))
+ .andExpectAll(
+ status().isForbidden(),
+ content().json("{\"message\":\"Insufficient privileges\"}"));
+ }
+
+ @Test
+ public void delete_whenInstanceIsManaged_shouldReturnException() throws Exception {
+ userSession.logIn().setSystemAdministrator();
+ doThrow(BadRequestException.create("the instance is managed")).when(managedInstanceChecker).throwIfInstanceIsManaged();
+ mockMvc.perform(
+ delete(GROUP_MEMBERSHIPS_ENDPOINT + "/" + GROUP_MEMBERSHIP_UUID)
+ .contentType(MediaType.APPLICATION_JSON_VALUE)
+ .content(CREATE_PAYLOAD))
+ .andExpectAll(
+ status().isBadRequest(),
+ content().json("{\"message\":\"the instance is managed\"}"));
+ }
+
+ @Test
+ public void delete_whenUserIsAnAdmin_shouldDelete() throws Exception {
+ userSession.logIn().setSystemAdministrator();
+ UserGroupDto userGroupDto = new UserGroupDto().setUuid(GROUP_MEMBERSHIP_UUID).setGroupUuid(GROUP_UUID).setUserUuid(USER_UUID);
+
+ mockMvc.perform(
+ delete(GROUP_MEMBERSHIPS_ENDPOINT + "/" + GROUP_MEMBERSHIP_UUID)
+ .contentType(MediaType.APPLICATION_JSON_VALUE)
+ .content(CREATE_PAYLOAD))
+ .andExpectAll(
+ status().isNoContent(),
+ content().string("")
+ );
+
+ verify(groupMembershipService).removeMembership(GROUP_MEMBERSHIP_UUID);
+ }
+
+ @Test
+ public void search_whenCallerIsNotAdmin_shouldReturnForbidden() throws Exception {
+ userSession.logIn().setNonSystemAdministrator();
+ mockMvc.perform(
+ get(GROUP_MEMBERSHIPS_ENDPOINT))
+ .andExpectAll(
+ status().isForbidden(),
+ content().json("{\"message\":\"Insufficient privileges\"}"));
+ }
+
+ @Test
+ public void search_whenNoParameters_shouldUseDefaultAndForwardToGroupMembershipService() throws Exception {
+ userSession.logIn().setSystemAdministrator();
+ when(groupMembershipService.searchMembers(any(), anyInt(), anyInt())).thenReturn(new SearchResults<>(List.of(), 0));
+
+ mockMvc.perform(get(GROUP_MEMBERSHIPS_ENDPOINT)).andExpect(status().isOk());
+
+ verify(groupMembershipService).searchMembers(new GroupMembershipSearchRequest(null, null), Integer.parseInt(DEFAULT_PAGE_INDEX), Integer.parseInt(DEFAULT_PAGE_SIZE));
+ }
+
+ @Test
+ public void search_whenParametersUsed_shouldForwardWithParameters() throws Exception {
+ userSession.logIn().setSystemAdministrator();
+ when(groupMembershipService.searchMembers(any(), anyInt(), anyInt())).thenReturn(new SearchResults<>(List.of(), 0));
+
+ mockMvc.perform(get(GROUP_MEMBERSHIPS_ENDPOINT)
+ .param("userId", USER_UUID)
+ .param("groupId", GROUP_UUID)
+ .param("pageSize", "100")
+ .param("pageIndex", "2"))
+ .andExpect(status().isOk());
+
+ verify(groupMembershipService).searchMembers(new GroupMembershipSearchRequest(GROUP_UUID, USER_UUID), 2, 100);
+ }
+
+ @Test
+ public void search_whenGroupMembershipServiceReturnGroupMemberships_shouldReturnThem() throws Exception {
+ userSession.logIn().setSystemAdministrator();
+
+ UserGroupDto userGroupDto1 = generateUserGroupDto("1");
+ UserGroupDto userGroupDto2 = generateUserGroupDto("2");
+ UserGroupDto userGroupDto3 = generateUserGroupDto("3");
+ List<UserGroupDto> groups = List.of(userGroupDto1, userGroupDto2, userGroupDto3);
+ SearchResults<UserGroupDto> searchResult = new SearchResults<>(groups, groups.size());
+ when(groupMembershipService.searchMembers(any(), anyInt(), anyInt())).thenReturn(searchResult);
+
+ MvcResult mvcResult = mockMvc.perform(get(GROUP_MEMBERSHIPS_ENDPOINT))
+ .andExpect(status().isOk())
+ .andReturn();
+
+ GroupsMembershipSearchRestResponse actualGroupsSearchRestResponse = GSON.fromJson(mvcResult.getResponse().getContentAsString(), GroupsMembershipSearchRestResponse.class);
+
+ Map<String, GroupMembershipRestResponse> groupIdToGroupResponse = actualGroupsSearchRestResponse.groupMemberships().stream()
+ .collect(toMap(GroupMembershipRestResponse::id, identity()));
+ assertResponseContains(groupIdToGroupResponse, userGroupDto1);
+ assertResponseContains(groupIdToGroupResponse, userGroupDto2);
+ assertResponseContains(groupIdToGroupResponse, userGroupDto3);
+
+ assertThat(actualGroupsSearchRestResponse.page().pageIndex()).hasToString(DEFAULT_PAGE_INDEX);
+ assertThat(actualGroupsSearchRestResponse.page().pageSize()).hasToString(DEFAULT_PAGE_SIZE);
+ assertThat(actualGroupsSearchRestResponse.page().total()).isEqualTo(groups.size());
+
+ }
+
+ private void assertResponseContains(Map<String, GroupMembershipRestResponse> groupIdToGroupResponse, UserGroupDto expectedUserGroupDto) {
+ GroupMembershipRestResponse restGroupMembership = groupIdToGroupResponse.get(expectedUserGroupDto.getUuid());
+ assertThat(restGroupMembership).isNotNull();
+ assertThat(restGroupMembership.groupId()).isEqualTo(expectedUserGroupDto.getGroupUuid());
+ assertThat(restGroupMembership.userId()).isEqualTo(expectedUserGroupDto.getUserUuid());
+ }
+
+ private UserGroupDto generateUserGroupDto(String id) {
+ return new UserGroupDto().setUuid(id).setGroupUuid("uuid_"+id).setUserUuid("user_"+id);
+ }
+}
diff --git a/server/sonar-webserver-webapi-v2/src/test/java/org/sonar/server/v2/api/user/controller/DefaultUserControllerTest.java b/server/sonar-webserver-webapi-v2/src/test/java/org/sonar/server/v2/api/user/controller/DefaultUserControllerTest.java
index f055935a9a7..ed920b4830b 100644
--- a/server/sonar-webserver-webapi-v2/src/test/java/org/sonar/server/v2/api/user/controller/DefaultUserControllerTest.java
+++ b/server/sonar-webserver-webapi-v2/src/test/java/org/sonar/server/v2/api/user/controller/DefaultUserControllerTest.java
@@ -110,6 +110,8 @@ public class DefaultUserControllerTest {
.param("sonarQubeLastConnectionDateTo", "2020-01-01T00:00:00+0100")
.param("sonarLintLastConnectionDateFrom", "2020-01-01T00:00:00+0100")
.param("sonarLintLastConnectionDateTo", "2020-01-01T00:00:00+0100")
+ .param("groupId", "groupId1")
+ .param("groupId!", "groupId2")
.param("pageSize", "100")
.param("pageIndex", "2"))
.andExpect(status().isOk());
@@ -125,6 +127,8 @@ public class DefaultUserControllerTest {
assertThat(requestCaptor.getValue().getLastConnectionDateTo()).contains(parseOffsetDateTime("2020-01-01T00:00:00+0100"));
assertThat(requestCaptor.getValue().getSonarLintLastConnectionDateFrom()).contains(parseOffsetDateTime("2020-01-01T00:00:00+0100"));
assertThat(requestCaptor.getValue().getSonarLintLastConnectionDateTo()).contains(parseOffsetDateTime("2020-01-01T00:00:00+0100"));
+ assertThat(requestCaptor.getValue().getGroupUuid()).contains("groupId1");
+ assertThat(requestCaptor.getValue().getExcludedGroupUuid()).contains("groupId2");
assertThat(requestCaptor.getValue().getPageSize()).isEqualTo(100);
assertThat(requestCaptor.getValue().getPage()).isEqualTo(2);
}
@@ -160,6 +164,18 @@ public class DefaultUserControllerTest {
.andExpectAll(
status().isForbidden(),
content().string("{\"message\":\"Parameter externalIdentity requires Administer System permission.\"}"));
+
+ mockMvc.perform(get(USER_ENDPOINT)
+ .param("groupId", "groupId"))
+ .andExpectAll(
+ status().isForbidden(),
+ content().string("{\"message\":\"Parameter groupId requires Administer System permission.\"}"));
+
+ mockMvc.perform(get(USER_ENDPOINT)
+ .param("groupId!", "groupId"))
+ .andExpectAll(
+ status().isForbidden(),
+ content().string("{\"message\":\"Parameter groupId! requires Administer System permission.\"}"));
}
@Test
diff --git a/server/sonar-webserver-webapi/src/it/java/org/sonar/server/usergroups/ws/AddUserActionIT.java b/server/sonar-webserver-webapi/src/it/java/org/sonar/server/usergroups/ws/AddUserActionIT.java
index 99f386075fa..897895254b5 100644
--- a/server/sonar-webserver-webapi/src/it/java/org/sonar/server/usergroups/ws/AddUserActionIT.java
+++ b/server/sonar-webserver-webapi/src/it/java/org/sonar/server/usergroups/ws/AddUserActionIT.java
@@ -27,10 +27,11 @@ import org.sonar.api.server.ws.WebService.Action;
import org.sonar.db.DbTester;
import org.sonar.db.user.GroupDto;
import org.sonar.db.user.UserDto;
+import org.sonar.server.common.group.service.GroupMembershipService;
+import org.sonar.server.common.management.ManagedInstanceChecker;
import org.sonar.server.exceptions.BadRequestException;
import org.sonar.server.exceptions.NotFoundException;
import org.sonar.server.exceptions.UnauthorizedException;
-import org.sonar.server.common.management.ManagedInstanceChecker;
import org.sonar.server.tester.UserSessionRule;
import org.sonar.server.usergroups.DefaultGroupFinder;
import org.sonar.server.ws.TestRequest;
@@ -39,6 +40,7 @@ import org.sonar.server.ws.WsActionTester;
import static java.net.HttpURLConnection.HTTP_NO_CONTENT;
import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.assertj.core.api.Assertions.tuple;
import static org.mockito.Mockito.doThrow;
@@ -56,7 +58,10 @@ public class AddUserActionIT {
private ManagedInstanceChecker managedInstanceChecker = mock(ManagedInstanceChecker.class);
- private final WsActionTester ws = new WsActionTester(new AddUserAction(db.getDbClient(), userSession, newGroupWsSupport(), managedInstanceChecker));
+ private GroupMembershipService groupMembershipService = new GroupMembershipService(db.getDbClient(), db.getDbClient().userGroupDao(), db.getDbClient().userDao(), db.getDbClient()
+ .groupDao());
+
+ private final WsActionTester ws = new WsActionTester(new AddUserAction(db.getDbClient(), userSession, newGroupWsSupport(), managedInstanceChecker, groupMembershipService));
@Test
public void verify_definition() {
@@ -70,7 +75,6 @@ public class AddUserActionIT {
tuple("8.4", "Parameter 'id' is deprecated. Format changes from integer to string. Use 'name' instead."));
}
-
@Test
public void add_user_to_group_referenced_by_its_name() {
insertDefaultGroup();
@@ -104,20 +108,19 @@ public class AddUserActionIT {
}
@Test
- public void do_not_fail_if_user_is_already_member_of_group() {
+ public void fail_if_user_is_already_member_of_group() {
insertDefaultGroup();
- GroupDto users = db.users().insertGroup();
- UserDto user = db.users().insertUser();
- db.users().insertMember(users, user);
+ GroupDto group = db.users().insertGroup(g -> g.setName("group1"));
+ UserDto user = db.users().insertUser(u -> u.setLogin("user1"));
+ db.users().insertMember(group, user);
loginAsAdmin();
- newRequest()
- .setParam(PARAM_GROUP_NAME, users.getName())
- .setParam(PARAM_LOGIN, user.getLogin())
- .execute();
+ TestRequest testRequest = newRequest()
+ .setParam(PARAM_GROUP_NAME, group.getName())
+ .setParam(PARAM_LOGIN, user.getLogin());
- // do not insert duplicated row
- assertThat(db.users().selectGroupUuidsOfUser(user)).hasSize(1).containsOnly(users.getUuid());
+ assertThatIllegalArgumentException().isThrownBy(testRequest::execute)
+ .withMessage("User 'user1' is already a member of group 'group1'");
}
@Test
@@ -205,20 +208,6 @@ public class AddUserActionIT {
}
@Test
- public void fail_when_no_default_group() {
- GroupDto group = db.users().insertGroup();
- UserDto user = db.users().insertUser();
- loginAsAdmin();
- TestRequest request = newRequest()
- .setParam(PARAM_LOGIN, user.getLogin())
- .setParam(PARAM_GROUP_NAME, group.getName());
-
- assertThatThrownBy(request::execute)
- .isInstanceOf(IllegalStateException.class)
- .hasMessage("Default group cannot be found");
- }
-
- @Test
public void fail_if_instance_is_externally_managed() {
loginAsAdmin();
BadRequestException exception = BadRequestException.create("Not allowed");
diff --git a/server/sonar-webserver-webapi/src/it/java/org/sonar/server/usergroups/ws/RemoveUserActionIT.java b/server/sonar-webserver-webapi/src/it/java/org/sonar/server/usergroups/ws/RemoveUserActionIT.java
index 69a3c6913db..0519c80e3ad 100644
--- a/server/sonar-webserver-webapi/src/it/java/org/sonar/server/usergroups/ws/RemoveUserActionIT.java
+++ b/server/sonar-webserver-webapi/src/it/java/org/sonar/server/usergroups/ws/RemoveUserActionIT.java
@@ -27,10 +27,11 @@ import org.sonar.api.server.ws.WebService.Action;
import org.sonar.db.DbTester;
import org.sonar.db.user.GroupDto;
import org.sonar.db.user.UserDto;
+import org.sonar.server.common.group.service.GroupMembershipService;
+import org.sonar.server.common.management.ManagedInstanceChecker;
import org.sonar.server.exceptions.BadRequestException;
import org.sonar.server.exceptions.ForbiddenException;
import org.sonar.server.exceptions.NotFoundException;
-import org.sonar.server.common.management.ManagedInstanceChecker;
import org.sonar.server.tester.UserSessionRule;
import org.sonar.server.usergroups.DefaultGroupFinder;
import org.sonar.server.ws.TestRequest;
@@ -55,8 +56,12 @@ public class RemoveUserActionIT {
public UserSessionRule userSession = UserSessionRule.standalone();
private ManagedInstanceChecker managedInstanceChecker = mock(ManagedInstanceChecker.class);
+
+ private final GroupMembershipService groupMembershipService = new GroupMembershipService(db.getDbClient(), db.getDbClient().userGroupDao(), db.getDbClient().userDao(),
+ db.getDbClient().groupDao());
private final WsActionTester ws = new WsActionTester(
- new RemoveUserAction(db.getDbClient(), userSession, new GroupWsSupport(db.getDbClient(), new DefaultGroupFinder(db.getDbClient())), managedInstanceChecker));
+ new RemoveUserAction(db.getDbClient(), userSession, new GroupWsSupport(db.getDbClient(), new DefaultGroupFinder(db.getDbClient())), managedInstanceChecker,
+ groupMembershipService));
@Test
public void verify_definition() {
diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/usergroups/ws/AddUserAction.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/usergroups/ws/AddUserAction.java
index abc7d8577fd..206c0a4540a 100644
--- a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/usergroups/ws/AddUserAction.java
+++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/usergroups/ws/AddUserAction.java
@@ -28,7 +28,7 @@ import org.sonar.db.DbClient;
import org.sonar.db.DbSession;
import org.sonar.db.user.GroupDto;
import org.sonar.db.user.UserDto;
-import org.sonar.db.user.UserGroupDto;
+import org.sonar.server.common.group.service.GroupMembershipService;
import org.sonar.server.common.management.ManagedInstanceChecker;
import org.sonar.server.user.UserSession;
@@ -47,19 +47,23 @@ public class AddUserAction implements UserGroupsWsAction {
private final GroupWsSupport support;
private final ManagedInstanceChecker managedInstanceChecker;
- public AddUserAction(DbClient dbClient, UserSession userSession, GroupWsSupport support, ManagedInstanceChecker managedInstanceChecker) {
+ private final GroupMembershipService groupMembershipService;
+
+ public AddUserAction(DbClient dbClient, UserSession userSession, GroupWsSupport support, ManagedInstanceChecker managedInstanceChecker,
+ GroupMembershipService groupMembershipService) {
this.dbClient = dbClient;
this.userSession = userSession;
this.support = support;
this.managedInstanceChecker = managedInstanceChecker;
+ this.groupMembershipService = groupMembershipService;
}
@Override
public void define(NewController context) {
NewAction action = context.createAction("add_user")
.setDescription(format("Add a user to a group.<br />" +
- "'%s' must be provided.<br />" +
- "Requires the following permission: 'Administer System'.", PARAM_GROUP_NAME))
+ "'%s' must be provided.<br />" +
+ "Requires the following permission: 'Administer System'.", PARAM_GROUP_NAME))
.setHandler(this)
.setPost(true)
.setSince("5.2")
@@ -79,22 +83,14 @@ public class AddUserAction implements UserGroupsWsAction {
GroupDto group = support.findGroupDto(dbSession, request);
String login = request.mandatoryParam(PARAM_LOGIN);
- UserDto user = dbClient.userDao().selectActiveUserByLogin(dbSession, login);
+ UserDto user = dbClient.userDao().selectByLogin(dbSession, login);
checkFound(user, "Could not find a user with login '%s'", login);
- support.checkGroupIsNotDefault(dbSession, group);
-
- if (!isMemberOf(dbSession, user, group)) {
- UserGroupDto membershipDto = new UserGroupDto().setGroupUuid(group.getUuid()).setUserUuid(user.getUuid());
- dbClient.userGroupDao().insert(dbSession, membershipDto, group.getName(), login);
- dbSession.commit();
- }
+ groupMembershipService.addMembership(group.getUuid(), user.getUuid());
+ dbSession.commit();
response.noContent();
}
}
- private boolean isMemberOf(DbSession dbSession, UserDto user, GroupDto group) {
- return dbClient.groupMembershipDao().selectGroupUuidsByUserUuid(dbSession, user.getUuid()).contains(group.getUuid());
- }
}
diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/usergroups/ws/RemoveUserAction.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/usergroups/ws/RemoveUserAction.java
index c0ca2d56bc8..eb5227df95d 100644
--- a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/usergroups/ws/RemoveUserAction.java
+++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/usergroups/ws/RemoveUserAction.java
@@ -29,11 +29,11 @@ import org.sonar.db.DbSession;
import org.sonar.db.permission.GlobalPermission;
import org.sonar.db.user.GroupDto;
import org.sonar.db.user.UserDto;
+import org.sonar.server.common.group.service.GroupMembershipService;
import org.sonar.server.common.management.ManagedInstanceChecker;
import org.sonar.server.user.UserSession;
import static java.lang.String.format;
-import static org.sonar.server.exceptions.BadRequestException.checkRequest;
import static org.sonar.server.exceptions.NotFoundException.checkFound;
import static org.sonar.server.usergroups.ws.GroupWsSupport.PARAM_GROUP_NAME;
import static org.sonar.server.usergroups.ws.GroupWsSupport.PARAM_LOGIN;
@@ -45,22 +45,24 @@ public class RemoveUserAction implements UserGroupsWsAction {
private final DbClient dbClient;
private final UserSession userSession;
private final GroupWsSupport support;
-
private final ManagedInstanceChecker managedInstanceChecker;
+ private final GroupMembershipService groupMembershipService;
- public RemoveUserAction(DbClient dbClient, UserSession userSession, GroupWsSupport support, ManagedInstanceChecker managedInstanceChecker) {
+ public RemoveUserAction(DbClient dbClient, UserSession userSession, GroupWsSupport support, ManagedInstanceChecker managedInstanceChecker,
+ GroupMembershipService groupMembershipService) {
this.dbClient = dbClient;
this.userSession = userSession;
this.support = support;
this.managedInstanceChecker = managedInstanceChecker;
+ this.groupMembershipService = groupMembershipService;
}
@Override
public void define(NewController context) {
NewAction action = context.createAction("remove_user")
.setDescription(format("Remove a user from a group.<br />" +
- "'%s' must be provided.<br>" +
- "Requires the following permission: 'Administer System'.", PARAM_GROUP_NAME))
+ "'%s' must be provided.<br>" +
+ "Requires the following permission: 'Administer System'.", PARAM_GROUP_NAME))
.setHandler(this)
.setPost(true)
.setSince("5.2")
@@ -75,34 +77,16 @@ public class RemoveUserAction implements UserGroupsWsAction {
@Override
public void handle(Request request, Response response) throws Exception {
userSession.checkLoggedIn();
-
+ userSession.checkPermission(GlobalPermission.ADMINISTER);
+ managedInstanceChecker.throwIfInstanceIsManaged();
try (DbSession dbSession = dbClient.openSession(false)) {
- userSession.checkPermission(GlobalPermission.ADMINISTER);
- managedInstanceChecker.throwIfInstanceIsManaged();
- GroupDto group = support.findGroupDto(dbSession, request);
- support.checkGroupIsNotDefault(dbSession, group);
-
- String login = request.mandatoryParam(PARAM_LOGIN);
- UserDto user = getUser(dbSession, login);
-
- ensureLastAdminIsNotRemoved(dbSession, group, user);
-
- dbClient.userGroupDao().delete(dbSession, group, user);
- dbSession.commit();
-
+ GroupDto groupDto = support.findGroupDto(dbSession, request);
+ UserDto userDto = getUser(dbSession, request.mandatoryParam(PARAM_LOGIN));
+ groupMembershipService.removeMembership(groupDto.getUuid(), userDto.getUuid());
response.noContent();
}
}
- /**
- * Ensure that there are still users with admin global permission if user is removed from the group.
- */
- private void ensureLastAdminIsNotRemoved(DbSession dbSession, GroupDto group, UserDto user) {
- int remainingAdmins = dbClient.authorizationDao().countUsersWithGlobalPermissionExcludingGroupMember(dbSession,
- GlobalPermission.ADMINISTER.getKey(), group.getUuid(), user.getUuid());
- checkRequest(remainingAdmins > 0, "The last administrator user cannot be removed");
- }
-
private UserDto getUser(DbSession dbSession, String userLogin) {
return checkFound(dbClient.userDao().selectActiveUserByLogin(dbSession, userLogin),
"User with login '%s' is not found'", userLogin);
diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/usergroups/ws/UserGroupsModule.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/usergroups/ws/UserGroupsModule.java
index 9f92f4881cd..0b27ae1a17e 100644
--- a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/usergroups/ws/UserGroupsModule.java
+++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/usergroups/ws/UserGroupsModule.java
@@ -20,7 +20,6 @@
package org.sonar.server.usergroups.ws;
import org.sonar.core.platform.Module;
-import org.sonar.server.common.group.service.GroupService;
import org.sonar.server.common.management.ManagedInstanceChecker;
public class UserGroupsModule extends Module {
@@ -31,7 +30,6 @@ public class UserGroupsModule extends Module {
UserGroupsWs.class,
GroupWsSupport.class,
ManagedInstanceChecker.class,
- GroupService.class,
// actions
SearchAction.class,
CreateAction.class,
diff --git a/server/sonar-webserver/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel4.java b/server/sonar-webserver/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel4.java
index c55b30243a9..10c159af6f8 100644
--- a/server/sonar-webserver/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel4.java
+++ b/server/sonar-webserver/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel4.java
@@ -31,6 +31,7 @@ import org.sonar.alm.client.github.GithubApplicationClientImpl;
import org.sonar.alm.client.github.GithubApplicationHttpClientImpl;
import org.sonar.alm.client.github.GithubGlobalSettingsValidator;
import org.sonar.alm.client.github.GithubPaginatedHttpClientImpl;
+import org.sonar.alm.client.github.GithubPermissionConverter;
import org.sonar.alm.client.github.RatioBasedRateLimitChecker;
import org.sonar.alm.client.github.config.GithubProvisioningConfigValidator;
import org.sonar.alm.client.github.security.GithubAppSecurityImpl;
@@ -41,7 +42,6 @@ import org.sonar.api.server.rule.RulesDefinitionXmlLoader;
import org.sonar.auth.bitbucket.BitbucketModule;
import org.sonar.auth.github.GitHubModule;
import org.sonar.auth.github.GitHubSettings;
-import org.sonar.alm.client.github.GithubPermissionConverter;
import org.sonar.auth.gitlab.GitLabModule;
import org.sonar.auth.ldap.LdapModule;
import org.sonar.auth.saml.SamlModule;
@@ -78,6 +78,8 @@ import org.sonar.server.branch.ws.BranchWsModule;
import org.sonar.server.ce.CeModule;
import org.sonar.server.ce.projectdump.ProjectExportWsModule;
import org.sonar.server.ce.ws.CeWsModule;
+import org.sonar.server.common.group.service.GroupMembershipService;
+import org.sonar.server.common.group.service.GroupService;
import org.sonar.server.component.ComponentCleanerService;
import org.sonar.server.component.ComponentFinder;
import org.sonar.server.component.ComponentService;
@@ -392,6 +394,8 @@ public class PlatformLevel4 extends PlatformLevel {
new LdapModule(),
new SamlModule(),
new SamlValidationModule(),
+ GroupService.class,
+ GroupMembershipService.class,
DefaultAdminCredentialsVerifierImpl.class,
DefaultAdminCredentialsVerifierNotificationTemplate.class,
DefaultAdminCredentialsVerifierNotificationHandler.class,