Add account predicate that checks if user can see a certain change

This predicate is useful if account queries are used to suggest users
in the context of a change, because then users that can't see the
change should be filtered out. E.g. this predicate should be used when
suggesting assignees for a change.

Change-Id: I2c43d04c65718e1e6419cf94c4bbfb21aaf7d9f8
Signed-off-by: Edwin Kempin <ekempin@google.com>
This commit is contained in:
Edwin Kempin 2018-03-13 11:06:39 +01:00 committed by David Pursehouse
parent 0f9ae13ad2
commit 1815347a33
10 changed files with 252 additions and 4 deletions

View File

@ -23,6 +23,11 @@ are added to the same query string, they further restrict the
returned results. Search can also be performed by typing only a
text with no operator, which will match against a variety of fields.
[[cansee]]
cansee:'CHANGE'::
+
Matches accounts that can see the change 'CHANGE'.
[[email]]
email:'EMAIL'::
+

View File

@ -21,6 +21,7 @@ import com.google.gerrit.index.query.IndexPredicate;
import com.google.gerrit.index.query.IntegerRangePredicate;
import com.google.gerrit.index.query.NotPredicate;
import com.google.gerrit.index.query.OrPredicate;
import com.google.gerrit.index.query.PostFilterPredicate;
import com.google.gerrit.index.query.Predicate;
import com.google.gerrit.index.query.QueryParseException;
import com.google.gerrit.index.query.RegexPredicate;
@ -43,6 +44,8 @@ public class ElasticQueryBuilder {
return not(p);
} else if (p instanceof IndexPredicate) {
return fieldQuery((IndexPredicate<T>) p);
} else if (p instanceof PostFilterPredicate) {
return QueryBuilders.matchAllQuery();
} else {
throw new QueryParseException("cannot create query for index: " + p);
}

View File

@ -0,0 +1,21 @@
// Copyright (C) 2016 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.index.query;
/**
* Matches all documents in the index, with additional filtering done in the subclass's {@code
* match} method.
*/
public abstract class PostFilterPredicate<T> extends Predicate<T> implements Matchable<T> {}

View File

@ -28,6 +28,7 @@ import com.google.gerrit.index.query.IndexPredicate;
import com.google.gerrit.index.query.IntegerRangePredicate;
import com.google.gerrit.index.query.NotPredicate;
import com.google.gerrit.index.query.OrPredicate;
import com.google.gerrit.index.query.PostFilterPredicate;
import com.google.gerrit.index.query.Predicate;
import com.google.gerrit.index.query.QueryParseException;
import com.google.gerrit.index.query.RegexPredicate;
@ -76,6 +77,8 @@ public class QueryBuilder<V> {
return not(p);
} else if (p instanceof IndexPredicate) {
return fieldQuery((IndexPredicate<V>) p);
} else if (p instanceof PostFilterPredicate) {
return new MatchAllDocsQuery();
} else {
throw new QueryParseException("cannot create query for index: " + p);
}

View File

