All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.google.gerrit.server.git.validators.MergeValidators Maven / Gradle / Ivy

There is a newer version: 3.11.0-rc3
Show newest version
// 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.entities.Account;
import com.google.gerrit.entities.BranchNameKey;
import com.google.gerrit.entities.PatchSet;
import com.google.gerrit.entities.Project;
import com.google.gerrit.entities.RefNames;
import com.google.gerrit.exceptions.StorageException;
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.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.git.CodeReviewCommit.CodeReviewRevWalk;
import com.google.gerrit.server.group.db.GroupConfig;
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.inject.Inject;
import java.io.IOException;
import java.util.List;
import java.util.Objects;
import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.lib.Config;
import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.lib.Repository;

/**
 * Collection of validators that run inside Gerrit before a change is submitted. The main purpose is
 * to ensure that NoteDb data is mutated in a controlled way.
 *
 * 

The difference between this and {@link OnSubmitValidators} is that this validates the original * commit. Depending on the {@link com.google.gerrit.server.submit.SubmitStrategy} that the project * chooses, the resulting commit in the repo might differ from this original commit. In case you * want to validate the resulting commit, use {@link OnSubmitValidators} */ public class MergeValidators { private static final FluentLogger logger = FluentLogger.forEnclosingClass(); private final PluginSetContext mergeValidationListeners; private final ProjectConfigValidator.Factory projectConfigValidatorFactory; private final AccountMergeValidator.Factory accountValidatorFactory; private final GroupMergeValidator.Factory groupValidatorFactory; public interface Factory { MergeValidators create(); } @Inject MergeValidators( PluginSetContext mergeValidationListeners, ProjectConfigValidator.Factory projectConfigValidatorFactory, AccountMergeValidator.Factory accountValidatorFactory, GroupMergeValidator.Factory groupValidatorFactory) { this.mergeValidationListeners = mergeValidationListeners; this.projectConfigValidatorFactory = projectConfigValidatorFactory; this.accountValidatorFactory = accountValidatorFactory; this.groupValidatorFactory = groupValidatorFactory; } /** * Runs all validators and throws a {@link MergeValidationException} for the first validator that * failed. Only the first violation is propagated and processing is stopped thereafter. */ public void validatePreMerge( Repository repo, CodeReviewRevWalk revWalk, CodeReviewCommit commit, ProjectState destProject, BranchNameKey destBranch, PatchSet.Id patchSetId, IdentifiedUser caller) throws MergeValidationException { List validators = ImmutableList.of( new PluginMergeValidationListener(mergeValidationListeners), projectConfigValidatorFactory.create(), accountValidatorFactory.create(), groupValidatorFactory.create(), new DestBranchRefValidator()); for (MergeValidationListener validator : validators) { validator.onPreMerge(repo, revWalk, commit, destProject, destBranch, patchSetId, caller); } } /** Validator for any commits to {@code refs/meta/config}. */ 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 pluginConfigEntries; private final ProjectConfig.Factory projectConfigFactory; private final boolean allowProjectOwnersToChangeParent; public interface Factory { ProjectConfigValidator create(); } @Inject public ProjectConfigValidator( AllProjectsName allProjectsName, AllUsersName allUsersName, ProjectCache projectCache, PermissionBackend permissionBackend, DynamicMap pluginConfigEntries, ProjectConfig.Factory projectConfigFactory, @GerritServerConfig Config config) { this.allProjectsName = allProjectsName; this.allUsersName = allUsersName; this.projectCache = projectCache; this.permissionBackend = permissionBackend; this.pluginConfigEntries = pluginConfigEntries; this.projectConfigFactory = projectConfigFactory; this.allowProjectOwnersToChangeParent = config.getBoolean("receive", "allowProjectOwnersToChangeParent", false); } @Override public void onPreMerge( Repository repo, CodeReviewRevWalk revWalk, CodeReviewCommit commit, ProjectState destProject, BranchNameKey destBranch, PatchSet.Id patchSetId, IdentifiedUser caller) throws MergeValidationException { if (RefNames.REFS_CONFIG.equals(destBranch.branch())) { final Project.NameKey newParent; try { ProjectConfig cfg = projectConfigFactory.create(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 { if (!permissionBackend.user(caller).test(GlobalPermission.ADMINISTRATE_SERVER)) { throw new MergeValidationException(SET_BY_ADMIN); } } catch (PermissionBackendException e) { logger.atWarning().withCause(e).log("Cannot check ADMINISTRATE_SERVER"); throw new MergeValidationException("validation unavailable", e); } } else { try { permissionBackend .user(caller) .project(destProject.getNameKey()) .check(ProjectPermission.WRITE_CONFIG); } catch (AuthException e) { throw new MergeValidationException(SET_BY_OWNER, e); } catch (PermissionBackendException e) { logger.atWarning().withCause(e).log("Cannot check WRITE_CONFIG"); throw new MergeValidationException("validation unavailable", e); } } 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).isPresent()) { throw new MergeValidationException(PARENT_NOT_FOUND); } } } for (Extension e : pluginConfigEntries) { PluginConfig pluginCfg = cfg.getPluginConfig(e.getPluginName()); ProjectConfigEntry configEntry = e.getProvider().get(); String value = pluginCfg.getString(e.getExportName()); String oldValue = destProject.getPluginConfig(e.getPluginName()).getString(e.getExportName()); if (!Objects.equals(value, 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, e); } } } } /** Validator that calls to plugins that provide additional validators. */ public static class PluginMergeValidationListener implements MergeValidationListener { private final PluginSetContext mergeValidationListeners; public PluginMergeValidationListener( PluginSetContext mergeValidationListeners) { this.mergeValidationListeners = mergeValidationListeners; } @Override public void onPreMerge( Repository repo, CodeReviewRevWalk revWalk, CodeReviewCommit commit, ProjectState destProject, BranchNameKey destBranch, PatchSet.Id patchSetId, IdentifiedUser caller) throws MergeValidationException { mergeValidationListeners.runEach( l -> l.onPreMerge(repo, revWalk, commit, destProject, destBranch, patchSetId, caller), MergeValidationException.class); } } public static class AccountMergeValidator implements MergeValidationListener { public interface Factory { AccountMergeValidator create(); } private final AllUsersName allUsersName; private final ChangeData.Factory changeDataFactory; private final AccountValidator accountValidator; @Inject public AccountMergeValidator( AllUsersName allUsersName, ChangeData.Factory changeDataFactory, AccountValidator accountValidator) { this.allUsersName = allUsersName; this.changeDataFactory = changeDataFactory; this.accountValidator = accountValidator; } @Override public void onPreMerge( Repository repo, CodeReviewRevWalk revWalk, CodeReviewCommit commit, ProjectState destProject, BranchNameKey destBranch, PatchSet.Id patchSetId, IdentifiedUser caller) throws MergeValidationException { Account.Id accountId = Account.Id.fromRef(destBranch.branch()); if (!allUsersName.equals(destProject.getNameKey()) || accountId == null) { return; } ChangeData cd = changeDataFactory.create(destProject.getProject().getNameKey(), patchSetId.changeId()); try { if (!cd.currentFilePaths().contains(AccountProperties.ACCOUNT_CONFIG)) { return; } } catch (StorageException e) { logger.atSevere().withCause(e).log("Cannot validate account update"); throw new MergeValidationException("account validation unavailable", e); } try { List errorMessages = accountValidator.validate(accountId, repo, revWalk, 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", e); } } } /** Validator to ensure that group refs are not mutated. */ public static class GroupMergeValidator implements MergeValidationListener { public interface Factory { GroupMergeValidator create(); } private final AllUsersName allUsersName; private final ChangeData.Factory changeDataFactory; @Inject public GroupMergeValidator(AllUsersName allUsersName, ChangeData.Factory changeDataFactory) { this.allUsersName = allUsersName; this.changeDataFactory = changeDataFactory; } @Override public void onPreMerge( Repository repo, CodeReviewRevWalk revWalk, CodeReviewCommit commit, ProjectState destProject, BranchNameKey destBranch, PatchSet.Id patchSetId, IdentifiedUser caller) throws MergeValidationException { // Groups are stored inside the 'All-Users' repository. if (!allUsersName.equals(destProject.getNameKey()) || !RefNames.isGroupRef(destBranch.branch())) { return; } // Update to group files is not supported because there are no validations // on the changes being done to these files, without which the group data // might get corrupted. Thus don't allow merges into All-Users group refs // which updates group files (i.e., group.config, members and subgroups). // But it is still useful to allow users to update files apart from group // files. For example, users can maintain task config in group refs which // allows users to collaborate and review changes on group specific task configs. ChangeData cd = changeDataFactory.create(destProject.getProject().getNameKey(), patchSetId.changeId()); try { if (cd.currentFilePaths().contains(GroupConfig.GROUP_CONFIG_FILE) || cd.currentFilePaths().contains(GroupConfig.MEMBERS_FILE) || cd.currentFilePaths().contains(GroupConfig.SUBGROUPS_FILE)) { throw new MergeValidationException( String.format( "update to group files (%s, %s, %s) not allowed", GroupConfig.GROUP_CONFIG_FILE, GroupConfig.MEMBERS_FILE, GroupConfig.SUBGROUPS_FILE)); } } catch (StorageException e) { logger.atSevere().withCause(e).log("Cannot validate group update"); throw new MergeValidationException("group validation unavailable", e); } } } /** * Validator to ensure that destBranch is not a symbolic reference (an attempt to merge into a * symbolic ref branch leads to LOCK_FAILURE exception). */ private static class DestBranchRefValidator implements MergeValidationListener { @Override public void onPreMerge( Repository repo, CodeReviewRevWalk revWalk, CodeReviewCommit commit, ProjectState destProject, BranchNameKey destBranch, PatchSet.Id patchSetId, IdentifiedUser caller) throws MergeValidationException { try { Ref ref = repo.exactRef(destBranch.branch()); // Usually the target branch exists, but there is an exception for some branches (see // {@link com.google.gerrit.server.git.receive.ReceiveCommits} for details). // Such non-existing branches should be ignored. if (ref != null && ref.isSymbolic()) { throw new MergeValidationException("the target branch is a symbolic ref"); } } catch (IOException e) { logger.atSevere().withCause(e).log("Cannot validate destination branch"); throw new MergeValidationException("symref validation unavailable", e); } } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy