Line data Source code
1 : // Copyright (C) 2021 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 static com.google.common.collect.ImmutableMap.toImmutableMap; 18 : import static com.google.gerrit.server.project.ProjectCache.illegalState; 19 : 20 : import com.google.common.collect.ImmutableMap; 21 : import com.google.common.flogger.FluentLogger; 22 : import com.google.gerrit.entities.SubmitRequirement; 23 : import com.google.gerrit.entities.SubmitRequirementExpression; 24 : import com.google.gerrit.entities.SubmitRequirementExpressionResult; 25 : import com.google.gerrit.entities.SubmitRequirementExpressionResult.PredicateResult; 26 : import com.google.gerrit.entities.SubmitRequirementResult; 27 : import com.google.gerrit.index.query.Predicate; 28 : import com.google.gerrit.index.query.QueryParseException; 29 : import com.google.gerrit.server.plugincontext.PluginSetContext; 30 : import com.google.gerrit.server.query.change.ChangeData; 31 : import com.google.gerrit.server.query.change.SubmitRequirementChangeQueryBuilder; 32 : import com.google.gerrit.server.util.ManualRequestContext; 33 : import com.google.gerrit.server.util.OneOffRequestContext; 34 : import com.google.inject.AbstractModule; 35 : import com.google.inject.Inject; 36 : import com.google.inject.Module; 37 : import com.google.inject.Provider; 38 : import com.google.inject.Scopes; 39 : import java.util.Map; 40 : import java.util.Optional; 41 : import java.util.function.Function; 42 : import java.util.stream.Stream; 43 : 44 : /** Evaluates submit requirements for different change data. */ 45 : public class SubmitRequirementsEvaluatorImpl implements SubmitRequirementsEvaluator { 46 152 : private static final FluentLogger logger = FluentLogger.forEnclosingClass(); 47 : 48 : private final Provider<SubmitRequirementChangeQueryBuilder> queryBuilder; 49 : private final ProjectCache projectCache; 50 : private final PluginSetContext<SubmitRequirement> globalSubmitRequirements; 51 : private final OneOffRequestContext requestContext; 52 : 53 : public static Module module() { 54 152 : return new AbstractModule() { 55 : @Override 56 : protected void configure() { 57 152 : bind(SubmitRequirementsEvaluator.class) 58 152 : .to(SubmitRequirementsEvaluatorImpl.class) 59 152 : .in(Scopes.SINGLETON); 60 152 : } 61 : }; 62 : } 63 : 64 : @Inject 65 : private SubmitRequirementsEvaluatorImpl( 66 : Provider<SubmitRequirementChangeQueryBuilder> queryBuilder, 67 : ProjectCache projectCache, 68 : PluginSetContext<SubmitRequirement> globalSubmitRequirements, 69 146 : OneOffRequestContext requestContext) { 70 146 : this.queryBuilder = queryBuilder; 71 146 : this.projectCache = projectCache; 72 146 : this.globalSubmitRequirements = globalSubmitRequirements; 73 146 : this.requestContext = requestContext; 74 146 : } 75 : 76 : @Override 77 : public void validateExpression(SubmitRequirementExpression expression) 78 : throws QueryParseException { 79 4 : queryBuilder.get().parse(expression.expressionString()); 80 4 : } 81 : 82 : @Override 83 : public ImmutableMap<SubmitRequirement, SubmitRequirementResult> evaluateAllRequirements( 84 : ChangeData cd) { 85 103 : return getRequirements(cd); 86 : } 87 : 88 : @Override 89 : public SubmitRequirementResult evaluateRequirement(SubmitRequirement sr, ChangeData cd) { 90 2 : try (ManualRequestContext ignored = requestContext.open()) { 91 : // Use a request context to execute predicates as an internal user with expanded visibility. 92 : // This is so that the evaluation does not depend on who is running the current request (e.g. 93 : // a "ownerin" predicate with group that is not visible to the person making this request). 94 : 95 : Optional<SubmitRequirementExpressionResult> applicabilityResult = 96 2 : sr.applicabilityExpression().isPresent() 97 2 : ? Optional.of(evaluateExpression(sr.applicabilityExpression().get(), cd)) 98 2 : : Optional.empty(); 99 2 : Optional<SubmitRequirementExpressionResult> submittabilityResult = 100 2 : Optional.of( 101 2 : SubmitRequirementExpressionResult.notEvaluated(sr.submittabilityExpression())); 102 : Optional<SubmitRequirementExpressionResult> overrideResult = 103 2 : sr.overrideExpression().isPresent() 104 2 : ? Optional.of( 105 2 : SubmitRequirementExpressionResult.notEvaluated(sr.overrideExpression().get())) 106 2 : : Optional.empty(); 107 2 : if (!sr.applicabilityExpression().isPresent() 108 2 : || SubmitRequirementResult.assertPass(applicabilityResult)) { 109 2 : submittabilityResult = Optional.of(evaluateExpression(sr.submittabilityExpression(), cd)); 110 : overrideResult = 111 2 : sr.overrideExpression().isPresent() 112 2 : ? Optional.of(evaluateExpression(sr.overrideExpression().get(), cd)) 113 2 : : Optional.empty(); 114 : } 115 : 116 2 : if (applicabilityResult.isPresent()) { 117 2 : logger.atFine().log( 118 : "Applicability expression result for SR name '%s':" 119 : + " passing atoms: %s, failing atoms: %s", 120 2 : sr.name(), 121 2 : applicabilityResult.get().passingAtoms(), 122 2 : applicabilityResult.get().failingAtoms()); 123 : } 124 2 : if (submittabilityResult.isPresent()) { 125 2 : logger.atFine().log( 126 : "Submittability expression result for SR name '%s':" 127 : + " passing atoms: %s, failing atoms: %s", 128 2 : sr.name(), 129 2 : submittabilityResult.get().passingAtoms(), 130 2 : submittabilityResult.get().failingAtoms()); 131 : } 132 2 : if (overrideResult.isPresent()) { 133 2 : logger.atFine().log( 134 : "Override expression result for SR name '%s':" 135 : + " passing atoms: %s, failing atoms: %s", 136 2 : sr.name(), overrideResult.get().passingAtoms(), overrideResult.get().failingAtoms()); 137 : } 138 : 139 2 : return SubmitRequirementResult.builder() 140 2 : .legacy(Optional.of(false)) 141 2 : .submitRequirement(sr) 142 2 : .patchSetCommitId(cd.currentPatchSet().commitId()) 143 2 : .submittabilityExpressionResult(submittabilityResult) 144 2 : .applicabilityExpressionResult(applicabilityResult) 145 2 : .overrideExpressionResult(overrideResult) 146 2 : .build(); 147 : } 148 : } 149 : 150 : @Override 151 : public SubmitRequirementExpressionResult evaluateExpression( 152 : SubmitRequirementExpression expression, ChangeData changeData) { 153 : try { 154 3 : Predicate<ChangeData> predicate = queryBuilder.get().parse(expression.expressionString()); 155 3 : PredicateResult predicateResult = evaluatePredicateTree(predicate, changeData); 156 3 : return SubmitRequirementExpressionResult.create(expression, predicateResult); 157 3 : } catch (QueryParseException | SubmitRequirementEvaluationException e) { 158 3 : return SubmitRequirementExpressionResult.error(expression, e.getMessage()); 159 : } 160 : } 161 : 162 : /** 163 : * Evaluate and return all {@link SubmitRequirement}s. 164 : * 165 : * <p>This includes all globally bound {@link SubmitRequirement}s, as well as requirements stored 166 : * in this project's config and its parents. 167 : * 168 : * <p>The behaviour in case of the name match is controlled by {@link 169 : * SubmitRequirement#allowOverrideInChildProjects} of global {@link SubmitRequirement}. 170 : */ 171 : private ImmutableMap<SubmitRequirement, SubmitRequirementResult> getRequirements(ChangeData cd) { 172 103 : Map<String, SubmitRequirement> globalRequirements = getGlobalRequirements(); 173 : 174 103 : ProjectState state = projectCache.get(cd.project()).orElseThrow(illegalState(cd.project())); 175 103 : Map<String, SubmitRequirement> projectConfigRequirements = state.getSubmitRequirements(); 176 : 177 103 : ImmutableMap<String, SubmitRequirement> requirements = 178 103 : Stream.concat( 179 103 : globalRequirements.entrySet().stream(), 180 103 : projectConfigRequirements.entrySet().stream()) 181 103 : .collect( 182 103 : toImmutableMap( 183 : Map.Entry::getKey, 184 : Map.Entry::getValue, 185 : (globalSubmitRequirement, projectConfigRequirement) -> 186 : // Override with projectConfigRequirement if allowed by 187 : // globalSubmitRequirement configuration 188 2 : globalSubmitRequirement.allowOverrideInChildProjects() 189 2 : ? projectConfigRequirement 190 2 : : globalSubmitRequirement)); 191 : ImmutableMap.Builder<SubmitRequirement, SubmitRequirementResult> results = 192 103 : ImmutableMap.builder(); 193 103 : for (SubmitRequirement requirement : requirements.values()) { 194 2 : results.put(requirement, evaluateRequirement(requirement, cd)); 195 2 : } 196 103 : return results.build(); 197 : } 198 : 199 : /** 200 : * Returns a map of all global {@link SubmitRequirement}s, keyed by their lower-case name. 201 : * 202 : * <p>The global {@link SubmitRequirement}s apply to all projects and can be bound by plugins. 203 : */ 204 : private Map<String, SubmitRequirement> getGlobalRequirements() { 205 103 : return globalSubmitRequirements.stream() 206 103 : .collect( 207 103 : toImmutableMap( 208 103 : globalRequirement -> globalRequirement.name().toLowerCase(), Function.identity())); 209 : } 210 : 211 : /** Evaluate the predicate recursively using change data. */ 212 : private PredicateResult evaluatePredicateTree( 213 : Predicate<ChangeData> predicate, ChangeData changeData) { 214 : PredicateResult.Builder predicateResult = 215 3 : PredicateResult.builder() 216 3 : .predicateString(predicate.isLeaf() ? predicate.getPredicateString() : "") 217 3 : .status(predicate.asMatchable().match(changeData)); 218 3 : predicate 219 3 : .getChildren() 220 3 : .forEach( 221 2 : c -> predicateResult.addChildPredicateResult(evaluatePredicateTree(c, changeData))); 222 3 : return predicateResult.build(); 223 : } 224 : }