--- /dev/null
+/*
+ * 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.Optional;
+import javax.annotation.Nullable;
+import org.sonar.api.security.DefaultGroups;
+import org.sonar.api.server.ServerSide;
+import org.sonar.api.user.UserGroupValidation;
+import org.sonar.core.util.UuidFactory;
+import org.sonar.db.DbClient;
+import org.sonar.db.DbSession;
+import org.sonar.db.permission.GlobalPermission;
+import org.sonar.db.user.GroupDto;
+import org.sonar.server.exceptions.BadRequestException;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static org.sonar.server.exceptions.BadRequestException.checkRequest;
+
+@ServerSide
+public class GroupService {
+
+ private final DbClient dbClient;
+ private final UuidFactory uuidFactory;
+
+ public GroupService(DbClient dbClient, UuidFactory uuidFactory) {
+ this.dbClient = dbClient;
+ this.uuidFactory = uuidFactory;
+ }
+
+ public Optional<GroupDto> findGroup(DbSession dbSession, String groupName) {
+ return dbClient.groupDao().selectByName(dbSession, groupName);
+ }
+
+ public Optional<GroupDto> findGroupByUuid(DbSession dbSession, String groupUuid) {
+ return Optional.ofNullable(dbClient.groupDao().selectByUuid(dbSession, groupUuid));
+ }
+
+ public void delete(DbSession dbSession, GroupDto group) {
+ checkGroupIsNotDefault(dbSession, group);
+ checkNotTryingToDeleteLastAdminGroup(dbSession, group);
+
+ removeGroupPermissions(dbSession, group);
+ removeGroupFromPermissionTemplates(dbSession, group);
+ removeGroupMembers(dbSession, group);
+ removeGroupFromQualityProfileEdit(dbSession, group);
+ removeGroupFromQualityGateEdit(dbSession, group);
+ removeGroupScimLink(dbSession, group);
+ removeExternalGroupMapping(dbSession, group);
+ removeGithubOrganizationGroup(dbSession, group);
+
+ removeGroup(dbSession, group);
+ }
+
+ public GroupDto updateGroup(DbSession dbSession, GroupDto group, @Nullable String newName) {
+ checkGroupIsNotDefault(dbSession, group);
+ return updateName(dbSession, group, newName);
+ }
+
+ public GroupDto updateGroup(DbSession dbSession, GroupDto group, @Nullable String newName, @Nullable String newDescription) {
+ checkGroupIsNotDefault(dbSession, group);
+ GroupDto withUpdatedName = updateName(dbSession, group, newName);
+ return updateDescription(dbSession, withUpdatedName, newDescription);
+ }
+
+ public GroupDto createGroup(DbSession dbSession, String name, @Nullable String description) {
+ validateGroupName(name);
+ checkNameDoesNotExist(dbSession, name);
+
+ GroupDto group = new GroupDto()
+ .setUuid(uuidFactory.create())
+ .setName(name)
+ .setDescription(description);
+ return dbClient.groupDao().insert(dbSession, group);
+ }
+
+ private GroupDto updateName(DbSession dbSession, GroupDto group, @Nullable String newName) {
+ if (newName != null && !newName.equals(group.getName())) {
+ validateGroupName(newName);
+ checkNameDoesNotExist(dbSession, newName);
+ group.setName(newName);
+ return dbClient.groupDao().update(dbSession, group);
+ }
+ return group;
+ }
+
+ private static void validateGroupName(String name) {
+ try {
+ UserGroupValidation.validateGroupName(name);
+ } catch (IllegalArgumentException e) {
+ BadRequestException.throwBadRequestException(e.getMessage());
+ }
+ }
+
+ private void checkNameDoesNotExist(DbSession dbSession, String name) {
+ // There is no database constraint on column groups.name
+ // because MySQL cannot create a unique index
+ // on a UTF-8 VARCHAR larger than 255 characters on InnoDB
+ checkRequest(!dbClient.groupDao().selectByName(dbSession, name).isPresent(), "Group '%s' already exists", name);
+ }
+
+ private GroupDto updateDescription(DbSession dbSession, GroupDto group, @Nullable String newDescription) {
+ if (newDescription != null) {
+ group.setDescription(newDescription);
+ return dbClient.groupDao().update(dbSession, group);
+ }
+ return group;
+ }
+
+ private void checkGroupIsNotDefault(DbSession dbSession, GroupDto groupDto) {
+ GroupDto defaultGroup = findDefaultGroup(dbSession);
+ checkArgument(!defaultGroup.getUuid().equals(groupDto.getUuid()), "Default group '%s' cannot be used to perform this action", groupDto.getName());
+ }
+
+ private GroupDto findDefaultGroup(DbSession dbSession) {
+ return dbClient.groupDao().selectByName(dbSession, DefaultGroups.USERS)
+ .orElseThrow(() -> new IllegalStateException("Default group cannot be found"));
+ }
+
+ private void checkNotTryingToDeleteLastAdminGroup(DbSession dbSession, GroupDto group) {
+ int remaining = dbClient.authorizationDao().countUsersWithGlobalPermissionExcludingGroup(dbSession,
+ GlobalPermission.ADMINISTER.getKey(), group.getUuid());
+
+ checkArgument(remaining > 0, "The last system admin group cannot be deleted");
+ }
+
+ private void removeGroupPermissions(DbSession dbSession, GroupDto group) {
+ dbClient.roleDao().deleteGroupRolesByGroupUuid(dbSession, group.getUuid());
+ }
+
+ private void removeGroupFromPermissionTemplates(DbSession dbSession, GroupDto group) {
+ dbClient.permissionTemplateDao().deleteByGroup(dbSession, group.getUuid(), group.getName());
+ }
+
+ private void removeGroupMembers(DbSession dbSession, GroupDto group) {
+ dbClient.userGroupDao().deleteByGroupUuid(dbSession, group.getUuid(), group.getName());
+ }
+
+ private void removeGroupFromQualityProfileEdit(DbSession dbSession, GroupDto group) {
+ dbClient.qProfileEditGroupsDao().deleteByGroup(dbSession, group);
+ }
+
+ private void removeGroupFromQualityGateEdit(DbSession dbSession, GroupDto group) {
+ dbClient.qualityGateGroupPermissionsDao().deleteByGroup(dbSession, group);
+ }
+
+ private void removeGroupScimLink(DbSession dbSession, GroupDto group) {
+ dbClient.scimGroupDao().deleteByGroupUuid(dbSession, group.getUuid());
+ }
+
+ private void removeExternalGroupMapping(DbSession dbSession, GroupDto group) {
+ dbClient.externalGroupDao().deleteByGroupUuid(dbSession, group.getUuid());
+ }
+ private void removeGithubOrganizationGroup(DbSession dbSession, GroupDto group) {
+ dbClient.githubOrganizationGroupDao().deleteByGroupUuid(dbSession, group.getUuid());
+ }
+
+ private void removeGroup(DbSession dbSession, GroupDto group) {
+ dbClient.groupDao().deleteByUuid(dbSession, group.getUuid(), group.getName());
+ }
+
+}
--- /dev/null
+/*
+ * 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.common.group.service;
+
+import javax.annotation.ParametersAreNonnullByDefault;
--- /dev/null
+/*
+ * 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 com.tngtech.java.junit.dataprovider.DataProvider;
+import com.tngtech.java.junit.dataprovider.DataProviderRunner;
+import com.tngtech.java.junit.dataprovider.UseDataProvider;
+import java.util.Optional;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+import org.sonar.api.security.DefaultGroups;
+import org.sonar.core.util.UuidFactory;
+import org.sonar.db.DbClient;
+import org.sonar.db.DbSession;
+import org.sonar.db.permission.AuthorizationDao;
+import org.sonar.db.permission.GlobalPermission;
+import org.sonar.db.permission.template.PermissionTemplateDao;
+import org.sonar.db.provisioning.GithubOrganizationGroupDao;
+import org.sonar.db.qualitygate.QualityGateGroupPermissionsDao;
+import org.sonar.db.qualityprofile.QProfileEditGroupsDao;
+import org.sonar.db.scim.ScimGroupDao;
+import org.sonar.db.user.ExternalGroupDao;
+import org.sonar.db.user.GroupDao;
+import org.sonar.db.user.GroupDto;
+import org.sonar.db.user.RoleDao;
+import org.sonar.db.user.UserGroupDao;
+import org.sonar.server.exceptions.BadRequestException;
+
+import static java.lang.String.format;
+import static org.apache.commons.lang.RandomStringUtils.randomAlphanumeric;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatNoException;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.assertj.core.api.AssertionsForClassTypes.assertThatExceptionOfType;
+import static org.mockito.ArgumentMatchers.eq;
+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(DataProviderRunner.class)
+public class GroupServiceTest {
+
+ private static final String GROUP_NAME = "GROUP_NAME";
+ private static final String GROUP_UUID = "GROUP_UUID";
+ private static final String DEFAULT_GROUP_NAME = "sonar-users";
+ private static final String DEFAULT_GROUP_UUID = "DEFAULT_GROUP_UUID";
+ @Mock
+ private DbSession dbSession;
+ @Mock
+ private DbClient dbClient;
+ @Mock
+ private UuidFactory uuidFactory;
+ @InjectMocks
+ private GroupService groupService;
+
+ @Rule
+ public MockitoRule rule = MockitoJUnit.rule();
+
+ @Before
+ public void setUp() {
+ mockNeededDaos();
+ }
+
+ private void mockNeededDaos() {
+ when(dbClient.authorizationDao()).thenReturn(mock(AuthorizationDao.class));
+ when(dbClient.roleDao()).thenReturn(mock(RoleDao.class));
+ when(dbClient.permissionTemplateDao()).thenReturn(mock(PermissionTemplateDao.class));
+ when(dbClient.userGroupDao()).thenReturn(mock(UserGroupDao.class));
+ when(dbClient.qProfileEditGroupsDao()).thenReturn(mock(QProfileEditGroupsDao.class));
+ when(dbClient.qualityGateGroupPermissionsDao()).thenReturn(mock(QualityGateGroupPermissionsDao.class));
+ when(dbClient.scimGroupDao()).thenReturn(mock(ScimGroupDao.class));
+ when(dbClient.externalGroupDao()).thenReturn(mock(ExternalGroupDao.class));
+ when(dbClient.groupDao()).thenReturn(mock(GroupDao.class));
+ when(dbClient.githubOrganizationGroupDao()).thenReturn(mock(GithubOrganizationGroupDao.class));
+ }
+
+ @Test
+ public void findGroup_whenGroupExists_returnsIt() {
+ GroupDto groupDto = mockGroupDto();
+
+ when(dbClient.groupDao().selectByName(dbSession, GROUP_NAME))
+ .thenReturn(Optional.of(groupDto));
+
+ assertThat(groupService.findGroup(dbSession, GROUP_NAME)).contains(groupDto);
+ }
+
+ @Test
+ public void findGroup_whenGroupDoesntExist_returnsEmtpyOptional() {
+ when(dbClient.groupDao().selectByName(dbSession, GROUP_NAME))
+ .thenReturn(Optional.empty());
+
+ assertThat(groupService.findGroup(dbSession, GROUP_NAME)).isEmpty();
+ }
+
+ @Test
+ public void findGroupByUuid_whenGroupExists_returnsIt() {
+ GroupDto groupDto = mockGroupDto();
+
+ when(dbClient.groupDao().selectByUuid(dbSession, GROUP_UUID))
+ .thenReturn(groupDto);
+
+ assertThat(groupService.findGroupByUuid(dbSession, GROUP_UUID)).contains(groupDto);
+ }
+
+ @Test
+ public void findGroupByUuid_whenGroupDoesntExist_returnsEmptyOptional() {
+ when(dbClient.groupDao().selectByUuid(dbSession, GROUP_UUID))
+ .thenReturn(null);
+
+ assertThat(groupService.findGroupByUuid(dbSession, GROUP_UUID)).isEmpty();
+ }
+
+ @Test
+ public void delete_whenNotDefaultAndNotLastAdminGroup_deleteGroup() {
+ GroupDto groupDto = mockGroupDto();
+
+ when(dbClient.groupDao().selectByName(dbSession, DefaultGroups.USERS))
+ .thenReturn(Optional.of(new GroupDto().setUuid("another_group_uuid")));
+ when(dbClient.authorizationDao().countUsersWithGlobalPermissionExcludingGroup(dbSession, GlobalPermission.ADMINISTER.getKey(), groupDto.getUuid()))
+ .thenReturn(2);
+
+ groupService.delete(dbSession, groupDto);
+
+ verifyGroupDelete(dbSession, groupDto);
+ }
+
+ @Test
+ public void delete_whenDefaultGroup_throwAndDontDeleteGroup() {
+ GroupDto groupDto = mockGroupDto();
+
+ when(dbClient.groupDao().selectByName(dbSession, DefaultGroups.USERS))
+ .thenReturn(Optional.of(groupDto));
+
+ assertThatThrownBy(() -> groupService.delete(dbSession, groupDto))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessage(format("Default group '%s' cannot be used to perform this action", GROUP_NAME));
+
+ verifyNoGroupDelete(dbSession, groupDto);
+ }
+
+ @Test
+ public void delete_whenLastAdminGroup_throwAndDontDeleteGroup() {
+ GroupDto groupDto = mockGroupDto();
+
+ when(dbClient.groupDao().selectByName(dbSession, DefaultGroups.USERS))
+ .thenReturn(Optional.of(new GroupDto().setUuid("another_group_uuid"))); // We must pass the default group check
+ when(dbClient.authorizationDao().countUsersWithGlobalPermissionExcludingGroup(dbSession, GlobalPermission.ADMINISTER.getKey(), groupDto.getUuid()))
+ .thenReturn(0);
+
+ assertThatThrownBy(() -> groupService.delete(dbSession, groupDto))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessage("The last system admin group cannot be deleted");
+
+ verifyNoGroupDelete(dbSession, groupDto);
+ }
+
+ @Test
+ public void updateGroup_updatesGroupNameAndDescription() {
+ GroupDto group = mockGroupDto();
+ GroupDto groupWithUpdatedName = mockGroupDto();
+ mockDefaultGroup();
+ when(dbClient.groupDao().update(dbSession, group)).thenReturn(groupWithUpdatedName);
+
+ groupService.updateGroup(dbSession, group, "new-name", "New Description");
+ verify(group).setName("new-name");
+ verify(groupWithUpdatedName).setDescription("New Description");
+ verify(dbClient.groupDao()).update(dbSession, group);
+ verify(dbClient.groupDao()).update(dbSession, groupWithUpdatedName);
+ }
+
+ @Test
+ public void updateGroup_updatesGroupName() {
+ GroupDto group = mockGroupDto();
+ mockDefaultGroup();
+
+ groupService.updateGroup(dbSession, group, "new-name");
+ verify(group).setName("new-name");
+ verify(dbClient.groupDao()).update(dbSession, group);
+ }
+
+ @Test
+ public void updateGroup_whenGroupIsDefault_throws() {
+ GroupDto defaultGroup = mockDefaultGroup();
+ when(dbClient.groupDao().selectByName(dbSession, DEFAULT_GROUP_NAME)).thenReturn(Optional.of(defaultGroup));
+
+ assertThatExceptionOfType(IllegalArgumentException.class)
+ .isThrownBy(() -> groupService.updateGroup(dbSession, defaultGroup, "new-name", "New Description"))
+ .withMessage("Default group 'sonar-users' cannot be used to perform this action");
+
+ assertThatExceptionOfType(IllegalArgumentException.class)
+ .isThrownBy(() -> groupService.updateGroup(dbSession, defaultGroup, "new-name"))
+ .withMessage("Default group 'sonar-users' cannot be used to perform this action");
+ }
+
+ @Test
+ public void updateGroup_whenGroupNameDoesntChange_succeedsWithDescription() {
+ GroupDto group = mockGroupDto();
+ mockDefaultGroup();
+
+ groupService.updateGroup(dbSession, group, group.getName(), "New Description");
+ verify(group).setDescription("New Description");
+ verify(dbClient.groupDao()).update(dbSession, group);
+ }
+
+ @Test
+ public void updateGroup_whenGroupNameDoesntChange_succeeds() {
+ GroupDto group = mockGroupDto();
+ mockDefaultGroup();
+
+ assertThatNoException()
+ .isThrownBy(() -> groupService.updateGroup(dbSession, group, group.getName()));
+
+ verify(dbClient.groupDao(), never()).update(dbSession, group);
+ }
+
+ @Test
+ public void updateGroup_whenGroupExist_throws() {
+ GroupDto group = mockGroupDto();
+ GroupDto group2 = mockGroupDto();
+ mockDefaultGroup();
+ String group2Name = GROUP_NAME + "2";
+
+ when(dbClient.groupDao().selectByName(dbSession, group2Name)).thenReturn(Optional.of(group2));
+
+ assertThatExceptionOfType(BadRequestException.class)
+ .isThrownBy(() -> groupService.updateGroup(dbSession, group, group2Name, "New Description"))
+ .withMessage("Group '" + group2Name + "' already exists");
+
+ assertThatExceptionOfType(BadRequestException.class)
+ .isThrownBy(() -> groupService.updateGroup(dbSession, group, group2Name))
+ .withMessage("Group '" + group2Name + "' already exists");
+ }
+
+ @Test
+ @UseDataProvider("invalidGroupNames")
+ public void updateGroup_whenGroupNameIsInvalid_throws(String groupName, String errorMessage) {
+ GroupDto group = mockGroupDto();
+ mockDefaultGroup();
+
+ assertThatExceptionOfType(BadRequestException.class)
+ .isThrownBy(() -> groupService.updateGroup(dbSession, group, groupName, "New Description"))
+ .withMessage(errorMessage);
+
+ assertThatExceptionOfType(BadRequestException.class)
+ .isThrownBy(() -> groupService.updateGroup(dbSession, group, groupName))
+ .withMessage(errorMessage);
+ }
+
+ @Test
+ public void createGroup_whenNameAndDescriptionIsProvided_createsGroup() {
+
+ when(uuidFactory.create()).thenReturn("1234");
+ groupService.createGroup(dbSession, "Name", "Description");
+
+ ArgumentCaptor<GroupDto> groupCaptor = ArgumentCaptor.forClass(GroupDto.class);
+ verify(dbClient.groupDao()).insert(eq(dbSession), groupCaptor.capture());
+ GroupDto createdGroup = groupCaptor.getValue();
+ assertThat(createdGroup.getName()).isEqualTo("Name");
+ assertThat(createdGroup.getDescription()).isEqualTo("Description");
+ assertThat(createdGroup.getUuid()).isEqualTo("1234");
+ }
+
+ @Test
+ public void createGroup_whenGroupExist_throws() {
+ GroupDto group = mockGroupDto();
+
+ when(dbClient.groupDao().selectByName(dbSession, GROUP_NAME)).thenReturn(Optional.of(group));
+
+ assertThatExceptionOfType(BadRequestException.class)
+ .isThrownBy(() -> groupService.createGroup(dbSession, GROUP_NAME, "New Description"))
+ .withMessage("Group '" + GROUP_NAME + "' already exists");
+
+ }
+
+ @Test
+ @UseDataProvider("invalidGroupNames")
+ public void createGroup_whenGroupNameIsInvalid_throws(String groupName, String errorMessage) {
+ mockDefaultGroup();
+
+ assertThatExceptionOfType(BadRequestException.class)
+ .isThrownBy(() -> groupService.createGroup(dbSession, groupName, "Description"))
+ .withMessage(errorMessage);
+
+ }
+
+ @DataProvider
+ public static Object[][] invalidGroupNames() {
+ return new Object[][] {
+ {"", "Group name cannot be empty"},
+ {randomAlphanumeric(256), "Group name cannot be longer than 255 characters"},
+ {"Anyone", "Anyone group cannot be used"},
+ };
+ }
+
+ private static GroupDto mockGroupDto() {
+ GroupDto groupDto = mock(GroupDto.class);
+ when(groupDto.getName()).thenReturn(GROUP_NAME);
+ when(groupDto.getUuid()).thenReturn(GROUP_UUID);
+ return groupDto;
+ }
+
+ private GroupDto mockDefaultGroup() {
+ GroupDto defaultGroup = mock(GroupDto.class);
+ when(defaultGroup.getName()).thenReturn(DEFAULT_GROUP_NAME);
+ when(defaultGroup.getUuid()).thenReturn(DEFAULT_GROUP_UUID);
+ when(dbClient.groupDao().selectByName(dbSession, DEFAULT_GROUP_NAME)).thenReturn(Optional.of(defaultGroup));
+ return defaultGroup;
+ }
+
+ private void verifyNoGroupDelete(DbSession dbSession, GroupDto groupDto) {
+ verify(dbClient.roleDao(), never()).deleteGroupRolesByGroupUuid(dbSession, groupDto.getUuid());
+ verify(dbClient.permissionTemplateDao(), never()).deleteByGroup(dbSession, groupDto.getUuid(), groupDto.getName());
+ verify(dbClient.userGroupDao(), never()).deleteByGroupUuid(dbSession, groupDto.getUuid(), groupDto.getName());
+ verify(dbClient.qProfileEditGroupsDao(), never()).deleteByGroup(dbSession, groupDto);
+ verify(dbClient.qualityGateGroupPermissionsDao(), never()).deleteByGroup(dbSession, groupDto);
+ verify(dbClient.scimGroupDao(), never()).deleteByGroupUuid(dbSession, groupDto.getUuid());
+ verify(dbClient.groupDao(), never()).deleteByUuid(dbSession, groupDto.getUuid(), groupDto.getName());
+ verify(dbClient.githubOrganizationGroupDao(), never()).deleteByGroupUuid(dbSession, groupDto.getUuid());
+ }
+
+ private void verifyGroupDelete(DbSession dbSession, GroupDto groupDto) {
+ verify(dbClient.roleDao()).deleteGroupRolesByGroupUuid(dbSession, groupDto.getUuid());
+ verify(dbClient.permissionTemplateDao()).deleteByGroup(dbSession, groupDto.getUuid(), groupDto.getName());
+ verify(dbClient.userGroupDao()).deleteByGroupUuid(dbSession, groupDto.getUuid(), groupDto.getName());
+ verify(dbClient.qProfileEditGroupsDao()).deleteByGroup(dbSession, groupDto);
+ verify(dbClient.qualityGateGroupPermissionsDao()).deleteByGroup(dbSession, groupDto);
+ verify(dbClient.scimGroupDao()).deleteByGroupUuid(dbSession, groupDto.getUuid());
+ verify(dbClient.externalGroupDao()).deleteByGroupUuid(dbSession, groupDto.getUuid());
+ verify(dbClient.groupDao()).deleteByUuid(dbSession, groupDto.getUuid(), groupDto.getName());
+ verify(dbClient.githubOrganizationGroupDao()).deleteByGroupUuid(dbSession, groupDto.getUuid());
+ }
+}
public static final String USERS_MANAGEMENT_DOMAIN = "/users-management";
public static final String USER_ENDPOINT = USERS_MANAGEMENT_DOMAIN + "/users";
public static final String JSON_MERGE_PATCH_CONTENT_TYPE = "application/merge-patch+json";
+ public static final String AUTHORIZATIONS_DOMAIN = "/authorizations";
+
+ public static final String GROUPS_ENDPOINT = AUTHORIZATIONS_DOMAIN + "/groups";
private WebApiEndpoints() {
}
--- /dev/null
+/*
+ * 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.group.controller;
+
+import org.sonar.db.DbClient;
+import org.sonar.db.DbSession;
+import org.sonar.db.user.GroupDto;
+import org.sonar.server.common.group.service.GroupService;
+import org.sonar.server.exceptions.NotFoundException;
+import org.sonar.server.user.UserSession;
+import org.sonar.server.v2.api.group.response.RestGroupResponse;
+
+public class DefaultGroupController implements GroupController {
+
+ private static final String GROUP_NOT_FOUND_MESSAGE = "Group '%s' not found";
+ private final GroupService groupService;
+ private final DbClient dbClient;
+ private final UserSession userSession;
+
+ public DefaultGroupController(GroupService groupService, DbClient dbClient, UserSession userSession) {
+ this.groupService = groupService;
+ this.dbClient = dbClient;
+ this.userSession = userSession;
+ }
+
+ @Override
+ public RestGroupResponse fetchGroup(String id) {
+ userSession.checkLoggedIn().checkIsSystemAdministrator();
+ try (DbSession session = dbClient.openSession(false)) {
+ return groupService.findGroupByUuid(session, id)
+ .map(DefaultGroupController::groupDtoToResponse)
+ .orElseThrow(() -> new NotFoundException(String.format(GROUP_NOT_FOUND_MESSAGE, id)));
+ }
+ }
+
+ private static RestGroupResponse groupDtoToResponse(GroupDto group) {
+ return new RestGroupResponse(group.getUuid(), group.getName(), group.getDescription());
+ }
+}
--- /dev/null
+/*
+ * 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.group.controller;
+
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.enums.ParameterIn;
+import org.sonar.server.v2.api.group.response.RestGroupResponse;
+import org.springframework.http.HttpStatus;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+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.GROUPS_ENDPOINT;
+
+@RequestMapping(GROUPS_ENDPOINT)
+@RestController
+public interface GroupController {
+
+ @GetMapping(path = "/{id}")
+ @ResponseStatus(HttpStatus.OK)
+ @Operation(summary = "Fetch a single group", description = "Fetch a single group.")
+ RestGroupResponse fetchGroup(@PathVariable("id") @Parameter(description = "The id of the group to fetch.", required = true, in = ParameterIn.PATH) String id);
+}
--- /dev/null
+/*
+ * 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.group.controller;
+
+import javax.annotation.ParametersAreNonnullByDefault;
--- /dev/null
+/*
+ * 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.group.response;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import javax.annotation.Nullable;
+
+public record RestGroupResponse(
+ @Schema(accessMode = Schema.AccessMode.READ_ONLY)
+ String id,
+ String name,
+ @Nullable
+ String description) {
+}
--- /dev/null
+/*
+ * 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.group.response;
+
+import javax.annotation.ParametersAreNonnullByDefault;
package org.sonar.server.v2.config;
import javax.annotation.Nullable;
+import org.sonar.db.DbClient;
+import org.sonar.server.common.group.service.GroupService;
import org.sonar.server.common.health.CeStatusNodeCheck;
import org.sonar.server.common.health.DbConnectionNodeCheck;
import org.sonar.server.common.health.EsStatusNodeCheck;
import org.sonar.server.platform.NodeInformation;
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.system.controller.DefaultLivenessController;
import org.sonar.server.v2.api.system.controller.HealthController;
import org.sonar.server.v2.api.system.controller.LivenessController;
return new DefaultUserController(userSession, userService, usersSearchResponseGenerator);
}
+ @Bean
+ public GroupController groupController(GroupService groupService, DbClient dbClient, UserSession userSession) {
+ return new DefaultGroupController(groupService, dbClient, userSession);
+ }
+
}
--- /dev/null
+/*
+ * 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.group.controller;
+
+import java.util.Optional;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.sonar.db.DbClient;
+import org.sonar.db.DbSession;
+import org.sonar.db.user.GroupDto;
+import org.sonar.server.common.group.service.GroupService;
+import org.sonar.server.tester.UserSessionRule;
+import org.sonar.server.v2.api.ControllerTester;
+import org.springframework.test.web.servlet.MockMvc;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+import static org.sonar.server.v2.WebApiEndpoints.GROUPS_ENDPOINT;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+public class DefaultGroupControllerTest {
+
+ private static final String GROUP_UUID = "1234";
+ @Rule
+ public UserSessionRule userSession = UserSessionRule.standalone();
+
+ private final GroupService groupService = mock(GroupService.class);
+ private final DbClient dbClient = mock(DbClient.class);
+
+ private final DbSession dbSession = mock(DbSession.class);
+
+ private final MockMvc mockMvc = ControllerTester.getMockMvc(new DefaultGroupController(groupService, dbClient, userSession));
+
+ @Before
+ public void setUp() {
+ when(dbClient.openSession(false)).thenReturn(dbSession);
+ }
+
+ @Test
+ public void fetchGroup_whenGroupExists_returnsTheGroup() throws Exception {
+
+ GroupDto groupDto = new GroupDto().setUuid(GROUP_UUID).setName("name").setDescription("description");
+
+ when(groupService.findGroupByUuid(dbSession, GROUP_UUID)).thenReturn(Optional.of(groupDto));
+
+ userSession.logIn().setSystemAdministrator();
+ mockMvc.perform(get(GROUPS_ENDPOINT + "/" + GROUP_UUID))
+ .andExpectAll(
+ status().isOk(),
+ content().json("""
+ {
+ "id": "1234",
+ "name": "name",
+ "description": "description"
+ }
+ """));
+ }
+
+ @Test
+ public void fetchGroup_whenCallerIsNotAdmin_shouldReturnForbidden() throws Exception {
+ userSession.logIn().setNonSystemAdministrator();
+ mockMvc.perform(
+ get(GROUPS_ENDPOINT + "/" + GROUP_UUID))
+ .andExpectAll(
+ status().isForbidden(),
+ content().json("{\"message\":\"Insufficient privileges\"}"));
+ }
+
+ @Test
+ public void fetchGroup_whenGroupDoesntExist_shouldReturnNotFound() throws Exception {
+ userSession.logIn().setSystemAdministrator();
+ when(groupService.findGroupByUuid(dbSession, GROUP_UUID)).thenReturn(Optional.empty());
+ mockMvc.perform(
+ get(GROUPS_ENDPOINT + "/" + GROUP_UUID)
+ .content("{}"))
+ .andExpectAll(
+ status().isNotFound(),
+ content().json("{\"message\":\"Group '1234' not found\"}"));
+ }
+
+}
import org.sonar.core.util.SequenceUuidFactory;
import org.sonar.db.DbTester;
import org.sonar.db.user.GroupDto;
+import org.sonar.server.common.group.service.GroupService;
import org.sonar.server.exceptions.BadRequestException;
import org.sonar.server.exceptions.ForbiddenException;
import org.sonar.server.exceptions.ServerException;
import org.sonar.db.qualityprofile.QProfileDto;
import org.sonar.db.user.GroupDto;
import org.sonar.db.user.UserDto;
+import org.sonar.server.common.group.service.GroupService;
import org.sonar.server.exceptions.BadRequestException;
import org.sonar.server.exceptions.NotFoundException;
import org.sonar.server.management.ManagedInstanceService;
import org.sonar.db.DbTester;
import org.sonar.db.user.ExternalGroupDto;
import org.sonar.db.user.GroupDto;
+import org.sonar.server.common.group.service.GroupService;
import static org.assertj.core.api.Assertions.assertThat;
import org.sonar.db.DbTester;
import org.sonar.db.user.GroupDto;
import org.sonar.db.user.UserDto;
+import org.sonar.server.common.group.service.GroupService;
import org.sonar.server.exceptions.BadRequestException;
import org.sonar.server.exceptions.ForbiddenException;
import org.sonar.server.exceptions.NotFoundException;
import org.sonar.db.DbClient;
import org.sonar.db.DbSession;
import org.sonar.db.user.GroupDto;
+import org.sonar.server.common.group.service.GroupService;
import org.sonar.server.common.management.ManagedInstanceChecker;
import org.sonar.server.user.UserSession;
import org.sonarqube.ws.UserGroups;
import org.sonar.db.DbSession;
import org.sonar.db.permission.GlobalPermission;
import org.sonar.db.user.GroupDto;
+import org.sonar.server.common.group.service.GroupService;
import org.sonar.server.exceptions.BadRequestException;
import org.sonar.server.exceptions.NotFoundException;
import org.sonar.server.management.ManagedInstanceService;
import org.sonar.db.DbSession;
import org.sonar.db.user.ExternalGroupDto;
import org.sonar.db.user.GroupDto;
+import org.sonar.server.common.group.service.GroupService;
public class ExternalGroupService {
+++ /dev/null
-/*
- * 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.usergroups.ws;
-
-import java.util.Optional;
-import javax.annotation.Nullable;
-import org.sonar.api.security.DefaultGroups;
-import org.sonar.api.server.ServerSide;
-import org.sonar.api.user.UserGroupValidation;
-import org.sonar.core.util.UuidFactory;
-import org.sonar.db.DbClient;
-import org.sonar.db.DbSession;
-import org.sonar.db.permission.GlobalPermission;
-import org.sonar.db.user.GroupDto;
-import org.sonar.server.exceptions.BadRequestException;
-
-import static com.google.common.base.Preconditions.checkArgument;
-import static org.sonar.server.exceptions.BadRequestException.checkRequest;
-
-@ServerSide
-public class GroupService {
-
- private final DbClient dbClient;
- private final UuidFactory uuidFactory;
-
- public GroupService(DbClient dbClient, UuidFactory uuidFactory) {
- this.dbClient = dbClient;
- this.uuidFactory = uuidFactory;
- }
-
- public Optional<GroupDto> findGroup(DbSession dbSession, String groupName) {
- return dbClient.groupDao().selectByName(dbSession, groupName);
- }
-
- public void delete(DbSession dbSession, GroupDto group) {
- checkGroupIsNotDefault(dbSession, group);
- checkNotTryingToDeleteLastAdminGroup(dbSession, group);
-
- removeGroupPermissions(dbSession, group);
- removeGroupFromPermissionTemplates(dbSession, group);
- removeGroupMembers(dbSession, group);
- removeGroupFromQualityProfileEdit(dbSession, group);
- removeGroupFromQualityGateEdit(dbSession, group);
- removeGroupScimLink(dbSession, group);
- removeExternalGroupMapping(dbSession, group);
- removeGithubOrganizationGroup(dbSession, group);
-
- removeGroup(dbSession, group);
- }
-
- public GroupDto updateGroup(DbSession dbSession, GroupDto group, @Nullable String newName) {
- checkGroupIsNotDefault(dbSession, group);
- return updateName(dbSession, group, newName);
- }
-
- public GroupDto updateGroup(DbSession dbSession, GroupDto group, @Nullable String newName, @Nullable String newDescription) {
- checkGroupIsNotDefault(dbSession, group);
- GroupDto withUpdatedName = updateName(dbSession, group, newName);
- return updateDescription(dbSession, withUpdatedName, newDescription);
- }
-
- public GroupDto createGroup(DbSession dbSession, String name, @Nullable String description) {
- validateGroupName(name);
- checkNameDoesNotExist(dbSession, name);
-
- GroupDto group = new GroupDto()
- .setUuid(uuidFactory.create())
- .setName(name)
- .setDescription(description);
- return dbClient.groupDao().insert(dbSession, group);
- }
-
- private GroupDto updateName(DbSession dbSession, GroupDto group, @Nullable String newName) {
- if (newName != null && !newName.equals(group.getName())) {
- validateGroupName(newName);
- checkNameDoesNotExist(dbSession, newName);
- group.setName(newName);
- return dbClient.groupDao().update(dbSession, group);
- }
- return group;
- }
-
- private static void validateGroupName(String name) {
- try {
- UserGroupValidation.validateGroupName(name);
- } catch (IllegalArgumentException e) {
- BadRequestException.throwBadRequestException(e.getMessage());
- }
- }
-
- private void checkNameDoesNotExist(DbSession dbSession, String name) {
- // There is no database constraint on column groups.name
- // because MySQL cannot create a unique index
- // on a UTF-8 VARCHAR larger than 255 characters on InnoDB
- checkRequest(!dbClient.groupDao().selectByName(dbSession, name).isPresent(), "Group '%s' already exists", name);
- }
-
- private GroupDto updateDescription(DbSession dbSession, GroupDto group, @Nullable String newDescription) {
- if (newDescription != null) {
- group.setDescription(newDescription);
- return dbClient.groupDao().update(dbSession, group);
- }
- return group;
- }
-
- private void checkGroupIsNotDefault(DbSession dbSession, GroupDto groupDto) {
- GroupDto defaultGroup = findDefaultGroup(dbSession);
- checkArgument(!defaultGroup.getUuid().equals(groupDto.getUuid()), "Default group '%s' cannot be used to perform this action", groupDto.getName());
- }
-
- private GroupDto findDefaultGroup(DbSession dbSession) {
- return dbClient.groupDao().selectByName(dbSession, DefaultGroups.USERS)
- .orElseThrow(() -> new IllegalStateException("Default group cannot be found"));
- }
-
- private void checkNotTryingToDeleteLastAdminGroup(DbSession dbSession, GroupDto group) {
- int remaining = dbClient.authorizationDao().countUsersWithGlobalPermissionExcludingGroup(dbSession,
- GlobalPermission.ADMINISTER.getKey(), group.getUuid());
-
- checkArgument(remaining > 0, "The last system admin group cannot be deleted");
- }
-
- private void removeGroupPermissions(DbSession dbSession, GroupDto group) {
- dbClient.roleDao().deleteGroupRolesByGroupUuid(dbSession, group.getUuid());
- }
-
- private void removeGroupFromPermissionTemplates(DbSession dbSession, GroupDto group) {
- dbClient.permissionTemplateDao().deleteByGroup(dbSession, group.getUuid(), group.getName());
- }
-
- private void removeGroupMembers(DbSession dbSession, GroupDto group) {
- dbClient.userGroupDao().deleteByGroupUuid(dbSession, group.getUuid(), group.getName());
- }
-
- private void removeGroupFromQualityProfileEdit(DbSession dbSession, GroupDto group) {
- dbClient.qProfileEditGroupsDao().deleteByGroup(dbSession, group);
- }
-
- private void removeGroupFromQualityGateEdit(DbSession dbSession, GroupDto group) {
- dbClient.qualityGateGroupPermissionsDao().deleteByGroup(dbSession, group);
- }
-
- private void removeGroupScimLink(DbSession dbSession, GroupDto group) {
- dbClient.scimGroupDao().deleteByGroupUuid(dbSession, group.getUuid());
- }
-
- private void removeExternalGroupMapping(DbSession dbSession, GroupDto group) {
- dbClient.externalGroupDao().deleteByGroupUuid(dbSession, group.getUuid());
- }
- private void removeGithubOrganizationGroup(DbSession dbSession, GroupDto group) {
- dbClient.githubOrganizationGroupDao().deleteByGroupUuid(dbSession, group.getUuid());
- }
-
- private void removeGroup(DbSession dbSession, GroupDto group) {
- dbClient.groupDao().deleteByUuid(dbSession, group.getUuid(), group.getName());
- }
-
-}
import org.sonar.db.DbSession;
import org.sonar.db.user.GroupDto;
import org.sonar.db.user.UserMembershipQuery;
+import org.sonar.server.common.group.service.GroupService;
import org.sonar.server.exceptions.NotFoundException;
import org.sonar.server.common.management.ManagedInstanceChecker;
import org.sonar.server.user.UserSession;
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 {
+++ /dev/null
-/*
- * 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.usergroups.ws;
-
-import com.tngtech.java.junit.dataprovider.DataProvider;
-import com.tngtech.java.junit.dataprovider.DataProviderRunner;
-import com.tngtech.java.junit.dataprovider.UseDataProvider;
-import java.util.Optional;
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.ArgumentCaptor;
-import org.mockito.InjectMocks;
-import org.mockito.Mock;
-import org.mockito.junit.MockitoJUnit;
-import org.mockito.junit.MockitoRule;
-import org.sonar.api.security.DefaultGroups;
-import org.sonar.core.util.UuidFactory;
-import org.sonar.db.DbClient;
-import org.sonar.db.DbSession;
-import org.sonar.db.permission.AuthorizationDao;
-import org.sonar.db.permission.GlobalPermission;
-import org.sonar.db.permission.template.PermissionTemplateDao;
-import org.sonar.db.provisioning.GithubOrganizationGroupDao;
-import org.sonar.db.qualitygate.QualityGateGroupPermissionsDao;
-import org.sonar.db.qualityprofile.QProfileEditGroupsDao;
-import org.sonar.db.scim.ScimGroupDao;
-import org.sonar.db.user.ExternalGroupDao;
-import org.sonar.db.user.GroupDao;
-import org.sonar.db.user.GroupDto;
-import org.sonar.db.user.RoleDao;
-import org.sonar.db.user.UserGroupDao;
-import org.sonar.server.exceptions.BadRequestException;
-
-import static java.lang.String.format;
-import static org.apache.commons.lang.RandomStringUtils.randomAlphanumeric;
-import static org.assertj.core.api.Assertions.assertThat;
-import static org.assertj.core.api.Assertions.assertThatNoException;
-import static org.assertj.core.api.Assertions.assertThatThrownBy;
-import static org.assertj.core.api.AssertionsForClassTypes.assertThatExceptionOfType;
-import static org.mockito.ArgumentMatchers.eq;
-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(DataProviderRunner.class)
-public class GroupServiceTest {
-
- private static final String GROUP_NAME = "GROUP_NAME";
- private static final String GROUP_UUID = "GROUP_UUID";
- private static final String DEFAULT_GROUP_NAME = "sonar-users";
- private static final String DEFAULT_GROUP_UUID = "DEFAULT_GROUP_UUID";
- @Mock
- private DbSession dbSession;
- @Mock
- private DbClient dbClient;
- @Mock
- private UuidFactory uuidFactory;
- @InjectMocks
- private GroupService groupService;
-
- @Rule
- public MockitoRule rule = MockitoJUnit.rule();
-
- @Before
- public void setUp() {
- mockNeededDaos();
- }
-
- private void mockNeededDaos() {
- when(dbClient.authorizationDao()).thenReturn(mock(AuthorizationDao.class));
- when(dbClient.roleDao()).thenReturn(mock(RoleDao.class));
- when(dbClient.permissionTemplateDao()).thenReturn(mock(PermissionTemplateDao.class));
- when(dbClient.userGroupDao()).thenReturn(mock(UserGroupDao.class));
- when(dbClient.qProfileEditGroupsDao()).thenReturn(mock(QProfileEditGroupsDao.class));
- when(dbClient.qualityGateGroupPermissionsDao()).thenReturn(mock(QualityGateGroupPermissionsDao.class));
- when(dbClient.scimGroupDao()).thenReturn(mock(ScimGroupDao.class));
- when(dbClient.externalGroupDao()).thenReturn(mock(ExternalGroupDao.class));
- when(dbClient.groupDao()).thenReturn(mock(GroupDao.class));
- when(dbClient.githubOrganizationGroupDao()).thenReturn(mock(GithubOrganizationGroupDao.class));
- }
-
- @Test
- public void findGroupDtoOrThrow_whenGroupExists_returnsIt() {
- GroupDto groupDto = mockGroupDto();
-
- when(dbClient.groupDao().selectByName(dbSession, GROUP_NAME))
- .thenReturn(Optional.of(groupDto));
-
- assertThat(groupService.findGroup(dbSession, GROUP_NAME)).contains(groupDto);
- }
-
- @Test
- public void findGroupDtoOrThrow_whenGroupDoesntExist_throw() {
- when(dbClient.groupDao().selectByName(dbSession, GROUP_NAME))
- .thenReturn(Optional.empty());
-
- assertThat(groupService.findGroup(dbSession, GROUP_NAME)).isEmpty();
- }
-
- @Test
- public void delete_whenNotDefaultAndNotLastAdminGroup_deleteGroup() {
- GroupDto groupDto = mockGroupDto();
-
- when(dbClient.groupDao().selectByName(dbSession, DefaultGroups.USERS))
- .thenReturn(Optional.of(new GroupDto().setUuid("another_group_uuid")));
- when(dbClient.authorizationDao().countUsersWithGlobalPermissionExcludingGroup(dbSession, GlobalPermission.ADMINISTER.getKey(), groupDto.getUuid()))
- .thenReturn(2);
-
- groupService.delete(dbSession, groupDto);
-
- verifyGroupDelete(dbSession, groupDto);
- }
-
- @Test
- public void delete_whenDefaultGroup_throwAndDontDeleteGroup() {
- GroupDto groupDto = mockGroupDto();
-
- when(dbClient.groupDao().selectByName(dbSession, DefaultGroups.USERS))
- .thenReturn(Optional.of(groupDto));
-
- assertThatThrownBy(() -> groupService.delete(dbSession, groupDto))
- .isInstanceOf(IllegalArgumentException.class)
- .hasMessage(format("Default group '%s' cannot be used to perform this action", GROUP_NAME));
-
- verifyNoGroupDelete(dbSession, groupDto);
- }
-
- @Test
- public void delete_whenLastAdminGroup_throwAndDontDeleteGroup() {
- GroupDto groupDto = mockGroupDto();
-
- when(dbClient.groupDao().selectByName(dbSession, DefaultGroups.USERS))
- .thenReturn(Optional.of(new GroupDto().setUuid("another_group_uuid"))); // We must pass the default group check
- when(dbClient.authorizationDao().countUsersWithGlobalPermissionExcludingGroup(dbSession, GlobalPermission.ADMINISTER.getKey(), groupDto.getUuid()))
- .thenReturn(0);
-
- assertThatThrownBy(() -> groupService.delete(dbSession, groupDto))
- .isInstanceOf(IllegalArgumentException.class)
- .hasMessage("The last system admin group cannot be deleted");
-
- verifyNoGroupDelete(dbSession, groupDto);
- }
-
- @Test
- public void updateGroup_updatesGroupNameAndDescription() {
- GroupDto group = mockGroupDto();
- GroupDto groupWithUpdatedName = mockGroupDto();
- mockDefaultGroup();
- when(dbClient.groupDao().update(dbSession, group)).thenReturn(groupWithUpdatedName);
-
- groupService.updateGroup(dbSession, group, "new-name", "New Description");
- verify(group).setName("new-name");
- verify(groupWithUpdatedName).setDescription("New Description");
- verify(dbClient.groupDao()).update(dbSession, group);
- verify(dbClient.groupDao()).update(dbSession, groupWithUpdatedName);
- }
-
- @Test
- public void updateGroup_updatesGroupName() {
- GroupDto group = mockGroupDto();
- mockDefaultGroup();
-
- groupService.updateGroup(dbSession, group, "new-name");
- verify(group).setName("new-name");
- verify(dbClient.groupDao()).update(dbSession, group);
- }
-
- @Test
- public void updateGroup_whenGroupIsDefault_throws() {
- GroupDto defaultGroup = mockDefaultGroup();
- when(dbClient.groupDao().selectByName(dbSession, DEFAULT_GROUP_NAME)).thenReturn(Optional.of(defaultGroup));
-
- assertThatExceptionOfType(IllegalArgumentException.class)
- .isThrownBy(() -> groupService.updateGroup(dbSession, defaultGroup, "new-name", "New Description"))
- .withMessage("Default group 'sonar-users' cannot be used to perform this action");
-
- assertThatExceptionOfType(IllegalArgumentException.class)
- .isThrownBy(() -> groupService.updateGroup(dbSession, defaultGroup, "new-name"))
- .withMessage("Default group 'sonar-users' cannot be used to perform this action");
- }
-
- @Test
- public void updateGroup_whenGroupNameDoesntChange_succeedsWithDescription() {
- GroupDto group = mockGroupDto();
- mockDefaultGroup();
-
- groupService.updateGroup(dbSession, group, group.getName(), "New Description");
- verify(group).setDescription("New Description");
- verify(dbClient.groupDao()).update(dbSession, group);
- }
-
- @Test
- public void updateGroup_whenGroupNameDoesntChange_succeeds() {
- GroupDto group = mockGroupDto();
- mockDefaultGroup();
-
- assertThatNoException()
- .isThrownBy(() -> groupService.updateGroup(dbSession, group, group.getName()));
-
- verify(dbClient.groupDao(), never()).update(dbSession, group);
- }
-
- @Test
- public void updateGroup_whenGroupExist_throws() {
- GroupDto group = mockGroupDto();
- GroupDto group2 = mockGroupDto();
- mockDefaultGroup();
- String group2Name = GROUP_NAME + "2";
-
- when(dbClient.groupDao().selectByName(dbSession, group2Name)).thenReturn(Optional.of(group2));
-
- assertThatExceptionOfType(BadRequestException.class)
- .isThrownBy(() -> groupService.updateGroup(dbSession, group, group2Name, "New Description"))
- .withMessage("Group '" + group2Name + "' already exists");
-
- assertThatExceptionOfType(BadRequestException.class)
- .isThrownBy(() -> groupService.updateGroup(dbSession, group, group2Name))
- .withMessage("Group '" + group2Name + "' already exists");
- }
-
- @Test
- @UseDataProvider("invalidGroupNames")
- public void updateGroup_whenGroupNameIsInvalid_throws(String groupName, String errorMessage) {
- GroupDto group = mockGroupDto();
- mockDefaultGroup();
-
- assertThatExceptionOfType(BadRequestException.class)
- .isThrownBy(() -> groupService.updateGroup(dbSession, group, groupName, "New Description"))
- .withMessage(errorMessage);
-
- assertThatExceptionOfType(BadRequestException.class)
- .isThrownBy(() -> groupService.updateGroup(dbSession, group, groupName))
- .withMessage(errorMessage);
- }
-
- @Test
- public void createGroup_whenNameAndDescriptionIsProvided_createsGroup() {
-
- when(uuidFactory.create()).thenReturn("1234");
- groupService.createGroup(dbSession, "Name", "Description");
-
- ArgumentCaptor<GroupDto> groupCaptor = ArgumentCaptor.forClass(GroupDto.class);
- verify(dbClient.groupDao()).insert(eq(dbSession), groupCaptor.capture());
- GroupDto createdGroup = groupCaptor.getValue();
- assertThat(createdGroup.getName()).isEqualTo("Name");
- assertThat(createdGroup.getDescription()).isEqualTo("Description");
- assertThat(createdGroup.getUuid()).isEqualTo("1234");
- }
-
- @Test
- public void createGroup_whenGroupExist_throws() {
- GroupDto group = mockGroupDto();
-
- when(dbClient.groupDao().selectByName(dbSession, GROUP_NAME)).thenReturn(Optional.of(group));
-
- assertThatExceptionOfType(BadRequestException.class)
- .isThrownBy(() -> groupService.createGroup(dbSession, GROUP_NAME, "New Description"))
- .withMessage("Group '" + GROUP_NAME + "' already exists");
-
- }
-
- @Test
- @UseDataProvider("invalidGroupNames")
- public void createGroup_whenGroupNameIsInvalid_throws(String groupName, String errorMessage) {
- mockDefaultGroup();
-
- assertThatExceptionOfType(BadRequestException.class)
- .isThrownBy(() -> groupService.createGroup(dbSession, groupName, "Description"))
- .withMessage(errorMessage);
-
- }
-
- @DataProvider
- public static Object[][] invalidGroupNames() {
- return new Object[][] {
- {"", "Group name cannot be empty"},
- {randomAlphanumeric(256), "Group name cannot be longer than 255 characters"},
- {"Anyone", "Anyone group cannot be used"},
- };
- }
-
- private static GroupDto mockGroupDto() {
- GroupDto groupDto = mock(GroupDto.class);
- when(groupDto.getName()).thenReturn(GROUP_NAME);
- when(groupDto.getUuid()).thenReturn(GROUP_UUID);
- return groupDto;
- }
-
- private GroupDto mockDefaultGroup() {
- GroupDto defaultGroup = mock(GroupDto.class);
- when(defaultGroup.getName()).thenReturn(DEFAULT_GROUP_NAME);
- when(defaultGroup.getUuid()).thenReturn(DEFAULT_GROUP_UUID);
- when(dbClient.groupDao().selectByName(dbSession, DEFAULT_GROUP_NAME)).thenReturn(Optional.of(defaultGroup));
- return defaultGroup;
- }
-
- private void verifyNoGroupDelete(DbSession dbSession, GroupDto groupDto) {
- verify(dbClient.roleDao(), never()).deleteGroupRolesByGroupUuid(dbSession, groupDto.getUuid());
- verify(dbClient.permissionTemplateDao(), never()).deleteByGroup(dbSession, groupDto.getUuid(), groupDto.getName());
- verify(dbClient.userGroupDao(), never()).deleteByGroupUuid(dbSession, groupDto.getUuid(), groupDto.getName());
- verify(dbClient.qProfileEditGroupsDao(), never()).deleteByGroup(dbSession, groupDto);
- verify(dbClient.qualityGateGroupPermissionsDao(), never()).deleteByGroup(dbSession, groupDto);
- verify(dbClient.scimGroupDao(), never()).deleteByGroupUuid(dbSession, groupDto.getUuid());
- verify(dbClient.groupDao(), never()).deleteByUuid(dbSession, groupDto.getUuid(), groupDto.getName());
- verify(dbClient.githubOrganizationGroupDao(), never()).deleteByGroupUuid(dbSession, groupDto.getUuid());
- }
-
- private void verifyGroupDelete(DbSession dbSession, GroupDto groupDto) {
- verify(dbClient.roleDao()).deleteGroupRolesByGroupUuid(dbSession, groupDto.getUuid());
- verify(dbClient.permissionTemplateDao()).deleteByGroup(dbSession, groupDto.getUuid(), groupDto.getName());
- verify(dbClient.userGroupDao()).deleteByGroupUuid(dbSession, groupDto.getUuid(), groupDto.getName());
- verify(dbClient.qProfileEditGroupsDao()).deleteByGroup(dbSession, groupDto);
- verify(dbClient.qualityGateGroupPermissionsDao()).deleteByGroup(dbSession, groupDto);
- verify(dbClient.scimGroupDao()).deleteByGroupUuid(dbSession, groupDto.getUuid());
- verify(dbClient.externalGroupDao()).deleteByGroupUuid(dbSession, groupDto.getUuid());
- verify(dbClient.groupDao()).deleteByUuid(dbSession, groupDto.getUuid(), groupDto.getName());
- verify(dbClient.githubOrganizationGroupDao()).deleteByGroupUuid(dbSession, groupDto.getUuid());
- }
-}