LCOV - code coverage report
Current view: top level - server/git/validators - MergeValidators.java (source / functions) Hit Total Coverage
Test: _coverage_report.dat Lines: 79 122 64.8 %
Date: 2022-11-19 15:00:39 Functions: 12 13 92.3 %

          Line data    Source code
       1             : // Copyright (C) 2013 The Android Open Source Project
       2             : //
       3             : // Licensed under the Apache License, Version 2.0 (the "License");
       4             : // you may not use this file except in compliance with the License.
       5             : // You may obtain a copy of the License at
       6             : //
       7             : // http://www.apache.org/licenses/LICENSE-2.0
       8             : //
       9             : // Unless required by applicable law or agreed to in writing, software
      10             : // distributed under the License is distributed on an "AS IS" BASIS,
      11             : // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
      12             : // See the License for the specific language governing permissions and
      13             : // limitations under the License.
      14             : 
      15             : package com.google.gerrit.server.git.validators;
      16             : 
      17             : import com.google.common.base.Joiner;
      18             : import com.google.common.collect.ImmutableList;
      19             : import com.google.common.flogger.FluentLogger;
      20             : import com.google.gerrit.entities.Account;
      21             : import com.google.gerrit.entities.BranchNameKey;
      22             : import com.google.gerrit.entities.PatchSet;
      23             : import com.google.gerrit.entities.Project;
      24             : import com.google.gerrit.entities.RefNames;
      25             : import com.google.gerrit.exceptions.StorageException;
      26             : import com.google.gerrit.extensions.api.projects.ProjectConfigEntryType;
      27             : import com.google.gerrit.extensions.registration.DynamicMap;
      28             : import com.google.gerrit.extensions.registration.Extension;
      29             : import com.google.gerrit.extensions.restapi.AuthException;
      30             : import com.google.gerrit.server.IdentifiedUser;
      31             : import com.google.gerrit.server.account.AccountProperties;
      32             : import com.google.gerrit.server.config.AllProjectsName;
      33             : import com.google.gerrit.server.config.AllUsersName;
      34             : import com.google.gerrit.server.config.GerritServerConfig;
      35             : import com.google.gerrit.server.config.PluginConfig;
      36             : import com.google.gerrit.server.config.ProjectConfigEntry;
      37             : import com.google.gerrit.server.git.CodeReviewCommit;
      38             : import com.google.gerrit.server.git.CodeReviewCommit.CodeReviewRevWalk;
      39             : import com.google.gerrit.server.permissions.GlobalPermission;
      40             : import com.google.gerrit.server.permissions.PermissionBackend;
      41             : import com.google.gerrit.server.permissions.PermissionBackendException;
      42             : import com.google.gerrit.server.permissions.ProjectPermission;
      43             : import com.google.gerrit.server.plugincontext.PluginSetContext;
      44             : import com.google.gerrit.server.project.ProjectCache;
      45             : import com.google.gerrit.server.project.ProjectConfig;
      46             : import com.google.gerrit.server.project.ProjectState;
      47             : import com.google.gerrit.server.query.change.ChangeData;
      48             : import com.google.inject.Inject;
      49             : import java.io.IOException;
      50             : import java.util.List;
      51             : import java.util.Objects;
      52             : import org.eclipse.jgit.errors.ConfigInvalidException;
      53             : import org.eclipse.jgit.lib.Config;
      54             : import org.eclipse.jgit.lib.Ref;
      55             : import org.eclipse.jgit.lib.Repository;
      56             : 
      57             : /**
      58             :  * Collection of validators that run inside Gerrit before a change is submitted. The main purpose is
      59             :  * to ensure that NoteDb data is mutated in a controlled way.
      60             :  *
      61             :  * <p>The difference between this and {@link OnSubmitValidators} is that this validates the original
      62             :  * commit. Depending on the {@link com.google.gerrit.server.submit.SubmitStrategy} that the project
      63             :  * chooses, the resulting commit in the repo might differ from this original commit. In case you
      64             :  * want to validate the resulting commit, use {@link OnSubmitValidators}
      65             :  */
      66             : public class MergeValidators {
      67          53 :   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
      68             : 
      69             :   private final PluginSetContext<MergeValidationListener> mergeValidationListeners;
      70             :   private final ProjectConfigValidator.Factory projectConfigValidatorFactory;
      71             :   private final AccountMergeValidator.Factory accountValidatorFactory;
      72             :   private final GroupMergeValidator.Factory groupValidatorFactory;
      73             : 
      74             :   public interface Factory {
      75             :     MergeValidators create();
      76             :   }
      77             : 
      78             :   @Inject
      79             :   MergeValidators(
      80             :       PluginSetContext<MergeValidationListener> mergeValidationListeners,
      81             :       ProjectConfigValidator.Factory projectConfigValidatorFactory,
      82             :       AccountMergeValidator.Factory accountValidatorFactory,
      83          53 :       GroupMergeValidator.Factory groupValidatorFactory) {
      84          53 :     this.mergeValidationListeners = mergeValidationListeners;
      85          53 :     this.projectConfigValidatorFactory = projectConfigValidatorFactory;
      86          53 :     this.accountValidatorFactory = accountValidatorFactory;
      87          53 :     this.groupValidatorFactory = groupValidatorFactory;
      88          53 :   }
      89             : 
      90             :   /**
      91             :    * Runs all validators and throws a {@link MergeValidationException} for the first validator that
      92             :    * failed. Only the first violation is propagated and processing is stopped thereafter.
      93             :    */
      94             :   public void validatePreMerge(
      95             :       Repository repo,
      96             :       CodeReviewRevWalk revWalk,
      97             :       CodeReviewCommit commit,
      98             :       ProjectState destProject,
      99             :       BranchNameKey destBranch,
     100             :       PatchSet.Id patchSetId,
     101             :       IdentifiedUser caller)
     102             :       throws MergeValidationException {
     103          53 :     List<MergeValidationListener> validators =
     104          53 :         ImmutableList.of(
     105             :             new PluginMergeValidationListener(mergeValidationListeners),
     106          53 :             projectConfigValidatorFactory.create(),
     107          53 :             accountValidatorFactory.create(),
     108          53 :             groupValidatorFactory.create(),
     109             :             new DestBranchRefValidator());
     110             : 
     111          53 :     for (MergeValidationListener validator : validators) {
     112          53 :       validator.onPreMerge(repo, revWalk, commit, destProject, destBranch, patchSetId, caller);
     113          53 :     }
     114          53 :   }
     115             : 
     116             :   /** Validator for any commits to {@code refs/meta/config}. */
     117             :   public static class ProjectConfigValidator implements MergeValidationListener {
     118             :     private static final String INVALID_CONFIG =
     119             :         "Change contains an invalid project configuration.";
     120             :     private static final String PARENT_NOT_FOUND =
     121             :         "Change contains an invalid project configuration:\nParent project does not exist.";
     122             :     private static final String PLUGIN_VALUE_NOT_EDITABLE =
     123             :         "Change contains an invalid project configuration:\n"
     124             :             + "One of the plugin configuration parameters is not editable.";
     125             :     private static final String PLUGIN_VALUE_NOT_PERMITTED =
     126             :         "Change contains an invalid project configuration:\n"
     127             :             + "One of the plugin configuration parameters has a value that is not"
     128             :             + " permitted.";
     129             :     private static final String ROOT_NO_PARENT =
     130             :         "Change contains an invalid project configuration:\n"
     131             :             + "The root project cannot have a parent.";
     132             :     private static final String SET_BY_ADMIN =
     133             :         "Change contains a project configuration that changes the parent"
     134             :             + " project.\n"
     135             :             + "The change must be submitted by a Gerrit administrator.";
     136             :     private static final String SET_BY_OWNER =
     137             :         "Change contains a project configuration that changes the parent"
     138             :             + " project.\n"
     139             :             + "The change must be submitted by a Gerrit administrator or the project owner.";
     140             : 
     141             :     private final AllProjectsName allProjectsName;
     142             :     private final AllUsersName allUsersName;
     143             :     private final ProjectCache projectCache;
     144             :     private final PermissionBackend permissionBackend;
     145             :     private final DynamicMap<ProjectConfigEntry> pluginConfigEntries;
     146             :     private final ProjectConfig.Factory projectConfigFactory;
     147             :     private final boolean allowProjectOwnersToChangeParent;
     148             : 
     149             :     public interface Factory {
     150             :       ProjectConfigValidator create();
     151             :     }
     152             : 
     153             :     @Inject
     154             :     public ProjectConfigValidator(
     155             :         AllProjectsName allProjectsName,
     156             :         AllUsersName allUsersName,
     157             :         ProjectCache projectCache,
     158             :         PermissionBackend permissionBackend,
     159             :         DynamicMap<ProjectConfigEntry> pluginConfigEntries,
     160             :         ProjectConfig.Factory projectConfigFactory,
     161          53 :         @GerritServerConfig Config config) {
     162          53 :       this.allProjectsName = allProjectsName;
     163          53 :       this.allUsersName = allUsersName;
     164          53 :       this.projectCache = projectCache;
     165          53 :       this.permissionBackend = permissionBackend;
     166          53 :       this.pluginConfigEntries = pluginConfigEntries;
     167          53 :       this.projectConfigFactory = projectConfigFactory;
     168          53 :       this.allowProjectOwnersToChangeParent =
     169          53 :           config.getBoolean("receive", "allowProjectOwnersToChangeParent", false);
     170          53 :     }
     171             : 
     172             :     @Override
     173             :     public void onPreMerge(
     174             :         Repository repo,
     175             :         CodeReviewRevWalk revWalk,
     176             :         CodeReviewCommit commit,
     177             :         ProjectState destProject,
     178             :         BranchNameKey destBranch,
     179             :         PatchSet.Id patchSetId,
     180             :         IdentifiedUser caller)
     181             :         throws MergeValidationException {
     182          53 :       if (RefNames.REFS_CONFIG.equals(destBranch.branch())) {
     183             :         final Project.NameKey newParent;
     184             :         try {
     185           7 :           ProjectConfig cfg = projectConfigFactory.create(destProject.getNameKey());
     186           7 :           cfg.load(destProject.getNameKey(), repo, commit);
     187           7 :           newParent = cfg.getProject().getParent(allProjectsName);
     188           7 :           final Project.NameKey oldParent = destProject.getProject().getParent(allProjectsName);
     189           7 :           if (oldParent == null) {
     190             :             // update of the 'All-Projects' project
     191           0 :             if (newParent != null) {
     192           0 :               throw new MergeValidationException(ROOT_NO_PARENT);
     193             :             }
     194             :           } else {
     195           7 :             if (!oldParent.equals(newParent)) {
     196           1 :               if (!allowProjectOwnersToChangeParent) {
     197             :                 try {
     198           1 :                   if (!permissionBackend.user(caller).test(GlobalPermission.ADMINISTRATE_SERVER)) {
     199           1 :                     throw new MergeValidationException(SET_BY_ADMIN);
     200             :                   }
     201           0 :                 } catch (PermissionBackendException e) {
     202           0 :                   logger.atWarning().withCause(e).log("Cannot check ADMINISTRATE_SERVER");
     203           0 :                   throw new MergeValidationException("validation unavailable", e);
     204           1 :                 }
     205             :               } else {
     206             :                 try {
     207           0 :                   permissionBackend
     208           0 :                       .user(caller)
     209           0 :                       .project(destProject.getNameKey())
     210           0 :                       .check(ProjectPermission.WRITE_CONFIG);
     211           0 :                 } catch (AuthException e) {
     212           0 :                   throw new MergeValidationException(SET_BY_OWNER, e);
     213           0 :                 } catch (PermissionBackendException e) {
     214           0 :                   logger.atWarning().withCause(e).log("Cannot check WRITE_CONFIG");
     215           0 :                   throw new MergeValidationException("validation unavailable", e);
     216           0 :                 }
     217             :               }
     218           1 :               if (allUsersName.equals(destProject.getNameKey())
     219           0 :                   && !allProjectsName.equals(newParent)) {
     220           0 :                 throw new MergeValidationException(
     221           0 :                     String.format(
     222           0 :                         " %s must inherit from %s", allUsersName.get(), allProjectsName.get()));
     223             :               }
     224           1 :               if (!projectCache.get(newParent).isPresent()) {
     225           0 :                 throw new MergeValidationException(PARENT_NOT_FOUND);
     226             :               }
     227             :             }
     228             :           }
     229             : 
     230           7 :           for (Extension<ProjectConfigEntry> e : pluginConfigEntries) {
     231           0 :             PluginConfig pluginCfg = cfg.getPluginConfig(e.getPluginName());
     232           0 :             ProjectConfigEntry configEntry = e.getProvider().get();
     233             : 
     234           0 :             String value = pluginCfg.getString(e.getExportName());
     235           0 :             String oldValue =
     236           0 :                 destProject.getPluginConfig(e.getPluginName()).getString(e.getExportName());
     237             : 
     238           0 :             if (!Objects.equals(value, oldValue) && !configEntry.isEditable(destProject)) {
     239           0 :               throw new MergeValidationException(PLUGIN_VALUE_NOT_EDITABLE);
     240             :             }
     241             : 
     242           0 :             if (ProjectConfigEntryType.LIST.equals(configEntry.getType())
     243             :                 && value != null
     244           0 :                 && !configEntry.getPermittedValues().contains(value)) {
     245           0 :               throw new MergeValidationException(PLUGIN_VALUE_NOT_PERMITTED);
     246             :             }
     247           0 :           }
     248           0 :         } catch (ConfigInvalidException | IOException e) {
     249           0 :           throw new MergeValidationException(INVALID_CONFIG, e);
     250           7 :         }
     251             :       }
     252          53 :     }
     253             :   }
     254             : 
     255             :   /** Validator that calls to plugins that provide additional validators. */
     256             :   public static class PluginMergeValidationListener implements MergeValidationListener {
     257             :     private final PluginSetContext<MergeValidationListener> mergeValidationListeners;
     258             : 
     259             :     public PluginMergeValidationListener(
     260          53 :         PluginSetContext<MergeValidationListener> mergeValidationListeners) {
     261          53 :       this.mergeValidationListeners = mergeValidationListeners;
     262          53 :     }
     263             : 
     264             :     @Override
     265             :     public void onPreMerge(
     266             :         Repository repo,
     267             :         CodeReviewRevWalk revWalk,
     268             :         CodeReviewCommit commit,
     269             :         ProjectState destProject,
     270             :         BranchNameKey destBranch,
     271             :         PatchSet.Id patchSetId,
     272             :         IdentifiedUser caller)
     273             :         throws MergeValidationException {
     274          53 :       mergeValidationListeners.runEach(
     275           0 :           l -> l.onPreMerge(repo, revWalk, commit, destProject, destBranch, patchSetId, caller),
     276             :           MergeValidationException.class);
     277          53 :     }
     278             :   }
     279             : 
     280             :   public static class AccountMergeValidator implements MergeValidationListener {
     281             :     public interface Factory {
     282             :       AccountMergeValidator create();
     283             :     }
     284             : 
     285             :     private final AllUsersName allUsersName;
     286             :     private final ChangeData.Factory changeDataFactory;
     287             :     private final AccountValidator accountValidator;
     288             : 
     289             :     @Inject
     290             :     public AccountMergeValidator(
     291             :         AllUsersName allUsersName,
     292             :         ChangeData.Factory changeDataFactory,
     293          53 :         AccountValidator accountValidator) {
     294          53 :       this.allUsersName = allUsersName;
     295          53 :       this.changeDataFactory = changeDataFactory;
     296          53 :       this.accountValidator = accountValidator;
     297          53 :     }
     298             : 
     299             :     @Override
     300             :     public void onPreMerge(
     301             :         Repository repo,
     302             :         CodeReviewRevWalk revWalk,
     303             :         CodeReviewCommit commit,
     304             :         ProjectState destProject,
     305             :         BranchNameKey destBranch,
     306             :         PatchSet.Id patchSetId,
     307             :         IdentifiedUser caller)
     308             :         throws MergeValidationException {
     309          53 :       Account.Id accountId = Account.Id.fromRef(destBranch.branch());
     310          53 :       if (!allUsersName.equals(destProject.getNameKey()) || accountId == null) {
     311          52 :         return;
     312             :       }
     313             : 
     314           1 :       ChangeData cd =
     315           1 :           changeDataFactory.create(destProject.getProject().getNameKey(), patchSetId.changeId());
     316             :       try {
     317           1 :         if (!cd.currentFilePaths().contains(AccountProperties.ACCOUNT_CONFIG)) {
     318           1 :           return;
     319             :         }
     320           0 :       } catch (StorageException e) {
     321           0 :         logger.atSevere().withCause(e).log("Cannot validate account update");
     322           0 :         throw new MergeValidationException("account validation unavailable", e);
     323           1 :       }
     324             : 
     325             :       try {
     326           1 :         List<String> errorMessages =
     327           1 :             accountValidator.validate(accountId, repo, revWalk, null, commit);
     328           1 :         if (!errorMessages.isEmpty()) {
     329           1 :           throw new MergeValidationException(
     330           1 :               "invalid account configuration: " + Joiner.on("; ").join(errorMessages));
     331             :         }
     332           0 :       } catch (IOException e) {
     333           0 :         logger.atSevere().withCause(e).log("Cannot validate account update");
     334           0 :         throw new MergeValidationException("account validation unavailable", e);
     335           1 :       }
     336           1 :     }
     337             :   }
     338             : 
     339             :   /** Validator to ensure that group refs are not mutated. */
     340             :   public static class GroupMergeValidator implements MergeValidationListener {
     341             :     public interface Factory {
     342             :       GroupMergeValidator create();
     343             :     }
     344             : 
     345             :     private final AllUsersName allUsersName;
     346             : 
     347             :     @Inject
     348          53 :     public GroupMergeValidator(AllUsersName allUsersName) {
     349          53 :       this.allUsersName = allUsersName;
     350          53 :     }
     351             : 
     352             :     @Override
     353             :     public void onPreMerge(
     354             :         Repository repo,
     355             :         CodeReviewRevWalk revWalk,
     356             :         CodeReviewCommit commit,
     357             :         ProjectState destProject,
     358             :         BranchNameKey destBranch,
     359             :         PatchSet.Id patchSetId,
     360             :         IdentifiedUser caller)
     361             :         throws MergeValidationException {
     362             :       // Groups are stored inside the 'All-Users' repository.
     363          53 :       if (!allUsersName.equals(destProject.getNameKey())
     364           2 :           || !RefNames.isGroupRef(destBranch.branch())) {
     365          53 :         return;
     366             :       }
     367             : 
     368           1 :       throw new MergeValidationException("group update not allowed");
     369             :     }
     370             :   }
     371             : 
     372             :   /**
     373             :    * Validator to ensure that destBranch is not a symbolic reference (an attempt to merge into a
     374             :    * symbolic ref branch leads to LOCK_FAILURE exception).
     375             :    */
     376             :   private static class DestBranchRefValidator implements MergeValidationListener {
     377             :     @Override
     378             :     public void onPreMerge(
     379             :         Repository repo,
     380             :         CodeReviewRevWalk revWalk,
     381             :         CodeReviewCommit commit,
     382             :         ProjectState destProject,
     383             :         BranchNameKey destBranch,
     384             :         PatchSet.Id patchSetId,
     385             :         IdentifiedUser caller)
     386             :         throws MergeValidationException {
     387             :       try {
     388          53 :         Ref ref = repo.exactRef(destBranch.branch());
     389             :         // Usually the target branch exists, but there is an exception for some branches (see
     390             :         // {@link com.google.gerrit.server.git.receive.ReceiveCommits} for details).
     391             :         // Such non-existing branches should be ignored.
     392          53 :         if (ref != null && ref.isSymbolic()) {
     393           1 :           throw new MergeValidationException("the target branch is a symbolic ref");
     394             :         }
     395           0 :       } catch (IOException e) {
     396           0 :         logger.atSevere().withCause(e).log("Cannot validate destination branch");
     397           0 :         throw new MergeValidationException("symref validation unavailable", e);
     398          53 :       }
     399          53 :     }
     400             :   }
     401             : }

Generated by: LCOV version 1.16+git.20220603.dfeb750