blob: f1790454e53068779fef7ef859438e6e232df5fa [file] [log] [blame]
// Copyright (C) 2010 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.permissions;
import static com.google.common.base.Preconditions.checkState;
import static com.google.common.collect.ImmutableSet.toImmutableSet;
import static com.google.common.flogger.LazyArgs.lazy;
import static com.google.gerrit.entities.RefNames.REFS_CONFIG;
import static java.util.stream.Collectors.toCollection;
import com.google.auto.value.AutoValue;
import com.google.common.base.Supplier;
import com.google.common.base.Suppliers;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.flogger.FluentLogger;
import com.google.gerrit.entities.BranchNameKey;
import com.google.gerrit.entities.Change;
import com.google.gerrit.entities.RefNames;
import com.google.gerrit.metrics.Counter0;
import com.google.gerrit.metrics.Description;
import com.google.gerrit.metrics.MetricMaker;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.config.GerritServerConfig;
import com.google.gerrit.server.git.ChangesByProjectCache;
import com.google.gerrit.server.git.TagCache;
import com.google.gerrit.server.git.TagMatcher;
import com.google.gerrit.server.logging.TraceContext;
import com.google.gerrit.server.logging.TraceContext.TraceTimer;
import com.google.gerrit.server.permissions.PermissionBackend.RefFilterOptions;
import com.google.gerrit.server.project.ProjectState;
import com.google.gerrit.server.query.change.ChangeData;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import com.google.inject.assistedinject.Assisted;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
import org.eclipse.jgit.lib.Config;
import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.lib.Repository;
class DefaultRefFilter {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
interface Factory {
DefaultRefFilter create(ProjectControl projectControl);
}
@Singleton
private static class Metrics {
final Counter0 fullFilterCount;
final Counter0 skipFilterCount;
@Inject
Metrics(MetricMaker metricMaker) {
fullFilterCount =
metricMaker.newCounter(
"permissions/ref_filter/full_filter_count",
new Description("Rate of full ref filter operations").setRate());
skipFilterCount =
metricMaker.newCounter(
"permissions/ref_filter/skip_filter_count",
new Description(
"Rate of ref filter operations where we skip full evaluation"
+ " because the user can read all refs")
.setRate());
}
}
private final TagCache tagCache;
private final PermissionBackend permissionBackend;
private final RefVisibilityControl refVisibilityControl;
private final ProjectControl projectControl;
private final CurrentUser user;
private final ProjectState projectState;
private final PermissionBackend.ForProject permissionBackendForProject;
private final ChangesByProjectCache changesByProjectCache;
private final ChangeData.Factory changeDataFactory;
private final Metrics metrics;
private final boolean skipFullRefEvaluationIfAllRefsAreVisible;
@Inject
DefaultRefFilter(
TagCache tagCache,
PermissionBackend permissionBackend,
RefVisibilityControl refVisibilityControl,
@GerritServerConfig Config config,
Metrics metrics,
ChangesByProjectCache changesByProjectCache,
ChangeData.Factory changeDataFactory,
@Assisted ProjectControl projectControl) {
this.tagCache = tagCache;
this.permissionBackend = permissionBackend;
this.refVisibilityControl = refVisibilityControl;
this.changesByProjectCache = changesByProjectCache;
this.changeDataFactory = changeDataFactory;
this.skipFullRefEvaluationIfAllRefsAreVisible =
config.getBoolean("auth", "skipFullRefEvaluationIfAllRefsAreVisible", true);
this.projectControl = projectControl;
this.user = projectControl.getUser();
this.projectState = projectControl.getProjectState();
this.permissionBackendForProject =
permissionBackend.user(user).project(projectState.getNameKey());
this.metrics = metrics;
}
/** Filters given refs and tags by visibility. */
ImmutableList<Ref> filter(Collection<Ref> refs, Repository repo, RefFilterOptions opts)
throws PermissionBackendException {
logger.atFinest().log(
"Filter refs for repository %s by visibility (options = %s, refs = %s)",
projectState.getNameKey(), opts, refs);
logger.atFinest().log("Calling user: %s", user.getLoggableName());
logger.atFinest().log("Groups: %s", lazy(() -> user.getEffectiveGroups().getKnownGroups()));
logger.atFinest().log(
"auth.skipFullRefEvaluationIfAllRefsAreVisible = %s",
skipFullRefEvaluationIfAllRefsAreVisible);
logger.atFinest().log(
"Project state %s permits read = %s",
projectState.getProject().getState(), projectState.statePermitsRead());
// Perform an initial ref filtering with all the refs the caller asked for. If we find tags that
// we have to investigate separately (deferred tags) then perform a reachability check starting
// from all visible branches (refs/heads/*).
Supplier<ImmutableMap<Change.Id, ChangeData>> visibleChanges =
Suppliers.memoize(
() ->
GitVisibleChangeFilter.getVisibleChanges(
changesByProjectCache,
changeDataFactory,
projectState.getNameKey(),
permissionBackendForProject,
repo,
changes(refs)));
Result initialRefFilter = filterRefs(new ArrayList<>(refs), opts, visibleChanges);
ImmutableList.Builder<Ref> visibleRefs = ImmutableList.builder();
visibleRefs.addAll(initialRefFilter.visibleRefs());
if (!initialRefFilter.deferredTags().isEmpty()) {
try (TraceTimer traceTimer = TraceContext.newTimer("Check visibility of deferred tags")) {
Result allVisibleBranches = filterRefs(getTaggableRefs(repo), opts, visibleChanges);
checkState(
allVisibleBranches.deferredTags().isEmpty(),
"unexpected tags found when filtering refs/heads/* "
+ allVisibleBranches.deferredTags());
TagMatcher tags =
tagCache
.get(projectState.getNameKey())
.matcher(tagCache, repo, allVisibleBranches.visibleRefs());
for (Ref tag : initialRefFilter.deferredTags()) {
try {
if (tags.isReachable(tag)) {
logger.atFinest().log("Include reachable tag %s", tag.getName());
visibleRefs.add(tag);
} else {
logger.atFinest().log("Filter out non-reachable tag %s", tag.getName());
}
} catch (IOException e) {
throw new PermissionBackendException(e);
}
}
}
}
ImmutableList<Ref> visibleRefList = visibleRefs.build();
logger.atFinest().log("visible refs = %s", visibleRefList);
return visibleRefList;
}
/**
* Filters refs by visibility. Returns tags where visibility can't be trivially computed
* separately for later rev-walk-based visibility computation. Tags where visibility is trivial to
* compute will be returned as part of {@link Result#visibleRefs()}.
*/
Result filterRefs(
List<Ref> refs,
RefFilterOptions opts,
Supplier<ImmutableMap<Change.Id, ChangeData>> visibleChanges)
throws PermissionBackendException {
logger.atFinest().log("Filter refs (refs = %s)", refs);
if (!projectState.statePermitsRead()) {
return new AutoValue_DefaultRefFilter_Result(ImmutableList.of(), ImmutableList.of());
}
// TODO(hiesel): Remove when optimization is done.
boolean hasReadOnRefsStar =
checkProjectPermission(permissionBackendForProject, ProjectPermission.READ);
logger.atFinest().log("User has READ on refs/* = %s", hasReadOnRefsStar);
if (skipFullRefEvaluationIfAllRefsAreVisible && !projectState.isAllUsers()) {
if (hasReadOnRefsStar) {
metrics.skipFilterCount.increment();
logger.atFinest().log(
"Fast path, all refs are visible because user has READ on refs/*: %s", refs);
return new AutoValue_DefaultRefFilter_Result(
ImmutableList.copyOf(refs), ImmutableList.of());
} else if (projectControl.allRefsAreVisible(ImmutableSet.of(RefNames.REFS_CONFIG))) {
metrics.skipFilterCount.increment();
refs = fastHideRefsMetaConfig(refs);
logger.atFinest().log(
"Fast path, all refs except %s are visible: %s", RefNames.REFS_CONFIG, refs);
return new AutoValue_DefaultRefFilter_Result(
ImmutableList.copyOf(refs), ImmutableList.of());
}
}
logger.atFinest().log("Doing full ref filtering");
metrics.fullFilterCount.increment();
boolean hasAccessDatabase =
permissionBackend
.user(projectControl.getUser())
.testOrFalse(GlobalPermission.ACCESS_DATABASE);
ImmutableList.Builder<Ref> resultRefs = ImmutableList.builderWithExpectedSize(refs.size());
ImmutableList.Builder<Ref> deferredTags = ImmutableList.builder();
for (Ref ref : refs) {
String refName = ref.getName();
Change.Id changeId;
if (opts.filterMeta() && isMetadata(refName)) {
logger.atFinest().log("Filter out metadata ref %s", refName);
} else if (isTag(ref)) {
if (hasReadOnRefsStar) {
// The user has READ on refs/* with no effective block permission. This is the broadest
// permission one can assign. There is no way to grant access to (specific) tags in
// Gerrit,
// so we have to assume that these users can see all tags because there could be tags that
// aren't reachable by any visible ref while the user can see all non-Gerrit refs. This
// matches Gerrit's historic behavior.
// This makes it so that these users could see commits that they can't see otherwise
// (e.g. a private change ref) if a tag was attached to it. Tags are meant to be used on
// the regular Git tree that users interact with, not on any of the Gerrit trees, so this
// is a negligible risk.
logger.atFinest().log("Include tag ref %s because user has read on refs/*", refName);
resultRefs.add(ref);
} else {
// If its a tag, consider it later.
if (ref.getObjectId() != null) {
logger.atFinest().log("Defer tag ref %s", refName);
deferredTags.add(ref);
} else {
logger.atFinest().log("Filter out tag ref %s that is not a tag", refName);
}
}
} else if ((changeId = Change.Id.fromRef(refName)) != null) {
// This is a mere performance optimization. RefVisibilityControl could determine the
// visibility of these refs just fine. But instead, we use highly-optimized logic that
// looks only on the available changes in the change index and cache (which are the
// most recent changes).
if (hasAccessDatabase) {
resultRefs.add(ref);
} else if (!visibleChanges.get().containsKey(changeId)) {
logger.atFinest().log("Filter out invisible change ref %s", refName);
} else if (RefNames.isRefsEdit(refName) && !visibleEdit(refName, visibleChanges.get())) {
logger.atFinest().log("Filter out invisible change edit ref %s", refName);
} else {
// Change is visible
resultRefs.add(ref);
}
} else if (refVisibilityControl.isVisible(projectControl, ref.getLeaf().getName())) {
resultRefs.add(ref);
}
}
Result result = new AutoValue_DefaultRefFilter_Result(resultRefs.build(), deferredTags.build());
logger.atFinest().log("Result of ref filtering = %s", result);
return result;
}
/**
* Returns all refs tag we regard as starting points for reachability computation for tags. In
* general, these are all refs not managed by Gerrit excluding symbolic refs and tags.
*
* <p>We exclude symbolic refs because their target will be included and this will suffice for
* computing reachability.
*/
private static List<Ref> getTaggableRefs(Repository repo) throws PermissionBackendException {
try {
List<Ref> allRefs = repo.getRefDatabase().getRefs();
return allRefs.stream()
.filter(
r ->
!RefNames.isGerritRef(r.getName())
&& !r.getName().startsWith(RefNames.REFS_TAGS)
&& !r.isSymbolic()
&& !r.getName().equals(RefNames.REFS_CONFIG))
.collect(Collectors.toList());
} catch (IOException e) {
throw new PermissionBackendException(e);
}
}
/**
* Returns the number of changes contained in {@code refs}. A change has one meta ref and many
* patch set refs. We count over the meta refs to make sure we get the number of unique changes in
* the provided refs.
*/
private static ImmutableSet<Change.Id> changes(Collection<Ref> refs) {
return refs.stream()
.map(Ref::getName)
.map(Change.Id::fromRef)
.filter(Objects::nonNull)
.collect(toImmutableSet());
}
private List<Ref> fastHideRefsMetaConfig(List<Ref> refs) throws PermissionBackendException {
if (!canReadRef(REFS_CONFIG)) {
return refs.stream()
.filter(r -> !r.getName().equals(REFS_CONFIG))
.collect(toCollection(() -> new ArrayList<>(refs.size())));
}
return refs;
}
private boolean visibleEdit(String name, ImmutableMap<Change.Id, ChangeData> visibleChanges)
throws PermissionBackendException {
Change.Id id = Change.Id.fromEditRefPart(name);
if (id == null) {
logger.atWarning().log("Couldn't extract change ID from edit ref %s", name);
return false;
}
if (user.isIdentifiedUser()
&& name.startsWith(RefNames.refsEditPrefix(user.asIdentifiedUser().getAccountId()))
&& visibleChanges.containsKey(id)) {
logger.atFinest().log("Own change edit ref is visible: %s", name);
return true;
}
if (visibleChanges.containsKey(id)) {
// Default to READ_PRIVATE_CHANGES as there is no special permission for reading edits.
BranchNameKey dest = visibleChanges.get(id).change().getDest();
boolean canRead =
permissionBackendForProject.ref(dest.branch()).test(RefPermission.READ_PRIVATE_CHANGES);
logger.atFinest().log(
"Foreign change edit ref is " + (canRead ? "visible" : "invisible") + ": %s", name);
return canRead;
}
logger.atFinest().log("Change %d of change edit ref %s is not visible", id.get(), name);
return false;
}
private boolean isMetadata(String name) {
boolean isMetaData = RefNames.isRefsChanges(name) || RefNames.isRefsEdit(name);
logger.atFinest().log("ref %s is " + (isMetaData ? "" : "not ") + "a metadata ref", name);
return isMetaData;
}
private static boolean isTag(Ref ref) {
return ref.getLeaf().getName().startsWith(Constants.R_TAGS);
}
private boolean canReadRef(String ref) throws PermissionBackendException {
return permissionBackendForProject.ref(ref).test(RefPermission.READ);
}
private boolean checkProjectPermission(
PermissionBackend.ForProject forProject, ProjectPermission perm)
throws PermissionBackendException {
return forProject.test(perm);
}
@AutoValue
abstract static class Result {
/** Subset of the refs passed into the computation that is visible to the user. */
abstract ImmutableList<Ref> visibleRefs();
/**
* List of tags where we couldn't figure out visibility in the first pass and need to do an
* expensive ref walk.
*/
abstract ImmutableList<Ref> deferredTags();
}
}