| // Copyright (C) 2012 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.project; | |
| import static java.util.Objects.requireNonNull; | |
| import com.google.common.base.MoreObjects; | |
| import com.google.common.base.Strings; | |
| import com.google.common.collect.Iterables; | |
| import com.google.common.collect.Multimap; | |
| import com.google.gerrit.entities.Project; | |
| import com.google.gerrit.extensions.api.projects.ParentInput; | |
| 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.IdentifiedUser; | |
| import com.google.gerrit.server.config.AllProjectsName; | |
| import com.google.gerrit.server.config.AllUsersName; | |
| import com.google.gerrit.server.config.ConfigKey; | |
| import com.google.gerrit.server.config.ConfigUpdatedEvent; | |
| import com.google.gerrit.server.config.ConfigUpdatedEvent.ConfigUpdateEntry; | |
| import com.google.gerrit.server.config.ConfigUpdatedEvent.UpdateResult; | |
| import com.google.gerrit.server.config.GerritConfigListener; | |
| import com.google.gerrit.server.config.GerritServerConfig; | |
| import com.google.gerrit.server.git.meta.MetaDataUpdate; | |
| import com.google.gerrit.server.permissions.GlobalPermission; | |
| import com.google.gerrit.server.permissions.PermissionBackend; | |
| import com.google.gerrit.server.permissions.PermissionBackendException; | |
| import com.google.gerrit.server.permissions.ProjectPermission; | |
| import com.google.gerrit.server.project.ProjectCache; | |
| import com.google.gerrit.server.project.ProjectConfig; | |
| import com.google.gerrit.server.project.ProjectResource; | |
| 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 org.eclipse.jgit.errors.ConfigInvalidException; | |
| import org.eclipse.jgit.errors.RepositoryNotFoundException; | |
| import org.eclipse.jgit.lib.Config; | |
| @Singleton | |
| public class SetParent | |
| implements RestModifyView<ProjectResource, ParentInput>, GerritConfigListener { | |
| private final ProjectCache cache; | |
| private final PermissionBackend permissionBackend; | |
| private final Provider<MetaDataUpdate.Server> updateFactory; | |
| private final AllProjectsName allProjects; | |
| private final AllUsersName allUsers; | |
| private final ProjectConfig.Factory projectConfigFactory; | |
| private volatile boolean allowProjectOwnersToChangeParent; | |
| @Inject | |
| SetParent( | |
| ProjectCache cache, | |
| PermissionBackend permissionBackend, | |
| Provider<MetaDataUpdate.Server> updateFactory, | |
| AllProjectsName allProjects, | |
| AllUsersName allUsers, | |
| ProjectConfig.Factory projectConfigFactory, | |
| @GerritServerConfig Config config) { | |
| this.cache = cache; | |
| this.permissionBackend = permissionBackend; | |
| this.updateFactory = updateFactory; | |
| this.allProjects = allProjects; | |
| this.allUsers = allUsers; | |
| this.projectConfigFactory = projectConfigFactory; | |
| this.allowProjectOwnersToChangeParent = | |
| config.getBoolean("receive", "allowProjectOwnersToChangeParent", false); | |
| } | |
| @Override | |
| public Response<String> apply(ProjectResource rsrc, ParentInput input) | |
| throws AuthException, ResourceConflictException, ResourceNotFoundException, | |
| UnprocessableEntityException, IOException, PermissionBackendException, | |
| BadRequestException { | |
| return Response.ok(apply(rsrc, input, true)); | |
| } | |
| public String apply(ProjectResource rsrc, ParentInput input, boolean checkIfAdmin) | |
| throws AuthException, ResourceConflictException, ResourceNotFoundException, | |
| UnprocessableEntityException, IOException, PermissionBackendException, | |
| BadRequestException { | |
| IdentifiedUser user = rsrc.getUser().asIdentifiedUser(); | |
| String parentName = | |
| MoreObjects.firstNonNull(Strings.emptyToNull(input.parent), allProjects.get()); | |
| validateParentUpdate(rsrc.getProjectState().getNameKey(), user, parentName, checkIfAdmin); | |
| try (MetaDataUpdate md = updateFactory.get().create(rsrc.getNameKey())) { | |
| ProjectConfig config = projectConfigFactory.read(md); | |
| config.updateProject(p -> p.setParent(parentName)); | |
| String msg = Strings.emptyToNull(input.commitMessage); | |
| if (msg == null) { | |
| msg = String.format("Changed parent to %s.\n", parentName); | |
| } else if (!msg.endsWith("\n")) { | |
| msg += "\n"; | |
| } | |
| md.setAuthor(user); | |
| md.setMessage(msg); | |
| config.commit(md); | |
| cache.evictAndReindex(rsrc.getProjectState().getProject()); | |
| Project.NameKey parent = config.getProject().getParent(allProjects); | |
| requireNonNull(parent); | |
| return parent.get(); | |
| } catch (RepositoryNotFoundException notFound) { | |
| throw new ResourceNotFoundException(rsrc.getName(), notFound); | |
| } catch (ConfigInvalidException e) { | |
| throw new ResourceConflictException( | |
| String.format("invalid project.config: %s", e.getMessage())); | |
| } | |
| } | |
| public void validateParentUpdate( | |
| Project.NameKey project, IdentifiedUser user, String newParent, boolean checkIfAdmin) | |
| throws AuthException, ResourceConflictException, UnprocessableEntityException, | |
| PermissionBackendException, BadRequestException { | |
| if (checkIfAdmin) { | |
| if (allowProjectOwnersToChangeParent) { | |
| permissionBackend.user(user).project(project).check(ProjectPermission.WRITE_CONFIG); | |
| } else { | |
| permissionBackend.user(user).check(GlobalPermission.ADMINISTRATE_SERVER); | |
| } | |
| } | |
| if (project.equals(allUsers) && !allProjects.get().equals(newParent)) { | |
| throw new BadRequestException( | |
| String.format("%s must inherit from %s", allUsers.get(), allProjects.get())); | |
| } | |
| if (project.equals(allProjects)) { | |
| throw new ResourceConflictException("cannot set parent of " + allProjects.get()); | |
| } | |
| if (allUsers.get().equals(newParent)) { | |
| throw new ResourceConflictException( | |
| String.format("Cannot inherit from '%s' project", allUsers.get())); | |
| } | |
| newParent = Strings.emptyToNull(newParent); | |
| if (newParent != null) { | |
| Project.NameKey newParentNameKey = Project.nameKey(newParent); | |
| ProjectState parent = | |
| cache | |
| .get(newParentNameKey) | |
| .orElseThrow( | |
| () -> | |
| new UnprocessableEntityException( | |
| "parent project " + newParentNameKey + " not found")); | |
| if (parent.getName().equals(project.get())) { | |
| throw new ResourceConflictException("cannot set parent to self"); | |
| } | |
| if (Iterables.tryFind(parent.tree(), p -> p.getNameKey().equals(project)).isPresent()) { | |
| throw new ResourceConflictException( | |
| "cycle exists between " + project.get() + " and " + parent.getName()); | |
| } | |
| } | |
| } | |
| @Override | |
| public Multimap<UpdateResult, ConfigUpdateEntry> configUpdated(ConfigUpdatedEvent event) { | |
| ConfigKey receiveSetParent = ConfigKey.create("receive", "allowProjectOwnersToChangeParent"); | |
| if (!event.isValueUpdated(receiveSetParent)) { | |
| return ConfigUpdatedEvent.NO_UPDATES; | |
| } | |
| try { | |
| boolean enabled = | |
| event.getNewConfig().getBoolean("receive", "allowProjectOwnersToChangeParent", false); | |
| this.allowProjectOwnersToChangeParent = enabled; | |
| } catch (IllegalArgumentException iae) { | |
| return event.reject(receiveSetParent); | |
| } | |
| return event.accept(receiveSetParent); | |
| } | |
| } |