Merge "Require 'Modify Account' to access another user's secondary emails"

This commit is contained in:
Edwin Kempin 2018-01-11 09:33:52 +00:00 committed by Gerrit Code Review
commit 0a997939ab
15 changed files with 403 additions and 42 deletions

View File

@ -1299,7 +1299,8 @@ Implies the following capabilities:
Allow to link:cmd-set-account.html[modify accounts over the ssh prompt]. Allow to link:cmd-set-account.html[modify accounts over the ssh prompt].
This capability allows the granted group members to modify any user account This capability allows the granted group members to modify any user account
setting. setting. In addition this capability is required to view secondary emails
of other accounts.
[[capability_priority]] [[capability_priority]]
=== Priority === Priority
@ -1386,6 +1387,10 @@ Allow viewing all accounts for purposes of auto-completion, regardless
of link:config-gerrit.html#accounts.visibility[accounts.visibility] of link:config-gerrit.html#accounts.visibility[accounts.visibility]
setting. setting.
This capability allows to view all accounts but not all account data.
E.g. secondary emails of all accounts can only be viewed with the
link:#capability_modifyAccount[Modify Account] capability.
[[capability_viewCaches]] [[capability_viewCaches]]
=== View Caches === View Caches

View File

@ -63,7 +63,9 @@ for each account.
[[all-emails]] [[all-emails]]
-- --
* `ALL_EMAILS`: Includes all registered emails. * `ALL_EMAILS`: Includes all registered emails. Requires the caller
to have the link:access-control.html#capability_modifyAccount[Modify
Account] global capability.
-- --
[[suggest-account]] [[suggest-account]]
@ -79,6 +81,10 @@ link:#all-emails[all emails] are always returned.
GET /accounts/?suggest&q=John HTTP/1.0 GET /accounts/?suggest&q=John HTTP/1.0
---- ----
Secondary emails are only included if the calling user has the
link:access-control.html#capability_modifyAccount[Modify Account]
capability.
.Response .Response
---- ----
HTTP/1.1 200 OK HTTP/1.1 200 OK
@ -2159,7 +2165,10 @@ ALL_EMAILS] for account queries.
|`secondary_emails`|optional| |`secondary_emails`|optional|
A list of the secondary email addresses of the user. + A list of the secondary email addresses of the user. +
Only set for account queries when the link:#all-emails[ALL_EMAILS] Only set for account queries when the link:#all-emails[ALL_EMAILS]
option is set. option or the link:#suggest-account[suggest] parameter is set. +
Secondary emails are only included if the calling user has the
link:access-control.html#capability_modifyAccount[Modify Account], and
hence is allowed to see secondary emails of other users.
|`username` |optional|The username of the user. + |`username` |optional|The username of the user. +
Only set if detailed account information is requested. + Only set if detailed account information is requested. +
See option link:rest-api-changes.html#detailed-accounts[ See option link:rest-api-changes.html#detailed-accounts[

View File

@ -137,6 +137,7 @@ public interface Accounts {
private String query; private String query;
private int limit; private int limit;
private int start; private int start;
private boolean suggest;
private EnumSet<ListAccountsOption> options = EnumSet.noneOf(ListAccountsOption.class); private EnumSet<ListAccountsOption> options = EnumSet.noneOf(ListAccountsOption.class);
/** Execute query and return a list of accounts. */ /** Execute query and return a list of accounts. */
@ -166,6 +167,11 @@ public interface Accounts {
return this; return this;
} }
public QueryRequest withSuggest(boolean suggest) {
this.suggest = suggest;
return this;
}
public QueryRequest withOption(ListAccountsOption options) { public QueryRequest withOption(ListAccountsOption options) {
this.options.add(options); this.options.add(options);
return this; return this;
@ -193,6 +199,10 @@ public interface Accounts {
return start; return start;
} }
public boolean getSuggest() {
return suggest;
}
public EnumSet<ListAccountsOption> getOptions() { public EnumSet<ListAccountsOption> getOptions() {
return options; return options;
} }

View File

@ -382,8 +382,12 @@ public class AccountApiImpl implements AccountApi {
} }
@Override @Override
public List<EmailInfo> getEmails() { public List<EmailInfo> getEmails() throws RestApiException {
return getEmails.apply(account); try {
return getEmails.apply(account);
} catch (Exception e) {
throw asRestApiException("Cannot get emails", e);
}
} }
@Override @Override

View File

