blob: deeb84309f00c5768215c1aa1cb6b3d37b37d2bc [file] [log] [blame]
// 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);
}
}