| // Copyright (C) 2018 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.acceptance.testsuite.project; | |
| import static com.google.common.collect.ImmutableList.toImmutableList; | |
| import static com.google.gerrit.entities.RefNames.REFS_CONFIG; | |
| import static com.google.gerrit.server.project.ProjectConfig.PROJECT_CONFIG; | |
| import static com.google.gerrit.testing.TestActionRefUpdateContext.openTestRefUpdateContext; | |
| import static java.nio.charset.StandardCharsets.UTF_8; | |
| import static java.util.Objects.requireNonNull; | |
| import com.google.common.base.Throwables; | |
| import com.google.common.collect.ImmutableList; | |
| import com.google.common.collect.ImmutableMap; | |
| import com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.TestCapability; | |
| import com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.TestLabelPermission; | |
| import com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.TestPermission; | |
| import com.google.gerrit.common.Nullable; | |
| import com.google.gerrit.entities.AccessSection; | |
| import com.google.gerrit.entities.AccountGroup; | |
| import com.google.gerrit.entities.GroupReference; | |
| import com.google.gerrit.entities.Permission; | |
| import com.google.gerrit.entities.PermissionRule; | |
| import com.google.gerrit.entities.Project; | |
| import com.google.gerrit.entities.RefNames; | |
| import com.google.gerrit.server.config.AllProjectsName; | |
| import com.google.gerrit.server.git.GitRepositoryManager; | |
| import com.google.gerrit.server.git.meta.MetaDataUpdate; | |
| import com.google.gerrit.server.project.CreateProjectArgs; | |
| import com.google.gerrit.server.project.ProjectCache; | |
| import com.google.gerrit.server.project.ProjectConfig; | |
| import com.google.gerrit.server.project.ProjectCreator; | |
| import com.google.gerrit.server.update.context.RefUpdateContext; | |
| import com.google.inject.Inject; | |
| import java.io.IOException; | |
| import java.util.ArrayList; | |
| import org.apache.commons.lang3.RandomStringUtils; | |
| import org.eclipse.jgit.errors.ConfigInvalidException; | |
| import org.eclipse.jgit.junit.TestRepository; | |
| import org.eclipse.jgit.lib.Config; | |
| import org.eclipse.jgit.lib.ObjectLoader; | |
| import org.eclipse.jgit.lib.Ref; | |
| import org.eclipse.jgit.lib.Repository; | |
| import org.eclipse.jgit.revwalk.RevCommit; | |
| import org.eclipse.jgit.revwalk.RevTree; | |
| import org.eclipse.jgit.revwalk.RevWalk; | |
| import org.eclipse.jgit.treewalk.TreeWalk; | |
| public class ProjectOperationsImpl implements ProjectOperations { | |
| private final AllProjectsName allProjectsName; | |
| private final GitRepositoryManager repoManager; | |
| private final MetaDataUpdate.Server metaDataUpdateFactory; | |
| private final ProjectCache projectCache; | |
| private final ProjectConfig.Factory projectConfigFactory; | |
| private final ProjectCreator projectCreator; | |
| @Inject | |
| ProjectOperationsImpl( | |
| AllProjectsName allProjectsName, | |
| GitRepositoryManager repoManager, | |
| MetaDataUpdate.Server metaDataUpdateFactory, | |
| ProjectCache projectCache, | |
| ProjectConfig.Factory projectConfigFactory, | |
| ProjectCreator projectCreator) { | |
| this.allProjectsName = allProjectsName; | |
| this.repoManager = repoManager; | |
| this.metaDataUpdateFactory = metaDataUpdateFactory; | |
| this.projectCache = projectCache; | |
| this.projectConfigFactory = projectConfigFactory; | |
| this.projectCreator = projectCreator; | |
| } | |
| @Override | |
| public TestProjectCreation.Builder newProject() { | |
| return TestProjectCreation.builder(this::createNewProject); | |
| } | |
| private Project.NameKey createNewProject(TestProjectCreation projectCreation) throws Exception { | |
| String name = projectCreation.name().orElse(RandomStringUtils.randomAlphabetic(8)); | |
| CreateProjectArgs args = new CreateProjectArgs(); | |
| args.setProjectName(name); | |
| args.permissionsOnly = projectCreation.permissionOnly().orElse(false); | |
| args.branch = | |
| projectCreation.branches().stream().map(RefNames::fullName).collect(toImmutableList()); | |
| args.createEmptyCommit = projectCreation.createEmptyCommit().orElse(true); | |
| projectCreation.parent().ifPresent(p -> args.newParent = p); | |
| // ProjectCreator wants non-null owner IDs. | |
| args.ownerIds = new ArrayList<>(projectCreation.owners()); | |
| projectCreation.submitType().ifPresent(st -> args.submitType = st); | |
| projectCreator.createProject(args); | |
| return Project.nameKey(name); | |
| } | |
| @Override | |
| public ProjectOperations.PerProjectOperations project(Project.NameKey key) { | |
| return new PerProjectOperations(key); | |
| } | |
| @Override | |
| public TestProjectUpdate.Builder allProjectsForUpdate() { | |
| return project(allProjectsName).forUpdate(); | |
| } | |
| private class PerProjectOperations implements ProjectOperations.PerProjectOperations { | |
| Project.NameKey nameKey; | |
| PerProjectOperations(Project.NameKey nameKey) { | |
| this.nameKey = nameKey; | |
| } | |
| @Override | |
| public RevCommit getHead(String branch) { | |
| return requireNonNull(headOrNull(branch)); | |
| } | |
| @Override | |
| public boolean hasHead(String branch) { | |
| return headOrNull(branch) != null; | |
| } | |
| @Override | |
| public TestProjectUpdate.Builder forUpdate() { | |
| return TestProjectUpdate.builder(nameKey, allProjectsName, this::updateProject); | |
| } | |
| private void updateProject(TestProjectUpdate projectUpdate) | |
| throws IOException, ConfigInvalidException { | |
| try (RefUpdateContext ctx = openTestRefUpdateContext()) { | |
| try (MetaDataUpdate metaDataUpdate = metaDataUpdateFactory.create(nameKey)) { | |
| ProjectConfig projectConfig = projectConfigFactory.read(metaDataUpdate); | |
| if (projectUpdate.removeAllAccessSections()) { | |
| projectConfig.getAccessSections().forEach(as -> projectConfig.remove(as)); | |
| } | |
| removePermissions(projectConfig, projectUpdate.removedPermissions()); | |
| addCapabilities(projectConfig, projectUpdate.addedCapabilities()); | |
| addPermissions(projectConfig, projectUpdate.addedPermissions()); | |
| addLabelPermissions(projectConfig, projectUpdate.addedLabelPermissions()); | |
| setExclusiveGroupPermissions(projectConfig, projectUpdate.exclusiveGroupPermissions()); | |
| projectConfig.commit(metaDataUpdate); | |
| } | |
| projectCache.evictAndReindex(nameKey); | |
| } | |
| } | |
| private void removePermissions( | |
| ProjectConfig projectConfig, | |
| ImmutableList<TestProjectUpdate.TestPermissionKey> removedPermissions) { | |
| for (TestProjectUpdate.TestPermissionKey p : removedPermissions) { | |
| projectConfig.upsertAccessSection( | |
| p.section(), | |
| as -> { | |
| Permission.Builder permission = as.upsertPermission(p.name()); | |
| if (p.group().isPresent()) { | |
| GroupReference group = | |
| GroupReference.create(p.group().get(), p.group().get().get()); | |
| group = projectConfig.resolve(group); | |
| permission.removeRule(group); | |
| } else { | |
| permission.clearRules(); | |
| } | |
| }); | |
| } | |
| } | |
| private void addCapabilities( | |
| ProjectConfig projectConfig, ImmutableList<TestCapability> addedCapabilities) { | |
| for (TestCapability c : addedCapabilities) { | |
| PermissionRule.Builder rule = newRule(projectConfig, c.group()); | |
| rule.setRange(c.min(), c.max()); | |
| projectConfig.upsertAccessSection( | |
| AccessSection.GLOBAL_CAPABILITIES, as -> as.upsertPermission(c.name()).add(rule)); | |
| } | |
| } | |
| private void addPermissions( | |
| ProjectConfig projectConfig, ImmutableList<TestPermission> addedPermissions) { | |
| for (TestPermission p : addedPermissions) { | |
| PermissionRule.Builder rule = newRule(projectConfig, p.group()); | |
| rule.setAction(p.action()); | |
| rule.setForce(p.force()); | |
| projectConfig.upsertAccessSection(p.ref(), as -> as.upsertPermission(p.name()).add(rule)); | |
| } | |
| } | |
| private void addLabelPermissions( | |
| ProjectConfig projectConfig, ImmutableList<TestLabelPermission> addedLabelPermissions) { | |
| for (TestLabelPermission p : addedLabelPermissions) { | |
| PermissionRule.Builder rule = newRule(projectConfig, p.group()); | |
| rule.setAction(p.action()); | |
| rule.setRange(p.min(), p.max()); | |
| String permissionName; | |
| if (p.isAddPermission()) { | |
| permissionName = | |
| p.impersonation() ? Permission.forLabelAs(p.name()) : Permission.forLabel(p.name()); | |
| } else { | |
| permissionName = Permission.forRemoveLabel(p.name()); | |
| } | |
| projectConfig.upsertAccessSection( | |
| p.ref(), as -> as.upsertPermission(permissionName).add(rule)); | |
| } | |
| } | |
| private void setExclusiveGroupPermissions( | |
| ProjectConfig projectConfig, | |
| ImmutableMap<TestProjectUpdate.TestPermissionKey, Boolean> exclusiveGroupPermissions) { | |
| exclusiveGroupPermissions.forEach( | |
| (key, exclusive) -> | |
| projectConfig.upsertAccessSection( | |
| key.section(), | |
| as -> as.upsertPermission(key.name()).setExclusiveGroup(exclusive))); | |
| } | |
| @Nullable | |
| private RevCommit headOrNull(String branch) { | |
| branch = RefNames.fullName(branch); | |
| try (Repository repo = repoManager.openRepository(nameKey); | |
| RevWalk rw = new RevWalk(repo)) { | |
| Ref r = repo.exactRef(branch); | |
| return r == null ? null : rw.parseCommit(r.getObjectId()); | |
| } catch (Exception e) { | |
| throw new IllegalStateException(e); | |
| } | |
| } | |
| @Override | |
| public ProjectConfig getProjectConfig() { | |
| try (Repository repo = repoManager.openRepository(nameKey)) { | |
| ProjectConfig projectConfig = projectConfigFactory.create(nameKey); | |
| projectConfig.load(nameKey, repo); | |
| return projectConfig; | |
| } catch (Exception e) { | |
| throw new IllegalStateException(e); | |
| } | |
| } | |
| @Override | |
| public Config getConfig() { | |
| try (Repository repo = repoManager.openRepository(nameKey); | |
| RevWalk rw = new RevWalk(repo)) { | |
| Ref ref = repo.exactRef(REFS_CONFIG); | |
| if (ref == null) { | |
| return new Config(); | |
| } | |
| RevTree tree = rw.parseTree(ref.getObjectId()); | |
| TreeWalk tw = TreeWalk.forPath(rw.getObjectReader(), PROJECT_CONFIG, tree); | |
| if (tw == null) { | |
| return new Config(); | |
| } | |
| ObjectLoader loader = rw.getObjectReader().open(tw.getObjectId(0)); | |
| String text = new String(loader.getCachedBytes(), UTF_8); | |
| Config config = new Config(); | |
| config.fromText(text); | |
| return config; | |
| } catch (Exception e) { | |
| throw new IllegalStateException(e); | |
| } | |
| } | |
| private void setConfig(Config projectConfig) { | |
| try (TestRepository<Repository> repo = | |
| new TestRepository<>(repoManager.openRepository(nameKey))) { | |
| repo.update( | |
| RefNames.REFS_CONFIG, | |
| repo.commit() | |
| .message("Update project.config from test") | |
| .parent(getHead(RefNames.REFS_CONFIG)) | |
| .add(ProjectConfig.PROJECT_CONFIG, projectConfig.toText())); | |
| } catch (Exception e) { | |
| throw new IllegalStateException( | |
| "updating project.config of project " + nameKey + " failed", e); | |
| } | |
| } | |
| @Override | |
| public TestProjectInvalidation.Builder forInvalidation() { | |
| return TestProjectInvalidation.builder(this::invalidateProject); | |
| } | |
| private void invalidateProject(TestProjectInvalidation testProjectInvalidation) | |
| throws Exception { | |
| if (testProjectInvalidation.makeProjectConfigInvalid()) { | |
| Config projectConfig = new Config(); | |
| projectConfig.fromText(getConfig().toText()); | |
| // Make the project config invalid by adding a permission entry with an invalid permission | |
| // name. | |
| projectConfig.setString( | |
| "access", "refs/*", "Invalid Permission Name", "group Administrators"); | |
| setConfig(projectConfig); | |
| try { | |
| projectCache.evictAndReindex(nameKey); | |
| } catch (Exception e) { | |
| // Evicting the project from the cache, also triggers a reindex of the project. | |
| // The reindex step fails if the project config is invalid. That's fine, since it was our | |
| // intention to make the project config invalid. Hence we ignore exceptions that are cause | |
| // by an invalid project config here. | |
| if (!Throwables.getCausalChain(e).stream() | |
| .anyMatch(ConfigInvalidException.class::isInstance)) { | |
| throw e; | |
| } | |
| } | |
| } | |
| if (!testProjectInvalidation.projectConfigUpdater().isEmpty()) { | |
| Config projectConfig = new Config(); | |
| projectConfig.fromText(getConfig().toText()); | |
| testProjectInvalidation.projectConfigUpdater().forEach(c -> c.accept(projectConfig)); | |
| setConfig(projectConfig); | |
| try { | |
| projectCache.evictAndReindex(nameKey); | |
| } catch (Exception e) { | |
| // Evicting the project from the cache, also triggers a reindex of the project. | |
| // The reindex step fails if the project config is invalid. That's fine, since it was our | |
| // intention to make the project config invalid. Hence we ignore exceptions that are cause | |
| // by an invalid project config here. | |
| if (!Throwables.getCausalChain(e).stream() | |
| .anyMatch(ConfigInvalidException.class::isInstance)) { | |
| throw e; | |
| } | |
| } | |
| } | |
| } | |
| } | |
| private static PermissionRule.Builder newRule( | |
| ProjectConfig project, AccountGroup.UUID groupUUID) { | |
| GroupReference group = GroupReference.create(groupUUID, groupUUID.get()); | |
| group = project.resolve(group); | |
| return PermissionRule.builder(group); | |
| } | |
| } |