@ -156,6 +156,7 @@ public class AccountsImpl implements Accounts {
myQueryAccounts.setQuery(r.getQuery()); myQueryAccounts.setQuery(r.getQuery());
myQueryAccounts.setLimit(r.getLimit()); myQueryAccounts.setLimit(r.getLimit());
myQueryAccounts.setStart(r.getStart()); myQueryAccounts.setStart(r.getStart());
myQueryAccounts.setSuggest(r.getSuggest());
for (ListAccountsOption option : r.getOptions()) { for (ListAccountsOption option : r.getOptions()) {
myQueryAccounts.addOption(option); myQueryAccounts.addOption(option);
} }

View File

@ -17,6 +17,7 @@ package com.google.gerrit.server.query.account;
import com.google.common.collect.Lists; import com.google.common.collect.Lists;
import com.google.common.primitives.Ints; import com.google.common.primitives.Ints;
import com.google.gerrit.index.FieldDef; import com.google.gerrit.index.FieldDef;
import com.google.gerrit.index.Schema;
import com.google.gerrit.index.query.IndexPredicate; import com.google.gerrit.index.query.IndexPredicate;
import com.google.gerrit.index.query.Predicate; import com.google.gerrit.index.query.Predicate;
import com.google.gerrit.index.query.QueryBuilder; import com.google.gerrit.index.query.QueryBuilder;
@ -35,14 +36,26 @@ public class AccountPredicates {
return Predicate.and(p, isActive()); return Predicate.and(p, isActive());
} }
public static Predicate<AccountState> defaultPredicate(String query) { public static Predicate<AccountState> defaultPredicate(
Schema<AccountState> schema, boolean canSeeSecondaryEmails, String query) {
// Adapt the capacity of this list when adding more default predicates. // Adapt the capacity of this list when adding more default predicates.
List<Predicate<AccountState>> preds = Lists.newArrayListWithCapacity(3); List<Predicate<AccountState>> preds = Lists.newArrayListWithCapacity(3);
Integer id = Ints.tryParse(query); Integer id = Ints.tryParse(query);
if (id != null) { if (id != null) {
preds.add(id(new Account.Id(id))); preds.add(id(new Account.Id(id)));
} }
preds.add(equalsName(query)); if (canSeeSecondaryEmails) {
preds.add(equalsNameIcludingSecondaryEmails(query));
} else {
if (schema.hasField(AccountField.NAME_PART_NO_SECONDARY_EMAIL)) {
preds.add(equalsName(query));
} else {
preds.add(AccountPredicates.fullName(query));
if (schema.hasField(AccountField.PREFERRED_EMAIL)) {
preds.add(AccountPredicates.preferredEmail(query));
}
}
}
preds.add(username(query)); preds.add(username(query));
// Adapt the capacity of the "predicates" list when adding more default // Adapt the capacity of the "predicates" list when adding more default
// predicates. // predicates.
@ -54,7 +67,7 @@ public class AccountPredicates {
AccountField.ID, AccountQueryBuilder.FIELD_ACCOUNT, accountId.toString()); AccountField.ID, AccountQueryBuilder.FIELD_ACCOUNT, accountId.toString());
} }
public static Predicate<AccountState> email(String email) { public static Predicate<AccountState> emailIncludingSecondaryEmails(String email) {
return new AccountPredicate( return new AccountPredicate(
AccountField.EMAIL, AccountQueryBuilder.FIELD_EMAIL, email.toLowerCase()); AccountField.EMAIL, AccountQueryBuilder.FIELD_EMAIL, email.toLowerCase());
} }
@ -71,12 +84,19 @@ public class AccountPredicates {
AccountField.PREFERRED_EMAIL_EXACT, AccountQueryBuilder.FIELD_PREFERRED_EMAIL_EXACT, email); AccountField.PREFERRED_EMAIL_EXACT, AccountQueryBuilder.FIELD_PREFERRED_EMAIL_EXACT, email);
} }
public static Predicate<AccountState> equalsName(String name) { public static Predicate<AccountState> equalsNameIcludingSecondaryEmails(String name) {
return new AccountPredicate( return new AccountPredicate(
AccountField.NAME_PART, AccountQueryBuilder.FIELD_NAME, name.toLowerCase()); AccountField.NAME_PART, AccountQueryBuilder.FIELD_NAME, name.toLowerCase());
} }
public static Predicate<AccountState> externalId(String externalId) { public static Predicate<AccountState> equalsName(String name) {
return new AccountPredicate(
AccountField.NAME_PART_NO_SECONDARY_EMAIL,
AccountQueryBuilder.FIELD_NAME,
name.toLowerCase());
}
public static Predicate<AccountState> externalIdIncludingSecondaryEmails(String externalId) {
return new AccountPredicate(AccountField.EXTERNAL_ID, externalId); return new AccountPredicate(AccountField.EXTERNAL_ID, externalId);
} }

View File

