diff options
Diffstat (limited to 'java/com/google/gerrit/server/git/validators/MergeValidators.java')
-rw-r--r-- | java/com/google/gerrit/server/git/validators/MergeValidators.java | 354 |
1 files changed, 354 insertions, 0 deletions
diff --git a/java/com/google/gerrit/server/git/validators/MergeValidators.java b/java/com/google/gerrit/server/git/validators/MergeValidators.java new file mode 100644 index 0000000000..0422c51f1b --- /dev/null +++ b/java/com/google/gerrit/server/git/validators/MergeValidators.java @@ -0,0 +1,354 @@ +// Copyright (C) 2013 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.git.validators; + +import com.google.common.base.Joiner; +import com.google.common.collect.ImmutableList; +import com.google.common.flogger.FluentLogger; +import com.google.gerrit.extensions.api.projects.ProjectConfigEntryType; +import com.google.gerrit.extensions.registration.DynamicMap; +import com.google.gerrit.extensions.registration.Extension; +import com.google.gerrit.extensions.restapi.AuthException; +import com.google.gerrit.reviewdb.client.Account; +import com.google.gerrit.reviewdb.client.Branch; +import com.google.gerrit.reviewdb.client.PatchSet; +import com.google.gerrit.reviewdb.client.Project; +import com.google.gerrit.reviewdb.client.RefNames; +import com.google.gerrit.reviewdb.server.ReviewDb; +import com.google.gerrit.server.IdentifiedUser; +import com.google.gerrit.server.account.AccountProperties; +import com.google.gerrit.server.config.AllProjectsName; +import com.google.gerrit.server.config.AllUsersName; +import com.google.gerrit.server.config.GerritServerConfig; +import com.google.gerrit.server.config.PluginConfig; +import com.google.gerrit.server.config.ProjectConfigEntry; +import com.google.gerrit.server.git.CodeReviewCommit; +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.plugincontext.PluginSetContext; +import com.google.gerrit.server.project.ProjectCache; +import com.google.gerrit.server.project.ProjectConfig; +import com.google.gerrit.server.project.ProjectState; +import com.google.gerrit.server.query.change.ChangeData; +import com.google.gwtorm.server.OrmException; +import com.google.inject.Inject; +import com.google.inject.Provider; +import java.io.IOException; +import java.util.List; +import org.eclipse.jgit.errors.ConfigInvalidException; +import org.eclipse.jgit.lib.Config; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.revwalk.RevWalk; + +public class MergeValidators { + private static final FluentLogger logger = FluentLogger.forEnclosingClass(); + + private final PluginSetContext<MergeValidationListener> mergeValidationListeners; + private final ProjectConfigValidator.Factory projectConfigValidatorFactory; + private final AccountMergeValidator.Factory accountValidatorFactory; + private final GroupMergeValidator.Factory groupValidatorFactory; + + public interface Factory { + MergeValidators create(); + } + + @Inject + MergeValidators( + PluginSetContext<MergeValidationListener> mergeValidationListeners, + ProjectConfigValidator.Factory projectConfigValidatorFactory, + AccountMergeValidator.Factory accountValidatorFactory, + GroupMergeValidator.Factory groupValidatorFactory) { + this.mergeValidationListeners = mergeValidationListeners; + this.projectConfigValidatorFactory = projectConfigValidatorFactory; + this.accountValidatorFactory = accountValidatorFactory; + this.groupValidatorFactory = groupValidatorFactory; + } + + public void validatePreMerge( + Repository repo, + CodeReviewCommit commit, + ProjectState destProject, + Branch.NameKey destBranch, + PatchSet.Id patchSetId, + IdentifiedUser caller) + throws MergeValidationException { + List<MergeValidationListener> validators = + ImmutableList.of( + new PluginMergeValidationListener(mergeValidationListeners), + projectConfigValidatorFactory.create(), + accountValidatorFactory.create(), + groupValidatorFactory.create()); + + for (MergeValidationListener validator : validators) { + validator.onPreMerge(repo, commit, destProject, destBranch, patchSetId, caller); + } + } + + public static class ProjectConfigValidator implements MergeValidationListener { + private static final String INVALID_CONFIG = + "Change contains an invalid project configuration."; + private static final String PARENT_NOT_FOUND = + "Change contains an invalid project configuration:\nParent project does not exist."; + private static final String PLUGIN_VALUE_NOT_EDITABLE = + "Change contains an invalid project configuration:\n" + + "One of the plugin configuration parameters is not editable."; + private static final String PLUGIN_VALUE_NOT_PERMITTED = + "Change contains an invalid project configuration:\n" + + "One of the plugin configuration parameters has a value that is not" + + " permitted."; + private static final String ROOT_NO_PARENT = + "Change contains an invalid project configuration:\n" + + "The root project cannot have a parent."; + private static final String SET_BY_ADMIN = + "Change contains a project configuration that changes the parent" + + " project.\n" + + "The change must be submitted by a Gerrit administrator."; + private static final String SET_BY_OWNER = + "Change contains a project configuration that changes the parent" + + " project.\n" + + "The change must be submitted by a Gerrit administrator or the project owner."; + + private final AllProjectsName allProjectsName; + private final AllUsersName allUsersName; + private final ProjectCache projectCache; + private final PermissionBackend permissionBackend; + private final DynamicMap<ProjectConfigEntry> pluginConfigEntries; + private final boolean allowProjectOwnersToChangeParent; + + public interface Factory { + ProjectConfigValidator create(); + } + + @Inject + public ProjectConfigValidator( + AllProjectsName allProjectsName, + AllUsersName allUsersName, + ProjectCache projectCache, + PermissionBackend permissionBackend, + DynamicMap<ProjectConfigEntry> pluginConfigEntries, + @GerritServerConfig Config config) { + this.allProjectsName = allProjectsName; + this.allUsersName = allUsersName; + this.projectCache = projectCache; + this.permissionBackend = permissionBackend; + this.pluginConfigEntries = pluginConfigEntries; + this.allowProjectOwnersToChangeParent = + config.getBoolean("receive", "allowProjectOwnersToChangeParent", false); + } + + @Override + public void onPreMerge( + final Repository repo, + final CodeReviewCommit commit, + final ProjectState destProject, + final Branch.NameKey destBranch, + final PatchSet.Id patchSetId, + IdentifiedUser caller) + throws MergeValidationException { + if (RefNames.REFS_CONFIG.equals(destBranch.get())) { + final Project.NameKey newParent; + try { + ProjectConfig cfg = new ProjectConfig(destProject.getNameKey()); + cfg.load(destProject.getNameKey(), repo, commit); + newParent = cfg.getProject().getParent(allProjectsName); + final Project.NameKey oldParent = destProject.getProject().getParent(allProjectsName); + if (oldParent == null) { + // update of the 'All-Projects' project + if (newParent != null) { + throw new MergeValidationException(ROOT_NO_PARENT); + } + } else { + if (!oldParent.equals(newParent)) { + if (!allowProjectOwnersToChangeParent) { + try { + permissionBackend.user(caller).check(GlobalPermission.ADMINISTRATE_SERVER); + } catch (AuthException e) { + throw new MergeValidationException(SET_BY_ADMIN); + } catch (PermissionBackendException e) { + logger.atWarning().withCause(e).log("Cannot check ADMINISTRATE_SERVER"); + throw new MergeValidationException("validation unavailable"); + } + } else { + try { + permissionBackend + .user(caller) + .project(destProject.getNameKey()) + .check(ProjectPermission.WRITE_CONFIG); + } catch (AuthException e) { + throw new MergeValidationException(SET_BY_OWNER); + } catch (PermissionBackendException e) { + logger.atWarning().withCause(e).log("Cannot check WRITE_CONFIG"); + throw new MergeValidationException("validation unavailable"); + } + } + if (allUsersName.equals(destProject.getNameKey()) + && !allProjectsName.equals(newParent)) { + throw new MergeValidationException( + String.format( + " %s must inherit from %s", allUsersName.get(), allProjectsName.get())); + } + if (projectCache.get(newParent) == null) { + throw new MergeValidationException(PARENT_NOT_FOUND); + } + } + } + + for (Extension<ProjectConfigEntry> e : pluginConfigEntries) { + PluginConfig pluginCfg = cfg.getPluginConfig(e.getPluginName()); + ProjectConfigEntry configEntry = e.getProvider().get(); + + String value = pluginCfg.getString(e.getExportName()); + String oldValue = + destProject + .getConfig() + .getPluginConfig(e.getPluginName()) + .getString(e.getExportName()); + + if ((value == null ? oldValue != null : !value.equals(oldValue)) + && !configEntry.isEditable(destProject)) { + throw new MergeValidationException(PLUGIN_VALUE_NOT_EDITABLE); + } + + if (ProjectConfigEntryType.LIST.equals(configEntry.getType()) + && value != null + && !configEntry.getPermittedValues().contains(value)) { + throw new MergeValidationException(PLUGIN_VALUE_NOT_PERMITTED); + } + } + } catch (ConfigInvalidException | IOException e) { + throw new MergeValidationException(INVALID_CONFIG); + } + } + } + } + + /** Execute merge validation plug-ins */ + public static class PluginMergeValidationListener implements MergeValidationListener { + private final PluginSetContext<MergeValidationListener> mergeValidationListeners; + + public PluginMergeValidationListener( + PluginSetContext<MergeValidationListener> mergeValidationListeners) { + this.mergeValidationListeners = mergeValidationListeners; + } + + @Override + public void onPreMerge( + Repository repo, + CodeReviewCommit commit, + ProjectState destProject, + Branch.NameKey destBranch, + PatchSet.Id patchSetId, + IdentifiedUser caller) + throws MergeValidationException { + mergeValidationListeners.runEach( + l -> l.onPreMerge(repo, commit, destProject, destBranch, patchSetId, caller), + MergeValidationException.class); + } + } + + public static class AccountMergeValidator implements MergeValidationListener { + public interface Factory { + AccountMergeValidator create(); + } + + private final Provider<ReviewDb> dbProvider; + private final AllUsersName allUsersName; + private final ChangeData.Factory changeDataFactory; + private final AccountValidator accountValidator; + + @Inject + public AccountMergeValidator( + Provider<ReviewDb> dbProvider, + AllUsersName allUsersName, + ChangeData.Factory changeDataFactory, + AccountValidator accountValidator) { + this.dbProvider = dbProvider; + this.allUsersName = allUsersName; + this.changeDataFactory = changeDataFactory; + this.accountValidator = accountValidator; + } + + @Override + public void onPreMerge( + Repository repo, + CodeReviewCommit commit, + ProjectState destProject, + Branch.NameKey destBranch, + PatchSet.Id patchSetId, + IdentifiedUser caller) + throws MergeValidationException { + Account.Id accountId = Account.Id.fromRef(destBranch.get()); + if (!allUsersName.equals(destProject.getNameKey()) || accountId == null) { + return; + } + + ChangeData cd = + changeDataFactory.create( + dbProvider.get(), destProject.getProject().getNameKey(), patchSetId.getParentKey()); + try { + if (!cd.currentFilePaths().contains(AccountProperties.ACCOUNT_CONFIG)) { + return; + } + } catch (IOException | OrmException e) { + logger.atSevere().withCause(e).log("Cannot validate account update"); + throw new MergeValidationException("account validation unavailable"); + } + + try (RevWalk rw = new RevWalk(repo)) { + List<String> errorMessages = accountValidator.validate(accountId, repo, rw, null, commit); + if (!errorMessages.isEmpty()) { + throw new MergeValidationException( + "invalid account configuration: " + Joiner.on("; ").join(errorMessages)); + } + } catch (IOException e) { + logger.atSevere().withCause(e).log("Cannot validate account update"); + throw new MergeValidationException("account validation unavailable"); + } + } + } + + public static class GroupMergeValidator implements MergeValidationListener { + public interface Factory { + GroupMergeValidator create(); + } + + private final AllUsersName allUsersName; + + @Inject + public GroupMergeValidator(AllUsersName allUsersName) { + this.allUsersName = allUsersName; + } + + @Override + public void onPreMerge( + Repository repo, + CodeReviewCommit commit, + ProjectState destProject, + Branch.NameKey destBranch, + PatchSet.Id patchSetId, + IdentifiedUser caller) + throws MergeValidationException { + // Groups are stored inside the 'All-Users' repository. + if (!allUsersName.equals(destProject.getNameKey()) + || !RefNames.isGroupRef(destBranch.get())) { + return; + } + + throw new MergeValidationException("group update not allowed"); + } + } +} |