| // 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 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.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.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 (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 = |
| p.impersonation() ? Permission.forLabelAs(p.name()) : Permission.forLabel(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))); |
| } |
| |
| 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); |
| } |
| } |