@ -18,6 +18,8 @@ import com.google.common.base.Splitter;
import com.google.common.collect.Lists; import com.google.common.collect.Lists;
import com.google.common.primitives.Ints; import com.google.common.primitives.Ints;
import com.google.gerrit.common.errors.NotSignedInException; import com.google.gerrit.common.errors.NotSignedInException;
import com.google.gerrit.index.Index;
import com.google.gerrit.index.Schema;
import com.google.gerrit.index.query.LimitPredicate; import com.google.gerrit.index.query.LimitPredicate;
import com.google.gerrit.index.query.Predicate; import com.google.gerrit.index.query.Predicate;
import com.google.gerrit.index.query.QueryBuilder; import com.google.gerrit.index.query.QueryBuilder;
@ -26,12 +28,21 @@ import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.server.CurrentUser; import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.IdentifiedUser; import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.account.AccountState; import com.google.gerrit.server.account.AccountState;
import com.google.gerrit.server.index.account.AccountField;
import com.google.gerrit.server.index.account.AccountIndexCollection;
import com.google.gerrit.server.permissions.GlobalPermission;
import com.google.gerrit.server.permissions.PermissionBackend;
import com.google.gerrit.server.permissions.PermissionBackendException;
import com.google.inject.Inject; import com.google.inject.Inject;
import com.google.inject.Provider; import com.google.inject.Provider;
import com.google.inject.ProvisionException; import com.google.inject.ProvisionException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/** Parses a query string meant to be applied to account objects. */ /** Parses a query string meant to be applied to account objects. */
public class AccountQueryBuilder extends QueryBuilder<AccountState> { public class AccountQueryBuilder extends QueryBuilder<AccountState> {
private static final Logger log = LoggerFactory.getLogger(AccountQueryBuilder.class);
public static final String FIELD_ACCOUNT = "account"; public static final String FIELD_ACCOUNT = "account";
public static final String FIELD_EMAIL = "email"; public static final String FIELD_EMAIL = "email";
public static final String FIELD_LIMIT = "limit"; public static final String FIELD_LIMIT = "limit";
@ -46,10 +57,17 @@ public class AccountQueryBuilder extends QueryBuilder<AccountState> {
public static class Arguments { public static class Arguments {
private final Provider<CurrentUser> self; private final Provider<CurrentUser> self;
private final AccountIndexCollection indexes;
private final PermissionBackend permissionBackend;
@Inject @Inject
public Arguments(Provider<CurrentUser> self) { public Arguments(
Provider<CurrentUser> self,
AccountIndexCollection indexes,
PermissionBackend permissionBackend) {
this.self = self; this.self = self;
this.indexes = indexes;
this.permissionBackend = permissionBackend;
} }
IdentifiedUser getIdentifiedUser() throws QueryParseException { IdentifiedUser getIdentifiedUser() throws QueryParseException {
@ -71,6 +89,11 @@ public class AccountQueryBuilder extends QueryBuilder<AccountState> {
throw new QueryParseException(NotSignedInException.MESSAGE, e); throw new QueryParseException(NotSignedInException.MESSAGE, e);
} }
} }
Schema<AccountState> schema() {
Index<?, AccountState> index = indexes != null ? indexes.getSearchIndex() : null;
return index != null ? index.getSchema() : null;
}
} }
private final Arguments args; private final Arguments args;
@ -82,8 +105,17 @@ public class AccountQueryBuilder extends QueryBuilder<AccountState> {
} }
@Operator @Operator
public Predicate<AccountState> email(String email) { public Predicate<AccountState> email(String email)
return AccountPredicates.email(email); throws PermissionBackendException, QueryParseException {
if (canSeeSecondaryEmails()) {
return AccountPredicates.emailIncludingSecondaryEmails(email);
}
if (args.schema().hasField(AccountField.PREFERRED_EMAIL)) {
return AccountPredicates.preferredEmail(email);
}
throw new QueryParseException("'email' operator is not supported by account index version");
} }
@Operator @Operator
@ -107,8 +139,17 @@ public class AccountQueryBuilder extends QueryBuilder<AccountState> {
} }
@Operator @Operator
public Predicate<AccountState> name(String name) { public Predicate<AccountState> name(String name)
return AccountPredicates.equalsName(name); throws PermissionBackendException, QueryParseException {
if (canSeeSecondaryEmails()) {
return AccountPredicates.equalsNameIcludingSecondaryEmails(name);
}
if (args.schema().hasField(AccountField.NAME_PART_NO_SECONDARY_EMAIL)) {
return AccountPredicates.equalsName(name);
}
return AccountPredicates.fullName(name);
} }
@Operator @Operator
@ -124,7 +165,8 @@ public class AccountQueryBuilder extends QueryBuilder<AccountState> {
@Override @Override
protected Predicate<AccountState> defaultField(String query) { protected Predicate<AccountState> defaultField(String query) {
Predicate<AccountState> defaultPredicate = AccountPredicates.defaultPredicate(query); Predicate<AccountState> defaultPredicate =
AccountPredicates.defaultPredicate(args.schema(), checkedCanSeeSecondaryEmails(), query);
if ("self".equalsIgnoreCase(query) || "me".equalsIgnoreCase(query)) { if ("self".equalsIgnoreCase(query) || "me".equalsIgnoreCase(query)) {
try { try {
return Predicate.or(defaultPredicate, AccountPredicates.id(self())); return Predicate.or(defaultPredicate, AccountPredicates.id(self()));
@ -138,4 +180,20 @@ public class AccountQueryBuilder extends QueryBuilder<AccountState> {
private Account.Id self() throws QueryParseException { private Account.Id self() throws QueryParseException {
return args.getIdentifiedUser().getAccountId(); return args.getIdentifiedUser().getAccountId();
} }
private boolean canSeeSecondaryEmails() throws PermissionBackendException, QueryParseException {
return args.permissionBackend.user(args.getUser()).test(GlobalPermission.MODIFY_ACCOUNT);
}
private boolean checkedCanSeeSecondaryEmails() {
try {
return canSeeSecondaryEmails();
} catch (PermissionBackendException e) {
log.error("Permission check failed", e);
return false;
} catch (QueryParseException e) {
// User is not signed in.
return false;
}
}
} }

View File

@ -83,7 +83,7 @@ public class InternalAccountQuery extends InternalQuery<AccountState> {
} }
public List<AccountState> byDefault(String query) throws OrmException { public List<AccountState> byDefault(String query) throws OrmException {
return query(AccountPredicates.defaultPredicate(query)); return query(AccountPredicates.defaultPredicate(schema(), true, query));
} }
public List<AccountState> byExternalId(String scheme, String id) throws OrmException { public List<AccountState> byExternalId(String scheme, String id) throws OrmException {
@ -91,7 +91,7 @@ public class InternalAccountQuery extends InternalQuery<AccountState> {
} }
public List<AccountState> byExternalId(ExternalId.Key externalId) throws OrmException { public List<AccountState> byExternalId(ExternalId.Key externalId) throws OrmException {
return query(AccountPredicates.externalId(externalId.toString())); return query(AccountPredicates.externalIdIncludingSecondaryEmails(externalId.toString()));
} }
public AccountState oneByExternalId(String externalId) throws OrmException { public AccountState oneByExternalId(String externalId) throws OrmException {

View File

@ -15,8 +15,15 @@
package com.google.gerrit.server.restapi.account; package com.google.gerrit.server.restapi.account;
import com.google.gerrit.extensions.common.EmailInfo; import com.google.gerrit.extensions.common.EmailInfo;
import com.google.gerrit.extensions.restapi.AuthException;
import com.google.gerrit.extensions.restapi.RestReadView; import com.google.gerrit.extensions.restapi.RestReadView;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.account.AccountResource; import com.google.gerrit.server.account.AccountResource;
import com.google.gerrit.server.permissions.GlobalPermission;
import com.google.gerrit.server.permissions.PermissionBackend;
import com.google.gerrit.server.permissions.PermissionBackendException;
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.Singleton; import com.google.inject.Singleton;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
@ -25,9 +32,22 @@ import java.util.List;
@Singleton @Singleton
public class GetEmails implements RestReadView<AccountResource> { public class GetEmails implements RestReadView<AccountResource> {
private final Provider<CurrentUser> self;
private final PermissionBackend permissionBackend;
@Inject
GetEmails(Provider<CurrentUser> self, PermissionBackend permissionBackend) {
this.self = self;
this.permissionBackend = permissionBackend;
}
@Override @Override
public List<EmailInfo> apply(AccountResource rsrc) { public List<EmailInfo> apply(AccountResource rsrc)
throws AuthException, PermissionBackendException {
if (self.get() != rsrc.getUser()) {
permissionBackend.user(self).check(GlobalPermission.MODIFY_ACCOUNT);
}
List<EmailInfo> emails = new ArrayList<>(); List<EmailInfo> emails = new ArrayList<>();
for (String email : rsrc.getUser().getEmailAddresses()) { for (String email : rsrc.getUser().getEmailAddresses()) {
if (email != null) { if (email != null) {

View File

@ -21,22 +21,28 @@ import com.google.gerrit.extensions.common.AccountInfo;
import com.google.gerrit.extensions.common.AccountVisibility; import com.google.gerrit.extensions.common.AccountVisibility;
import com.google.gerrit.extensions.restapi.BadRequestException; import com.google.gerrit.extensions.restapi.BadRequestException;
import com.google.gerrit.extensions.restapi.MethodNotAllowedException; import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.extensions.restapi.RestReadView; import com.google.gerrit.extensions.restapi.RestReadView;
import com.google.gerrit.extensions.restapi.TopLevelResource; import com.google.gerrit.extensions.restapi.TopLevelResource;
import com.google.gerrit.index.query.Predicate; import com.google.gerrit.index.query.Predicate;
import com.google.gerrit.index.query.QueryParseException; import com.google.gerrit.index.query.QueryParseException;
import com.google.gerrit.index.query.QueryResult; import com.google.gerrit.index.query.QueryResult;
import com.google.gerrit.reviewdb.client.Account; import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.account.AccountDirectory.FillOptions; import com.google.gerrit.server.account.AccountDirectory.FillOptions;
import com.google.gerrit.server.account.AccountInfoComparator; import com.google.gerrit.server.account.AccountInfoComparator;
import com.google.gerrit.server.account.AccountLoader; import com.google.gerrit.server.account.AccountLoader;
import com.google.gerrit.server.account.AccountState; import com.google.gerrit.server.account.AccountState;
import com.google.gerrit.server.config.GerritServerConfig; import com.google.gerrit.server.config.GerritServerConfig;
import com.google.gerrit.server.permissions.GlobalPermission;
import com.google.gerrit.server.permissions.PermissionBackend;
import com.google.gerrit.server.permissions.PermissionBackendException;
import com.google.gerrit.server.query.account.AccountPredicates; import com.google.gerrit.server.query.account.AccountPredicates;
import com.google.gerrit.server.query.account.AccountQueryBuilder; import com.google.gerrit.server.query.account.AccountQueryBuilder;
import com.google.gerrit.server.query.account.AccountQueryProcessor; import com.google.gerrit.server.query.account.AccountQueryProcessor;
import com.google.gwtorm.server.OrmException; import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject; import com.google.inject.Inject;
import com.google.inject.Provider;
import java.util.Collections; import java.util.Collections;
import java.util.EnumSet; import java.util.EnumSet;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
@ -49,6 +55,8 @@ import org.kohsuke.args4j.Option;
public class QueryAccounts implements RestReadView<TopLevelResource> { public class QueryAccounts implements RestReadView<TopLevelResource> {
private static final int MAX_SUGGEST_RESULTS = 100; private static final int MAX_SUGGEST_RESULTS = 100;
private final Provider<CurrentUser> self;
private final PermissionBackend permissionBackend;
private final AccountLoader.Factory accountLoaderFactory; private final AccountLoader.Factory accountLoaderFactory;
private final AccountQueryBuilder queryBuilder; private final AccountQueryBuilder queryBuilder;
private final AccountQueryProcessor queryProcessor; private final AccountQueryProcessor queryProcessor;
@ -117,10 +125,14 @@ public class QueryAccounts implements RestReadView<TopLevelResource> {
@Inject @Inject
QueryAccounts( QueryAccounts(
Provider<CurrentUser> self,
PermissionBackend permissionBackend,
AccountLoader.Factory accountLoaderFactory, AccountLoader.Factory accountLoaderFactory,
AccountQueryBuilder queryBuilder, AccountQueryBuilder queryBuilder,
AccountQueryProcessor queryProcessor, AccountQueryProcessor queryProcessor,
@GerritServerConfig Config cfg) { @GerritServerConfig Config cfg) {
this.self = self;
this.permissionBackend = permissionBackend;
this.accountLoaderFactory = accountLoaderFactory; this.accountLoaderFactory = accountLoaderFactory;
this.queryBuilder = queryBuilder; this.queryBuilder = queryBuilder;
this.queryProcessor = queryProcessor; this.queryProcessor = queryProcessor;
@ -143,7 +155,7 @@ public class QueryAccounts implements RestReadView<TopLevelResource> {
@Override @Override
public List<AccountInfo> apply(TopLevelResource rsrc) public List<AccountInfo> apply(TopLevelResource rsrc)
throws OrmException, BadRequestException, MethodNotAllowedException { throws OrmException, RestApiException, PermissionBackendException {
if (Strings.isNullOrEmpty(query)) { if (Strings.isNullOrEmpty(query)) {
throw new BadRequestException("missing query field"); throw new BadRequestException("missing query field");
} }
@ -156,14 +168,21 @@ public class QueryAccounts implements RestReadView<TopLevelResource> {
if (options.contains(ListAccountsOption.DETAILS)) { if (options.contains(ListAccountsOption.DETAILS)) {
fillOptions.addAll(AccountLoader.DETAILED_OPTIONS); fillOptions.addAll(AccountLoader.DETAILED_OPTIONS);
} }
boolean modifyAccountCapabilityChecked = false;
if (options.contains(ListAccountsOption.ALL_EMAILS)) { if (options.contains(ListAccountsOption.ALL_EMAILS)) {
permissionBackend.user(self).check(GlobalPermission.MODIFY_ACCOUNT);
modifyAccountCapabilityChecked = true;
fillOptions.add(FillOptions.EMAIL); fillOptions.add(FillOptions.EMAIL);
fillOptions.add(FillOptions.SECONDARY_EMAILS); fillOptions.add(FillOptions.SECONDARY_EMAILS);
} }
if (suggest) { if (suggest) {
fillOptions.addAll(AccountLoader.DETAILED_OPTIONS); fillOptions.addAll(AccountLoader.DETAILED_OPTIONS);
fillOptions.add(FillOptions.EMAIL); fillOptions.add(FillOptions.EMAIL);
fillOptions.add(FillOptions.SECONDARY_EMAILS);
if (modifyAccountCapabilityChecked
|| permissionBackend.user(self).test(GlobalPermission.MODIFY_ACCOUNT)) {
fillOptions.add(FillOptions.SECONDARY_EMAILS);
}
} }
accountLoader = accountLoaderFactory.create(fillOptions); accountLoader = accountLoaderFactory.create(fillOptions);

View File

@ -36,6 +36,7 @@ import com.google.gerrit.metrics.MetricMaker;
import com.google.gerrit.metrics.Timer0; import com.google.gerrit.metrics.Timer0;
import com.google.gerrit.reviewdb.client.Account; import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.Project; import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.account.AccountControl; import com.google.gerrit.server.account.AccountControl;
import com.google.gerrit.server.account.AccountDirectory.FillOptions; import com.google.gerrit.server.account.AccountDirectory.FillOptions;
import com.google.gerrit.server.account.AccountLoader; import com.google.gerrit.server.account.AccountLoader;
@ -44,6 +45,9 @@ import com.google.gerrit.server.account.GroupMembers;
import com.google.gerrit.server.index.account.AccountField; import com.google.gerrit.server.index.account.AccountField;
import com.google.gerrit.server.index.account.AccountIndexCollection; import com.google.gerrit.server.index.account.AccountIndexCollection;
import com.google.gerrit.server.notedb.ChangeNotes; import com.google.gerrit.server.notedb.ChangeNotes;
import com.google.gerrit.server.permissions.GlobalPermission;
import com.google.gerrit.server.permissions.PermissionBackend;
import com.google.gerrit.server.permissions.PermissionBackendException;
import com.google.gerrit.server.project.NoSuchProjectException; import com.google.gerrit.server.project.NoSuchProjectException;
import com.google.gerrit.server.project.ProjectState; import com.google.gerrit.server.project.ProjectState;
import com.google.gerrit.server.query.account.AccountPredicates; import com.google.gerrit.server.query.account.AccountPredicates;
@ -51,6 +55,7 @@ import com.google.gerrit.server.query.account.AccountQueryBuilder;
import com.google.gwtorm.server.OrmException; import com.google.gwtorm.server.OrmException;
import com.google.gwtorm.server.ResultSet; import com.google.gwtorm.server.ResultSet;
import com.google.inject.Inject; import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.Singleton; import com.google.inject.Singleton;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
@ -109,7 +114,7 @@ public class ReviewersUtil {
// give the ranking algorithm a good set of candidates it can work with // give the ranking algorithm a good set of candidates it can work with
private static final int CANDIDATE_LIST_MULTIPLIER = 2; private static final int CANDIDATE_LIST_MULTIPLIER = 2;
private final AccountLoader accountLoader; private final AccountLoader.Factory accountLoaderFactory;
private final AccountQueryBuilder accountQueryBuilder; private final AccountQueryBuilder accountQueryBuilder;
private final GroupBackend groupBackend; private final GroupBackend groupBackend;
private final GroupMembers groupMembers; private final GroupMembers groupMembers;
@ -118,6 +123,8 @@ public class ReviewersUtil {
private final AccountIndexCollection accountIndexes; private final AccountIndexCollection accountIndexes;
private final IndexConfig indexConfig; private final IndexConfig indexConfig;
private final AccountControl.Factory accountControlFactory; private final AccountControl.Factory accountControlFactory;
private final Provider<CurrentUser> self;
private final PermissionBackend permissionBackend;
@Inject @Inject
ReviewersUtil( ReviewersUtil(
@ -129,10 +136,10 @@ public class ReviewersUtil {
Metrics metrics, Metrics metrics,
AccountIndexCollection accountIndexes, AccountIndexCollection accountIndexes,
IndexConfig indexConfig, IndexConfig indexConfig,
AccountControl.Factory accountControlFactory) { AccountControl.Factory accountControlFactory,
Set<FillOptions> fillOptions = EnumSet.of(FillOptions.SECONDARY_EMAILS); Provider<CurrentUser> self,
fillOptions.addAll(AccountLoader.DETAILED_OPTIONS); PermissionBackend permissionBackend) {
this.accountLoader = accountLoaderFactory.create(fillOptions); this.accountLoaderFactory = accountLoaderFactory;
this.accountQueryBuilder = accountQueryBuilder; this.accountQueryBuilder = accountQueryBuilder;
this.groupBackend = groupBackend; this.groupBackend = groupBackend;
this.groupMembers = groupMembers; this.groupMembers = groupMembers;
@ -141,6 +148,8 @@ public class ReviewersUtil {
this.accountIndexes = accountIndexes; this.accountIndexes = accountIndexes;
this.indexConfig = indexConfig; this.indexConfig = indexConfig;
this.accountControlFactory = accountControlFactory; this.accountControlFactory = accountControlFactory;
this.self = self;
this.permissionBackend = permissionBackend;
} }
public interface VisibilityControl { public interface VisibilityControl {
@ -153,7 +162,7 @@ public class ReviewersUtil {
ProjectState projectState, ProjectState projectState,
VisibilityControl visibilityControl, VisibilityControl visibilityControl,
boolean excludeGroups) boolean excludeGroups)
throws IOException, OrmException, ConfigInvalidException { throws IOException, OrmException, ConfigInvalidException, PermissionBackendException {
String query = suggestReviewers.getQuery(); String query = suggestReviewers.getQuery();
int limit = suggestReviewers.getLimit(); int limit = suggestReviewers.getLimit();
@ -242,7 +251,14 @@ public class ReviewersUtil {
} }
private List<SuggestedReviewerInfo> loadAccounts(List<Account.Id> accountIds) private List<SuggestedReviewerInfo> loadAccounts(List<Account.Id> accountIds)
throws OrmException { throws OrmException, PermissionBackendException {
Set<FillOptions> fillOptions =
permissionBackend.user(self).test(GlobalPermission.MODIFY_ACCOUNT)
? EnumSet.of(FillOptions.SECONDARY_EMAILS)
: EnumSet.noneOf(FillOptions.class);
fillOptions.addAll(AccountLoader.DETAILED_OPTIONS);
AccountLoader accountLoader = accountLoaderFactory.create(fillOptions);
try (Timer0.Context ctx = metrics.loadAccountsLatency.start()) { try (Timer0.Context ctx = metrics.loadAccountsLatency.start()) {
List<SuggestedReviewerInfo> reviewer = List<SuggestedReviewerInfo> reviewer =
accountIds accountIds

View File

@ -26,6 +26,7 @@ import com.google.gerrit.server.IdentifiedUser.GenericFactory;
import com.google.gerrit.server.change.ChangeResource; import com.google.gerrit.server.change.ChangeResource;
import com.google.gerrit.server.config.GerritServerConfig; import com.google.gerrit.server.config.GerritServerConfig;
import com.google.gerrit.server.permissions.ChangePermission; import com.google.gerrit.server.permissions.ChangePermission;
import com.google.gerrit.server.permissions.PermissionBackendException;
import com.google.gerrit.server.project.ProjectCache; import com.google.gerrit.server.project.ProjectCache;
import com.google.gerrit.server.restapi.change.ReviewersUtil.VisibilityControl; import com.google.gerrit.server.restapi.change.ReviewersUtil.VisibilityControl;
import com.google.gwtorm.server.OrmException; import com.google.gwtorm.server.OrmException;
@ -66,7 +67,8 @@ public class SuggestChangeReviewers extends SuggestReviewers
@Override @Override
public List<SuggestedReviewerInfo> apply(ChangeResource rsrc) public List<SuggestedReviewerInfo> apply(ChangeResource rsrc)
throws AuthException, BadRequestException, OrmException, IOException, ConfigInvalidException { throws AuthException, BadRequestException, OrmException, IOException, ConfigInvalidException,
PermissionBackendException {
if (!self.get().isIdentifiedUser()) { if (!self.get().isIdentifiedUser()) {
throw new AuthException("Authentication required"); throw new AuthException("Authentication required");
} }

View File

@ -713,14 +713,24 @@ public class AccountIT extends AbstractDaemonTest {
} }
@Test @Test
public void getEmailsOfOtherAccount() throws Exception { public void cannotGetEmailsOfOtherAccountWithoutModifyAccount() throws Exception {
String email = "preferred2@example.com"; String email = "preferred2@example.com";
String secondaryEmail = "secondary2@example.com"; TestAccount foo = accountCreator.create(name("foo"), email, "Foo");
setApiUser(user);
exception.expect(AuthException.class);
exception.expectMessage("modify account not permitted");
gApi.accounts().id(foo.id.get()).getEmails();
}
@Test
public void getEmailsOfOtherAccount() throws Exception {
String email = "preferred3@example.com";
String secondaryEmail = "secondary3@example.com";
TestAccount foo = accountCreator.create(name("foo"), email, "Foo"); TestAccount foo = accountCreator.create(name("foo"), email, "Foo");
EmailInput input = newEmailInput(secondaryEmail); EmailInput input = newEmailInput(secondaryEmail);
gApi.accounts().id(foo.id.hashCode()).addEmail(input); gApi.accounts().id(foo.id.hashCode()).addEmail(input);
setApiUser(user);
assertThat( assertThat(
gApi.accounts() gApi.accounts()
.id(foo.id.get()) .id(foo.id.get())

View File

@ -24,6 +24,7 @@ import com.google.gerrit.acceptance.AbstractDaemonTest;
import com.google.gerrit.acceptance.GerritConfig; import com.google.gerrit.acceptance.GerritConfig;
import com.google.gerrit.acceptance.TestAccount; import com.google.gerrit.acceptance.TestAccount;
import com.google.gerrit.common.data.GlobalCapability; import com.google.gerrit.common.data.GlobalCapability;
import com.google.gerrit.extensions.api.accounts.EmailInput;
import com.google.gerrit.extensions.api.changes.ReviewInput; import com.google.gerrit.extensions.api.changes.ReviewInput;
import com.google.gerrit.extensions.common.ChangeInput; import com.google.gerrit.extensions.common.ChangeInput;
import com.google.gerrit.extensions.common.GroupInfo; import com.google.gerrit.extensions.common.GroupInfo;
@ -413,6 +414,60 @@ public class SuggestReviewersIT extends AbstractDaemonTest {
assertReviewers(suggestReviewers(changeId, name), ImmutableList.of(foo1), ImmutableList.of()); assertReviewers(suggestReviewers(changeId, name), ImmutableList.of(foo1), ImmutableList.of());
} }
@Test
public void suggestBySecondaryEmailWithModifyAccount() throws Exception {
String secondaryEmail = "foo.secondary@example.com";
TestAccount foo = createAccountWithSecondaryEmail("foo", secondaryEmail);
List<SuggestedReviewerInfo> reviewers =
suggestReviewers(createChange().getChangeId(), secondaryEmail, 4);
assertReviewers(reviewers, ImmutableList.of(foo), ImmutableList.of());
reviewers = suggestReviewers(createChange().getChangeId(), "secondary", 4);
assertReviewers(reviewers, ImmutableList.of(foo), ImmutableList.of());
}
@Test
public void cannotSuggestBySecondaryEmailWithoutModifyAccount() throws Exception {
String secondaryEmail = "foo.secondary@example.com";
createAccountWithSecondaryEmail("foo", secondaryEmail);
setApiUser(user);
List<SuggestedReviewerInfo> reviewers =
suggestReviewers(createChange().getChangeId(), secondaryEmail, 4);
assertThat(reviewers).isEmpty();
reviewers = suggestReviewers(createChange().getChangeId(), "secondary2", 4);
assertThat(reviewers).isEmpty();
}
@Test
public void secondaryEmailsInSuggestions() throws Exception {
String secondaryEmail = "foo.secondary@example.com";
TestAccount foo = createAccountWithSecondaryEmail("foo", secondaryEmail);
List<SuggestedReviewerInfo> reviewers =
suggestReviewers(createChange().getChangeId(), "foo", 4);
assertReviewers(reviewers, ImmutableList.of(foo), ImmutableList.of());
assertThat(Iterables.getOnlyElement(reviewers).account.secondaryEmails)
.containsExactly(secondaryEmail);
setApiUser(user);
reviewers = suggestReviewers(createChange().getChangeId(), "foo", 4);
assertReviewers(reviewers, ImmutableList.of(foo), ImmutableList.of());
assertThat(Iterables.getOnlyElement(reviewers).account.secondaryEmails).isNull();
}
private TestAccount createAccountWithSecondaryEmail(String name, String secondaryEmail)
throws Exception {
TestAccount foo = accountCreator.create(name(name), "foo.primary@example.com", "Foo");
EmailInput input = new EmailInput();
input.email = secondaryEmail;
input.noConfirmation = true;
gApi.accounts().id(foo.id.get()).addEmail(input);
return foo;
}
private List<SuggestedReviewerInfo> suggestReviewers(String changeId, String query) private List<SuggestedReviewerInfo> suggestReviewers(String changeId, String query)
throws Exception { throws Exception {
return gApi.changes().id(changeId).suggestReviewers(query).get(); return gApi.changes().id(changeId).suggestReviewers(query).get();

View File

@ -26,10 +26,14 @@ import com.google.gerrit.extensions.client.ListAccountsOption;
import com.google.gerrit.extensions.client.ProjectWatchInfo; import com.google.gerrit.extensions.client.ProjectWatchInfo;
import com.google.gerrit.extensions.common.AccountExternalIdInfo; import com.google.gerrit.extensions.common.AccountExternalIdInfo;
import com.google.gerrit.extensions.common.AccountInfo; import com.google.gerrit.extensions.common.AccountInfo;
import com.google.gerrit.extensions.restapi.AuthException;
import com.google.gerrit.extensions.restapi.BadRequestException;
import com.google.gerrit.extensions.restapi.ResourceNotFoundException; import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
import com.google.gerrit.extensions.restapi.RestApiException; import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.index.FieldDef;
import com.google.gerrit.index.IndexConfig; import com.google.gerrit.index.IndexConfig;
import com.google.gerrit.index.QueryOptions; import com.google.gerrit.index.QueryOptions;
import com.google.gerrit.index.Schema;
import com.google.gerrit.index.query.FieldBundle; import com.google.gerrit.index.query.FieldBundle;
import com.google.gerrit.lifecycle.LifecycleManager; import com.google.gerrit.lifecycle.LifecycleManager;
import com.google.gerrit.reviewdb.client.Account; import com.google.gerrit.reviewdb.client.Account;
@ -121,7 +125,7 @@ public abstract class AbstractQueryAccountsTest extends GerritServerTests {
protected Injector injector; protected Injector injector;
protected ReviewDb db; protected ReviewDb db;
protected AccountInfo currentUserInfo; protected AccountInfo currentUserInfo;
protected CurrentUser user; protected CurrentUser admin;
protected abstract Injector createInjector(); protected abstract Injector createInjector();
@ -145,10 +149,10 @@ public abstract class AbstractQueryAccountsTest extends GerritServerTests {
db = schemaFactory.open(); db = schemaFactory.open();
schemaCreator.create(db); schemaCreator.create(db);
Account.Id userId = createAccount("user", "User", "user@example.com", true); Account.Id adminId = createAccount("admin", "Administrator", "admin@example.com", true);
user = userFactory.create(userId); admin = userFactory.create(adminId);
requestContext.setContext(newRequestContext(userId)); requestContext.setContext(newRequestContext(adminId));
currentUserInfo = gApi.accounts().id(userId.get()).get(); currentUserInfo = gApi.accounts().id(adminId.get()).get();
} }
protected RequestContext newRequestContext(Account.Id requestUserId) { protected RequestContext newRequestContext(Account.Id requestUserId) {
@ -237,6 +241,52 @@ public abstract class AbstractQueryAccountsTest extends GerritServerTests {
assertQuery("email:" + user5.email.toUpperCase(), user5); assertQuery("email:" + user5.email.toUpperCase(), user5);
} }
@Test
public void bySecondaryEmail() throws Exception {
String prefix = name("secondary");
String domain = name("test.com");
String secondaryEmail = prefix + "@" + domain;
AccountInfo user1 = newAccountWithEmail("user1", name("user1@example.com"));
addEmails(user1, secondaryEmail);
AccountInfo user2 = newAccountWithEmail("user2", name("user2@example.com"));
addEmails(user2, name("other@" + domain));
assertQuery(secondaryEmail, user1);
assertQuery("email:" + secondaryEmail, user1);
assertQuery("email:" + prefix, user1);
assertQuery(domain, user1, user2);
}
@Test
public void byEmailWithoutModifyAccountCapability() throws Exception {
String preferredEmail = name("primary@test.com");
String secondaryEmail = name("secondary@test.com");
AccountInfo user1 = newAccountWithEmail("user1", preferredEmail);
addEmails(user1, secondaryEmail);
AccountInfo user2 = newAccount("user");
requestContext.setContext(newRequestContext(new Account.Id(user2._accountId)));
if (getSchemaVersion() < 5) {
assertMissingField(AccountField.PREFERRED_EMAIL);
assertFailingQuery("email:foo", "'email' operator is not supported by account index version");
return;
}
// This at least needs the PREFERRED_EMAIL field which is available from schema version 5.
if (getSchemaVersion() >= 5) {
assertQuery(preferredEmail, user1);
} else {
assertQuery(preferredEmail);
}
assertQuery(secondaryEmail);
assertQuery("email:" + preferredEmail, user1);
assertQuery("email:" + secondaryEmail);
}
@Test @Test
public void byUsername() throws Exception { public void byUsername() throws Exception {
AccountInfo user1 = newAccount("myuser"); AccountInfo user1 = newAccount("myuser");
@ -297,6 +347,49 @@ public abstract class AbstractQueryAccountsTest extends GerritServerTests {
assertQuery("name:" + quote(user2.name), user2); assertQuery("name:" + quote(user2.name), user2);
} }
@Test
public void byNameWithoutModifyAccountCapability() throws Exception {
AccountInfo user1 = newAccountWithFullName("jdoe", "John Doe");
AccountInfo user2 = newAccountWithFullName("jroe", "Jane Roe");
AccountInfo user3 = newAccount("user");
requestContext.setContext(newRequestContext(new Account.Id(user3._accountId)));
assertQuery("notexisting");
assertQuery("Not Existing");
// by full name works with any index version
assertQuery(quote(user1.name), user1);
assertQuery("name:" + quote(user1.name), user1);
assertQuery(quote(user2.name), user2);
assertQuery("name:" + quote(user2.name), user2);
// by self/me works with any index version
assertQuery("self", user3);
assertQuery("me", user3);
if (getSchemaVersion() < 8) {
assertMissingField(AccountField.NAME_PART_NO_SECONDARY_EMAIL);
// prefix queries only work if the NAME_PART_NO_SECONDARY_EMAIL field is available
assertQuery("john");
return;
}
assertQuery("John", user1);
assertQuery("john", user1);
assertQuery("Doe", user1);
assertQuery("doe", user1);
assertQuery("DOE", user1);
assertQuery("Jo Do", user1);
assertQuery("jo do", user1);
assertQuery("name:John", user1);
assertQuery("name:john", user1);
assertQuery("name:Doe", user1);
assertQuery("name:doe", user1);
assertQuery("name:DOE", user1);
}
@Test @Test
public void byWatchedProject() throws Exception { public void byWatchedProject() throws Exception {
Project.NameKey p = createProject(name("p")); Project.NameKey p = createProject(name("p"));
@ -375,6 +468,11 @@ public abstract class AbstractQueryAccountsTest extends GerritServerTests {
List<AccountInfo> result = assertQuery(user1.username, user1); List<AccountInfo> result = assertQuery(user1.username, user1);
assertThat(result.get(0).secondaryEmails).isNull(); assertThat(result.get(0).secondaryEmails).isNull();
result = assertQuery(newQuery(user1.username).withSuggest(true), user1);
assertThat(result.get(0).secondaryEmails)
.containsExactlyElementsIn(Arrays.asList(secondaryEmails))
.inOrder();
result = assertQuery(newQuery(user1.username).withOption(ListAccountsOption.DETAILS), user1); result = assertQuery(newQuery(user1.username).withOption(ListAccountsOption.DETAILS), user1);
assertThat(result.get(0).secondaryEmails).isNull(); assertThat(result.get(0).secondaryEmails).isNull();
@ -393,6 +491,21 @@ public abstract class AbstractQueryAccountsTest extends GerritServerTests {
.inOrder(); .inOrder();
} }
@Test
public void withSecondaryEmailsWithoutModifyAccountCapability() throws Exception {
AccountInfo user = newAccount("myuser", "My User", "abc@example.com", true);
String[] secondaryEmails = new String[] {"dfg@example.com", "hij@example.com"};
addEmails(user, secondaryEmails);
requestContext.setContext(newRequestContext(new Account.Id(user._accountId)));
List<AccountInfo> result = newQuery(user.username).withSuggest(true).get();
assertThat(result.get(0).secondaryEmails).isNull();
exception.expect(AuthException.class);
newQuery(user.username).withOption(ListAccountsOption.ALL_EMAILS).get();
}
@Test @Test
public void asAnonymous() throws Exception { public void asAnonymous() throws Exception {
AccountInfo user1 = newAccount("user1"); AccountInfo user1 = newAccount("user1");
@ -432,7 +545,7 @@ public abstract class AbstractQueryAccountsTest extends GerritServerTests {
@Test @Test
public void rawDocument() throws Exception { public void rawDocument() throws Exception {
AccountInfo userInfo = gApi.accounts().id(user.getAccountId().get()).get(); AccountInfo userInfo = gApi.accounts().id(admin.getAccountId().get()).get();
Optional<FieldBundle> rawFields = Optional<FieldBundle> rawFields =
indexes indexes
@ -633,6 +746,29 @@ public abstract class AbstractQueryAccountsTest extends GerritServerTests {
return accounts.stream().map(a -> a._accountId).collect(toList()); return accounts.stream().map(a -> a._accountId).collect(toList());
} }
protected void assertMissingField(FieldDef<AccountState, ?> field) {
assertThat(getSchema().hasField(field))
.named("schema %s has field %s", getSchemaVersion(), field.getName())
.isFalse();
}
protected void assertFailingQuery(String query, String expectedMessage) throws Exception {
try {
assertQuery(query);
fail("expected BadRequestException for query '" + query + "'");
} catch (BadRequestException e) {
assertThat(e.getMessage()).isEqualTo(expectedMessage);
}
}
protected int getSchemaVersion() {
return getSchema().getVersion();
}
protected Schema<AccountState> getSchema() {
return indexes.getSearchIndex().getSchema();
}
/** Boiler plate code to check two byte arrays for equality */ /** Boiler plate code to check two byte arrays for equality */
private static class ByteArrayWrapper { private static class ByteArrayWrapper {
private byte[] arr; private byte[] arr;
@ -654,8 +790,4 @@ public abstract class AbstractQueryAccountsTest extends GerritServerTests {
return Arrays.hashCode(arr); return Arrays.hashCode(arr);
} }
} }
protected int getSchemaVersion() {
return indexes.getSearchIndex().getSchema().getVersion();
}
} }