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