Merge "Merge branch 'stable-2.15' into master"
diff --git a/java/com/google/gerrit/httpd/GitOverHttpServlet.java b/java/com/google/gerrit/httpd/GitOverHttpServlet.java
index 05c1d58..739726e 100644
--- a/java/com/google/gerrit/httpd/GitOverHttpServlet.java
+++ b/java/com/google/gerrit/httpd/GitOverHttpServlet.java
@@ -184,7 +184,7 @@
try {
Project.NameKey nameKey = new Project.NameKey(projectName);
ProjectState state = projectCache.checkedGet(nameKey);
- if (state == null) {
+ if (state == null || !state.statePermitsRead()) {
throw new RepositoryNotFoundException(nameKey.get());
}
req.setAttribute(ATT_STATE, state);
diff --git a/java/com/google/gerrit/httpd/restapi/RestApiServlet.java b/java/com/google/gerrit/httpd/restapi/RestApiServlet.java
index 28fbb7d..515624f 100644
--- a/java/com/google/gerrit/httpd/restapi/RestApiServlet.java
+++ b/java/com/google/gerrit/httpd/restapi/RestApiServlet.java
@@ -839,10 +839,10 @@
}
Object obj = createInstance(type);
- Field[] fields = obj.getClass().getDeclaredFields();
- if (fields.length == 0 && Strings.isNullOrEmpty(value)) {
+ if (Strings.isNullOrEmpty(value)) {
return obj;
}
+ Field[] fields = obj.getClass().getDeclaredFields();
for (Field f : fields) {
if (f.getAnnotation(DefaultInput.class) != null && f.getType() == String.class) {
f.setAccessible(true);
diff --git a/java/com/google/gerrit/httpd/rpc/project/ProjectAccessFactory.java b/java/com/google/gerrit/httpd/rpc/project/ProjectAccessFactory.java
index c51fb9b..f117b24 100644
--- a/java/com/google/gerrit/httpd/rpc/project/ProjectAccessFactory.java
+++ b/java/com/google/gerrit/httpd/rpc/project/ProjectAccessFactory.java
@@ -259,8 +259,14 @@
throws NoSuchProjectException, IOException, PermissionBackendException,
ResourceConflictException {
ProjectState state = projectCache.checkedGet(projectName);
+ // Hidden projects(permitsRead = false) should only be accessible by the project owners.
+ // READ_CONFIG is checked here because it's only allowed to project owners(ACCESS may also
+ // be allowed for other users). Allowing project owners to access here will help them to view
+ // and update the config of hidden projects easily.
+ ProjectPermission permissionToCheck =
+ state.statePermitsRead() ? ProjectPermission.ACCESS : ProjectPermission.READ_CONFIG;
try {
- permissionBackend.currentUser().project(projectName).check(ProjectPermission.ACCESS);
+ permissionBackend.currentUser().project(projectName).check(permissionToCheck);
} catch (AuthException e) {
throw new NoSuchProjectException(projectName);
}
diff --git a/java/com/google/gerrit/server/account/externalids/ExternalId.java b/java/com/google/gerrit/server/account/externalids/ExternalId.java
index ffd413a..442bc2a 100644
--- a/java/com/google/gerrit/server/account/externalids/ExternalId.java
+++ b/java/com/google/gerrit/server/account/externalids/ExternalId.java
@@ -278,8 +278,6 @@
*/
public static ExternalId parse(String noteId, byte[] raw, ObjectId blobId)
throws ConfigInvalidException {
- checkNotNull(blobId);
-
Config externalIdConfig = new Config();
try {
externalIdConfig.fromText(new String(raw, UTF_8));
@@ -287,6 +285,13 @@
throw invalidConfig(noteId, e.getMessage());
}
+ return parse(noteId, externalIdConfig, blobId);
+ }
+
+ public static ExternalId parse(String noteId, Config externalIdConfig, ObjectId blobId)
+ throws ConfigInvalidException {
+ checkNotNull(blobId);
+
Set<String> externalIdKeys = externalIdConfig.getSubsections(EXTERNAL_ID_SECTION);
if (externalIdKeys.size() != 1) {
throw invalidConfig(
@@ -439,11 +444,17 @@
// c.setString(...) ensures that account IDs are human readable.
c.setString(
EXTERNAL_ID_SECTION, externalIdKey, ACCOUNT_ID_KEY, Integer.toString(accountId().get()));
+
if (email() != null) {
c.setString(EXTERNAL_ID_SECTION, externalIdKey, EMAIL_KEY, email());
+ } else {
+ c.unset(EXTERNAL_ID_SECTION, externalIdKey, EMAIL_KEY);
}
+
if (password() != null) {
c.setString(EXTERNAL_ID_SECTION, externalIdKey, PASSWORD_KEY, password());
+ } else {
+ c.unset(EXTERNAL_ID_SECTION, externalIdKey, PASSWORD_KEY);
}
}
}
diff --git a/java/com/google/gerrit/server/account/externalids/ExternalIdNotes.java b/java/com/google/gerrit/server/account/externalids/ExternalIdNotes.java
index 972dfbd..ecb201f 100644
--- a/java/com/google/gerrit/server/account/externalids/ExternalIdNotes.java
+++ b/java/com/google/gerrit/server/account/externalids/ExternalIdNotes.java
@@ -17,6 +17,7 @@
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;
import static java.nio.charset.StandardCharsets.UTF_8;
+import static java.util.stream.Collectors.joining;
import static java.util.stream.Collectors.toSet;
import static org.eclipse.jgit.lib.Constants.OBJ_BLOB;
@@ -371,9 +372,9 @@
Set<ExternalId> newExtIds = new HashSet<>();
noteMapUpdates.add(
- (rw, n) -> {
+ (rw, n, f) -> {
for (ExternalId extId : extIds) {
- ExternalId insertedExtId = upsert(rw, inserter, noteMap, extId);
+ ExternalId insertedExtId = upsert(rw, inserter, noteMap, f, extId);
newExtIds.add(insertedExtId);
}
});
@@ -399,9 +400,9 @@
Set<ExternalId> removedExtIds = get(ExternalId.Key.from(extIds));
Set<ExternalId> updatedExtIds = new HashSet<>();
noteMapUpdates.add(
- (rw, n) -> {
+ (rw, n, f) -> {
for (ExternalId extId : extIds) {
- ExternalId updatedExtId = upsert(rw, inserter, noteMap, extId);
+ ExternalId updatedExtId = upsert(rw, inserter, noteMap, f, extId);
updatedExtIds.add(updatedExtId);
}
});
@@ -429,9 +430,9 @@
checkLoaded();
Set<ExternalId> removedExtIds = new HashSet<>();
noteMapUpdates.add(
- (rw, n) -> {
+ (rw, n, f) -> {
for (ExternalId extId : extIds) {
- remove(rw, noteMap, extId);
+ remove(rw, noteMap, f, extId);
removedExtIds.add(extId);
}
});
@@ -458,9 +459,9 @@
checkLoaded();
Set<ExternalId> removedExtIds = new HashSet<>();
noteMapUpdates.add(
- (rw, n) -> {
+ (rw, n, f) -> {
for (ExternalId.Key extIdKey : extIdKeys) {
- ExternalId removedExtId = remove(rw, noteMap, extIdKey, accountId);
+ ExternalId removedExtId = remove(rw, noteMap, f, extIdKey, accountId);
removedExtIds.add(removedExtId);
}
});
@@ -476,9 +477,9 @@
checkLoaded();
Set<ExternalId> removedExtIds = new HashSet<>();
noteMapUpdates.add(
- (rw, n) -> {
+ (rw, n, f) -> {
for (ExternalId.Key extIdKey : extIdKeys) {
- ExternalId extId = remove(rw, noteMap, extIdKey, null);
+ ExternalId extId = remove(rw, noteMap, f, extIdKey, null);
removedExtIds.add(extId);
}
});
@@ -506,16 +507,16 @@
Set<ExternalId> removedExtIds = new HashSet<>();
Set<ExternalId> updatedExtIds = new HashSet<>();
noteMapUpdates.add(
- (rw, n) -> {
+ (rw, n, f) -> {
for (ExternalId.Key extIdKey : toDelete) {
- ExternalId removedExtId = remove(rw, noteMap, extIdKey, accountId);
+ ExternalId removedExtId = remove(rw, noteMap, f, extIdKey, accountId);
if (removedExtId != null) {
removedExtIds.add(removedExtId);
}
}
for (ExternalId extId : toAdd) {
- ExternalId insertedExtId = upsert(rw, inserter, noteMap, extId);
+ ExternalId insertedExtId = upsert(rw, inserter, noteMap, f, extId);
updatedExtIds.add(insertedExtId);
}
});
@@ -540,14 +541,14 @@
Set<ExternalId> removedExtIds = new HashSet<>();
Set<ExternalId> updatedExtIds = new HashSet<>();
noteMapUpdates.add(
- (rw, n) -> {
+ (rw, n, f) -> {
for (ExternalId.Key extIdKey : toDelete) {
- ExternalId removedExtId = remove(rw, noteMap, extIdKey, null);
+ ExternalId removedExtId = remove(rw, noteMap, f, extIdKey, null);
removedExtIds.add(removedExtId);
}
for (ExternalId extId : toAdd) {
- ExternalId insertedExtId = upsert(rw, inserter, noteMap, extId);
+ ExternalId insertedExtId = upsert(rw, inserter, noteMap, f, extId);
updatedExtIds.add(insertedExtId);
}
});
@@ -660,14 +661,22 @@
}
try (RevWalk rw = new RevWalk(repo)) {
+ Set<String> footers = new HashSet<>();
for (NoteMapUpdate noteMapUpdate : noteMapUpdates) {
try {
- noteMapUpdate.execute(rw, noteMap);
+ noteMapUpdate.execute(rw, noteMap, footers);
} catch (DuplicateExternalIdKeyException e) {
throw new IOException(e);
}
}
noteMapUpdates.clear();
+ if (!footers.isEmpty()) {
+ commit.setMessage(
+ footers
+ .stream()
+ .sorted()
+ .collect(joining("\n", commit.getMessage().trim() + "\n\n", "")));
+ }
RevTree oldTree = revision != null ? rw.parseTree(revision) : null;
ObjectId newTreeId = noteMap.writeTree(inserter);
@@ -718,14 +727,17 @@
* <p>If the external ID already exists it is overwritten.
*/
private static ExternalId upsert(
- RevWalk rw, ObjectInserter ins, NoteMap noteMap, ExternalId extId)
+ RevWalk rw, ObjectInserter ins, NoteMap noteMap, Set<String> footers, ExternalId extId)
throws IOException, ConfigInvalidException {
ObjectId noteId = extId.key().sha1();
Config c = new Config();
if (noteMap.contains(extId.key().sha1())) {
- byte[] raw = readNoteData(rw, noteMap.get(noteId));
+ ObjectId noteDataId = noteMap.get(noteId);
+ byte[] raw = readNoteData(rw, noteDataId);
try {
c = new BlobBasedConfig(null, raw);
+ ExternalId oldExtId = ExternalId.parse(noteId.name(), c, noteDataId);
+ addFooters(footers, oldExtId);
} catch (ConfigInvalidException e) {
throw new ConfigInvalidException(
String.format("Invalid external id config for note %s: %s", noteId, e.getMessage()));
@@ -735,7 +747,9 @@
byte[] raw = c.toText().getBytes(UTF_8);
ObjectId noteData = ins.insert(OBJ_BLOB, raw);
noteMap.set(noteId, noteData);
- return ExternalId.create(extId, noteData);
+ ExternalId newExtId = ExternalId.create(extId, noteData);
+ addFooters(footers, newExtId);
+ return newExtId;
}
/**
@@ -744,7 +758,8 @@
* @throws IllegalStateException is thrown if there is an existing external ID that has the same
* key, but otherwise doesn't match the specified external ID.
*/
- private static ExternalId remove(RevWalk rw, NoteMap noteMap, ExternalId extId)
+ private static ExternalId remove(
+ RevWalk rw, NoteMap noteMap, Set<String> footers, ExternalId extId)
throws IOException, ConfigInvalidException {
ObjectId noteId = extId.key().sha1();
if (!noteMap.contains(noteId)) {
@@ -760,6 +775,7 @@
extId.toString(),
actualExtId.toString());
noteMap.remove(noteId);
+ addFooters(footers, actualExtId);
return actualExtId;
}
@@ -772,7 +788,11 @@
* exists
*/
private static ExternalId remove(
- RevWalk rw, NoteMap noteMap, ExternalId.Key extIdKey, Account.Id expectedAccountId)
+ RevWalk rw,
+ NoteMap noteMap,
+ Set<String> footers,
+ ExternalId.Key extIdKey,
+ Account.Id expectedAccountId)
throws IOException, ConfigInvalidException {
ObjectId noteId = extIdKey.sha1();
if (!noteMap.contains(noteId)) {
@@ -792,9 +812,17 @@
extId.accountId().get());
}
noteMap.remove(noteId);
+ addFooters(footers, extId);
return extId;
}
+ private static void addFooters(Set<String> footers, ExternalId extId) {
+ footers.add("Account: " + extId.accountId().get());
+ if (extId.email() != null) {
+ footers.add("Email: " + extId.email());
+ }
+ }
+
private void checkExternalIdsDontExist(Collection<ExternalId> extIds)
throws DuplicateExternalIdKeyException, IOException {
checkExternalIdKeysDontExist(ExternalId.Key.from(extIds));
@@ -823,7 +851,7 @@
@FunctionalInterface
private interface NoteMapUpdate {
- void execute(RevWalk rw, NoteMap noteMap)
+ void execute(RevWalk rw, NoteMap noteMap, Set<String> footers)
throws IOException, ConfigInvalidException, DuplicateExternalIdKeyException;
}
diff --git a/java/com/google/gerrit/server/args4j/ProjectHandler.java b/java/com/google/gerrit/server/args4j/ProjectHandler.java
index e97c171..1d40b53 100644
--- a/java/com/google/gerrit/server/args4j/ProjectHandler.java
+++ b/java/com/google/gerrit/server/args4j/ProjectHandler.java
@@ -79,7 +79,13 @@
if (state == null) {
throw new CmdLineException(owner, String.format("project %s not found", nameWithoutSuffix));
}
- permissionBackend.currentUser().project(nameKey).check(ProjectPermission.ACCESS);
+ // Hidden projects(permitsRead = false) should only be accessible by the project owners.
+ // READ_CONFIG is checked here because it's only allowed to project owners(ACCESS may also
+ // be allowed for other users). Allowing project owners to access here will help them to view
+ // and update the config of hidden projects easily.
+ ProjectPermission permissionToCheck =
+ state.statePermitsRead() ? ProjectPermission.ACCESS : ProjectPermission.READ_CONFIG;
+ permissionBackend.currentUser().project(nameKey).check(permissionToCheck);
} catch (AuthException e) {
throw new CmdLineException(owner, new NoSuchProjectException(nameKey).getMessage());
} catch (PermissionBackendException | IOException e) {
diff --git a/java/com/google/gerrit/server/events/EventBroker.java b/java/com/google/gerrit/server/events/EventBroker.java
index 8f12cb3..62e8d12 100644
--- a/java/com/google/gerrit/server/events/EventBroker.java
+++ b/java/com/google/gerrit/server/events/EventBroker.java
@@ -150,6 +150,11 @@
protected boolean isVisibleTo(Project.NameKey project, CurrentUser user) {
try {
+ ProjectState state = projectCache.get(project);
+ if (state == null || !state.statePermitsRead()) {
+ return false;
+ }
+
permissionBackend.user(user).project(project).check(ProjectPermission.ACCESS);
return true;
} catch (AuthException | PermissionBackendException e) {
diff --git a/java/com/google/gerrit/server/index/change/ChangeField.java b/java/com/google/gerrit/server/index/change/ChangeField.java
index 262c732..6dbad6a 100644
--- a/java/com/google/gerrit/server/index/change/ChangeField.java
+++ b/java/com/google/gerrit/server/index/change/ChangeField.java
@@ -517,7 +517,6 @@
/** Number of unresolved comments of the change. */
public static final FieldDef<ChangeData, Integer> UNRESOLVED_COMMENT_COUNT =
intRange(ChangeQueryBuilder.FIELD_UNRESOLVED_COMMENT_COUNT)
- .stored()
.build(ChangeData::unresolvedCommentCount);
/** Whether the change is mergeable. */
@@ -536,13 +535,11 @@
/** The number of inserted lines in this change. */
public static final FieldDef<ChangeData, Integer> ADDED =
intRange(ChangeQueryBuilder.FIELD_ADDED)
- .stored()
.build(cd -> cd.changedLines().isPresent() ? cd.changedLines().get().insertions : null);
/** The number of deleted lines in this change. */
public static final FieldDef<ChangeData, Integer> DELETED =
intRange(ChangeQueryBuilder.FIELD_DELETED)
- .stored()
.build(cd -> cd.changedLines().isPresent() ? cd.changedLines().get().deletions : null);
/** The total number of modified lines in this change. */
diff --git a/java/com/google/gerrit/server/mail/send/CreateChangeSender.java b/java/com/google/gerrit/server/mail/send/CreateChangeSender.java
index 10d14a8..a12e226 100644
--- a/java/com/google/gerrit/server/mail/send/CreateChangeSender.java
+++ b/java/com/google/gerrit/server/mail/send/CreateChangeSender.java
@@ -19,7 +19,6 @@
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.account.ProjectWatches.NotifyType;
import com.google.gerrit.server.mail.send.ProjectWatch.Watchers;
import com.google.gerrit.server.permissions.PermissionBackend;
@@ -39,19 +38,16 @@
CreateChangeSender create(Project.NameKey project, Change.Id id);
}
- private final IdentifiedUser.GenericFactory identifiedUserFactory;
private final PermissionBackend permissionBackend;
@Inject
public CreateChangeSender(
EmailArguments ea,
- IdentifiedUser.GenericFactory identifiedUserFactory,
PermissionBackend permissionBackend,
@Assisted Project.NameKey project,
@Assisted Change.Id id)
throws OrmException {
super(ea, newChangeData(ea, project, id));
- this.identifiedUserFactory = identifiedUserFactory;
this.permissionBackend = permissionBackend;
}
@@ -83,7 +79,7 @@
private boolean isOwnerOfProjectOrBranch(Account.Id userId) {
return permissionBackend
- .user(identifiedUserFactory.create(userId))
+ .absentUser(userId)
.ref(change.getDest())
.testOrFalse(RefPermission.WRITE_CONFIG);
}
diff --git a/java/com/google/gerrit/server/permissions/ChangeControl.java b/java/com/google/gerrit/server/permissions/ChangeControl.java
index 47c8543..e49686a 100644
--- a/java/com/google/gerrit/server/permissions/ChangeControl.java
+++ b/java/com/google/gerrit/server/permissions/ChangeControl.java
@@ -138,19 +138,6 @@
&& !isPatchSetLocked(db);
}
- /** Can this user delete this change? */
- private boolean canDelete(Change.Status status) {
- switch (status) {
- case NEW:
- case ABANDONED:
- return (isOwner() && refControl.canPerform(Permission.DELETE_OWN_CHANGES))
- || getProjectControl().isAdmin();
- case MERGED:
- default:
- return false;
- }
- }
-
/** Can this user rebase this change? */
private boolean canRebase(ReviewDb db) throws OrmException {
return (isOwner() || refControl.canSubmit(isOwner()) || refControl.canRebase())
@@ -369,7 +356,8 @@
case ABANDON:
return canAbandon(db());
case DELETE:
- return canDelete(getChange().getStatus());
+ return (isOwner() && refControl.canPerform(Permission.DELETE_OWN_CHANGES))
+ || getProjectControl().isAdmin();
case ADD_PATCH_SET:
return canAddPatchSet(db());
case EDIT_ASSIGNEE:
diff --git a/java/com/google/gerrit/server/permissions/DefaultPermissionBackend.java b/java/com/google/gerrit/server/permissions/DefaultPermissionBackend.java
index d8e01f9..2c8c951 100644
--- a/java/com/google/gerrit/server/permissions/DefaultPermissionBackend.java
+++ b/java/com/google/gerrit/server/permissions/DefaultPermissionBackend.java
@@ -23,9 +23,11 @@
import com.google.gerrit.extensions.api.access.GlobalOrPluginPermission;
import com.google.gerrit.extensions.api.access.PluginPermission;
import com.google.gerrit.extensions.restapi.AuthException;
+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.PeerDaemonUser;
import com.google.gerrit.server.account.CapabilityCollection;
import com.google.gerrit.server.project.NoSuchProjectException;
@@ -48,15 +50,18 @@
private final Provider<CurrentUser> currentUser;
private final ProjectCache projectCache;
private final ProjectControl.Factory projectControlFactory;
+ private final IdentifiedUser.GenericFactory identifiedUserFactory;
@Inject
DefaultPermissionBackend(
Provider<CurrentUser> currentUser,
ProjectCache projectCache,
- ProjectControl.Factory projectControlFactory) {
+ ProjectControl.Factory projectControlFactory,
+ IdentifiedUser.GenericFactory identifiedUserFactory) {
this.currentUser = currentUser;
this.projectCache = projectCache;
this.projectControlFactory = projectControlFactory;
+ this.identifiedUserFactory = identifiedUserFactory;
}
private CapabilityCollection capabilities() {
@@ -73,6 +78,12 @@
return new WithUserImpl(checkNotNull(user, "user"));
}
+ @Override
+ public WithUser absentUser(Account.Id user) {
+ IdentifiedUser identifiedUser = identifiedUserFactory.create(checkNotNull(user, "user"));
+ return new WithUserImpl(identifiedUser);
+ }
+
class WithUserImpl extends WithUser {
private final CurrentUser user;
private Boolean admin;
diff --git a/java/com/google/gerrit/server/permissions/PermissionBackend.java b/java/com/google/gerrit/server/permissions/PermissionBackend.java
index a15aa3a..ed10184 100644
--- a/java/com/google/gerrit/server/permissions/PermissionBackend.java
+++ b/java/com/google/gerrit/server/permissions/PermissionBackend.java
@@ -23,6 +23,7 @@
import com.google.gerrit.extensions.api.access.GlobalOrPluginPermission;
import com.google.gerrit.extensions.conditions.BooleanCondition;
import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.Branch;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.reviewdb.server.ReviewDb;
@@ -94,12 +95,24 @@
public abstract WithUser currentUser();
/**
- * Returns an instance scoped to the specified user. If an instance scoped to the current user is
- * desired, use {@code currentUser()} instead.
+ * Returns an instance scoped to the specified user. Should be used in cases where the user could
+ * either be the issuer of the current request or an impersonated user. PermissionBackends that do
+ * not support impersonation can fail with an {@code IllegalStateException}.
+ *
+ * <p>If an instance scoped to the current user is desired, use {@code currentUser()} instead.
*/
public abstract WithUser user(CurrentUser user);
/**
+ * Returns an instance scoped to the provided user. Should be used in cases where the caller wants
+ * to check the permissions of a user who is not the issuer of the current request and not the
+ * target of impersonation.
+ *
+ * <p>Usage should be very limited as this can expose a group-oracle.
+ */
+ public abstract WithUser absentUser(Account.Id user);
+
+ /**
* Bulk evaluate a set of {@link PermissionBackendCondition} for view handling.
*
* <p>Overridden implementations should call {@link PermissionBackendCondition#set(boolean)} to
diff --git a/java/com/google/gerrit/server/permissions/ProjectControl.java b/java/com/google/gerrit/server/permissions/ProjectControl.java
index 30ed180..dbd60ea 100644
--- a/java/com/google/gerrit/server/permissions/ProjectControl.java
+++ b/java/com/google/gerrit/server/permissions/ProjectControl.java
@@ -202,11 +202,6 @@
return false;
}
- /** Returns whether the project is hidden. */
- private boolean isHidden() {
- return getProject().getState().equals(com.google.gerrit.extensions.client.ProjectState.HIDDEN);
- }
-
private boolean canAddRefs() {
return (canPerformOnAnyRef(Permission.CREATE) || isAdmin());
}
@@ -400,8 +395,7 @@
private boolean can(ProjectPermission perm) throws PermissionBackendException {
switch (perm) {
case ACCESS:
- return (!isHidden() && (user.isInternalUser() || canPerformOnAnyRef(Permission.READ)))
- || isOwner();
+ return user.isInternalUser() || isOwner() || canPerformOnAnyRef(Permission.READ);
case READ:
return allRefsAreVisible(Collections.emptySet());
diff --git a/java/com/google/gerrit/server/project/ChildProjects.java b/java/com/google/gerrit/server/project/ChildProjects.java
index a29a152..868d0af 100644
--- a/java/com/google/gerrit/server/project/ChildProjects.java
+++ b/java/com/google/gerrit/server/project/ChildProjects.java
@@ -53,7 +53,7 @@
/** Gets all child projects recursively. */
public List<ProjectInfo> list(Project.NameKey parent) throws PermissionBackendException {
- Map<Project.NameKey, Project> projects = readAllProjects();
+ Map<Project.NameKey, Project> projects = readAllReadableProjects();
Multimap<Project.NameKey, Project.NameKey> children = parentToChildren(projects);
PermissionBackend.WithUser perm = permissionBackend.currentUser();
@@ -62,11 +62,11 @@
return results;
}
- private Map<Project.NameKey, Project> readAllProjects() {
+ private Map<Project.NameKey, Project> readAllReadableProjects() {
Map<Project.NameKey, Project> projects = new HashMap<>();
for (Project.NameKey name : projectCache.all()) {
ProjectState c = projectCache.get(name);
- if (c != null) {
+ if (c != null && c.statePermitsRead()) {
projects.put(c.getNameKey(), c.getProject());
}
}
diff --git a/java/com/google/gerrit/server/project/RefUtil.java b/java/com/google/gerrit/server/project/RefUtil.java
index 62e48be..e42a7df 100644
--- a/java/com/google/gerrit/server/project/RefUtil.java
+++ b/java/com/google/gerrit/server/project/RefUtil.java
@@ -39,6 +39,8 @@
public class RefUtil {
private static final Logger log = LoggerFactory.getLogger(RefUtil.class);
+ private RefUtil() {}
+
public static ObjectId parseBaseRevision(
Repository repo, Project.NameKey projectName, String baseRevision)
throws InvalidRevisionException {
diff --git a/java/com/google/gerrit/server/project/SuggestParentCandidates.java b/java/com/google/gerrit/server/project/SuggestParentCandidates.java
index 014029b..99833af 100644
--- a/java/com/google/gerrit/server/project/SuggestParentCandidates.java
+++ b/java/com/google/gerrit/server/project/SuggestParentCandidates.java
@@ -44,17 +44,17 @@
public List<Project.NameKey> getNameKeys() throws PermissionBackendException {
return permissionBackend
.currentUser()
- .filter(ProjectPermission.ACCESS, parents())
+ .filter(ProjectPermission.ACCESS, readableParents())
.stream()
.sorted()
.collect(toList());
}
- private Set<Project.NameKey> parents() {
+ private Set<Project.NameKey> readableParents() {
Set<Project.NameKey> parents = new HashSet<>();
for (Project.NameKey p : projectCache.all()) {
ProjectState ps = projectCache.get(p);
- if (ps != null) {
+ if (ps != null && ps.statePermitsRead()) {
Project.NameKey parent = ps.getProject().getParent();
if (parent != null) {
parents.add(parent);
diff --git a/java/com/google/gerrit/server/query/account/AccountPredicates.java b/java/com/google/gerrit/server/query/account/AccountPredicates.java
index acb963c..57a0dcc 100644
--- a/java/com/google/gerrit/server/query/account/AccountPredicates.java
+++ b/java/com/google/gerrit/server/query/account/AccountPredicates.java
@@ -126,8 +126,7 @@
public static Predicate<AccountState> cansee(
AccountQueryBuilder.Arguments args, ChangeNotes changeNotes) {
- return new CanSeeChangePredicate(
- args.db, args.permissionBackend, args.userFactory, changeNotes);
+ return new CanSeeChangePredicate(args.db, args.permissionBackend, changeNotes);
}
static class AccountPredicate extends IndexPredicate<AccountState>
diff --git a/java/com/google/gerrit/server/query/account/AccountQueryBuilder.java b/java/com/google/gerrit/server/query/account/AccountQueryBuilder.java
index 8b6e1e4..f627ec8 100644
--- a/java/com/google/gerrit/server/query/account/AccountQueryBuilder.java
+++ b/java/com/google/gerrit/server/query/account/AccountQueryBuilder.java
@@ -63,7 +63,6 @@
public static class Arguments {
final Provider<ReviewDb> db;
final ChangeFinder changeFinder;
- final IdentifiedUser.GenericFactory userFactory;
final PermissionBackend permissionBackend;
private final Provider<CurrentUser> self;
@@ -75,13 +74,11 @@
AccountIndexCollection indexes,
Provider<ReviewDb> db,
ChangeFinder changeFinder,
- IdentifiedUser.GenericFactory userFactory,
PermissionBackend permissionBackend) {
this.self = self;
this.indexes = indexes;
this.db = db;
this.changeFinder = changeFinder;
- this.userFactory = userFactory;
this.permissionBackend = permissionBackend;
}
diff --git a/java/com/google/gerrit/server/query/account/CanSeeChangePredicate.java b/java/com/google/gerrit/server/query/account/CanSeeChangePredicate.java
index c436a45..b5e7b90 100644
--- a/java/com/google/gerrit/server/query/account/CanSeeChangePredicate.java
+++ b/java/com/google/gerrit/server/query/account/CanSeeChangePredicate.java
@@ -17,7 +17,6 @@
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;
@@ -31,17 +30,12 @@
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) {
+ Provider<ReviewDb> db, PermissionBackend permissionBackend, ChangeNotes changeNotes) {
this.db = db;
this.permissionBackend = permissionBackend;
- this.userFactory = userFactory;
this.changeNotes = changeNotes;
}
@@ -49,7 +43,7 @@
public boolean match(AccountState accountState) throws OrmException {
try {
return permissionBackend
- .user(userFactory.create(accountState.getAccount().getId()))
+ .absentUser(accountState.getAccount().getId())
.database(db)
.change(changeNotes)
.test(ChangePermission.READ);
@@ -65,7 +59,7 @@
@Override
public Predicate<AccountState> copy(Collection<? extends Predicate<AccountState>> children) {
- return new CanSeeChangePredicate(db, permissionBackend, userFactory, changeNotes);
+ return new CanSeeChangePredicate(db, permissionBackend, changeNotes);
}
@Override
diff --git a/java/com/google/gerrit/server/query/account/InternalAccountQuery.java b/java/com/google/gerrit/server/query/account/InternalAccountQuery.java
index f1be580..02386ae 100644
--- a/java/com/google/gerrit/server/query/account/InternalAccountQuery.java
+++ b/java/com/google/gerrit/server/query/account/InternalAccountQuery.java
@@ -17,11 +17,9 @@
import static java.util.stream.Collectors.toList;
import static java.util.stream.Collectors.toSet;
-import com.google.common.base.Joiner;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableListMultimap;
-import com.google.common.collect.Lists;
import com.google.common.collect.Multimap;
import com.google.gerrit.index.FieldDef;
import com.google.gerrit.index.IndexConfig;
@@ -37,8 +35,6 @@
import java.util.Arrays;
import java.util.List;
import java.util.Set;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
/**
* Query wrapper for the account index.
@@ -47,8 +43,6 @@
* holding on to a single instance.
*/
public class InternalAccountQuery extends InternalQuery<AccountState> {
- private static final Logger log = LoggerFactory.getLogger(InternalAccountQuery.class);
-
@Inject
InternalAccountQuery(
AccountQueryProcessor queryProcessor,
@@ -94,28 +88,6 @@
return query(AccountPredicates.externalIdIncludingSecondaryEmails(externalId.toString()));
}
- public AccountState oneByExternalId(String externalId) throws OrmException {
- return oneByExternalId(ExternalId.Key.parse(externalId));
- }
-
- public AccountState oneByExternalId(String scheme, String id) throws OrmException {
- return oneByExternalId(ExternalId.Key.create(scheme, id));
- }
-
- public AccountState oneByExternalId(ExternalId.Key externalId) throws OrmException {
- List<AccountState> accountStates = byExternalId(externalId);
- if (accountStates.size() == 1) {
- return accountStates.get(0);
- } else if (accountStates.size() > 0) {
- StringBuilder msg = new StringBuilder();
- msg.append("Ambiguous external ID ").append(externalId).append(" for accounts: ");
- Joiner.on(", ")
- .appendTo(msg, Lists.transform(accountStates, AccountState.ACCOUNT_ID_FUNCTION));
- log.warn(msg.toString());
- }
- return null;
- }
-
public List<AccountState> byFullName(String fullName) throws OrmException {
return query(AccountPredicates.fullName(fullName));
}
diff --git a/java/com/google/gerrit/server/restapi/change/DeleteChange.java b/java/com/google/gerrit/server/restapi/change/DeleteChange.java
index e33b4a4..4bd10ed 100644
--- a/java/com/google/gerrit/server/restapi/change/DeleteChange.java
+++ b/java/com/google/gerrit/server/restapi/change/DeleteChange.java
@@ -56,7 +56,7 @@
protected Response<?> applyImpl(
BatchUpdate.Factory updateFactory, ChangeResource rsrc, Input input)
throws RestApiException, UpdateException, PermissionBackendException {
- if (rsrc.getChange().getStatus() == Change.Status.MERGED) {
+ if (!isChangeDeletable(rsrc.getChange().getStatus())) {
throw new MethodNotAllowedException("delete not permitted");
}
rsrc.permissions().database(db).check(ChangePermission.DELETE);
@@ -78,20 +78,15 @@
return new UiAction.Description()
.setLabel("Delete")
.setTitle("Delete change " + rsrc.getId())
- .setVisible(and(couldDeleteWhenIn(status), perm.testCond(ChangePermission.DELETE)));
+ .setVisible(and(isChangeDeletable(status), perm.testCond(ChangePermission.DELETE)));
}
- private boolean couldDeleteWhenIn(Change.Status status) {
- switch (status) {
- case NEW:
- case ABANDONED:
- // New or abandoned changes can be deleted with the right permissions.
- return true;
-
- case MERGED:
- // Merged changes should never be deleted.
- return false;
+ private static boolean isChangeDeletable(Change.Status status) {
+ if (status == Change.Status.MERGED) {
+ // Merged changes should never be deleted.
+ return false;
}
- return false;
+ // New or abandoned changes can be deleted with the right permissions.
+ return true;
}
}
diff --git a/java/com/google/gerrit/server/restapi/change/ReviewerRecommender.java b/java/com/google/gerrit/server/restapi/change/ReviewerRecommender.java
index 5f6b088..78687cd 100644
--- a/java/com/google/gerrit/server/restapi/change/ReviewerRecommender.java
+++ b/java/com/google/gerrit/server/restapi/change/ReviewerRecommender.java
@@ -20,6 +20,7 @@
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
+import com.google.gerrit.common.Nullable;
import com.google.gerrit.common.data.LabelType;
import com.google.gerrit.extensions.registration.DynamicMap;
import com.google.gerrit.index.query.Predicate;
@@ -100,7 +101,7 @@
}
public List<Account.Id> suggestReviewers(
- ChangeNotes changeNotes,
+ @Nullable ChangeNotes changeNotes,
SuggestReviewers suggestReviewers,
ProjectState projectState,
List<Account.Id> candidateList)
diff --git a/java/com/google/gerrit/server/restapi/change/ReviewersUtil.java b/java/com/google/gerrit/server/restapi/change/ReviewersUtil.java
index ac88b65..95557b5 100644
--- a/java/com/google/gerrit/server/restapi/change/ReviewersUtil.java
+++ b/java/com/google/gerrit/server/restapi/change/ReviewersUtil.java
@@ -21,6 +21,7 @@
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
+import com.google.gerrit.common.Nullable;
import com.google.gerrit.common.data.GroupReference;
import com.google.gerrit.extensions.common.GroupBaseInfo;
import com.google.gerrit.extensions.common.SuggestedReviewerInfo;
@@ -161,17 +162,25 @@
}
public List<SuggestedReviewerInfo> suggestReviewers(
- ChangeNotes changeNotes,
+ @Nullable ChangeNotes changeNotes,
SuggestReviewers suggestReviewers,
ProjectState projectState,
VisibilityControl visibilityControl,
boolean excludeGroups)
throws IOException, OrmException, ConfigInvalidException, PermissionBackendException {
CurrentUser currentUser = self.get();
- log.debug(
- "Suggesting reviewers for change {} to user {}.",
- changeNotes.getChangeId().get(),
- currentUser.getLoggableName());
+ if (changeNotes != null) {
+ log.debug(
+ "Suggesting reviewers for change {} to user {}.",
+ changeNotes.getChangeId().get(),
+ currentUser.getLoggableName());
+ } else {
+ log.debug(
+ "Suggesting default reviewers for project {} to user {}.",
+ projectState.getName(),
+ currentUser.getLoggableName());
+ }
+
String query = suggestReviewers.getQuery();
log.debug("Query: {}", query);
int limit = suggestReviewers.getLimit();
@@ -272,7 +281,7 @@
}
private List<Account.Id> recommendAccounts(
- ChangeNotes changeNotes,
+ @Nullable ChangeNotes changeNotes,
SuggestReviewers suggestReviewers,
ProjectState projectState,
List<Account.Id> candidateList)
diff --git a/java/com/google/gerrit/server/restapi/config/ListTasks.java b/java/com/google/gerrit/server/restapi/config/ListTasks.java
index 71ee5ad..fb2819c 100644
--- a/java/com/google/gerrit/server/restapi/config/ListTasks.java
+++ b/java/com/google/gerrit/server/restapi/config/ListTasks.java
@@ -28,6 +28,8 @@
import com.google.gerrit.server.permissions.PermissionBackend;
import com.google.gerrit.server.permissions.PermissionBackendException;
import com.google.gerrit.server.permissions.ProjectPermission;
+import com.google.gerrit.server.project.ProjectCache;
+import com.google.gerrit.server.project.ProjectState;
import com.google.gerrit.server.util.IdGenerator;
import com.google.inject.Inject;
import com.google.inject.Provider;
@@ -46,13 +48,18 @@
private final PermissionBackend permissionBackend;
private final WorkQueue workQueue;
private final Provider<CurrentUser> self;
+ private final ProjectCache projectCache;
@Inject
public ListTasks(
- PermissionBackend permissionBackend, WorkQueue workQueue, Provider<CurrentUser> self) {
+ PermissionBackend permissionBackend,
+ WorkQueue workQueue,
+ Provider<CurrentUser> self,
+ ProjectCache projectCache) {
this.permissionBackend = permissionBackend;
this.workQueue = workQueue;
this.self = self;
+ this.projectCache = projectCache;
}
@Override
@@ -77,14 +84,17 @@
if (task.projectName != null) {
Boolean visible = visibilityCache.get(task.projectName);
if (visible == null) {
- try {
- permissionBackend
- .user(user)
- .project(new Project.NameKey(task.projectName))
- .check(ProjectPermission.ACCESS);
- visible = true;
- } catch (AuthException e) {
+ Project.NameKey nameKey = new Project.NameKey(task.projectName);
+ ProjectState state = projectCache.get(nameKey);
+ if (state == null || !state.statePermitsRead()) {
visible = false;
+ } else {
+ try {
+ permissionBackend.user(user).project(nameKey).check(ProjectPermission.ACCESS);
+ visible = true;
+ } catch (AuthException e) {
+ visible = false;
+ }
}
visibilityCache.put(task.projectName, visible);
}
diff --git a/java/com/google/gerrit/server/restapi/config/TasksCollection.java b/java/com/google/gerrit/server/restapi/config/TasksCollection.java
index f5b6e56..dda54a0 100644
--- a/java/com/google/gerrit/server/restapi/config/TasksCollection.java
+++ b/java/com/google/gerrit/server/restapi/config/TasksCollection.java
@@ -18,8 +18,10 @@
import com.google.gerrit.extensions.restapi.AuthException;
import com.google.gerrit.extensions.restapi.ChildCollection;
import com.google.gerrit.extensions.restapi.IdString;
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
import com.google.gerrit.extensions.restapi.RestView;
+import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.config.ConfigResource;
import com.google.gerrit.server.config.TaskResource;
@@ -30,6 +32,8 @@
import com.google.gerrit.server.permissions.PermissionBackend;
import com.google.gerrit.server.permissions.PermissionBackendException;
import com.google.gerrit.server.permissions.ProjectPermission;
+import com.google.gerrit.server.project.ProjectCache;
+import com.google.gerrit.server.project.ProjectState;
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.Singleton;
@@ -41,6 +45,7 @@
private final WorkQueue workQueue;
private final Provider<CurrentUser> self;
private final PermissionBackend permissionBackend;
+ private final ProjectCache projectCache;
@Inject
TasksCollection(
@@ -48,12 +53,14 @@
ListTasks list,
WorkQueue workQueue,
Provider<CurrentUser> self,
- PermissionBackend permissionBackend) {
+ PermissionBackend permissionBackend,
+ ProjectCache projectCache) {
this.views = views;
this.list = list;
this.workQueue = workQueue;
this.self = self;
this.permissionBackend = permissionBackend;
+ this.projectCache = projectCache;
}
@Override
@@ -63,7 +70,8 @@
@Override
public TaskResource parse(ConfigResource parent, IdString id)
- throws ResourceNotFoundException, AuthException, PermissionBackendException {
+ throws ResourceNotFoundException, AuthException, PermissionBackendException,
+ ResourceConflictException {
CurrentUser user = self.get();
if (!user.isIdentifiedUser()) {
throw new AuthException("Authentication required");
@@ -78,11 +86,16 @@
Task<?> task = workQueue.getTask(taskId);
if (task instanceof ProjectTask) {
+ Project.NameKey nameKey = ((ProjectTask<?>) task).getProjectNameKey();
+ ProjectState state = projectCache.get(nameKey);
+ if (state == null) {
+ throw new ResourceNotFoundException(String.format("project %s not found", nameKey));
+ }
+
+ state.checkStatePermitsRead();
+
try {
- permissionBackend
- .user(user)
- .project(((ProjectTask<?>) task).getProjectNameKey())
- .check(ProjectPermission.ACCESS);
+ permissionBackend.user(user).project(nameKey).check(ProjectPermission.ACCESS);
return new TaskResource(task);
} catch (AuthException e) {
// Fall through and try view queue permission.
diff --git a/java/com/google/gerrit/server/restapi/project/ListChildProjects.java b/java/com/google/gerrit/server/restapi/project/ListChildProjects.java
index 7db4f84..3067c89 100644
--- a/java/com/google/gerrit/server/restapi/project/ListChildProjects.java
+++ b/java/com/google/gerrit/server/restapi/project/ListChildProjects.java
@@ -80,7 +80,9 @@
Map<Project.NameKey, Project> children = new HashMap<>();
for (Project.NameKey name : projectCache.all()) {
ProjectState c = projectCache.get(name);
- if (c != null && parent.equals(c.getProject().getParent(allProjects))) {
+ if (c != null
+ && parent.equals(c.getProject().getParent(allProjects))
+ && c.statePermitsRead()) {
children.put(c.getNameKey(), c.getProject());
}
}
diff --git a/java/com/google/gerrit/server/restapi/project/ListDashboards.java b/java/com/google/gerrit/server/restapi/project/ListDashboards.java
index 08b4069..882e922 100644
--- a/java/com/google/gerrit/server/restapi/project/ListDashboards.java
+++ b/java/com/google/gerrit/server/restapi/project/ListDashboards.java
@@ -88,8 +88,11 @@
private Collection<ProjectState> tree(ProjectResource rsrc) throws PermissionBackendException {
Map<Project.NameKey, ProjectState> tree = new LinkedHashMap<>();
for (ProjectState ps : rsrc.getProjectState().tree()) {
- tree.put(ps.getNameKey(), ps);
+ if (ps.statePermitsRead()) {
+ tree.put(ps.getNameKey(), ps);
+ }
}
+
tree.keySet()
.retainAll(permissionBackend.currentUser().filter(ProjectPermission.ACCESS, tree.keySet()));
return tree.values();
diff --git a/java/com/google/gerrit/server/restapi/project/ListProjects.java b/java/com/google/gerrit/server/restapi/project/ListProjects.java
index a1572c6..9a8232e 100644
--- a/java/com/google/gerrit/server/restapi/project/ListProjects.java
+++ b/java/com/google/gerrit/server/restapi/project/ListProjects.java
@@ -14,6 +14,7 @@
package com.google.gerrit.server.restapi.project;
+import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.gerrit.extensions.client.ProjectState.HIDDEN;
import static java.nio.charset.StandardCharsets.UTF_8;
import static java.util.stream.Collectors.toList;
@@ -521,11 +522,28 @@
if (type == FilterType.PARENT_CANDIDATES) {
matches = parentsOf(matches);
}
- // TODO(dborowitz): Streamified PermissionBackend#filter.
- return perm.filter(ProjectPermission.ACCESS, matches.collect(toList()))
- .stream()
- .sorted()
- .collect(toList());
+
+ List<Project.NameKey> results = new ArrayList<>();
+ List<Project.NameKey> projectNameKeys = matches.sorted().collect(toList());
+ for (Project.NameKey nameKey : projectNameKeys) {
+ ProjectState state = projectCache.get(nameKey);
+ checkNotNull(state, "Failed to load project %s", nameKey);
+
+ // Hidden projects(permitsRead = false) should only be accessible by the project owners.
+ // READ_CONFIG is checked here because it's only allowed to project owners(ACCESS may also
+ // be allowed for other users). Allowing project owners to access here will help them to view
+ // and update the config of hidden projects easily.
+ ProjectPermission permissionToCheck =
+ state.statePermitsRead() ? ProjectPermission.ACCESS : ProjectPermission.READ_CONFIG;
+ try {
+ perm.project(nameKey).check(permissionToCheck);
+ results.add(nameKey);
+ } catch (AuthException e) {
+ // Not added to results.
+ }
+ }
+
+ return results;
}
private Stream<Project.NameKey> parentsOf(Stream<Project.NameKey> matches) {
@@ -551,13 +569,19 @@
}
private boolean isParentAccessible(
- Map<Project.NameKey, Boolean> checked, PermissionBackend.WithUser perm, ProjectState p)
+ Map<Project.NameKey, Boolean> checked, PermissionBackend.WithUser perm, ProjectState state)
throws PermissionBackendException {
- Project.NameKey name = p.getNameKey();
+ Project.NameKey name = state.getNameKey();
Boolean b = checked.get(name);
if (b == null) {
try {
- perm.project(name).check(ProjectPermission.ACCESS);
+ // Hidden projects(permitsRead = false) should only be accessible by the project owners.
+ // READ_CONFIG is checked here because it's only allowed to project owners(ACCESS may also
+ // be allowed for other users). Allowing project owners to access here will help them to view
+ // and update the config of hidden projects easily.
+ ProjectPermission permissionToCheck =
+ state.statePermitsRead() ? ProjectPermission.ACCESS : ProjectPermission.READ_CONFIG;
+ perm.project(name).check(permissionToCheck);
b = true;
} catch (AuthException denied) {
b = false;
diff --git a/java/com/google/gerrit/server/restapi/project/ProjectsCollection.java b/java/com/google/gerrit/server/restapi/project/ProjectsCollection.java
index c5fb41e..3af8424 100644
--- a/java/com/google/gerrit/server/restapi/project/ProjectsCollection.java
+++ b/java/com/google/gerrit/server/restapi/project/ProjectsCollection.java
@@ -150,8 +150,14 @@
}
if (checkAccess) {
+ // Hidden projects(permitsRead = false) should only be accessible by the project owners.
+ // READ_CONFIG is checked here because it's only allowed to project owners(ACCESS may also
+ // be allowed for other users). Allowing project owners to access here will help them to view
+ // and update the config of hidden projects easily.
+ ProjectPermission permissionToCheck =
+ state.statePermitsRead() ? ProjectPermission.ACCESS : ProjectPermission.READ_CONFIG;
try {
- permissionBackend.currentUser().project(nameKey).check(ProjectPermission.ACCESS);
+ permissionBackend.currentUser().project(nameKey).check(permissionToCheck);
} catch (AuthException e) {
return null; // Pretend like not found on access denied.
}
diff --git a/java/com/google/gerrit/sshd/commands/KillCommand.java b/java/com/google/gerrit/sshd/commands/KillCommand.java
index a7e751a..ef12f5f 100644
--- a/java/com/google/gerrit/sshd/commands/KillCommand.java
+++ b/java/com/google/gerrit/sshd/commands/KillCommand.java
@@ -20,6 +20,7 @@
import com.google.gerrit.extensions.annotations.RequiresAnyCapability;
import com.google.gerrit.extensions.restapi.AuthException;
import com.google.gerrit.extensions.restapi.IdString;
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
import com.google.gerrit.server.config.ConfigResource;
import com.google.gerrit.server.config.TaskResource;
@@ -51,7 +52,10 @@
try {
TaskResource taskRsrc = tasksCollection.parse(cfgRsrc, IdString.fromDecoded(id));
deleteTask.apply(taskRsrc, null);
- } catch (AuthException | ResourceNotFoundException | PermissionBackendException e) {
+ } catch (AuthException
+ | ResourceNotFoundException
+ | ResourceConflictException
+ | PermissionBackendException e) {
stderr.print("kill: " + id + ": No such task\n");
}
}
diff --git a/javatests/com/google/gerrit/acceptance/rest/account/ExternalIdIT.java b/javatests/com/google/gerrit/acceptance/rest/account/ExternalIdIT.java
index 7d60b8d..95b64e0 100644
--- a/javatests/com/google/gerrit/acceptance/rest/account/ExternalIdIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/account/ExternalIdIT.java
@@ -15,6 +15,7 @@
package com.google.gerrit.acceptance.rest.account;
import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth8.assertThat;
import static com.google.gerrit.acceptance.GitUtil.fetch;
import static com.google.gerrit.acceptance.GitUtil.pushHead;
import static com.google.gerrit.server.account.externalids.ExternalId.SCHEME_MAILTO;
@@ -31,6 +32,7 @@
import com.google.common.collect.Iterables;
import com.google.gerrit.acceptance.AbstractDaemonTest;
import com.google.gerrit.acceptance.RestResponse;
+import com.google.gerrit.acceptance.TestAccount;
import com.google.gerrit.common.data.GlobalCapability;
import com.google.gerrit.common.data.Permission;
import com.google.gerrit.extensions.api.config.ConsistencyCheckInfo;
@@ -74,6 +76,7 @@
import org.eclipse.jgit.lib.RefUpdate;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.notes.NoteMap;
+import org.eclipse.jgit.revwalk.FooterLine;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevWalk;
import org.eclipse.jgit.transport.PushResult;
@@ -779,6 +782,130 @@
assertThat(externalIds.byAccount(admin.id)).containsExactlyElementsIn(expectedExternalIds);
}
+ @Test
+ public void unsetEmail() throws Exception {
+ ExternalId extId = ExternalId.createWithEmail("x", "1", user.id, "x@example.com");
+ insertExtId(extId);
+
+ ExternalId extIdWithoutEmail = ExternalId.create("x", "1", user.id);
+ try (Repository allUsersRepo = repoManager.openRepository(allUsers);
+ MetaDataUpdate md = metaDataUpdateFactory.create(allUsers)) {
+ ExternalIdNotes extIdNotes = externalIdNotesFactory.load(allUsersRepo);
+ extIdNotes.upsert(extIdWithoutEmail);
+ extIdNotes.commit(md);
+
+ assertThat(extIdNotes.get(extId.key())).hasValue(extIdWithoutEmail);
+ }
+ }
+
+ @Test
+ public void unsetHttpPassword() throws Exception {
+ ExternalId extId =
+ ExternalId.createWithPassword(ExternalId.Key.create("y", "1"), user.id, null, "secret");
+ insertExtId(extId);
+
+ ExternalId extIdWithoutPassword = ExternalId.create("y", "1", user.id);
+ try (Repository allUsersRepo = repoManager.openRepository(allUsers);
+ MetaDataUpdate md = metaDataUpdateFactory.create(allUsers)) {
+ ExternalIdNotes extIdNotes = externalIdNotesFactory.load(allUsersRepo);
+ extIdNotes.upsert(extIdWithoutPassword);
+ extIdNotes.commit(md);
+
+ assertThat(extIdNotes.get(extId.key())).hasValue(extIdWithoutPassword);
+ }
+ }
+
+ @Test
+ public void footers() throws Exception {
+ // Insert external ID for different accounts
+ TestAccount user1 = accountCreator.create("user1");
+ TestAccount user2 = accountCreator.create("user2");
+ ExternalId extId1 = ExternalId.create("foo", "1", user1.id);
+ ExternalId extId2 = ExternalId.create("foo", "2", user1.id);
+ ExternalId extId3 = ExternalId.create("foo", "3", user2.id);
+ try (Repository allUsersRepo = repoManager.openRepository(allUsers);
+ MetaDataUpdate md = metaDataUpdateFactory.create(allUsers)) {
+ ExternalIdNotes extIdNotes = externalIdNotesFactory.load(allUsersRepo);
+ extIdNotes.insert(ImmutableSet.of(extId1, extId2, extId3));
+ RevCommit c = extIdNotes.commit(md);
+ assertThat(getFooters(c))
+ .containsExactly("Account: " + user1.getId(), "Account: " + user2.getId())
+ .inOrder();
+ }
+
+ // Insert external ID with different emails
+ ExternalId extId4 = ExternalId.createWithEmail("foo", "4", user1.id, "foo4@example.com");
+ ExternalId extId5 = ExternalId.createWithEmail("foo", "5", user2.id, "foo5@example.com");
+ try (Repository allUsersRepo = repoManager.openRepository(allUsers);
+ MetaDataUpdate md = metaDataUpdateFactory.create(allUsers)) {
+ ExternalIdNotes extIdNotes = externalIdNotesFactory.load(allUsersRepo);
+ extIdNotes.insert(ImmutableSet.of(extId4, extId5));
+ RevCommit c = extIdNotes.commit(md);
+ assertThat(getFooters(c))
+ .containsExactly(
+ "Account: " + user1.getId(),
+ "Account: " + user2.getId(),
+ "Email: foo4@example.com",
+ "Email: foo5@example.com")
+ .inOrder();
+ }
+
+ // Update external ID - Add Email
+ ExternalId extId1a = ExternalId.createWithEmail("foo", "1", user1.id, "foo1@example.com");
+ try (Repository allUsersRepo = repoManager.openRepository(allUsers);
+ MetaDataUpdate md = metaDataUpdateFactory.create(allUsers)) {
+ ExternalIdNotes extIdNotes = externalIdNotesFactory.load(allUsersRepo);
+ extIdNotes.upsert(extId1a);
+ RevCommit c = extIdNotes.commit(md);
+ assertThat(getFooters(c))
+ .containsExactly("Account: " + user1.getId(), "Email: foo1@example.com")
+ .inOrder();
+ }
+
+ // Update external ID - Remove Email
+ try (Repository allUsersRepo = repoManager.openRepository(allUsers);
+ MetaDataUpdate md = metaDataUpdateFactory.create(allUsers)) {
+ ExternalIdNotes extIdNotes = externalIdNotesFactory.load(allUsersRepo);
+ extIdNotes.upsert(extId1);
+ RevCommit c = extIdNotes.commit(md);
+ assertThat(getFooters(c))
+ .containsExactly("Account: " + user1.getId(), "Email: foo1@example.com")
+ .inOrder();
+ }
+
+ // Delete external IDs
+ try (Repository allUsersRepo = repoManager.openRepository(allUsers);
+ MetaDataUpdate md = metaDataUpdateFactory.create(allUsers)) {
+ ExternalIdNotes extIdNotes = externalIdNotesFactory.load(allUsersRepo);
+ extIdNotes.delete(ImmutableSet.of(extId1, extId5));
+ RevCommit c = extIdNotes.commit(md);
+ assertThat(getFooters(c))
+ .containsExactly(
+ "Account: " + user1.getId(), "Account: " + user2.getId(), "Email: foo5@example.com")
+ .inOrder();
+ }
+
+ // Delete external ID by key without email
+ try (Repository allUsersRepo = repoManager.openRepository(allUsers);
+ MetaDataUpdate md = metaDataUpdateFactory.create(allUsers)) {
+ ExternalIdNotes extIdNotes = externalIdNotesFactory.load(allUsersRepo);
+ extIdNotes.delete(extId2.accountId(), extId2.key());
+ RevCommit c = extIdNotes.commit(md);
+ assertThat(getFooters(c)).containsExactly("Account: " + user1.getId()).inOrder();
+ }
+
+ // Delete external ID by key with email
+ try (Repository allUsersRepo = repoManager.openRepository(allUsers);
+ MetaDataUpdate md = metaDataUpdateFactory.create(allUsers)) {
+ ExternalIdNotes extIdNotes = externalIdNotesFactory.load(allUsersRepo);
+ extIdNotes.delete(extId4.accountId(), extId4.key());
+ RevCommit c = extIdNotes.commit(md);
+ assertThat(getFooters(c))
+ .containsExactly("Account: " + user1.getId(), "Email: foo4@example.com")
+ .inOrder();
+ }
+ }
+
private void insertExtId(ExternalId extId) throws Exception {
accountsUpdateProvider
.get()
@@ -823,6 +950,10 @@
}
}
+ private List<String> getFooters(RevCommit c) {
+ return c.getFooterLines().stream().map(FooterLine::toString).collect(toList());
+ }
+
private List<AccountExternalIdInfo> toExternalIdInfos(Collection<ExternalId> extIds) {
return extIds.stream().map(this::toExternalIdInfo).collect(toList());
}
diff --git a/plugins/replication b/plugins/replication
index dc1e7ff..9b08f32 160000
--- a/plugins/replication
+++ b/plugins/replication
@@ -1 +1 @@
-Subproject commit dc1e7ff434fd7fce90dd6f6c4e312280bd884bd0
+Subproject commit 9b08f324462e62872ac071175e16e7553ba76e2b
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog.html b/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog.html
index 8e179cc..5d69499 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog.html
+++ b/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog.html
@@ -105,6 +105,7 @@
<gr-autocomplete
id="parentInput"
query="[[_query]]"
+ no-debounce
text="{{_inputText}}"
on-tap="_handleEnterChangeNumberTap"
on-commit="_handleBaseSelected"
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog_test.html b/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog_test.html
index 24815db..b92fe1d 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog_test.html
+++ b/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog_test.html
@@ -166,7 +166,8 @@
test('input text change triggers function', () => {
sandbox.spy(element, '_getRecentChanges');
- element._inputText = '1';
+ element.$.parentInput.noDebounce = true;
+ element.$.parentInput.text = '1';
assert.isTrue(element._getRecentChanges.calledOnce);
element._inputText = '12';
assert.isTrue(element._getRecentChanges.calledTwice);
diff --git a/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar.html b/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar.html
index 169fd85..fdd730d 100644
--- a/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar.html
+++ b/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar.html
@@ -48,7 +48,6 @@
id="searchInput"
text="{{_inputVal}}"
query="[[query]]"
- debounce-wait="200"
on-commit="_handleInputCommit"
allow-non-suggested-values
multi
diff --git a/polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls_test.html b/polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls_test.html
index 9d85d37..c67a2af 100644
--- a/polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls_test.html
+++ b/polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls_test.html
@@ -61,12 +61,14 @@
suite('edit button CUJ', () => {
let navStubs;
+ let openAutoCcmplete;
setup(() => {
navStubs = [
sandbox.stub(Gerrit.Nav, 'getEditUrlForDiff'),
sandbox.stub(Gerrit.Nav, 'navigateToRelativeUrl'),
];
+ openAutoCcmplete = element.$.openDialog.querySelector('gr-autocomplete');
});
test('_isValidPath', () => {
@@ -84,8 +86,8 @@
assert.isTrue(element._hideAllDialogs.called);
assert.isTrue(element.$.openDialog.disabled);
assert.isFalse(queryStub.called);
- element.$.openDialog.querySelector('gr-autocomplete').text =
- 'src/test.cpp';
+ openAutoCcmplete.noDebounce = true;
+ openAutoCcmplete.text = 'src/test.cpp';
assert.isTrue(queryStub.called);
assert.isFalse(element.$.openDialog.disabled);
MockInteractions.tap(element.$.openDialog.$$('gr-button[primary]'));
@@ -100,8 +102,8 @@
MockInteractions.tap(element.$$('#open'));
return showDialogSpy.lastCall.returnValue.then(() => {
assert.isTrue(element.$.openDialog.disabled);
- element.$.openDialog.querySelector('gr-autocomplete').text =
- 'src/test.cpp';
+ openAutoCcmplete.noDebounce = true;
+ openAutoCcmplete.text = 'src/test.cpp';
assert.isFalse(element.$.openDialog.disabled);
MockInteractions.tap(element.$.openDialog.$$('gr-button'));
for (const stub of navStubs) { assert.isFalse(stub.called); }
@@ -114,10 +116,13 @@
suite('delete button CUJ', () => {
let navStub;
let deleteStub;
+ let deleteAutocomplete;
setup(() => {
navStub = sandbox.stub(Gerrit.Nav, 'navigateToChange');
deleteStub = sandbox.stub(element.$.restAPI, 'deleteFileInChangeEdit');
+ deleteAutocomplete =
+ element.$.deleteDialog.querySelector('gr-autocomplete');
});
test('delete', () => {
@@ -126,8 +131,8 @@
return showDialogSpy.lastCall.returnValue.then(() => {
assert.isTrue(element.$.deleteDialog.disabled);
assert.isFalse(queryStub.called);
- element.$.deleteDialog.querySelector('gr-autocomplete').text =
- 'src/test.cpp';
+ deleteAutocomplete.noDebounce = true;
+ deleteAutocomplete.text = 'src/test.cpp';
assert.isTrue(queryStub.called);
assert.isFalse(element.$.deleteDialog.disabled);
MockInteractions.tap(element.$.deleteDialog.$$('gr-button[primary]'));
@@ -149,8 +154,8 @@
return showDialogSpy.lastCall.returnValue.then(() => {
assert.isTrue(element.$.deleteDialog.disabled);
assert.isFalse(queryStub.called);
- element.$.deleteDialog.querySelector('gr-autocomplete').text =
- 'src/test.cpp';
+ deleteAutocomplete.noDebounce = true;
+ deleteAutocomplete.text = 'src/test.cpp';
assert.isTrue(queryStub.called);
assert.isFalse(element.$.deleteDialog.disabled);
MockInteractions.tap(element.$.deleteDialog.$$('gr-button[primary]'));
@@ -183,10 +188,13 @@
suite('rename button CUJ', () => {
let navStub;
let renameStub;
+ let renameAutocomplete;
setup(() => {
navStub = sandbox.stub(Gerrit.Nav, 'navigateToChange');
renameStub = sandbox.stub(element.$.restAPI, 'renameFileInChangeEdit');
+ renameAutocomplete =
+ element.$.renameDialog.querySelector('gr-autocomplete');
});
test('rename', () => {
@@ -195,8 +203,8 @@
return showDialogSpy.lastCall.returnValue.then(() => {
assert.isTrue(element.$.renameDialog.disabled);
assert.isFalse(queryStub.called);
- element.$.renameDialog.querySelector('gr-autocomplete').text =
- 'src/test.cpp';
+ renameAutocomplete.noDebounce = true;
+ renameAutocomplete.text = 'src/test.cpp';
assert.isTrue(queryStub.called);
assert.isTrue(element.$.renameDialog.disabled);
@@ -223,8 +231,8 @@
return showDialogSpy.lastCall.returnValue.then(() => {
assert.isTrue(element.$.renameDialog.disabled);
assert.isFalse(queryStub.called);
- element.$.renameDialog.querySelector('gr-autocomplete').text =
- 'src/test.cpp';
+ renameAutocomplete.noDebounce = true;
+ renameAutocomplete.text = 'src/test.cpp';
assert.isTrue(queryStub.called);
assert.isTrue(element.$.renameDialog.disabled);
diff --git a/polygerrit-ui/app/elements/settings/gr-watched-projects-editor/gr-watched-projects-editor.html b/polygerrit-ui/app/elements/settings/gr-watched-projects-editor/gr-watched-projects-editor.html
index 0b8c331..558140f 100644
--- a/polygerrit-ui/app/elements/settings/gr-watched-projects-editor/gr-watched-projects-editor.html
+++ b/polygerrit-ui/app/elements/settings/gr-watched-projects-editor/gr-watched-projects-editor.html
@@ -96,7 +96,6 @@
<th>
<gr-autocomplete
id="newProject"
- debounce-wait="200"
query="[[_query]]"
threshold="1"
placeholder="Project"></gr-autocomplete>
diff --git a/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.js b/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.js
index 5cfd1c0..1eaad4e 100644
--- a/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.js
+++ b/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.js
@@ -18,6 +18,7 @@
'use strict';
const TOKENIZE_REGEX = /(?:[^\s"]+|"[^"]*")+/g;
+ const DEBOUNCE_WAIT_MS = 200;
Polymer({
is: 'gr-autocomplete',
@@ -132,12 +133,11 @@
},
/**
- * The number of milliseconds to use as the debounce wait time. If null,
- * no debouncing is used.
+ * When true, querying for suggestions is not debounced w/r/t keypresses
*/
- debounceWait: {
- type: Number,
- value: null,
+ noDebounce: {
+ type: Boolean,
+ value: false,
},
/** @type {?} */
@@ -167,7 +167,7 @@
observers: [
'_maybeOpenDropdown(_suggestions, _focused)',
- '_updateSuggestions(text, threshold, debounceWait)',
+ '_updateSuggestions(text, threshold, noDebounce)',
],
attached() {
@@ -176,6 +176,7 @@
detached() {
this.unlisten(document.body, 'tap', '_handleBodyTap');
+ this.cancelDebouncer('update-suggestions');
},
get focusStart() {
@@ -219,7 +220,7 @@
_onInputFocus() {
this._focused = true;
- this._updateSuggestions(this.text, this.threshold, this.debounceWait);
+ this._updateSuggestions(this.text, this.threshold, this.noDebounce);
this.$.input.classList.remove('warnUncommitted');
// Needed so that --paper-input-container-input updated style is applied.
this.updateStyles();
@@ -232,7 +233,7 @@
this.updateStyles();
},
- _updateSuggestions(text, threshold, debounceWait) {
+ _updateSuggestions(text, threshold, noDebounce) {
if (this._disableSuggestions) { return; }
if (text === undefined || text.length < threshold) {
this._suggestions = [];
@@ -257,10 +258,10 @@
});
};
- if (debounceWait) {
- this.debounce('update-suggestions', update, debounceWait);
- } else {
+ if (noDebounce) {
update();
+ } else {
+ this.debounce('update-suggestions', update, DEBOUNCE_WAIT_MS);
}
},
diff --git a/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete_test.html b/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete_test.html
index 7b1eed2..872f79f 100644
--- a/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete_test.html
@@ -28,7 +28,7 @@
<test-fixture id="basic">
<template>
- <gr-autocomplete></gr-autocomplete>
+ <gr-autocomplete no-debounce></gr-autocomplete>
</template>
</test-fixture>
@@ -236,7 +236,7 @@
assert.isTrue(queryStub.called);
});
- test('debounceWait debounces the query', () => {
+ test('noDebounce=false debounces the query', () => {
const queryStub = sandbox.spy(() => {
return Promise.resolve([]);
});
@@ -244,11 +244,11 @@
const debounceStub = sandbox.stub(element, 'debounce',
(name, cb) => { callback = cb; });
element.query = queryStub;
- element.debounceWait = 100;
+ element.noDebounce = false;
element.text = 'a';
assert.isFalse(queryStub.called);
assert.isTrue(debounceStub.called);
- assert.equal(debounceStub.lastCall.args[2], 100);
+ assert.equal(debounceStub.lastCall.args[2], 200);
assert.isFunction(callback);
callback();
assert.isTrue(queryStub.called);