blob: f4e8982cc760cfa9632c6186691a6d9f5e1ad4c8 [file] [log] [blame]
// 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;
}
}