@ -76,6 +76,14 @@ public class ChangeFinder {
this.changeNotesFactory = changeNotesFactory;
}
public ChangeNotes findOne(String id) throws OrmException {
List<ChangeNotes> ctls = find(id);
if (ctls.size() != 1) {
return null;
}
return ctls.get(0);
}
/**
* Find changes matching the given identifier.
*

View File

@ -14,21 +14,41 @@
package com.google.gerrit.server.index.account;
import static com.google.common.base.Preconditions.checkState;
import com.google.gerrit.index.Index;
import com.google.gerrit.index.IndexedQuery;
import com.google.gerrit.index.QueryOptions;
import com.google.gerrit.index.query.DataSource;
import com.google.gerrit.index.query.Matchable;
import com.google.gerrit.index.query.Predicate;
import com.google.gerrit.index.query.QueryParseException;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.server.account.AccountState;
import com.google.gwtorm.server.OrmException;
public class IndexedAccountQuery extends IndexedQuery<Account.Id, AccountState>
implements DataSource<AccountState> {
implements DataSource<AccountState>, Matchable<AccountState> {
public IndexedAccountQuery(
Index<Account.Id, AccountState> index, Predicate<AccountState> pred, QueryOptions opts)
throws QueryParseException {
super(index, pred, opts.convertForBackend());
}
@Override
public boolean match(AccountState accountState) throws OrmException {
Predicate<AccountState> pred = getChild(0);
checkState(
pred.isMatchable(),
"match invoked, but child predicate %s doesn't implement %s",
pred,
Matchable.class.getName());
return pred.asMatchable().match(accountState);
}
@Override
public int getCost() {
return 1;
}
}

View File

@ -18,12 +18,15 @@ import com.google.common.collect.Lists;
import com.google.common.primitives.Ints;
import com.google.gerrit.index.FieldDef;
import com.google.gerrit.index.query.IndexPredicate;
import com.google.gerrit.index.query.Matchable;
import com.google.gerrit.index.query.Predicate;
import com.google.gerrit.index.query.QueryBuilder;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.server.account.AccountState;
import com.google.gerrit.server.index.account.AccountField;
import com.google.gerrit.server.notedb.ChangeNotes;
import com.google.gwtorm.server.OrmException;
import java.util.List;
public class AccountPredicates {
@ -101,7 +104,14 @@ public class AccountPredicates {
return new AccountPredicate(AccountField.WATCHED_PROJECT, project.get());
}
static class AccountPredicate extends IndexPredicate<AccountState> {
public static Predicate<AccountState> cansee(
AccountQueryBuilder.Arguments args, ChangeNotes changeNotes) {
return new CanSeeChangePredicate(
args.db, args.permissionBackend, args.userFactory, changeNotes);
}
static class AccountPredicate extends IndexPredicate<AccountState>
implements Matchable<AccountState> {
AccountPredicate(FieldDef<AccountState, ?> def, String value) {
super(def, value);
}
@ -109,6 +119,16 @@ public class AccountPredicates {
AccountPredicate(FieldDef<AccountState, ?> def, String name, String value) {
super(def, name, value);
}
@Override
public boolean match(AccountState object) throws OrmException {
return true;
}
@Override
public int getCost() {
return 1;
}
}
private AccountPredicates() {}

View File

@ -23,9 +23,16 @@ import com.google.gerrit.index.query.Predicate;
import com.google.gerrit.index.query.QueryBuilder;
import com.google.gerrit.index.query.QueryParseException;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.ChangeFinder;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.account.AccountState;
import com.google.gerrit.server.notedb.ChangeNotes;
import com.google.gerrit.server.permissions.ChangePermission;
import com.google.gerrit.server.permissions.PermissionBackend;
import com.google.gerrit.server.permissions.PermissionBackendException;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.ProvisionException;
@ -45,11 +52,25 @@ public class AccountQueryBuilder extends QueryBuilder<AccountState> {
new QueryBuilder.Definition<>(AccountQueryBuilder.class);
public static class Arguments {
final Provider<ReviewDb> db;
final ChangeFinder changeFinder;
final IdentifiedUser.GenericFactory userFactory;
final PermissionBackend permissionBackend;
private final Provider<CurrentUser> self;
@Inject
public Arguments(Provider<CurrentUser> self) {
public Arguments(
Provider<CurrentUser> self,
Provider<ReviewDb> db,
ChangeFinder changeFinder,
IdentifiedUser.GenericFactory userFactory,
PermissionBackend permissionBackend) {
this.self = self;
this.db = db;
this.changeFinder = changeFinder;
this.userFactory = userFactory;
this.permissionBackend = permissionBackend;
}
IdentifiedUser getIdentifiedUser() throws QueryParseException {
@ -81,6 +102,22 @@ public class AccountQueryBuilder extends QueryBuilder<AccountState> {
this.args = args;
}
@Operator
public Predicate<AccountState> cansee(String change)
throws QueryParseException, OrmException, PermissionBackendException {
ChangeNotes changeNotes = args.changeFinder.findOne(change);
if (changeNotes == null
|| !args.permissionBackend
.user(args.getUser())
.database(args.db)
.change(changeNotes)
.test(ChangePermission.READ)) {
throw error(String.format("change %s not found", change));
}
return AccountPredicates.cansee(args, changeNotes);
}
@Operator
public Predicate<AccountState> email(String email) {
return AccountPredicates.email(email);

View File

@ -0,0 +1,71 @@
package com.google.gerrit.server.query.account;
import com.google.gerrit.index.query.PostFilterPredicate;
import com.google.gerrit.index.query.Predicate;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.account.AccountState;
import com.google.gerrit.server.notedb.ChangeNotes;
import com.google.gerrit.server.permissions.ChangePermission;
import com.google.gerrit.server.permissions.PermissionBackend;
import com.google.gerrit.server.permissions.PermissionBackendException;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Provider;
import java.util.Collection;
import java.util.Objects;
public class CanSeeChangePredicate extends PostFilterPredicate<AccountState> {
private final Provider<ReviewDb> db;
private final PermissionBackend permissionBackend;
private final IdentifiedUser.GenericFactory userFactory;
private final ChangeNotes changeNotes;
CanSeeChangePredicate(
Provider<ReviewDb> db,
PermissionBackend permissionBackend,
IdentifiedUser.GenericFactory userFactory,
ChangeNotes changeNotes) {
this.db = db;
this.permissionBackend = permissionBackend;
this.userFactory = userFactory;
this.changeNotes = changeNotes;
}
@Override
public boolean match(AccountState accountState) throws OrmException {
try {
return permissionBackend
.user(userFactory.create(accountState.getAccount().getId()))
.database(db)
.change(changeNotes)
.test(ChangePermission.READ);
} catch (PermissionBackendException e) {
throw new OrmException("Failed to check if account can see change", e);
}
}
@Override
public int getCost() {
return 1;
}
@Override
public Predicate<AccountState> copy(Collection<? extends Predicate<AccountState>> children) {
return new CanSeeChangePredicate(db, permissionBackend, userFactory, changeNotes);
}
@Override
public int hashCode() {
return Objects.hash(changeNotes.getChange().getChangeId());
}
@Override
public boolean equals(Object other) {
if (other == null) {
return false;
}
return getClass() == other.getClass()
&& changeNotes.getChange().getChangeId()
== ((CanSeeChangePredicate) other).changeNotes.getChange().getChangeId();
}
}

View File

@ -18,12 +18,22 @@ import static com.google.common.truth.Truth.assertThat;
import static java.util.stream.Collectors.toList;
import static org.junit.Assert.fail;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Iterables;
import com.google.gerrit.extensions.api.GerritApi;
import com.google.gerrit.extensions.api.access.AccessSectionInfo;
import com.google.gerrit.extensions.api.access.PermissionInfo;
import com.google.gerrit.extensions.api.access.PermissionRuleInfo;
import com.google.gerrit.extensions.api.access.ProjectAccessInput;
import com.google.gerrit.extensions.api.accounts.Accounts.QueryRequest;
import com.google.gerrit.extensions.api.groups.GroupInput;
import com.google.gerrit.extensions.api.projects.ProjectInput;
import com.google.gerrit.extensions.client.ListAccountsOption;
import com.google.gerrit.extensions.client.ProjectWatchInfo;
import com.google.gerrit.extensions.common.AccountInfo;
import com.google.gerrit.extensions.common.ChangeInfo;
import com.google.gerrit.extensions.common.ChangeInput;
import com.google.gerrit.extensions.common.GroupInfo;
import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.lifecycle.LifecycleManager;
@ -60,6 +70,7 @@ import com.google.inject.Provider;
import com.google.inject.util.Providers;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import org.eclipse.jgit.lib.Config;
@ -291,6 +302,22 @@ public abstract class AbstractQueryAccountsTest extends GerritServerTests {
assertQuery("name:" + quote(user2.name), user2);
}
@Test
public void byCansee() throws Exception {
String domain = name("test.com");
AccountInfo user1 = newAccountWithEmail("account1", "account1@" + domain);
AccountInfo user2 = newAccountWithEmail("account2", "account2@" + domain);
AccountInfo user3 = newAccountWithEmail("account3", "account3@" + domain);
Project.NameKey p = createProject(name("p"));
ChangeInfo c = createChange(p);
assertQuery("name:" + domain + " cansee:" + c.changeId, user1, user2, user3);
GroupInfo group = createGroup(name("group"), user1, user2);
blockRead(p, group);
assertQuery("name:" + domain + " cansee:" + c.changeId, user3);
}
@Test
public void byWatchedProject() throws Exception {
Project.NameKey p = createProject(name("p"));
@ -456,10 +483,43 @@ public abstract class AbstractQueryAccountsTest extends GerritServerTests {
}
protected Project.NameKey createProject(String name) throws RestApiException {
gApi.projects().create(name);
ProjectInput in = new ProjectInput();
in.name = name;
in.createEmptyCommit = true;
gApi.projects().create(in);
return new Project.NameKey(name);
}
protected void blockRead(Project.NameKey project, GroupInfo group) throws RestApiException {
ProjectAccessInput in = new ProjectAccessInput();
in.add = new HashMap<>();
AccessSectionInfo a = new AccessSectionInfo();
PermissionInfo p = new PermissionInfo(null, null);
p.rules =
ImmutableMap.of(group.id, new PermissionRuleInfo(PermissionRuleInfo.Action.BLOCK, false));
a.permissions = ImmutableMap.of("read", p);
in.add = ImmutableMap.of("refs/*", a);
gApi.projects().name(project.get()).access(in);
}
protected ChangeInfo createChange(Project.NameKey project) throws RestApiException {
ChangeInput in = new ChangeInput();
in.subject = "A change";
in.project = project.get();
in.branch = "master";
return gApi.changes().create(in).get();
}
protected GroupInfo createGroup(String name, AccountInfo... members) throws RestApiException {
GroupInput in = new GroupInput();
in.name = name;
in.members =
Arrays.asList(members).stream().map(a -> String.valueOf(a._accountId)).collect(toList());
return gApi.groups().create(in).get();
}
protected void watch(AccountInfo account, Project.NameKey project, String filter)
throws RestApiException {
List<ProjectWatchInfo> projectsToWatch = new ArrayList<>();