3 * Copyright (C) 2009-2024 SonarSource SA
4 * mailto:info AT sonarsource DOT com
6 * This program is free software; you can redistribute it and/or
7 * modify it under the terms of the GNU Lesser General Public
8 * License as published by the Free Software Foundation; either
9 * version 3 of the License, or (at your option) any later version.
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14 * Lesser General Public License for more details.
16 * You should have received a copy of the GNU Lesser General Public License
17 * along with this program; if not, write to the Free Software Foundation,
18 * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
20 package org.sonar.server.common.group.service;
22 import com.tngtech.java.junit.dataprovider.DataProvider;
23 import com.tngtech.java.junit.dataprovider.DataProviderRunner;
24 import com.tngtech.java.junit.dataprovider.UseDataProvider;
25 import java.util.List;
27 import java.util.Optional;
28 import java.util.stream.Collectors;
29 import org.junit.Before;
30 import org.junit.Rule;
31 import org.junit.Test;
32 import org.junit.runner.RunWith;
33 import org.mockito.ArgumentCaptor;
34 import org.mockito.Captor;
35 import org.mockito.InjectMocks;
36 import org.mockito.Mock;
37 import org.mockito.junit.MockitoJUnit;
38 import org.mockito.junit.MockitoRule;
39 import org.sonar.api.security.DefaultGroups;
40 import org.sonar.core.util.UuidFactory;
41 import org.sonar.db.DbClient;
42 import org.sonar.db.DbSession;
43 import org.sonar.db.permission.AuthorizationDao;
44 import org.sonar.db.permission.GlobalPermission;
45 import org.sonar.db.permission.template.PermissionTemplateDao;
46 import org.sonar.db.provisioning.GithubOrganizationGroupDao;
47 import org.sonar.db.qualitygate.QualityGateGroupPermissionsDao;
48 import org.sonar.db.qualityprofile.QProfileEditGroupsDao;
49 import org.sonar.db.scim.ScimGroupDao;
50 import org.sonar.db.user.ExternalGroupDao;
51 import org.sonar.db.user.GroupDao;
52 import org.sonar.db.user.GroupDto;
53 import org.sonar.db.user.GroupQuery;
54 import org.sonar.db.user.RoleDao;
55 import org.sonar.db.user.UserGroupDao;
56 import org.sonar.server.common.SearchResults;
57 import org.sonar.server.exceptions.BadRequestException;
58 import org.sonar.server.management.ManagedInstanceService;
59 import org.sonar.server.usergroups.DefaultGroupFinder;
61 import static java.lang.String.format;
62 import static java.util.function.Function.identity;
63 import static org.apache.commons.lang3.RandomStringUtils.randomAlphanumeric;
64 import static org.assertj.core.api.Assertions.assertThat;
65 import static org.assertj.core.api.Assertions.assertThatNoException;
66 import static org.assertj.core.api.Assertions.assertThatThrownBy;
67 import static org.assertj.core.api.AssertionsForClassTypes.assertThatExceptionOfType;
68 import static org.mockito.ArgumentMatchers.any;
69 import static org.mockito.ArgumentMatchers.anyInt;
70 import static org.mockito.ArgumentMatchers.eq;
71 import static org.mockito.Mockito.mock;
72 import static org.mockito.Mockito.never;
73 import static org.mockito.Mockito.verify;
74 import static org.mockito.Mockito.when;
76 @RunWith(DataProviderRunner.class)
77 public class GroupServiceTest {
79 private static final String GROUP_NAME = "GROUP_NAME";
80 private static final String GROUP_UUID = "GROUP_UUID";
81 private static final String DEFAULT_GROUP_NAME = "sonar-users";
82 private static final String DEFAULT_GROUP_UUID = "DEFAULT_GROUP_UUID";
84 private DbSession dbSession;
86 private DbClient dbClient;
88 private UuidFactory uuidFactory;
90 private DefaultGroupFinder defaultGroupFinder;
92 private ManagedInstanceService managedInstanceService;
94 private GroupService groupService;
97 private ArgumentCaptor<GroupQuery> queryCaptor;
100 public MockitoRule rule = MockitoJUnit.rule();
103 public void setUp() {
107 private void mockNeededDaos() {
108 when(dbClient.authorizationDao()).thenReturn(mock(AuthorizationDao.class));
109 when(dbClient.roleDao()).thenReturn(mock(RoleDao.class));
110 when(dbClient.permissionTemplateDao()).thenReturn(mock(PermissionTemplateDao.class));
111 when(dbClient.userGroupDao()).thenReturn(mock(UserGroupDao.class));
112 when(dbClient.qProfileEditGroupsDao()).thenReturn(mock(QProfileEditGroupsDao.class));
113 when(dbClient.qualityGateGroupPermissionsDao()).thenReturn(mock(QualityGateGroupPermissionsDao.class));
114 when(dbClient.scimGroupDao()).thenReturn(mock(ScimGroupDao.class));
115 when(dbClient.externalGroupDao()).thenReturn(mock(ExternalGroupDao.class));
116 when(dbClient.groupDao()).thenReturn(mock(GroupDao.class));
117 when(dbClient.githubOrganizationGroupDao()).thenReturn(mock(GithubOrganizationGroupDao.class));
121 public void findGroup_whenGroupExists_returnsIt() {
122 GroupDto groupDto = mockGroupDto();
124 when(dbClient.groupDao().selectByName(dbSession, GROUP_NAME))
125 .thenReturn(Optional.of(groupDto));
127 assertThat(groupService.findGroup(dbSession, GROUP_NAME)).contains(groupDto);
131 public void findGroup_whenGroupDoesntExist_returnsEmtpyOptional() {
132 when(dbClient.groupDao().selectByName(dbSession, GROUP_NAME))
133 .thenReturn(Optional.empty());
135 assertThat(groupService.findGroup(dbSession, GROUP_NAME)).isEmpty();
139 public void findGroupByUuid_whenGroupExistsAndIsManagedAndDefault_returnsItWithCorrectValues() {
140 GroupDto groupDto = mockGroupDto();
142 when(defaultGroupFinder.findDefaultGroup(dbSession)).thenReturn(new GroupDto().setUuid(GROUP_UUID).setName("default-group"));
144 when(dbClient.groupDao().selectByUuid(dbSession, GROUP_UUID))
145 .thenReturn(groupDto);
146 when(managedInstanceService.isGroupManaged(dbSession, groupDto.getUuid())).thenReturn(true);
148 GroupInformation expected = new GroupInformation(groupDto, true, true);
149 assertThat(groupService.findGroupByUuid(dbSession, GROUP_UUID)).contains(expected);
153 public void findGroupByUuid_whenGroupExistsAndIsNotManagedAndDefault_returnsItWithCorrectValues() {
154 GroupDto groupDto = mockGroupDto();
156 when(defaultGroupFinder.findDefaultGroup(dbSession)).thenReturn(new GroupDto().setUuid("another-uuid").setName("default-group"));
158 when(dbClient.groupDao().selectByUuid(dbSession, GROUP_UUID))
159 .thenReturn(groupDto);
160 when(managedInstanceService.isGroupManaged(dbSession, groupDto.getUuid())).thenReturn(false);
162 GroupInformation expected = new GroupInformation(groupDto, false, false);
163 assertThat(groupService.findGroupByUuid(dbSession, GROUP_UUID)).contains(expected);
167 public void findGroupByUuid_whenGroupDoesntExist_returnsEmptyOptional() {
168 when(dbClient.groupDao().selectByUuid(dbSession, GROUP_UUID))
171 assertThat(groupService.findGroupByUuid(dbSession, GROUP_UUID)).isEmpty();
175 public void delete_whenNotDefaultAndNotLastAdminGroup_deleteGroup() {
176 GroupDto groupDto = mockGroupDto();
178 when(dbClient.groupDao().selectByName(dbSession, DefaultGroups.USERS))
179 .thenReturn(Optional.of(new GroupDto().setUuid("another_group_uuid")));
180 when(dbClient.authorizationDao().countUsersWithGlobalPermissionExcludingGroup(dbSession, GlobalPermission.ADMINISTER.getKey(), groupDto.getUuid()))
183 groupService.delete(dbSession, groupDto);
185 verifyGroupDelete(dbSession, groupDto);
189 public void delete_whenDefaultGroup_throwAndDontDeleteGroup() {
190 GroupDto groupDto = mockGroupDto();
192 when(dbClient.groupDao().selectByName(dbSession, DefaultGroups.USERS))
193 .thenReturn(Optional.of(groupDto));
195 assertThatThrownBy(() -> groupService.delete(dbSession, groupDto))
196 .isInstanceOf(IllegalArgumentException.class)
197 .hasMessage(format("Default group '%s' cannot be used to perform this action", GROUP_NAME));
199 verifyNoGroupDelete(dbSession, groupDto);
203 public void delete_whenLastAdminGroup_throwAndDontDeleteGroup() {
204 GroupDto groupDto = mockGroupDto();
206 when(dbClient.groupDao().selectByName(dbSession, DefaultGroups.USERS))
207 .thenReturn(Optional.of(new GroupDto().setUuid("another_group_uuid"))); // We must pass the default group check
208 when(dbClient.authorizationDao().countUsersWithGlobalPermissionExcludingGroup(dbSession, GlobalPermission.ADMINISTER.getKey(), groupDto.getUuid()))
211 assertThatThrownBy(() -> groupService.delete(dbSession, groupDto))
212 .isInstanceOf(IllegalArgumentException.class)
213 .hasMessage("The last system admin group cannot be deleted");
215 verifyNoGroupDelete(dbSession, groupDto);
219 public void updateGroup_updatesGroupNameAndDescription() {
220 GroupDto group = mockGroupDto();
221 GroupDto groupWithUpdatedName = mockGroupDto();
222 GroupDto groupWithUpdatedDescription = mockGroupDto();
224 when(dbClient.groupDao().update(dbSession, group)).thenReturn(groupWithUpdatedName);
225 when(dbClient.groupDao().update(dbSession, groupWithUpdatedName)).thenReturn(groupWithUpdatedDescription);
227 groupService.updateGroup(dbSession, group, "new-name", "New Description");
228 verify(group).setName("new-name");
229 verify(groupWithUpdatedName).setDescription("New Description");
230 verify(dbClient.groupDao()).update(dbSession, group);
231 verify(dbClient.groupDao()).update(dbSession, groupWithUpdatedName);
235 public void updateGroup_updatesGroupName() {
236 GroupDto group = mockGroupDto();
239 when(dbClient.groupDao().update(dbSession, group)).thenReturn(group);
240 groupService.updateGroup(dbSession, group, "new-name");
241 verify(group).setName("new-name");
242 verify(dbClient.groupDao()).update(dbSession, group);
246 public void updateGroup_whenGroupIsDefault_throws() {
247 GroupDto defaultGroup = mockDefaultGroup();
248 when(dbClient.groupDao().selectByName(dbSession, DEFAULT_GROUP_NAME)).thenReturn(Optional.of(defaultGroup));
250 assertThatExceptionOfType(IllegalArgumentException.class)
251 .isThrownBy(() -> groupService.updateGroup(dbSession, defaultGroup, "new-name", "New Description"))
252 .withMessage("Default group 'sonar-users' cannot be used to perform this action");
254 assertThatExceptionOfType(IllegalArgumentException.class)
255 .isThrownBy(() -> groupService.updateGroup(dbSession, defaultGroup, "new-name"))
256 .withMessage("Default group 'sonar-users' cannot be used to perform this action");
260 public void updateGroup_whenGroupNameDoesntChange_succeedsWithDescription() {
261 GroupDto group = mockGroupDto();
264 when(dbClient.groupDao().update(dbSession, group)).thenReturn(group);
265 groupService.updateGroup(dbSession, group, group.getName(), "New Description");
267 verify(group).setDescription("New Description");
268 verify(dbClient.groupDao()).update(dbSession, group);
272 public void updateGroup_whenGroupNameDoesntChange_succeeds() {
273 GroupDto group = mockGroupDto();
276 assertThatNoException()
277 .isThrownBy(() -> groupService.updateGroup(dbSession, group, group.getName()));
279 verify(dbClient.groupDao(), never()).update(dbSession, group);
283 public void updateGroup_whenGroupExist_throws() {
284 GroupDto group = mockGroupDto();
285 GroupDto group2 = mockGroupDto();
287 String group2Name = GROUP_NAME + "2";
289 when(dbClient.groupDao().selectByName(dbSession, group2Name)).thenReturn(Optional.of(group2));
291 assertThatExceptionOfType(BadRequestException.class)
292 .isThrownBy(() -> groupService.updateGroup(dbSession, group, group2Name, "New Description"))
293 .withMessage("Group '" + group2Name + "' already exists");
295 assertThatExceptionOfType(BadRequestException.class)
296 .isThrownBy(() -> groupService.updateGroup(dbSession, group, group2Name))
297 .withMessage("Group '" + group2Name + "' already exists");
301 @UseDataProvider("invalidGroupNames")
302 public void updateGroup_whenGroupNameIsInvalid_throws(String groupName, String errorMessage) {
303 GroupDto group = mockGroupDto();
306 assertThatExceptionOfType(BadRequestException.class)
307 .isThrownBy(() -> groupService.updateGroup(dbSession, group, groupName, "New Description"))
308 .withMessage(errorMessage);
310 assertThatExceptionOfType(BadRequestException.class)
311 .isThrownBy(() -> groupService.updateGroup(dbSession, group, groupName))
312 .withMessage(errorMessage);
316 public void createGroup_whenNameAndDescriptionIsProvided_createsGroup() {
318 when(uuidFactory.create()).thenReturn("1234");
319 GroupDto createdGroup = mockGroupDto();
320 when(dbClient.groupDao().insert(eq(dbSession), any())).thenReturn(createdGroup);
322 groupService.createGroup(dbSession, "Name", "Description");
324 ArgumentCaptor<GroupDto> groupCaptor = ArgumentCaptor.forClass(GroupDto.class);
325 verify(dbClient.groupDao()).insert(eq(dbSession), groupCaptor.capture());
326 GroupDto groupToCreate = groupCaptor.getValue();
327 assertThat(groupToCreate.getName()).isEqualTo("Name");
328 assertThat(groupToCreate.getDescription()).isEqualTo("Description");
329 assertThat(groupToCreate.getUuid()).isEqualTo("1234");
333 public void createGroup_whenGroupExist_throws() {
334 GroupDto group = mockGroupDto();
336 when(dbClient.groupDao().selectByName(dbSession, GROUP_NAME)).thenReturn(Optional.of(group));
338 assertThatExceptionOfType(BadRequestException.class)
339 .isThrownBy(() -> groupService.createGroup(dbSession, GROUP_NAME, "New Description"))
340 .withMessage("Group '" + GROUP_NAME + "' already exists");
345 @UseDataProvider("invalidGroupNames")
346 public void createGroup_whenGroupNameIsInvalid_throws(String groupName, String errorMessage) {
349 assertThatExceptionOfType(BadRequestException.class)
350 .isThrownBy(() -> groupService.createGroup(dbSession, groupName, "Description"))
351 .withMessage(errorMessage);
356 public static Object[][] invalidGroupNames() {
357 return new Object[][] {
358 {"", "Group name cannot be empty"},
359 {randomAlphanumeric(256), "Group name cannot be longer than 255 characters"},
360 {"Anyone", "Anyone group cannot be used"},
365 public void search_whenSeveralGroupFound_returnsThem() {
366 GroupDto groupDto1 = mockGroupDto("1");
367 GroupDto groupDto2 = mockGroupDto("2");
368 GroupDto defaultGroup = mockDefaultGroup();
370 when(dbClient.groupDao().selectByQuery(eq(dbSession), queryCaptor.capture(), eq(5), eq(24)))
371 .thenReturn(List.of(groupDto1, groupDto2, defaultGroup));
373 Map<String, Boolean> groupUuidToManaged = Map.of(
374 groupDto1.getUuid(), false,
375 groupDto2.getUuid(), true,
376 defaultGroup.getUuid(), false);
377 when(managedInstanceService.getGroupUuidToManaged(dbSession, groupUuidToManaged.keySet())).thenReturn(groupUuidToManaged);
379 when(dbClient.groupDao().countByQuery(eq(dbSession), any())).thenReturn(300);
381 SearchResults<GroupInformation> searchResults = groupService.search(dbSession, new GroupSearchRequest("query", null, 5, 24));
382 assertThat(searchResults.total()).isEqualTo(300);
384 Map<String, GroupInformation> uuidToGroupInformation = searchResults.searchResults().stream()
385 .collect(Collectors.toMap(groupInformation -> groupInformation.groupDto().getUuid(), identity()));
386 assertGroupInformation(uuidToGroupInformation, groupDto1, false, false);
387 assertGroupInformation(uuidToGroupInformation, groupDto2, true, false);
388 assertGroupInformation(uuidToGroupInformation, defaultGroup, false, true);
390 assertThat(queryCaptor.getValue().getSearchText()).isEqualTo("%QUERY%");
391 assertThat(queryCaptor.getValue().getIsManagedSqlClause()).isNull();
394 public void search_whenPageSizeEquals0_returnsOnlyTotal() {
395 when(dbClient.groupDao().countByQuery(eq(dbSession), any())).thenReturn(10);
397 SearchResults<GroupInformation> searchResults = groupService.search(dbSession, new GroupSearchRequest("query", null, 0, 24));
398 assertThat(searchResults.total()).isEqualTo(10);
399 assertThat(searchResults.searchResults()).isEmpty();
401 verify(dbClient.groupDao(), never()).selectByQuery(eq(dbSession), any(), anyInt(), anyInt());
405 public void search_whenInstanceManagedAndManagedIsTrue_addsManagedClause() {
406 mockManagedInstance();
407 when(dbClient.groupDao().selectByQuery(eq(dbSession), queryCaptor.capture(), anyInt(), anyInt())).thenReturn(List.of());
409 groupService.search(dbSession, new GroupSearchRequest("query", true, 5, 24));
411 assertThat(queryCaptor.getValue().getIsManagedSqlClause()).isEqualTo("managed_filter");
415 public void search_whenInstanceManagedAndManagedIsFalse_addsManagedClause() {
416 mockManagedInstance();
417 when(dbClient.groupDao().selectByQuery(eq(dbSession), queryCaptor.capture(), anyInt(), anyInt())).thenReturn(List.of());
419 groupService.search(dbSession, new GroupSearchRequest("query", false, 5, 24));
421 assertThat(queryCaptor.getValue().getIsManagedSqlClause()).isEqualTo("not_managed_filter");
425 public void search_whenInstanceNotManagedAndManagedIsTrue_throws() {
426 assertThatExceptionOfType(BadRequestException.class)
427 .isThrownBy(() -> groupService.search(dbSession, new GroupSearchRequest("query", true, 5, 24)))
428 .withMessage("The 'managed' parameter is only available for managed instances.");
431 private void mockManagedInstance() {
432 when(managedInstanceService.isInstanceExternallyManaged()).thenReturn(true);
433 when(managedInstanceService.getManagedGroupsSqlFilter(true)).thenReturn("managed_filter");
434 when(managedInstanceService.getManagedGroupsSqlFilter(false)).thenReturn("not_managed_filter");
437 private static void assertGroupInformation(Map<String, GroupInformation> uuidToGroupInformation, GroupDto expectedGroupDto, boolean expectedManaged, boolean expectedDefault) {
438 assertThat(uuidToGroupInformation.get(expectedGroupDto.getUuid()).groupDto()).isEqualTo(expectedGroupDto);
439 assertThat(uuidToGroupInformation.get(expectedGroupDto.getUuid()).isManaged()).isEqualTo(expectedManaged);
440 assertThat(uuidToGroupInformation.get(expectedGroupDto.getUuid()).isDefault()).isEqualTo(expectedDefault);
443 private static GroupDto mockGroupDto() {
444 GroupDto groupDto = mock(GroupDto.class);
445 when(groupDto.getName()).thenReturn(GROUP_NAME);
446 when(groupDto.getUuid()).thenReturn(GROUP_UUID);
450 private static GroupDto mockGroupDto(String id) {
451 GroupDto groupDto = mock(GroupDto.class);
452 when(groupDto.getUuid()).thenReturn(id);
453 when(groupDto.getName()).thenReturn("name_" + id);
457 private GroupDto mockDefaultGroup() {
458 GroupDto defaultGroup = mock(GroupDto.class);
459 when(defaultGroup.getName()).thenReturn(DEFAULT_GROUP_NAME);
460 when(defaultGroup.getUuid()).thenReturn(DEFAULT_GROUP_UUID);
461 when(dbClient.groupDao().selectByName(dbSession, DEFAULT_GROUP_NAME)).thenReturn(Optional.of(defaultGroup));
462 when(defaultGroupFinder.findDefaultGroup(dbSession)).thenReturn(defaultGroup);
466 private void verifyNoGroupDelete(DbSession dbSession, GroupDto groupDto) {
467 verify(dbClient.roleDao(), never()).deleteGroupRolesByGroupUuid(dbSession, groupDto.getUuid());
468 verify(dbClient.permissionTemplateDao(), never()).deleteByGroup(dbSession, groupDto.getUuid(), groupDto.getName());
469 verify(dbClient.userGroupDao(), never()).deleteByGroupUuid(dbSession, groupDto.getUuid(), groupDto.getName());
470 verify(dbClient.qProfileEditGroupsDao(), never()).deleteByGroup(dbSession, groupDto);
471 verify(dbClient.qualityGateGroupPermissionsDao(), never()).deleteByGroup(dbSession, groupDto);
472 verify(dbClient.scimGroupDao(), never()).deleteByGroupUuid(dbSession, groupDto.getUuid());
473 verify(dbClient.groupDao(), never()).deleteByUuid(dbSession, groupDto.getUuid(), groupDto.getName());
474 verify(dbClient.githubOrganizationGroupDao(), never()).deleteByGroupUuid(dbSession, groupDto.getUuid());
477 private void verifyGroupDelete(DbSession dbSession, GroupDto groupDto) {
478 verify(dbClient.roleDao()).deleteGroupRolesByGroupUuid(dbSession, groupDto.getUuid());
479 verify(dbClient.permissionTemplateDao()).deleteByGroup(dbSession, groupDto.getUuid(), groupDto.getName());
480 verify(dbClient.userGroupDao()).deleteByGroupUuid(dbSession, groupDto.getUuid(), groupDto.getName());
481 verify(dbClient.qProfileEditGroupsDao()).deleteByGroup(dbSession, groupDto);
482 verify(dbClient.qualityGateGroupPermissionsDao()).deleteByGroup(dbSession, groupDto);
483 verify(dbClient.scimGroupDao()).deleteByGroupUuid(dbSession, groupDto.getUuid());
484 verify(dbClient.externalGroupDao()).deleteByGroupUuid(dbSession, groupDto.getUuid());
485 verify(dbClient.groupDao()).deleteByUuid(dbSession, groupDto.getUuid(), groupDto.getName());
486 verify(dbClient.githubOrganizationGroupDao()).deleteByGroupUuid(dbSession, groupDto.getUuid());