| // Copyright (C) 2017 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.googlesource.gerrit.plugins.ratelimiter; |
| |
| import com.google.common.collect.ArrayTable; |
| import com.google.common.collect.ImmutableList; |
| import com.google.common.collect.ImmutableMap; |
| import com.google.common.collect.Table; |
| import com.google.gerrit.entities.AccountGroup; |
| import com.google.gerrit.entities.GroupDescription.Basic; |
| import com.google.gerrit.entities.ImmutableConfig; |
| import com.google.gerrit.entities.Project; |
| import com.google.gerrit.extensions.annotations.PluginName; |
| import com.google.gerrit.server.config.AllProjectsName; |
| import com.google.gerrit.server.config.GerritIsReplica; |
| import com.google.gerrit.server.config.PluginConfigFactory; |
| import com.google.gerrit.server.group.GroupResolver; |
| import com.google.gerrit.server.project.NoSuchProjectException; |
| import com.google.gerrit.server.project.ProjectConfig; |
| import com.google.inject.Inject; |
| import com.google.inject.ProvisionException; |
| import com.google.inject.Singleton; |
| import java.util.Arrays; |
| import java.util.LinkedHashMap; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Map.Entry; |
| import java.util.Optional; |
| import java.util.concurrent.CopyOnWriteArrayList; |
| import org.eclipse.jgit.errors.ConfigInvalidException; |
| import org.eclipse.jgit.lib.Config; |
| import org.slf4j.Logger; |
| import org.slf4j.LoggerFactory; |
| |
| @Singleton |
| class Configuration { |
| |
| static final String RATE_LIMIT_TOKEN = "${rateLimit}"; |
| private static final Logger log = LoggerFactory.getLogger(RateLimitUploadPack.class); |
| private static final String GROUP_SECTION = "group"; |
| private static final String DEFAULT_UPLOADPACK_LIMIT_EXCEEDED_MSG = |
| "Exceeded rate limit of " + RATE_LIMIT_TOKEN + " fetch requests/hour"; |
| private static final String RATE_LIMITER_CONFIG = "rate-limiter.config"; |
| private Table<RateLimitType, AccountGroup.UUID, RateLimit> rateLimits; |
| private List<AccountGroup.UUID> recipients; |
| private String rateLimitExceededMsg; |
| private final Boolean isReplica; |
| private final PluginConfigFactory pluginConfigFactory; |
| private final GroupResolver groupsCollection; |
| private final String pluginName; |
| private final Config defaultRateLimiterConfig; |
| |
| @Inject |
| Configuration( |
| AllProjectsName allProjectsName, |
| PluginConfigFactory pluginConfigFactory, |
| @PluginName String pluginName, |
| @GerritIsReplica Boolean isReplica, |
| GroupResolver groupsCollection) { |
| this.pluginConfigFactory = pluginConfigFactory; |
| this.groupsCollection = groupsCollection; |
| this.pluginName = pluginName; |
| this.isReplica = isReplica; |
| this.defaultRateLimiterConfig = pluginConfigFactory.getGlobalPluginConfig(pluginName); |
| initConfig(loadConfig(allProjectsName.get())); |
| } |
| |
| private void initConfig(Config config) { |
| recipients = parseUserGroupsForEmailNotification(config, groupsCollection); |
| rateLimitExceededMsg = parseLimitExceededMsg(config); |
| Map<String, AccountGroup.UUID> groups = getResolvedGroups(config, groupsCollection); |
| parseAllGroupsRateLimits(config, groups); |
| } |
| |
| private Config loadConfig(String projectName) { |
| if (isReplica) { |
| return defaultRateLimiterConfig; |
| } |
| try { |
| Config config = |
| pluginConfigFactory.getProjectPluginConfigWithInheritance( |
| Project.NameKey.parse(projectName), pluginName); |
| if (config == null || config.getSubsections(GROUP_SECTION).isEmpty()) { |
| config = defaultRateLimiterConfig; |
| } |
| return config; |
| } catch (NoSuchProjectException e) { |
| log.warn("No project {} found", projectName); |
| return defaultRateLimiterConfig; |
| } |
| } |
| |
| private List<AccountGroup.UUID> parseUserGroupsForEmailNotification( |
| Config config, GroupResolver groupsCollection) { |
| String sendEmailSection = "sendemail"; |
| String recipients = "recipients"; |
| Optional<String> rowValueOptional = |
| Optional.ofNullable(config.getString(sendEmailSection, null, recipients)); |
| return rowValueOptional |
| .map(s -> resolveGroupsFromParsedValue(s, groupsCollection)) |
| .orElseGet(ImmutableList::of); |
| } |
| |
| private List<AccountGroup.UUID> resolveGroupsFromParsedValue( |
| String configValue, GroupResolver groupsCollection) { |
| List<AccountGroup.UUID> groups = new CopyOnWriteArrayList<>(); |
| String[] groupNames = configValue.split("\\s*,\\s*"); |
| for (String groupName : groupNames) { |
| Basic basic = groupsCollection.parseId(groupName); |
| if (basic != null) { |
| groups.add(basic.getGroupUUID()); |
| } |
| } |
| return groups; |
| } |
| |
| void refreshTable(ProjectConfig newCfg, ProjectConfig oldCfg) { |
| if (oldCfg != null) { |
| try { |
| ImmutableMap<String, String> oldCacheable = oldCfg.getCacheable().getProjectLevelConfigs(); |
| String oldStringConfig = oldCacheable.getOrDefault(RATE_LIMITER_CONFIG, ""); |
| ImmutableMap<String, String> newCacheable = newCfg.getCacheable().getProjectLevelConfigs(); |
| String newStringConfig = newCacheable.getOrDefault(RATE_LIMITER_CONFIG, ""); |
| if (oldStringConfig.equals(newStringConfig)) { |
| return; |
| } |
| if (newStringConfig != "") { |
| Config newConfig = ImmutableConfig.parse(newStringConfig).mutableCopy(); |
| initConfig(newConfig); |
| } else { |
| initConfig(defaultRateLimiterConfig); |
| } |
| } catch (ConfigInvalidException e) { |
| log.warn("Invalid Configuration"); |
| } |
| } |
| } |
| |
| private void parseAllGroupsRateLimits(Config config, Map<String, AccountGroup.UUID> groups) { |
| if (groups.size() == 0) { |
| log.warn("No configuration found"); |
| rateLimits = null; |
| return; |
| } |
| rateLimits = ArrayTable.create(Arrays.asList(RateLimitType.values()), groups.values()); |
| for (Entry<String, AccountGroup.UUID> group : groups.entrySet()) { |
| parseGroupRateLimits(config, group.getKey(), group.getValue()); |
| } |
| } |
| |
| private Map<String, AccountGroup.UUID> getResolvedGroups( |
| Config config, GroupResolver groupsCollection) { |
| LinkedHashMap<String, AccountGroup.UUID> groups = new LinkedHashMap<>(); |
| for (String groupName : config.getSubsections(GROUP_SECTION)) { |
| Basic basic = groupsCollection.parseId(groupName); |
| if (basic != null) { |
| groups.put(groupName, basic.getGroupUUID()); |
| } |
| } |
| return groups; |
| } |
| |
| private void parseGroupRateLimits(Config config, String groupName, AccountGroup.UUID groupUUID) |
| throws ProvisionException { |
| for (String typeName : config.getNames(GROUP_SECTION, groupName, true)) { |
| RateLimitType rateLimitType = RateLimitType.from(typeName); |
| if (rateLimitType != null) { |
| rateLimits.put(rateLimitType, groupUUID, parseRateLimit(config, groupName, rateLimitType)); |
| } else { |
| throw new ProvisionException( |
| String.format("Invalid configuration, unsupported rate limit type: %s", typeName)); |
| } |
| } |
| } |
| |
| private static RateLimit parseRateLimit(Config c, String groupName, RateLimitType rateLimitType) { |
| String value = c.getString(GROUP_SECTION, groupName, rateLimitType.toString()); |
| try { |
| return new RateLimit(rateLimitType, Integer.parseInt(value)); |
| } catch (NumberFormatException e) { |
| throw new ProvisionException( |
| String.format( |
| "Invalid configuration, 'rate limit value '%s' for '%s.%s.%s' is not a valid number", |
| value, GROUP_SECTION, groupName, rateLimitType.toString())); |
| } |
| } |
| |
| private static String parseLimitExceededMsg(Config config) { |
| String msg = config.getString("configuration", null, "uploadpackLimitExceededMsg"); |
| return (msg != null) ? msg : DEFAULT_UPLOADPACK_LIMIT_EXCEEDED_MSG; |
| } |
| |
| String getRateLimitExceededMsg() { |
| return rateLimitExceededMsg; |
| } |
| |
| /** |
| * @param rateLimitType type of rate limit |
| * @return map of rate limits per group uuid |
| */ |
| Map<AccountGroup.UUID, RateLimit> getRateLimits(RateLimitType rateLimitType) { |
| return rateLimits != null ? rateLimits.row(rateLimitType) : ImmutableMap.of(); |
| } |
| |
| List<AccountGroup.UUID> getRecipients() { |
| return !recipients.isEmpty() ? recipients : ImmutableList.of(); |
| } |
| |
| static boolean isSameRateLimitType( |
| RateLimiter limiter, Optional<RateLimit> limit, Optional<RateLimit> warn) { |
| if (limit.isPresent() && warn.isPresent()) { |
| return limiter instanceof WarningRateLimiter; |
| } |
| if (limit.isEmpty() && warn.isPresent()) { |
| return limiter instanceof WarningUnlimitedRateLimiter; |
| } |
| if (limit.isPresent()) { |
| return limiter instanceof PeriodicRateLimiter; |
| } else { |
| return limiter instanceof UnlimitedRateLimiter; |
| } |
| } |
| |
| public static boolean validTimeLapse(Optional<RateLimit> timeLapse, int defaultTimeLapce) { |
| if (timeLapse.isPresent()) { |
| long providedTimeLapse = timeLapse.get().getRatePerHour(); |
| if (providedTimeLapse > 0 && providedTimeLapse <= defaultTimeLapce) { |
| return true; |
| } |
| log.warn( |
| "The time lapse is set to the default {} minutes, as the configured value is invalid.", |
| defaultTimeLapce); |
| } else { |
| log.warn( |
| "The time lapse is set to the default {} minutes, as the configured value is not present.", |
| defaultTimeLapce); |
| } |
| return false; |
| } |
| } |