| // 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. |
| * |
| * <p>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<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; |
| } |
| |
| /** |
| * 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<MergeValidationListener> 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<ProjectConfigEntry> 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<ProjectConfigEntry> 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<ProjectConfigEntry> 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<MergeValidationListener> mergeValidationListeners; |
| |
| public PluginMergeValidationListener( |
| PluginSetContext<MergeValidationListener> 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<String> 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 upload named destinations into group refs. |
| 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); |
| } |
| } |
| } |
| } |