Ver código fonte

SONAR-21073 Add endpoint /api/v2/authorizations/group-memberships (GET/POST/DELETE)

tags/10.4.0.87286
Wojtek Wajerowicz 6 meses atrás
pai
commit
982713f8d4
53 arquivos alterados com 1925 adições e 107 exclusões
  1. 34
    0
      server/sonar-db-dao/src/it/java/org/sonar/db/user/UserDaoIT.java
  2. 72
    0
      server/sonar-db-dao/src/it/java/org/sonar/db/user/UserGroupDaoIT.java
  3. 15
    1
      server/sonar-db-dao/src/main/java/org/sonar/db/user/UserGroupDao.java
  4. 33
    1
      server/sonar-db-dao/src/main/java/org/sonar/db/user/UserGroupDto.java
  5. 4
    0
      server/sonar-db-dao/src/main/java/org/sonar/db/user/UserGroupMapper.java
  6. 25
    0
      server/sonar-db-dao/src/main/java/org/sonar/db/user/UserGroupQuery.java
  7. 30
    2
      server/sonar-db-dao/src/main/java/org/sonar/db/user/UserQuery.java
  8. 34
    0
      server/sonar-db-dao/src/main/resources/org/sonar/db/user/UserGroupMapper.xml
  9. 6
    0
      server/sonar-db-dao/src/main/resources/org/sonar/db/user/UserMapper.xml
  10. 3
    1
      server/sonar-db-dao/src/schema/schema-sq.ddl
  11. 51
    0
      server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v104/AddUuidColumnToGroupsUsersIT.java
  12. 52
    0
      server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v104/CreatePrimaryKeyOnGroupsUsersTableIT.java
  13. 51
    0
      server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v104/MakeUuidInGroupsUsersNotNullableIT.java
  14. 107
    0
      server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v104/PopulateGroupsUsersUuidIT.java
  15. 1
    1
      server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/step/MassUpdate.java
  16. 55
    0
      server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v104/AddUuidColumnToGroupsUsers.java
  17. 52
    0
      server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v104/CreatePrimaryKeyOnGroupsUsersTable.java
  18. 5
    1
      server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v104/DbVersion104.java
  19. 48
    0
      server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v104/MakeUuidInGroupsUsersNotNullable.java
  20. 69
    0
      server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v104/PopulateGroupsUsersUuid.java
  21. 31
    1
      server/sonar-webserver-common/src/it/java/org/sonar/server/common/user/service/UserServiceIT.java
  22. 29
    0
      server/sonar-webserver-common/src/main/java/org/sonar/server/common/group/service/GroupMembershipSearchRequest.java
  23. 128
    0
      server/sonar-webserver-common/src/main/java/org/sonar/server/common/group/service/GroupMembershipService.java
  24. 5
    1
      server/sonar-webserver-common/src/main/java/org/sonar/server/common/group/service/GroupService.java
  25. 2
    1
      server/sonar-webserver-common/src/main/java/org/sonar/server/common/user/service/UserService.java
  26. 24
    0
      server/sonar-webserver-common/src/main/java/org/sonar/server/common/user/service/UsersSearchRequest.java
  27. 222
    0
      server/sonar-webserver-common/src/test/java/org/sonar/server/common/group/service/GroupMembershipServiceTest.java
  28. 10
    0
      server/sonar-webserver-common/src/test/java/org/sonar/server/common/group/service/GroupServiceTest.java
  29. 1
    0
      server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/WebApiEndpoints.java
  30. 1
    1
      server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/group/controller/DefaultGroupController.java
  31. 3
    4
      server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/group/controller/GroupController.java
  32. 92
    0
      server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/membership/controller/DefaultGroupMembershipController.java
  33. 69
    0
      server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/membership/controller/GroupMembershipController.java
  34. 23
    0
      server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/membership/controller/package-info.java
  35. 32
    0
      server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/membership/request/GroupMembershipCreateRestRequest.java
  36. 36
    0
      server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/membership/request/GroupsMembershipSearchRestRequest.java
  37. 23
    0
      server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/membership/request/package-info.java
  38. 31
    0
      server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/membership/response/GroupMembershipRestResponse.java
  39. 26
    0
      server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/membership/response/GroupsMembershipSearchRestResponse.java
  40. 23
    0
      server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/membership/response/package-info.java
  41. 9
    5
      server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/user/controller/DefaultUserController.java
  42. 6
    0
      server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/user/controller/UserController.java
  43. 8
    1
      server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/user/request/UsersSearchRestRequest.java
  44. 13
    2
      server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/config/PlatformLevel4WebConfig.java
  45. 18
    9
      server/sonar-webserver-webapi-v2/src/test/java/org/sonar/server/v2/api/group/controller/DefaultGroupControllerTest.java
  46. 246
    0
      server/sonar-webserver-webapi-v2/src/test/java/org/sonar/server/v2/api/membership/controller/DefaultGroupMembershipControllerTest.java
  47. 16
    0
      server/sonar-webserver-webapi-v2/src/test/java/org/sonar/server/v2/api/user/controller/DefaultUserControllerTest.java
  48. 16
    27
      server/sonar-webserver-webapi/src/it/java/org/sonar/server/usergroups/ws/AddUserActionIT.java
  49. 7
    2
      server/sonar-webserver-webapi/src/it/java/org/sonar/server/usergroups/ws/RemoveUserActionIT.java
  50. 11
    15
      server/sonar-webserver-webapi/src/main/java/org/sonar/server/usergroups/ws/AddUserAction.java
  51. 12
    28
      server/sonar-webserver-webapi/src/main/java/org/sonar/server/usergroups/ws/RemoveUserAction.java
  52. 0
    2
      server/sonar-webserver-webapi/src/main/java/org/sonar/server/usergroups/ws/UserGroupsModule.java
  53. 5
    1
      server/sonar-webserver/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel4.java

+ 34
- 0
server/sonar-db-dao/src/it/java/org/sonar/db/user/UserDaoIT.java Ver arquivo

@@ -266,6 +266,40 @@ public class UserDaoIT {
assertThat(underTest.countUsers(session, query)).isEqualTo(2);
}

@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();

+ 72
- 0
server/sonar-db-dao/src/it/java/org/sonar/db/user/UserGroupDaoIT.java Ver arquivo

@@ -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);
}

}

+ 15
- 1
server/sonar-db-dao/src/main/java/org/sonar/db/user/UserGroupDao.java Ver arquivo

@@ -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());


+ 33
- 1
server/sonar-db-dao/src/main/java/org/sonar/db/user/UserGroupDto.java Ver arquivo

@@ -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);
}
}

+ 4
- 0
server/sonar-db-dao/src/main/java/org/sonar/db/user/UserGroupMapper.java Ver arquivo

@@ -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);
}

+ 25
- 0
server/sonar-db-dao/src/main/java/org/sonar/db/user/UserGroupQuery.java Ver arquivo

@@ -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) {
}

+ 30
- 2
server/sonar-db-dao/src/main/java/org/sonar/db/user/UserQuery.java Ver arquivo

@@ -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);
}
}
}

+ 34
- 0
server/sonar-db-dao/src/main/resources/org/sonar/db/user/UserGroupMapper.xml Ver arquivo

@@ -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

+ 6
- 0
server/sonar-db-dao/src/main/resources/org/sonar/db/user/UserMapper.xml Ver arquivo

@@ -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>


+ 3
- 1
server/sonar-db-dao/src/schema/schema-sq.ddl Ver arquivo

@@ -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);

+ 51
- 0
server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v104/AddUuidColumnToGroupsUsersIT.java Ver arquivo

@@ -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();
}
}

+ 52
- 0
server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v104/CreatePrimaryKeyOnGroupsUsersTableIT.java Ver arquivo

@@ -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);
}

}

+ 51
- 0
server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v104/MakeUuidInGroupsUsersNotNullableIT.java Ver arquivo

@@ -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);
}
}

+ 107
- 0
server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v104/PopulateGroupsUsersUuidIT.java Ver arquivo

@@ -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);
}



}

+ 1
- 1
server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/step/MassUpdate.java Ver arquivo

@@ -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;
}

+ 55
- 0
server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v104/AddUuidColumnToGroupsUsers.java Ver arquivo

@@ -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());
}
}
}
}

+ 52
- 0
server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v104/CreatePrimaryKeyOnGroupsUsersTable.java Ver arquivo

@@ -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());
}
}
}

+ 5
- 1
server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v104/DbVersion104.java Ver arquivo

@@ -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);
}
}

+ 48
- 0
server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v104/MakeUuidInGroupsUsersNotNullable.java Ver arquivo

@@ -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());

}
}

+ 69
- 0
server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v104/PopulateGroupsUsersUuid.java Ver arquivo

@@ -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;
});
}
}
}

+ 31
- 1
server/sonar-webserver-common/src/it/java/org/sonar/server/common/user/service/UserServiceIT.java Ver arquivo

@@ -162,6 +162,36 @@ public class UserServiceIT {
.containsExactly(user.getLogin());
}

@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();
}

+ 29
- 0
server/sonar-webserver-common/src/main/java/org/sonar/server/common/group/service/GroupMembershipSearchRequest.java Ver arquivo

@@ -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
) {

}

