Files
gerrit/java/com/google/gerrit/server/mail/send/ProjectWatch.java
Edwin Kempin 2ee55bdd29 ProjectWatch: For invalid filters log only on info level and without stacktrace
Users and project owners can configure invalid watch filters. Logging
each invalid filter with stacktrace on warning level for each change
where the filter may apply is much too noisy in the log. Logging the
execption message is enough to know what's wrong with a filter.

Signed-off-by: Edwin Kempin <ekempin@google.com>
Change-Id: I7bd94adb9fc9608134a3b8e1570212250a694bf6
2019-07-18 10:27:38 +02:00

263 lines
9.1 KiB
Java

// Copyright (C) 2013 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.google.gerrit.server.mail.send;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableSet;
import com.google.common.flogger.FluentLogger;
import com.google.gerrit.common.data.GroupDescription;
import com.google.gerrit.common.data.GroupReference;
import com.google.gerrit.index.query.Predicate;
import com.google.gerrit.index.query.QueryParseException;
import com.google.gerrit.mail.Address;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.AccountGroup;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.account.AccountState;
import com.google.gerrit.server.account.ProjectWatches.NotifyType;
import com.google.gerrit.server.account.ProjectWatches.ProjectWatchKey;
import com.google.gerrit.server.git.NotifyConfig;
import com.google.gerrit.server.project.ProjectState;
import com.google.gerrit.server.query.change.ChangeData;
import com.google.gerrit.server.query.change.ChangeQueryBuilder;
import com.google.gerrit.server.query.change.SingleGroupUser;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
public class ProjectWatch {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
protected final EmailArguments args;
protected final ProjectState projectState;
protected final Project.NameKey project;
protected final ChangeData changeData;
public ProjectWatch(
EmailArguments args,
Project.NameKey project,
ProjectState projectState,
ChangeData changeData) {
this.args = args;
this.project = project;
this.projectState = projectState;
this.changeData = changeData;
}
/** Returns all watchers that are relevant */
public final Watchers getWatchers(NotifyType type, boolean includeWatchersFromNotifyConfig) {
Watchers matching = new Watchers();
Set<Account.Id> projectWatchers = new HashSet<>();
for (AccountState a : args.accountQueryProvider.get().byWatchedProject(project)) {
Account.Id accountId = a.getAccount().getId();
for (Map.Entry<ProjectWatchKey, ImmutableSet<NotifyType>> e :
a.getProjectWatches().entrySet()) {
if (project.equals(e.getKey().project())
&& add(matching, accountId, e.getKey(), e.getValue(), type)) {
// We only want to prevent matching All-Projects if this filter hits
projectWatchers.add(accountId);
}
}
}
for (AccountState a : args.accountQueryProvider.get().byWatchedProject(args.allProjectsName)) {
for (Map.Entry<ProjectWatchKey, ImmutableSet<NotifyType>> e :
a.getProjectWatches().entrySet()) {
if (args.allProjectsName.equals(e.getKey().project())) {
Account.Id accountId = a.getAccount().getId();
if (!projectWatchers.contains(accountId)) {
add(matching, accountId, e.getKey(), e.getValue(), type);
}
}
}
}
if (!includeWatchersFromNotifyConfig) {
return matching;
}
for (ProjectState state : projectState.tree()) {
for (NotifyConfig nc : state.getConfig().getNotifyConfigs()) {
if (nc.isNotify(type)) {
try {
add(matching, state.getNameKey(), nc);
} catch (QueryParseException e) {
logger.atInfo().log(
"Project %s has invalid notify %s filter \"%s\": %s",
state.getName(), nc.getName(), nc.getFilter(), e.getMessage());
}
}
}
}
return matching;
}
public static class Watchers {
static class List {
protected final Set<Account.Id> accounts = new HashSet<>();
protected final Set<Address> emails = new HashSet<>();
private static List union(List... others) {
List union = new List();
for (List other : others) {
union.accounts.addAll(other.accounts);
union.emails.addAll(other.emails);
}
return union;
}
}
protected final List to = new List();
protected final List cc = new List();
protected final List bcc = new List();
List all() {
return List.union(to, cc, bcc);
}
List list(NotifyConfig.Header header) {
switch (header) {
case TO:
return to;
case CC:
return cc;
default:
case BCC:
return bcc;
}
}
}
private void add(Watchers matching, Project.NameKey projectName, NotifyConfig nc)
throws QueryParseException {
logger.atFine().log("Checking watchers for notify config %s from project %s", nc, projectName);
for (GroupReference groupRef : nc.getGroups()) {
CurrentUser user = new SingleGroupUser(groupRef.getUUID());
if (filterMatch(user, nc.getFilter())) {
deliverToMembers(matching.list(nc.getHeader()), groupRef.getUUID());
logger.atFine().log("Added watchers for group %s", groupRef);
} else {
logger.atFine().log("The filter did not match for group %s; skip notification", groupRef);
}
}
if (!nc.getAddresses().isEmpty()) {
if (filterMatch(null, nc.getFilter())) {
matching.list(nc.getHeader()).emails.addAll(nc.getAddresses());
logger.atFine().log("Added watchers for these addresses: %s", nc.getAddresses());
} else {
logger.atFine().log(
"The filter did not match; skip notification for these addresses: %s",
nc.getAddresses());
}
}
}
private void deliverToMembers(Watchers.List matching, AccountGroup.UUID startUUID) {
Set<AccountGroup.UUID> seen = new HashSet<>();
List<AccountGroup.UUID> q = new ArrayList<>();
seen.add(startUUID);
q.add(startUUID);
while (!q.isEmpty()) {
AccountGroup.UUID uuid = q.remove(q.size() - 1);
GroupDescription.Basic group = args.groupBackend.get(uuid);
if (group == null) {
logger.atFine().log("group %s not found, skip notification", uuid);
continue;
}
if (!Strings.isNullOrEmpty(group.getEmailAddress())) {
// If the group has an email address, do not expand membership.
matching.emails.add(new Address(group.getEmailAddress()));
logger.atFine().log(
"notify group email address %s; skip expanding to members", group.getEmailAddress());
continue;
}
if (!(group instanceof GroupDescription.Internal)) {
// Non-internal groups cannot be expanded by the server.
logger.atFine().log("group %s is not an internal group, skip notification", uuid);
continue;
}
logger.atFine().log("adding the members of group %s as watchers", uuid);
GroupDescription.Internal ig = (GroupDescription.Internal) group;
matching.accounts.addAll(ig.getMembers());
for (AccountGroup.UUID m : ig.getSubgroups()) {
if (seen.add(m)) {
q.add(m);
}
}
}
}
private boolean add(
Watchers matching,
Account.Id accountId,
ProjectWatchKey key,
Set<NotifyType> watchedTypes,
NotifyType type) {
logger.atFine().log("Checking project watch %s of account %s", key, accountId);
IdentifiedUser user = args.identifiedUserFactory.create(accountId);
try {
if (filterMatch(user, key.filter())) {
// If we are set to notify on this type, add the user.
// Otherwise, still return true to stop notifications for this user.
if (watchedTypes.contains(type)) {
matching.bcc.accounts.add(accountId);
}
logger.atFine().log("Added account %s as watcher", accountId);
return true;
}
logger.atFine().log("The filter did not match for account %s; skip notification", accountId);
} catch (QueryParseException e) {
// Ignore broken filter expressions.
logger.atInfo().log(
"Account %s has invalid filter in project watch %s: %s", accountId, key, e.getMessage());
}
return false;
}
private boolean filterMatch(CurrentUser user, String filter) throws QueryParseException {
ChangeQueryBuilder qb;
Predicate<ChangeData> p = null;
if (user == null) {
qb = args.queryBuilder.asUser(args.anonymousUser);
} else {
qb = args.queryBuilder.asUser(user);
p = qb.is_visible();
}
if (filter != null) {
Predicate<ChangeData> filterPredicate = qb.parse(filter);
if (p == null) {
p = filterPredicate;
} else {
p = Predicate.and(filterPredicate, p);
}
}
return p == null || p.asMatchable().match(changeData);
}
}