LCOV - code coverage report
Current view: top level - server/project - LabelConfigValidator.java (source / functions) Hit Total Coverage
Test: _coverage_report.dat Lines: 93 112 83.0 %
Date: 2022-11-19 15:00:39 Functions: 10 10 100.0 %

          Line data    Source code
       1             : // Copyright (C) 2022 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.project;
      16             : 
      17             : import com.google.common.annotations.VisibleForTesting;
      18             : import com.google.common.collect.ImmutableList;
      19             : import com.google.common.collect.ImmutableMap;
      20             : import com.google.common.collect.ImmutableSet;
      21             : import com.google.common.collect.Sets;
      22             : import com.google.common.flogger.FluentLogger;
      23             : import com.google.gerrit.common.Nullable;
      24             : import com.google.gerrit.entities.LabelFunction;
      25             : import com.google.gerrit.entities.RefNames;
      26             : import com.google.gerrit.extensions.client.ChangeKind;
      27             : import com.google.gerrit.server.events.CommitReceivedEvent;
      28             : import com.google.gerrit.server.git.validators.CommitValidationException;
      29             : import com.google.gerrit.server.git.validators.CommitValidationListener;
      30             : import com.google.gerrit.server.git.validators.CommitValidationMessage;
      31             : import com.google.gerrit.server.git.validators.ValidationMessage;
      32             : import com.google.gerrit.server.patch.DiffNotAvailableException;
      33             : import com.google.gerrit.server.patch.DiffOperations;
      34             : import com.google.gerrit.server.patch.DiffOptions;
      35             : import com.google.gerrit.server.patch.filediff.FileDiffOutput;
      36             : import com.google.inject.Inject;
      37             : import com.google.inject.Singleton;
      38             : import java.io.IOException;
      39             : import java.util.List;
      40             : import java.util.Map;
      41             : import java.util.Optional;
      42             : import org.eclipse.jgit.errors.ConfigInvalidException;
      43             : import org.eclipse.jgit.lib.Config;
      44             : 
      45             : /**
      46             :  * Validates modifications to label configurations in the {@code project.config} file that is stored
      47             :  * in {@code refs/meta/config}.
      48             :  *
      49             :  * <p>Rejects setting/changing deprecated fields that are no longer supported (fields {@code
      50             :  * copyAnyScore}, {@code copyMinScore}, {@code copyMaxScore}, {@code copyAllScoresIfNoChange},
      51             :  * {@code copyAllScoresIfNoCodeChange}, {@code copyAllScoresOnMergeFirstParentUpdate}, {@code
      52             :  * copyAllScoresOnTrivialRebase}, {@code copyAllScoresIfListOfFilesDidNotChange}, {@code
      53             :  * copyValue}).
      54             :  *
      55             :  * <p>Updates that unset the deprecated fields or that don't touch them are allowed.
      56             :  */
      57             : @Singleton
      58             : public class LabelConfigValidator implements CommitValidationListener {
      59         110 :   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
      60             : 
      61             :   @VisibleForTesting public static final String KEY_COPY_ANY_SCORE = "copyAnyScore";
      62             : 
      63             :   @VisibleForTesting public static final String KEY_COPY_MIN_SCORE = "copyMinScore";
      64             : 
      65             :   @VisibleForTesting public static final String KEY_COPY_MAX_SCORE = "copyMaxScore";
      66             : 
      67             :   @VisibleForTesting public static final String KEY_COPY_VALUE = "copyValue";
      68             : 
      69             :   @VisibleForTesting
      70             :   public static final String KEY_COPY_ALL_SCORES_ON_MERGE_FIRST_PARENT_UPDATE =
      71             :       "copyAllScoresOnMergeFirstParentUpdate";
      72             : 
      73             :   @VisibleForTesting
      74             :   public static final String KEY_COPY_ALL_SCORES_ON_TRIVIAL_REBASE = "copyAllScoresOnTrivialRebase";
      75             : 
      76             :   @VisibleForTesting
      77             :   public static final String KEY_COPY_ALL_SCORES_IF_NO_CODE_CHANGE = "copyAllScoresIfNoCodeChange";
      78             : 
      79             :   @VisibleForTesting
      80             :   public static final String KEY_COPY_ALL_SCORES_IF_NO_CHANGE = "copyAllScoresIfNoChange";
      81             : 
      82             :   @VisibleForTesting
      83             :   public static final String KEY_COPY_ALL_SCORES_IF_LIST_OF_FILES_DID_NOT_CHANGE =
      84             :       "copyAllScoresIfListOfFilesDidNotChange";
      85             : 
      86             :   // Map of deprecated boolean flags to the predicates that should be used in the copy condition
      87             :   // instead.
      88         110 :   private static final ImmutableMap<String, String> DEPRECATED_FLAGS =
      89         110 :       ImmutableMap.<String, String>builder()
      90         110 :           .put(KEY_COPY_ANY_SCORE, "is:ANY")
      91         110 :           .put(KEY_COPY_MIN_SCORE, "is:MIN")
      92         110 :           .put(KEY_COPY_MAX_SCORE, "is:MAX")
      93         110 :           .put(KEY_COPY_ALL_SCORES_IF_NO_CHANGE, "changekind:" + ChangeKind.NO_CHANGE.name())
      94         110 :           .put(
      95             :               KEY_COPY_ALL_SCORES_IF_NO_CODE_CHANGE,
      96         110 :               "changekind:" + ChangeKind.NO_CODE_CHANGE.name())
      97         110 :           .put(
      98             :               KEY_COPY_ALL_SCORES_ON_MERGE_FIRST_PARENT_UPDATE,
      99         110 :               "changekind:" + ChangeKind.MERGE_FIRST_PARENT_UPDATE.name())
     100         110 :           .put(
     101             :               KEY_COPY_ALL_SCORES_ON_TRIVIAL_REBASE,
     102         110 :               "changekind:" + ChangeKind.TRIVIAL_REBASE.name())
     103         110 :           .put(KEY_COPY_ALL_SCORES_IF_LIST_OF_FILES_DID_NOT_CHANGE, "has:unchanged-files")
     104         110 :           .build();
     105             : 
     106             :   private final DiffOperations diffOperations;
     107             : 
     108             :   @Inject
     109         110 :   public LabelConfigValidator(DiffOperations diffOperations) {
     110         110 :     this.diffOperations = diffOperations;
     111         110 :   }
     112             : 
     113             :   @Override
     114             :   public List<CommitValidationMessage> onCommitReceived(CommitReceivedEvent receiveEvent)
     115             :       throws CommitValidationException {
     116             :     try {
     117         109 :       if (!receiveEvent.refName.equals(RefNames.REFS_CONFIG)
     118          13 :           || !isFileChanged(receiveEvent, ProjectConfig.PROJECT_CONFIG)) {
     119             :         // The project.config file in refs/meta/config was not modified, hence we do not need to do
     120             :         // any validation and can return early.
     121         109 :         return ImmutableList.of();
     122             :       }
     123             : 
     124             :       ImmutableList.Builder<CommitValidationMessage> validationMessageBuilder =
     125           6 :           ImmutableList.builder();
     126             : 
     127             :       // Load the new config
     128             :       Config newConfig;
     129             :       try {
     130           6 :         newConfig = loadNewConfig(receiveEvent);
     131           0 :       } catch (ConfigInvalidException e) {
     132             :         // The current config is invalid, hence we cannot inspect the delta.
     133             :         // Rejecting invalid configs is not the responsibility of this validator, hence ignore this
     134             :         // exception here.
     135           0 :         logger.atWarning().log(
     136             :             "cannot inspect the project config, because parsing %s from revision %s"
     137             :                 + " in project %s failed: %s",
     138             :             ProjectConfig.PROJECT_CONFIG,
     139           0 :             receiveEvent.commit.name(),
     140           0 :             receiveEvent.getProjectNameKey(),
     141           0 :             e.getMessage());
     142           0 :         return ImmutableList.of();
     143           6 :       }
     144             : 
     145             :       // Load the old config
     146           6 :       Optional<Config> oldConfig = loadOldConfig(receiveEvent);
     147             : 
     148           6 :       for (String labelName : newConfig.getSubsections(ProjectConfig.LABEL)) {
     149           1 :         for (String deprecatedFlag : DEPRECATED_FLAGS.keySet()) {
     150           1 :           if (flagChangedOrNewlySet(newConfig, oldConfig.orElse(null), labelName, deprecatedFlag)) {
     151           1 :             validationMessageBuilder.add(
     152             :                 new CommitValidationMessage(
     153           1 :                     String.format(
     154             :                         "Parameter '%s.%s.%s' is deprecated and cannot be set,"
     155             :                             + " use '%s' in '%s.%s.%s' instead.",
     156             :                         ProjectConfig.LABEL,
     157             :                         labelName,
     158             :                         deprecatedFlag,
     159           1 :                         DEPRECATED_FLAGS.get(deprecatedFlag),
     160             :                         ProjectConfig.LABEL,
     161             :                         labelName,
     162             :                         ProjectConfig.KEY_COPY_CONDITION),
     163             :                     ValidationMessage.Type.ERROR));
     164             :           }
     165           1 :         }
     166             : 
     167           1 :         if (copyValuesChangedOrNewlySet(newConfig, oldConfig.orElse(null), labelName)) {
     168           1 :           validationMessageBuilder.add(
     169             :               new CommitValidationMessage(
     170           1 :                   String.format(
     171             :                       "Parameter '%s.%s.%s' is deprecated and cannot be set,"
     172             :                           + " use 'is:<copy-value>' in '%s.%s.%s' instead.",
     173             :                       ProjectConfig.LABEL,
     174             :                       labelName,
     175             :                       KEY_COPY_VALUE,
     176             :                       ProjectConfig.LABEL,
     177             :                       labelName,
     178             :                       ProjectConfig.KEY_COPY_CONDITION),
     179             :                   ValidationMessage.Type.ERROR));
     180             :         }
     181             : 
     182             :         // Ban modifying label functions to any blocking function value
     183           1 :         if (flagChangedOrNewlySet(
     184           1 :             newConfig, oldConfig.orElse(null), labelName, ProjectConfig.KEY_FUNCTION)) {
     185           1 :           String fnName =
     186           1 :               newConfig.getString(ProjectConfig.LABEL, labelName, ProjectConfig.KEY_FUNCTION);
     187           1 :           Optional<LabelFunction> labelFn = LabelFunction.parse(fnName);
     188           1 :           if (labelFn.isPresent() && !isLabelFunctionAllowed(labelFn.get())) {
     189           1 :             validationMessageBuilder.add(
     190             :                 new CommitValidationMessage(
     191           1 :                     String.format(
     192             :                         "Value '%s' of '%s.%s.%s' is not allowed and cannot be set."
     193             :                             + " Label functions can only be set to {%s, %s, %s}."
     194             :                             + " Use submit requirements instead of label functions.",
     195             :                         fnName,
     196             :                         ProjectConfig.LABEL,
     197             :                         labelName,
     198             :                         ProjectConfig.KEY_FUNCTION,
     199             :                         LabelFunction.NO_BLOCK,
     200             :                         LabelFunction.NO_OP,
     201             :                         LabelFunction.PATCH_SET_LOCK),
     202             :                     ValidationMessage.Type.ERROR));
     203             :           }
     204             :         }
     205             : 
     206             :         // Ban deletions of label functions as well since the default is MaxWithBlock
     207           1 :         if (flagDeleted(newConfig, oldConfig.orElse(null), labelName, ProjectConfig.KEY_FUNCTION)) {
     208           1 :           validationMessageBuilder.add(
     209             :               new CommitValidationMessage(
     210           1 :                   String.format(
     211             :                       "Cannot delete '%s.%s.%s'."
     212             :                           + " Label functions can only be set to {%s, %s, %s}."
     213             :                           + " Use submit requirements instead of label functions.",
     214             :                       ProjectConfig.LABEL,
     215             :                       labelName,
     216             :                       ProjectConfig.KEY_FUNCTION,
     217             :                       LabelFunction.NO_BLOCK,
     218             :                       LabelFunction.NO_OP,
     219             :                       LabelFunction.PATCH_SET_LOCK),
     220             :                   ValidationMessage.Type.ERROR));
     221             :         }
     222           1 :       }
     223             : 
     224           6 :       ImmutableList<CommitValidationMessage> validationMessages = validationMessageBuilder.build();
     225           6 :       if (!validationMessages.isEmpty()) {
     226           1 :         throw new CommitValidationException(
     227           1 :             String.format(
     228             :                 "invalid %s file in revision %s",
     229           1 :                 ProjectConfig.PROJECT_CONFIG, receiveEvent.commit.getName()),
     230             :             validationMessages);
     231             :       }
     232           6 :       return ImmutableList.of();
     233           0 :     } catch (IOException | DiffNotAvailableException e) {
     234           0 :       String errorMessage =
     235           0 :           String.format(
     236             :               "failed to validate file %s for revision %s in ref %s of project %s",
     237             :               ProjectConfig.PROJECT_CONFIG,
     238           0 :               receiveEvent.commit.getName(),
     239             :               RefNames.REFS_CONFIG,
     240           0 :               receiveEvent.getProjectNameKey());
     241           0 :       logger.atSevere().withCause(e).log("%s", errorMessage);
     242           0 :       throw new CommitValidationException(errorMessage, e);
     243             :     }
     244             :   }
     245             : 
     246             :   /**
     247             :    * Whether the given file was changed in the given revision.
     248             :    *
     249             :    * @param receiveEvent the receive event
     250             :    * @param fileName the name of the file
     251             :    */
     252             :   private boolean isFileChanged(CommitReceivedEvent receiveEvent, String fileName)
     253             :       throws DiffNotAvailableException {
     254             :     Map<String, FileDiffOutput> fileDiffOutputs;
     255          13 :     if (receiveEvent.commit.getParentCount() > 0) {
     256             :       // normal commit with one parent: use listModifiedFilesAgainstParent with parentNum = 1 to
     257             :       // compare against the only parent (using parentNum = 0 to compare against the default parent
     258             :       // would also work)
     259             :       // merge commit with 2 or more parents: must use listModifiedFilesAgainstParent with parentNum
     260             :       // = 1 to compare against the first parent (using parentNum = 0 would compare against the
     261             :       // auto-merge)
     262          10 :       fileDiffOutputs =
     263          10 :           diffOperations.listModifiedFilesAgainstParent(
     264          10 :               receiveEvent.getProjectNameKey(), receiveEvent.commit, 1, DiffOptions.DEFAULTS);
     265             :     } else {
     266             :       // initial commit: must use listModifiedFilesAgainstParent with parentNum = 0
     267           4 :       fileDiffOutputs =
     268           4 :           diffOperations.listModifiedFilesAgainstParent(
     269           4 :               receiveEvent.getProjectNameKey(),
     270             :               receiveEvent.commit,
     271             :               /* parentNum=*/ 0,
     272             :               DiffOptions.DEFAULTS);
     273             :     }
     274          13 :     return fileDiffOutputs.keySet().contains(fileName);
     275             :   }
     276             : 
     277             :   private Config loadNewConfig(CommitReceivedEvent receiveEvent)
     278             :       throws IOException, ConfigInvalidException {
     279           6 :     ProjectLevelConfig.Bare bareConfig = new ProjectLevelConfig.Bare(ProjectConfig.PROJECT_CONFIG);
     280           6 :     bareConfig.load(receiveEvent.project.getNameKey(), receiveEvent.revWalk, receiveEvent.commit);
     281           6 :     return bareConfig.getConfig();
     282             :   }
     283             : 
     284             :   private Optional<Config> loadOldConfig(CommitReceivedEvent receiveEvent) throws IOException {
     285           6 :     if (receiveEvent.commit.getParentCount() == 0) {
     286             :       // initial commit, an old config doesn't exist
     287           1 :       return Optional.empty();
     288             :     }
     289             : 
     290             :     try {
     291           6 :       ProjectLevelConfig.Bare bareConfig =
     292             :           new ProjectLevelConfig.Bare(ProjectConfig.PROJECT_CONFIG);
     293           6 :       bareConfig.load(
     294           6 :           receiveEvent.project.getNameKey(),
     295             :           receiveEvent.revWalk,
     296           6 :           receiveEvent.commit.getParent(0));
     297           6 :       return Optional.of(bareConfig.getConfig());
     298           0 :     } catch (ConfigInvalidException e) {
     299             :       // the old config is not parseable, treat this the same way as if an old config didn't exist
     300             :       // so that all parameters in the new config are validated
     301           0 :       logger.atWarning().log(
     302             :           "cannot inspect the old project config, because parsing %s from parent revision %s"
     303             :               + " in project %s failed: %s",
     304             :           ProjectConfig.PROJECT_CONFIG,
     305           0 :           receiveEvent.commit.name(),
     306           0 :           receiveEvent.getProjectNameKey(),
     307           0 :           e.getMessage());
     308           0 :       return Optional.empty();
     309             :     }
     310             :   }
     311             : 
     312             :   private static boolean flagChangedOrNewlySet(
     313             :       Config newConfig, @Nullable Config oldConfig, String labelName, String key) {
     314           1 :     if (oldConfig == null) {
     315           1 :       return newConfig.getNames(ProjectConfig.LABEL, labelName).contains(key);
     316             :     }
     317             : 
     318             :     // Use getString rather than getBoolean so that we do not have to deal with values that cannot
     319             :     // be parsed as a boolean.
     320           1 :     String oldValue = oldConfig.getString(ProjectConfig.LABEL, labelName, key);
     321           1 :     String newValue = newConfig.getString(ProjectConfig.LABEL, labelName, key);
     322           1 :     return newValue != null && !newValue.equals(oldValue);
     323             :   }
     324             : 
     325             :   private static boolean flagDeleted(
     326             :       Config newConfig, @Nullable Config oldConfig, String labelName, String key) {
     327           1 :     if (oldConfig == null) {
     328           1 :       return false;
     329             :     }
     330           1 :     String oldValue = oldConfig.getString(ProjectConfig.LABEL, labelName, key);
     331           1 :     String newValue = newConfig.getString(ProjectConfig.LABEL, labelName, key);
     332           1 :     return oldValue != null && newValue == null;
     333             :   }
     334             : 
     335             :   private static boolean copyValuesChangedOrNewlySet(
     336             :       Config newConfig, @Nullable Config oldConfig, String labelName) {
     337           1 :     if (oldConfig == null) {
     338           1 :       return newConfig.getNames(ProjectConfig.LABEL, labelName).contains(KEY_COPY_VALUE);
     339             :     }
     340             : 
     341             :     // Ignore the order in which the copy values are defined in the new and old config, since the
     342             :     // order doesn't matter for this parameter.
     343           1 :     ImmutableSet<String> oldValues =
     344           1 :         ImmutableSet.copyOf(
     345           1 :             oldConfig.getStringList(ProjectConfig.LABEL, labelName, KEY_COPY_VALUE));
     346           1 :     ImmutableSet<String> newValues =
     347           1 :         ImmutableSet.copyOf(
     348           1 :             newConfig.getStringList(ProjectConfig.LABEL, labelName, KEY_COPY_VALUE));
     349           1 :     return !newValues.isEmpty() && !Sets.difference(newValues, oldValues).isEmpty();
     350             :   }
     351             : 
     352             :   private static boolean isLabelFunctionAllowed(LabelFunction labelFunction) {
     353           1 :     return labelFunction.equals(LabelFunction.NO_BLOCK)
     354           1 :         || labelFunction.equals(LabelFunction.NO_OP)
     355           1 :         || labelFunction.equals(LabelFunction.PATCH_SET_LOCK);
     356             :   }
     357             : }

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