Line data Source code
1 : // Copyright (C) 2012 The Android Open Source Project 2 : // 3 : // Licensed under the Apache License, Version 2.0 (the "License"); 4 : // you may not use this file except in compliance with the License. 5 : // You may obtain a copy of the License at 6 : // 7 : // http://www.apache.org/licenses/LICENSE-2.0 8 : // 9 : // Unless required by applicable law or agreed to in writing, software 10 : // distributed under the License is distributed on an "AS IS" BASIS, 11 : // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 : // See the License for the specific language governing permissions and 13 : // limitations under the License. 14 : 15 : package com.google.gerrit.server.restapi.project; 16 : 17 : import static java.util.Objects.requireNonNull; 18 : 19 : import com.google.common.base.MoreObjects; 20 : import com.google.common.base.Strings; 21 : import com.google.common.collect.Iterables; 22 : import com.google.common.collect.Multimap; 23 : import com.google.gerrit.entities.Project; 24 : import com.google.gerrit.extensions.api.projects.ParentInput; 25 : import com.google.gerrit.extensions.restapi.AuthException; 26 : import com.google.gerrit.extensions.restapi.BadRequestException; 27 : import com.google.gerrit.extensions.restapi.ResourceConflictException; 28 : import com.google.gerrit.extensions.restapi.ResourceNotFoundException; 29 : import com.google.gerrit.extensions.restapi.Response; 30 : import com.google.gerrit.extensions.restapi.RestModifyView; 31 : import com.google.gerrit.extensions.restapi.UnprocessableEntityException; 32 : import com.google.gerrit.server.IdentifiedUser; 33 : import com.google.gerrit.server.config.AllProjectsName; 34 : import com.google.gerrit.server.config.AllUsersName; 35 : import com.google.gerrit.server.config.ConfigKey; 36 : import com.google.gerrit.server.config.ConfigUpdatedEvent; 37 : import com.google.gerrit.server.config.ConfigUpdatedEvent.ConfigUpdateEntry; 38 : import com.google.gerrit.server.config.ConfigUpdatedEvent.UpdateResult; 39 : import com.google.gerrit.server.config.GerritConfigListener; 40 : import com.google.gerrit.server.config.GerritServerConfig; 41 : import com.google.gerrit.server.git.meta.MetaDataUpdate; 42 : import com.google.gerrit.server.permissions.GlobalPermission; 43 : import com.google.gerrit.server.permissions.PermissionBackend; 44 : import com.google.gerrit.server.permissions.PermissionBackendException; 45 : import com.google.gerrit.server.permissions.ProjectPermission; 46 : import com.google.gerrit.server.project.ProjectCache; 47 : import com.google.gerrit.server.project.ProjectConfig; 48 : import com.google.gerrit.server.project.ProjectResource; 49 : import com.google.gerrit.server.project.ProjectState; 50 : import com.google.inject.Inject; 51 : import com.google.inject.Provider; 52 : import com.google.inject.Singleton; 53 : import java.io.IOException; 54 : import org.eclipse.jgit.errors.ConfigInvalidException; 55 : import org.eclipse.jgit.errors.RepositoryNotFoundException; 56 : import org.eclipse.jgit.lib.Config; 57 : 58 : @Singleton 59 : public class SetParent 60 : implements RestModifyView<ProjectResource, ParentInput>, GerritConfigListener { 61 : private final ProjectCache cache; 62 : private final PermissionBackend permissionBackend; 63 : private final Provider<MetaDataUpdate.Server> updateFactory; 64 : private final AllProjectsName allProjects; 65 : private final AllUsersName allUsers; 66 : private final ProjectConfig.Factory projectConfigFactory; 67 : private volatile boolean allowProjectOwnersToChangeParent; 68 : 69 : @Inject 70 : SetParent( 71 : ProjectCache cache, 72 : PermissionBackend permissionBackend, 73 : Provider<MetaDataUpdate.Server> updateFactory, 74 : AllProjectsName allProjects, 75 : AllUsersName allUsers, 76 : ProjectConfig.Factory projectConfigFactory, 77 146 : @GerritServerConfig Config config) { 78 146 : this.cache = cache; 79 146 : this.permissionBackend = permissionBackend; 80 146 : this.updateFactory = updateFactory; 81 146 : this.allProjects = allProjects; 82 146 : this.allUsers = allUsers; 83 146 : this.projectConfigFactory = projectConfigFactory; 84 146 : this.allowProjectOwnersToChangeParent = 85 146 : config.getBoolean("receive", "allowProjectOwnersToChangeParent", false); 86 146 : } 87 : 88 : @Override 89 : public Response<String> apply(ProjectResource rsrc, ParentInput input) 90 : throws AuthException, ResourceConflictException, ResourceNotFoundException, 91 : UnprocessableEntityException, IOException, PermissionBackendException, 92 : BadRequestException { 93 2 : return Response.ok(apply(rsrc, input, true)); 94 : } 95 : 96 : public String apply(ProjectResource rsrc, ParentInput input, boolean checkIfAdmin) 97 : throws AuthException, ResourceConflictException, ResourceNotFoundException, 98 : UnprocessableEntityException, IOException, PermissionBackendException, 99 : BadRequestException { 100 2 : IdentifiedUser user = rsrc.getUser().asIdentifiedUser(); 101 2 : String parentName = 102 2 : MoreObjects.firstNonNull(Strings.emptyToNull(input.parent), allProjects.get()); 103 2 : validateParentUpdate(rsrc.getProjectState().getNameKey(), user, parentName, checkIfAdmin); 104 2 : try (MetaDataUpdate md = updateFactory.get().create(rsrc.getNameKey())) { 105 2 : ProjectConfig config = projectConfigFactory.read(md); 106 2 : config.updateProject(p -> p.setParent(parentName)); 107 : 108 2 : String msg = Strings.emptyToNull(input.commitMessage); 109 2 : if (msg == null) { 110 2 : msg = String.format("Changed parent to %s.\n", parentName); 111 0 : } else if (!msg.endsWith("\n")) { 112 0 : msg += "\n"; 113 : } 114 2 : md.setAuthor(user); 115 2 : md.setMessage(msg); 116 2 : config.commit(md); 117 2 : cache.evictAndReindex(rsrc.getProjectState().getProject()); 118 : 119 2 : Project.NameKey parent = config.getProject().getParent(allProjects); 120 2 : requireNonNull(parent); 121 2 : return parent.get(); 122 0 : } catch (RepositoryNotFoundException notFound) { 123 0 : throw new ResourceNotFoundException(rsrc.getName(), notFound); 124 0 : } catch (ConfigInvalidException e) { 125 0 : throw new ResourceConflictException( 126 0 : String.format("invalid project.config: %s", e.getMessage())); 127 : } 128 : } 129 : 130 : public void validateParentUpdate( 131 : Project.NameKey project, IdentifiedUser user, String newParent, boolean checkIfAdmin) 132 : throws AuthException, ResourceConflictException, UnprocessableEntityException, 133 : PermissionBackendException, BadRequestException { 134 2 : if (checkIfAdmin) { 135 2 : if (allowProjectOwnersToChangeParent) { 136 1 : permissionBackend.user(user).project(project).check(ProjectPermission.WRITE_CONFIG); 137 : } else { 138 2 : permissionBackend.user(user).check(GlobalPermission.ADMINISTRATE_SERVER); 139 : } 140 : } 141 : 142 2 : if (project.equals(allUsers) && !allProjects.get().equals(newParent)) { 143 1 : throw new BadRequestException( 144 1 : String.format("%s must inherit from %s", allUsers.get(), allProjects.get())); 145 : } 146 : 147 2 : if (project.equals(allProjects)) { 148 1 : throw new ResourceConflictException("cannot set parent of " + allProjects.get()); 149 : } 150 : 151 2 : if (allUsers.get().equals(newParent)) { 152 1 : throw new ResourceConflictException( 153 1 : String.format("Cannot inherit from '%s' project", allUsers.get())); 154 : } 155 : 156 2 : newParent = Strings.emptyToNull(newParent); 157 2 : if (newParent != null) { 158 2 : Project.NameKey newParentNameKey = Project.nameKey(newParent); 159 2 : ProjectState parent = 160 : cache 161 2 : .get(newParentNameKey) 162 2 : .orElseThrow( 163 : () -> 164 1 : new UnprocessableEntityException( 165 : "parent project " + newParentNameKey + " not found")); 166 : 167 2 : if (parent.getName().equals(project.get())) { 168 1 : throw new ResourceConflictException("cannot set parent to self"); 169 : } 170 : 171 2 : if (Iterables.tryFind(parent.tree(), p -> p.getNameKey().equals(project)).isPresent()) { 172 1 : throw new ResourceConflictException( 173 1 : "cycle exists between " + project.get() + " and " + parent.getName()); 174 : } 175 : } 176 2 : } 177 : 178 : @Override 179 : public Multimap<UpdateResult, ConfigUpdateEntry> configUpdated(ConfigUpdatedEvent event) { 180 1 : ConfigKey receiveSetParent = ConfigKey.create("receive", "allowProjectOwnersToChangeParent"); 181 1 : if (!event.isValueUpdated(receiveSetParent)) { 182 1 : return ConfigUpdatedEvent.NO_UPDATES; 183 : } 184 : try { 185 0 : boolean enabled = 186 0 : event.getNewConfig().getBoolean("receive", "allowProjectOwnersToChangeParent", false); 187 0 : this.allowProjectOwnersToChangeParent = enabled; 188 0 : } catch (IllegalArgumentException iae) { 189 0 : return event.reject(receiveSetParent); 190 0 : } 191 0 : return event.accept(receiveSetParent); 192 : } 193 : }