Line data Source code
1 : // Copyright (C) 2017 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.base.Preconditions.checkArgument; 18 : import static java.util.Objects.requireNonNull; 19 : 20 : import com.google.gerrit.entities.AccountGroup; 21 : import com.google.gerrit.entities.AccountGroup.UUID; 22 : import com.google.gerrit.entities.BooleanProjectConfig; 23 : import com.google.gerrit.entities.ContributorAgreement; 24 : import com.google.gerrit.entities.PermissionRule; 25 : import com.google.gerrit.entities.PermissionRule.Action; 26 : import com.google.gerrit.entities.Project; 27 : import com.google.gerrit.extensions.registration.DynamicItem; 28 : import com.google.gerrit.extensions.restapi.AuthException; 29 : import com.google.gerrit.metrics.Counter0; 30 : import com.google.gerrit.metrics.Description; 31 : import com.google.gerrit.metrics.MetricMaker; 32 : import com.google.gerrit.server.CurrentUser; 33 : import com.google.gerrit.server.IdentifiedUser; 34 : import com.google.gerrit.server.config.UrlFormatter; 35 : import com.google.inject.Inject; 36 : import com.google.inject.Singleton; 37 : import java.io.IOException; 38 : import java.util.ArrayList; 39 : import java.util.Collection; 40 : import java.util.List; 41 : import java.util.regex.Pattern; 42 : import java.util.regex.PatternSyntaxException; 43 : 44 : @Singleton 45 : public class ContributorAgreementsChecker { 46 : 47 : private final DynamicItem<UrlFormatter> urlFormatter; 48 : private final ProjectCache projectCache; 49 : private final Metrics metrics; 50 : 51 : @Singleton 52 : protected static class Metrics { 53 : final Counter0 claCheckCount; 54 : 55 : @Inject 56 149 : Metrics(MetricMaker metricMaker) { 57 149 : claCheckCount = 58 149 : metricMaker.newCounter( 59 : "license/cla_check_count", 60 149 : new Description("Total number of CLA check requests").setRate().setUnit("requests")); 61 149 : } 62 : } 63 : 64 : @Inject 65 : ContributorAgreementsChecker( 66 149 : DynamicItem<UrlFormatter> urlFormatter, ProjectCache projectCache, Metrics metrics) { 67 149 : this.urlFormatter = urlFormatter; 68 149 : this.projectCache = projectCache; 69 149 : this.metrics = metrics; 70 149 : } 71 : 72 : /** 73 : * Checks if the user has signed a contributor agreement for the project. 74 : * 75 : * @throws AuthException if the user has not signed a contributor agreement for the project 76 : * @throws IOException if project states could not be loaded 77 : */ 78 : public void check(Project.NameKey project, CurrentUser user) throws IOException, AuthException { 79 109 : metrics.claCheckCount.increment(); 80 : 81 109 : ProjectState projectState = 82 109 : projectCache.get(project).orElseThrow(() -> new IOException("Can't load " + project)); 83 109 : if (!projectState.is(BooleanProjectConfig.USE_CONTRIBUTOR_AGREEMENTS)) { 84 109 : return; 85 : } 86 : 87 1 : if (!user.isIdentifiedUser()) { 88 0 : throw new AuthException("Must be logged in to verify Contributor Agreement"); 89 : } 90 : 91 1 : IdentifiedUser iUser = user.asIdentifiedUser(); 92 1 : Collection<ContributorAgreement> contributorAgreements = 93 1 : projectCache.getAllProjects().getConfig().getContributorAgreements().values(); 94 1 : List<UUID> okGroupIds = new ArrayList<>(); 95 1 : for (ContributorAgreement ca : contributorAgreements) { 96 : List<AccountGroup.UUID> groupIds; 97 1 : groupIds = okGroupIds; 98 : 99 : // matchProjects defaults to match all projects when missing. 100 1 : List<String> matchProjectsRegexes = ca.getMatchProjectsRegexes(); 101 1 : if (!matchProjectsRegexes.isEmpty() 102 0 : && !projectMatchesAnyPattern(project.get(), matchProjectsRegexes)) { 103 : // Doesn't match, isn't checked. 104 0 : continue; 105 : } 106 : // excludeProjects defaults to exclude no projects when missing. 107 1 : List<String> excludeProjectsRegexes = ca.getExcludeProjectsRegexes(); 108 1 : if (!excludeProjectsRegexes.isEmpty() 109 1 : && projectMatchesAnyPattern(project.get(), excludeProjectsRegexes)) { 110 : // Matches, isn't checked. 111 1 : continue; 112 : } 113 1 : for (PermissionRule rule : ca.getAccepted()) { 114 1 : if ((rule.getAction() == Action.ALLOW) 115 1 : && (rule.getGroup() != null) 116 1 : && (rule.getGroup().getUUID() != null)) { 117 1 : groupIds.add(AccountGroup.uuid(rule.getGroup().getUUID().get())); 118 : } 119 1 : } 120 1 : } 121 : 122 1 : if (!okGroupIds.isEmpty() && !iUser.getEffectiveGroups().containsAnyOf(okGroupIds)) { 123 1 : final StringBuilder msg = new StringBuilder(); 124 1 : msg.append("No Contributor Agreement on file for user ") 125 1 : .append(iUser.getNameEmail()) 126 1 : .append(" (id=") 127 1 : .append(iUser.getAccountId()) 128 1 : .append(")"); 129 : 130 1 : msg.append(urlFormatter.get().getSettingsUrl("Agreements").orElse("")); 131 1 : throw new AuthException(msg.toString()); 132 : } 133 1 : } 134 : 135 : private boolean projectMatchesAnyPattern(String projectName, List<String> regexes) { 136 1 : requireNonNull(regexes); 137 1 : checkArgument(!regexes.isEmpty()); 138 1 : for (String patternString : regexes) { 139 : Pattern pattern; 140 : try { 141 1 : pattern = Pattern.compile(patternString); 142 0 : } catch (PatternSyntaxException e) { 143 : // Should never happen: Regular expressions validated when reading project.config. 144 0 : throw new IllegalStateException( 145 : "Invalid matchProjects or excludeProjects clause in project.config", e); 146 1 : } 147 1 : if (pattern.matcher(projectName).find()) { 148 1 : return true; 149 : } 150 1 : } 151 1 : return false; 152 : } 153 : }