| // Copyright (C) 2024 The Android Open Source Project |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| package com.google.gerrit.server.restapi.group; |
| |
| import com.google.gerrit.entities.AccountGroup; |
| import com.google.gerrit.entities.CachedProjectConfig; |
| import com.google.gerrit.entities.GroupDescription; |
| import com.google.gerrit.entities.InternalGroup; |
| import com.google.gerrit.entities.Project; |
| import com.google.gerrit.extensions.client.ListGroupsOption; |
| import com.google.gerrit.extensions.common.DeleteGroupInput; |
| import com.google.gerrit.extensions.common.GroupInfo; |
| import com.google.gerrit.extensions.restapi.AuthException; |
| import com.google.gerrit.extensions.restapi.BadRequestException; |
| import com.google.gerrit.extensions.restapi.ResourceConflictException; |
| import com.google.gerrit.extensions.restapi.ResourceNotFoundException; |
| import com.google.gerrit.extensions.restapi.Response; |
| import com.google.gerrit.extensions.restapi.RestModifyView; |
| import com.google.gerrit.extensions.restapi.UnprocessableEntityException; |
| import com.google.gerrit.server.UserInitiated; |
| import com.google.gerrit.server.account.GroupCache; |
| import com.google.gerrit.server.account.GroupControl; |
| import com.google.gerrit.server.config.GerritServerConfig; |
| import com.google.gerrit.server.group.GroupResource; |
| import com.google.gerrit.server.group.db.Groups; |
| import com.google.gerrit.server.group.db.GroupsUpdate; |
| import com.google.gerrit.server.permissions.PermissionBackendException; |
| import com.google.gerrit.server.project.ProjectCache; |
| import com.google.gerrit.server.project.ProjectState; |
| import com.google.inject.Inject; |
| import com.google.inject.Provider; |
| import com.google.inject.Singleton; |
| import java.io.IOException; |
| import java.util.ArrayList; |
| import java.util.List; |
| import java.util.Optional; |
| import java.util.stream.Collectors; |
| import org.eclipse.jgit.errors.ConfigInvalidException; |
| import org.eclipse.jgit.lib.Config; |
| |
| @Singleton |
| public class DeleteGroup implements RestModifyView<GroupResource, DeleteGroupInput> { |
| private final Provider<ListGroups> listGroupProvider; |
| private final GroupCache groupCache; |
| private final ProjectCache projectCache; |
| private final Provider<GroupsUpdate> groupsUpdateProvider; |
| private final GroupJson json; |
| private final Groups groups; |
| private final boolean deleteGroupEnabled; |
| |
| @Inject |
| DeleteGroup( |
| Provider<ListGroups> listGroupProvider, |
| GroupCache groupCache, |
| ProjectCache projectCache, |
| @UserInitiated Provider<GroupsUpdate> groupsUpdateProvider, |
| @GerritServerConfig Config cfg, |
| GroupJson json, |
| Groups groups) { |
| this.listGroupProvider = listGroupProvider; |
| this.groupCache = groupCache; |
| this.projectCache = projectCache; |
| this.groupsUpdateProvider = groupsUpdateProvider; |
| this.json = json; |
| this.groups = groups; |
| this.deleteGroupEnabled = cfg.getBoolean("groups", "enableDeleteGroup", false); |
| } |
| |
| public DeleteGroup addOption(ListGroupsOption o) { |
| json.addOption(o); |
| return this; |
| } |
| |
| @Override |
| public Response<String> apply(GroupResource resource, DeleteGroupInput input) |
| throws AuthException, |
| BadRequestException, |
| UnprocessableEntityException, |
| ResourceConflictException, |
| IOException, |
| ConfigInvalidException, |
| ResourceNotFoundException, |
| PermissionBackendException, |
| NotInternalGroupException { |
| if (deleteGroupEnabled) { |
| GroupDescription.Internal internalGroup = |
| resource.asInternalGroup().orElseThrow(NotInternalGroupException::new); |
| final GroupControl control = resource.getControl(); |
| if (!control.canDeleteGroup()) { |
| throw new AuthException("Cannot delete group " + internalGroup.getName()); |
| } |
| groupDeletionPrecondition(internalGroup); |
| deleteGroup(internalGroup); |
| return Response.ok(); |
| } |
| throw new ResourceNotFoundException("Deletion of Group is not enabled"); |
| } |
| |
| private void groupDeletionPrecondition(GroupDescription.Internal internalGroup) |
| throws ResourceConflictException, ConfigInvalidException, IOException { |
| AccountGroup.UUID uuid = internalGroup.getGroupUUID(); |
| if (groupCache.get(internalGroup.getGroupUUID()).isEmpty()) { |
| throw new ResourceConflictException( |
| String.format("group %s does not exist", internalGroup.getGroupUUID())); |
| } |
| List<InternalGroup> ownedGroup = getOwnedGroup(uuid, internalGroup.getName()); |
| if (!ownedGroup.isEmpty()) { |
| String msg = |
| "Cannot delete group that is owner of other groups: \n" |
| + ownedGroup.stream() |
| .map(InternalGroup::getName) |
| .collect(Collectors.joining(", ", "[", "]")); |
| throw new ResourceConflictException(msg); |
| } |
| List<String> inProjects = getProjectsWithGroupRefs(uuid); |
| if (!inProjects.isEmpty()) { |
| String msg = |
| "Cannot delete group that is referenced in access permissions for project: \n" |
| + inProjects; |
| throw new ResourceConflictException(msg); |
| } |
| List<String> subgroupsInGroups = getSubgroupsInGroups(uuid, internalGroup.getName()); |
| if (!subgroupsInGroups.isEmpty()) { |
| String msg = "Cannot delete group that is subgroup of another group: \n" + subgroupsInGroups; |
| throw new ResourceConflictException(msg); |
| } |
| } |
| |
| private List<InternalGroup> getOwnedGroup(AccountGroup.UUID uuid, String groupOwnerName) |
| throws IOException { |
| try { |
| ListGroups listOwner = listGroupProvider.get(); |
| listOwner.setOwnedBy(uuid.get()); |
| List<GroupInfo> groups = listOwner.get(); |
| return groups.stream() |
| .filter(group -> !group.name.equals(groupOwnerName)) |
| .map(group -> groupCache.get(AccountGroup.UUID.parse(group.id))) |
| .filter(Optional::isPresent) |
| .map(Optional::get) |
| .collect(Collectors.toList()); |
| } catch (Exception e) { |
| throw new IOException("Failed to check owned groups for group " + uuid.get(), e); |
| } |
| } |
| |
| private List<String> getProjectsWithGroupRefs(AccountGroup.UUID uuid) { |
| List<String> projects = new ArrayList<>(); |
| for (Project.NameKey projectName : projectCache.all()) { |
| Optional<ProjectState> projectState = projectCache.get(projectName); |
| if (projectState.isPresent()) { |
| CachedProjectConfig config = projectState.get().getConfig(); |
| if (config.getGroup(uuid).isPresent()) { |
| projects.add(projectName.toString()); |
| } |
| } |
| } |
| return projects; |
| } |
| |
| private List<String> getSubgroupsInGroups(AccountGroup.UUID uuid, String groupName) |
| throws ConfigInvalidException, IOException { |
| List<String> allGroupsWithSubGroups = new ArrayList<>(); |
| groups |
| .getAllGroupReferences() |
| .forEach( |
| entry -> { |
| try { |
| if (groups.getGroup(entry.getUUID()).isEmpty()) { |
| throw new ResourceNotFoundException( |
| String.format( |
| "Could not check if group %s is subgroup of %s", |
| groupName, entry.getName())); |
| } |
| if (groups.getGroup(entry.getUUID()).get().getSubgroups().contains(uuid)) { |
| allGroupsWithSubGroups.add(entry.getName()); |
| } |
| } catch (IOException | ConfigInvalidException | ResourceNotFoundException e) { |
| throw new RuntimeException(e); |
| } |
| }); |
| return allGroupsWithSubGroups; |
| } |
| |
| private void deleteGroup(GroupDescription.Internal internalGroup) |
| throws IOException, ConfigInvalidException { |
| AccountGroup.UUID uuid = internalGroup.getGroupUUID(); |
| groupsUpdateProvider.get().deleteGroup(uuid); |
| } |
| } |