+ 128
- 0
server/sonar-webserver-common/src/main/java/org/sonar/server/common/group/service/GroupMembershipService.java Ver arquivo

@@ -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");
}

}

+ 5
- 1
server/sonar-webserver-common/src/main/java/org/sonar/server/common/group/service/GroupService.java Ver arquivo

@@ -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);
}


+ 2
- 1
server/sonar-webserver-common/src/main/java/org/sonar/server/common/user/service/UserService.java Ver arquivo

@@ -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)

+ 24
- 0
server/sonar-webserver-common/src/main/java/org/sonar/server/common/user/service/UsersSearchRequest.java Ver arquivo

@@ -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);
}

+ 222
- 0
server/sonar-webserver-common/src/test/java/org/sonar/server/common/group/service/GroupMembershipServiceTest.java Ver arquivo

@@ -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);
}

}

+ 10
- 0
server/sonar-webserver-common/src/test/java/org/sonar/server/common/group/service/GroupServiceTest.java Ver arquivo

@@ -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() {

+ 1
- 0
server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/WebApiEndpoints.java Ver arquivo

@@ -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() {
}

+ 1
- 1
server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/group/controller/DefaultGroupController.java Ver arquivo

@@ -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;

+ 3
- 4
server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/group/controller/GroupController.java Ver arquivo

@@ -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);
}

+ 92
- 0
server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/membership/controller/DefaultGroupMembershipController.java Ver arquivo

@@ -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());
}

}

+ 69
- 0
server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/membership/controller/GroupMembershipController.java Ver arquivo

@@ -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);


}

+ 23
- 0
server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/membership/controller/package-info.java Ver arquivo

@@ -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;

+ 32
- 0
server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/membership/request/GroupMembershipCreateRestRequest.java Ver arquivo

@@ -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

) {}

+ 36
- 0
server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/membership/request/GroupsMembershipSearchRestRequest.java Ver arquivo

@@ -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

) {

}

+ 23
- 0
server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/membership/request/package-info.java Ver arquivo

@@ -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;

+ 31
- 0
server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/membership/response/GroupMembershipRestResponse.java Ver arquivo

@@ -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) {
}

+ 26
- 0
server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/membership/response/GroupsMembershipSearchRestResponse.java Ver arquivo

@@ -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) {
}

+ 23
- 0
server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/membership/response/package-info.java Ver arquivo

@@ -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;

+ 9
- 5
server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/user/controller/DefaultUserController.java Ver arquivo

@@ -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();

+ 6
- 0
server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/user/controller/UserController.java Ver arquivo

@@ -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}")

+ 8
- 1
server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/user/request/UsersSearchRestRequest.java Ver arquivo

@@ -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

) {


+ 13
- 2
server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/config/PlatformLevel4WebConfig.java Ver arquivo

@@ -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);
}


}

+ 18
- 9
server/sonar-webserver-webapi-v2/src/test/java/org/sonar/server/v2/api/group/controller/DefaultGroupControllerTest.java Ver arquivo

@@ -80,13 +80,22 @@ 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() {
when(dbClient.openSession(false)).thenReturn(dbSession);
}

@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 {

@@ -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\"}"));

+ 246
- 0
server/sonar-webserver-webapi-v2/src/test/java/org/sonar/server/v2/api/membership/controller/DefaultGroupMembershipControllerTest.java Ver arquivo

@@ -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);
}
}

+ 16
- 0
server/sonar-webserver-webapi-v2/src/test/java/org/sonar/server/v2/api/user/controller/DefaultUserControllerTest.java Ver arquivo

@@ -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

+ 16
- 27
server/sonar-webserver-webapi/src/it/java/org/sonar/server/usergroups/ws/AddUserActionIT.java Ver arquivo

@@ -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
@@ -204,20 +207,6 @@ public class AddUserActionIT {
.hasMessage("Default group 'sonar-users' cannot be used to perform this action");
}

@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();

+ 7
- 2
server/sonar-webserver-webapi/src/it/java/org/sonar/server/usergroups/ws/RemoveUserActionIT.java Ver arquivo

@@ -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() {

+ 11
- 15
server/sonar-webserver-webapi/src/main/java/org/sonar/server/usergroups/ws/AddUserAction.java Ver arquivo

@@ -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());
}
}

+ 12
- 28
server/sonar-webserver-webapi/src/main/java/org/sonar/server/usergroups/ws/RemoveUserAction.java Ver arquivo

@@ -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);

+ 0
- 2
server/sonar-webserver-webapi/src/main/java/org/sonar/server/usergroups/ws/UserGroupsModule.java Ver arquivo

@@ -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,

+ 5
- 1
server/sonar-webserver/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel4.java Ver arquivo

@@ -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,

Carregando…
Cancelar
Salvar