Fix infinite loops when walking project hierarchy

Always walk up the tree using a special iterator that knows how
to track visited project names and breaks any cycle, ensuring
All-Projects is always reached.

Change-Id: Ib6ad9505b3225bfa40ba067c799ce18130eafd29
This commit is contained in:
Shawn Pearce
2013-01-15 03:21:14 +00:00
parent 7cbbed1e7d
commit 740a217fac
10 changed files with 174 additions and 123 deletions

View File

@@ -14,6 +14,7 @@
package com.google.gerrit.httpd.rpc.project;
import com.google.common.collect.Iterables;
import com.google.gerrit.common.data.ProjectDetail;
import com.google.gerrit.httpd.rpc.Handler;
import com.google.gerrit.reviewdb.client.InheritedBoolean;
@@ -78,7 +79,7 @@ class ProjectDetailFactory extends Handler<ProjectDetail> {
useSignedOffBy.setValue(projectState.getProject().getUseSignedOffBy());
useContentMerge.setValue(projectState.getProject().getUseContentMerge());
requireChangeID.setValue(projectState.getProject().getRequireChangeID());
final ProjectState parentState = projectState.getParentState();
ProjectState parentState = Iterables.getFirst(projectState.parents(), null);
if (parentState != null) {
useContributorAgreements.setInheritedValue(parentState
.isUseContributorAgreements());

View File

@@ -375,8 +375,7 @@ public abstract class ChangeEmail extends NotificationEmail {
}
}
ProjectState state = projectState;
while (state != null) {
for (ProjectState state : projectState.tree()) {
for (NotifyConfig nc : state.getConfig().getNotifyConfigs()) {
if (nc.isNotify(type)) {
try {
@@ -389,7 +388,6 @@ public abstract class ChangeEmail extends NotificationEmail {
}
}
}
state = state.getParentState();
}
return matching;

View File

@@ -21,7 +21,6 @@ import com.google.common.base.Objects;
import com.google.common.base.Splitter;
import com.google.common.base.Strings;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import com.google.gerrit.extensions.registration.DynamicMap;
import com.google.gerrit.extensions.restapi.AcceptsCreate;
import com.google.gerrit.extensions.restapi.ChildCollection;
@@ -30,6 +29,7 @@ import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.extensions.restapi.RestModifyView;
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.UrlEncoded;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.util.Url;
@@ -49,7 +49,6 @@ import org.eclipse.jgit.lib.Repository;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.util.List;
import java.util.Set;
class DashboardsCollection implements
ChildCollection<ProjectResource, DashboardResource>,
@@ -99,13 +98,12 @@ class DashboardsCollection implements
throw new ResourceNotFoundException(id);
}
CurrentUser user = myCtl.getCurrentUser();
String ref = Url.decode(parts.get(0));
String path = Url.decode(parts.get(1));
ProjectControl ctl = myCtl;
Set<Project.NameKey> seen = Sets.newHashSet(ctl.getProject().getNameKey());
for (;;) {
for (ProjectState ps : myCtl.getProjectState().tree()) {
try {
return parse(ctl, ref, path, myCtl);
return parse(ps.controlFor(user), ref, path, myCtl);
} catch (AmbiguousObjectException e) {
throw new ResourceNotFoundException(id);
} catch (IncorrectObjectTypeException e) {
@@ -113,14 +111,10 @@ class DashboardsCollection implements
} catch (ConfigInvalidException e) {
throw new ResourceNotFoundException(id);
} catch (ResourceNotFoundException e) {
ProjectState ps = ctl.getProjectState().getParentState();
if (ps != null && seen.add(ps.getProject().getNameKey())) {
ctl = ps.controlFor(ctl.getCurrentUser());
continue;
}
throw new ResourceNotFoundException(id);
}
}
throw new ResourceNotFoundException(id);
}
private DashboardResource parse(ProjectControl ctl, String ref, String path,

View File

@@ -17,10 +17,8 @@ package com.google.gerrit.server.project;
import static com.google.gerrit.server.git.GitRepositoryManager.REFS_DASHBOARDS;
import com.google.common.base.Strings;
import com.google.common.collect.Sets;
import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
import com.google.gerrit.extensions.restapi.RestReadView;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.server.project.DashboardsCollection.DashboardInfo;
import com.google.inject.Inject;
@@ -28,7 +26,6 @@ import org.eclipse.jgit.errors.ConfigInvalidException;
import org.kohsuke.args4j.Option;
import java.io.IOException;
import java.util.Set;
class GetDashboard implements RestReadView<DashboardResource> {
private final DashboardsCollection dashboards;
@@ -78,10 +75,7 @@ class GetDashboard implements RestReadView<DashboardResource> {
throw new ResourceNotFoundException();
}
Set<Project.NameKey> seen = Sets.newHashSet();
seen.add(ctl.getProject().getNameKey());
ProjectState ps = ctl.getProjectState().getParentState();
while (ps != null && seen.add(ps.getProject().getNameKey())) {
for (ProjectState ps : ctl.getProjectState().tree()) {
id = ps.getProject().getDefaultDashboard();
if ("default".equals(id)) {
throw new ResourceNotFoundException();
@@ -89,7 +83,6 @@ class GetDashboard implements RestReadView<DashboardResource> {
ctl = ps.controlFor(ctl.getCurrentUser());
return dashboards.parse(new ProjectResource(ctl), id);
}
ps = ps.getParentState();
}
throw new ResourceNotFoundException();
}

View File

@@ -17,7 +17,6 @@ package com.google.gerrit.server.project;
import static com.google.gerrit.server.git.GitRepositoryManager.REFS_DASHBOARDS;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
import com.google.gerrit.extensions.restapi.RestReadView;
import com.google.gerrit.reviewdb.client.Project;
@@ -39,7 +38,6 @@ import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.util.List;
import java.util.Set;
class ListDashboards implements RestReadView<ProjectResource> {
private static final Logger log = LoggerFactory.getLogger(DashboardsCollection.class);
@@ -64,9 +62,9 @@ class ListDashboards implements RestReadView<ProjectResource> {
}
List<List<DashboardInfo>> all = Lists.newArrayList();
Set<Project.NameKey> seen = Sets.newHashSet();
boolean setDefault = true;
for (;;) {
for (ProjectState ps : ctl.getProjectState().tree()) {
ctl = ps.controlFor(ctl.getCurrentUser());
if (ctl.isVisible()) {
List<DashboardInfo> list = scan(ctl, project, setDefault);
for (DashboardInfo d : list) {
@@ -78,12 +76,6 @@ class ListDashboards implements RestReadView<ProjectResource> {
all.add(list);
}
}
ProjectState ps = ctl.getProjectState().getParentState();
if (ps == null || !seen.add(ps.getProject().getNameKey())) {
break;
}
ctl = ps.controlFor(ctl.getCurrentUser());
}
return all;
}

View File

@@ -239,7 +239,7 @@ public class ListProjects implements RestReadView<TopLevelResource> {
ProjectInfo info = new ProjectInfo();
if (type == FilterType.PARENT_CANDIDATES) {
ProjectState parentState = e.getParentState();
ProjectState parentState = Iterables.getFirst(e.parents(), null);
if (parentState != null
&& !output.keySet().contains(parentState.getProject().getName())
&& !rejected.contains(parentState.getProject().getName())) {
@@ -272,7 +272,7 @@ public class ListProjects implements RestReadView<TopLevelResource> {
info.setName(projectName.get());
if (showTree && format.isJson()) {
ProjectState parent = e.getParentState();
ProjectState parent = Iterables.getFirst(e.parents(), null);
if (parent != null) {
ProjectControl parentCtrl = parent.controlFor(currentUser);
if (parentCtrl.isVisible() || parentCtrl.isOwner()) {

View File

@@ -0,0 +1,107 @@
// Copyright (C) 2013 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.google.gerrit.server.project;
import com.google.common.base.Joiner;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.server.config.AllProjectsName;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Iterator;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.Set;
/**
* Iterates from a project up through its parents to All-Projects.
* <p>
* If a cycle is detected the cycle is broken and All-Projects is visited.
*/
class ProjectHierarchyIterator implements Iterator<ProjectState> {
private static final Logger log = LoggerFactory.getLogger(ProjectHierarchyIterator.class);
private final ProjectCache cache;
private final AllProjectsName allProjectsName;
private final Set<Project.NameKey> seen;
private ProjectState next;
ProjectHierarchyIterator(ProjectCache c,
AllProjectsName all,
ProjectState firstResult) {
cache = c;
allProjectsName = all;
seen = Sets.newLinkedHashSet();
seen.add(firstResult.getProject().getNameKey());
next = firstResult;
}
@Override
public boolean hasNext() {
return next != null;
}
@Override
public ProjectState next() {
ProjectState n = next;
if (n == null) {
throw new NoSuchElementException();
}
next = computeNext(n);
return n;
}
private ProjectState computeNext(ProjectState n) {
Project.NameKey parentName = n.getProject().getParent();
if (parentName != null && visit(parentName)) {
ProjectState p = cache.get(parentName);
if (p != null) {
return p;
}
}
// Parent does not exist or was already visited.
// Fall back to visit All-Projects exactly once.
if (seen.add(allProjectsName)) {
return cache.get(allProjectsName);
}
return null;
}
private boolean visit(Project.NameKey parentName) {
if (seen.add(parentName)) {
return true;
}
List<String> order = Lists.newArrayListWithCapacity(seen.size() + 1);
for (Project.NameKey p : seen) {
order.add(p.get());
}
int idx = order.lastIndexOf(parentName.get());
order.add(parentName.get());
log.warn("Cycle detected in projects: "
+ Joiner.on(" -> ").join(order.subList(idx, order.size())));
return false;
}
@Override
public void remove() {
throw new UnsupportedOperationException();
}
}

View File

@@ -15,8 +15,9 @@
package com.google.gerrit.server.project;
import com.google.common.base.Function;
import com.google.common.base.Predicate;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import com.google.gerrit.common.data.AccessSection;
import com.google.gerrit.common.data.GroupReference;
import com.google.gerrit.common.data.Permission;
@@ -47,6 +48,7 @@ import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
@@ -230,23 +232,9 @@ public class ProjectState {
return getLocalAccessSections();
}
List<SectionMatcher> all = new ArrayList<SectionMatcher>();
Set<Project.NameKey> seen = new HashSet<Project.NameKey>();
ProjectState allProjects = projectCache.getAllProjects();
seen.add(getProject().getNameKey());
ProjectState s = this;
do {
List<SectionMatcher> all = Lists.newArrayList();
for (ProjectState s : tree()) {
all.addAll(s.getLocalAccessSections());
Project.NameKey parent = s.getProject().getParent();
if (parent == null || !seen.add(parent)) {
break;
}
s = projectCache.get(parent);
} while (s != null);
if (seen.add(allProjects.getProject().getNameKey())) {
all.addAll(allProjects.getLocalAccessSections());
}
return all;
}
@@ -258,16 +246,11 @@ public class ProjectState {
* that has local owners are returned
*/
public Set<AccountGroup.UUID> getOwners() {
Project.NameKey parentName = getProject().getParent();
if (!localOwners.isEmpty() || parentName == null || isAllProjects) {
return localOwners;
for (ProjectState p : tree()) {
if (!p.localOwners.isEmpty()) {
return p.localOwners;
}
ProjectState parent = projectCache.get(parentName);
if (parent != null) {
return parent.getOwners();
}
return Collections.emptySet();
}
@@ -275,23 +258,13 @@ public class ProjectState {
* @return true if any of the groups listed in {@code groups} was declared to
* be an owner of this project, or one of its parent projects..
*/
boolean isOwner(GroupMembership groups) {
Set<Project.NameKey> seen = new HashSet<Project.NameKey>();
seen.add(getProject().getNameKey());
ProjectState s = this;
do {
if (groups.containsAnyOf(s.localOwners)) {
return true;
boolean isOwner(final GroupMembership groups) {
return Iterables.any(tree(), new Predicate<ProjectState>() {
@Override
public boolean apply(ProjectState in) {
return groups.containsAnyOf(in.localOwners);
}
Project.NameKey parent = s.getProject().getParent();
if (parent == null || !seen.add(parent)) {
break;
}
s = projectCache.get(parent);
} while (s != null);
return false;
});
}
public ProjectControl controlFor(final CurrentUser user) {
@@ -299,15 +272,28 @@ public class ProjectState {
}
/**
* @return ProjectState of project's parent. If the project does not have a
* parent, return state of the top level project, All-Projects. If
* this project is All-Projects, return null.
* @return an iterable that walks through this project and then the parents of
* this project. Starts from this project and progresses up the
* hierarchy to All-Projects.
*/
public ProjectState getParentState() {
if (isAllProjects) {
return null;
public Iterable<ProjectState> tree() {
return new Iterable<ProjectState>() {
@Override
public Iterator<ProjectState> iterator() {
return new ProjectHierarchyIterator(
projectCache, allProjectsName,
ProjectState.this);
}
return projectCache.get(getProject().getParent(allProjectsName));
};
}
/**
* @return an iterable that walks through the parents of this project. Starts
* from the immediate parent of this project and progresses up the
* hierarchy to All-Projects.
*/
public Iterable<ProjectState> parents() {
return Iterables.skip(tree(), 1);
}
public boolean isAllProjects() {
@@ -351,10 +337,7 @@ public class ProjectState {
}
private boolean getInheritableBoolean(Function<Project, InheritableBoolean> func) {
Set<Project.NameKey> seen = Sets.newHashSet();
seen.add(getProject().getNameKey());
ProjectState s = this;
do {
for (ProjectState s : tree()) {
switch (func.apply(s.getProject())) {
case TRUE:
return true;
@@ -362,14 +345,9 @@ public class ProjectState {
return false;
case INHERIT:
default:
Project.NameKey parent = s.getProject().getParent(allProjectsName);
if (parent != null && seen.add(parent)) {
s = projectCache.get(parent);
} else {
s = null;
continue;
}
}
} while (s != null);
return false;
}
}

View File

@@ -16,7 +16,6 @@ package com.google.gerrit.server.project;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.PatchSet;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.rules.PrologEnvironment;
import com.google.gerrit.rules.StoredValues;
@@ -31,9 +30,7 @@ import com.googlecode.prolog_cafe.lang.VariableTerm;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import javax.annotation.Nullable;
@@ -184,16 +181,8 @@ public class SubmitRuleEvaluator {
private Term runSubmitFilters(Term results, PrologEnvironment env) throws RuleEvalException {
ProjectState projectState = projectControl.getProjectState();
ProjectState parentState = projectState.getParentState();
PrologEnvironment childEnv = env;
Set<Project.NameKey> projectsSeen = new HashSet<Project.NameKey>();
projectsSeen.add(projectState.getProject().getNameKey());
while (parentState != null) {
if (!projectsSeen.add(parentState.getProject().getNameKey())) {
// parent has been seen before, stop walk up inheritance tree
break;
}
for (ProjectState parentState : projectState.parents()) {
PrologEnvironment parentEnv;
try {
parentEnv = parentState.newPrologEnvironment();
@@ -223,11 +212,8 @@ public class SubmitRuleEvaluator {
+ " on change " + change.getId() + " of "
+ parentState.getProject().getName(), err);
}
parentState = parentState.getParentState();
childEnv = parentEnv;
}
return results;
}

View File

@@ -14,6 +14,9 @@
package com.google.gerrit.sshd.commands;
import com.google.common.base.Function;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.gerrit.common.data.GlobalCapability;
import com.google.gerrit.extensions.annotations.RequiresCapability;
import com.google.gerrit.reviewdb.client.Project;
@@ -35,6 +38,7 @@ import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
@@ -197,17 +201,15 @@ final class AdminSetParent extends SshCommand {
}
private Set<Project.NameKey> getAllParents(final Project.NameKey projectName) {
final Set<Project.NameKey> parents = new HashSet<Project.NameKey>();
Project.NameKey p = projectName;
while (p != null && parents.add(p)) {
final ProjectState e = projectCache.get(p);
if (e == null) {
// If we can't get it from the cache, pretend it's not present.
break;
ProjectState ps = projectCache.get(projectName);
return ImmutableSet.copyOf(Iterables.transform(
ps != null ? ps.parents() : Collections.<ProjectState> emptySet(),
new Function<ProjectState, Project.NameKey> () {
@Override
public Project.NameKey apply(ProjectState in) {
return in.getProject().getNameKey();
}
p = e.getProject().getParent(allProjectsName);
}
return parents;
}));
}
private List<Project> getChildren(final Project.NameKey parentName) {