LCOV - code coverage report
Current view: top level - server/permissions - DefaultRefFilter.java (source / functions) Hit Total Coverage
Test: _coverage_report.dat Lines: 144 155 92.9 %
Date: 2022-11-19 15:00:39 Functions: 17 18 94.4 %

          Line data    Source code
       1             : // Copyright (C) 2010 The Android Open Source Project
       2             : //
       3             : // Licensed under the Apache License, Version 2.0 (the "License");
       4             : // you may not use this file except in compliance with the License.
       5             : // You may obtain a copy of the License at
       6             : //
       7             : // http://www.apache.org/licenses/LICENSE-2.0
       8             : //
       9             : // Unless required by applicable law or agreed to in writing, software
      10             : // distributed under the License is distributed on an "AS IS" BASIS,
      11             : // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
      12             : // See the License for the specific language governing permissions and
      13             : // limitations under the License.
      14             : 
      15             : package com.google.gerrit.server.permissions;
      16             : 
      17             : import static com.google.common.base.Preconditions.checkState;
      18             : import static com.google.common.collect.ImmutableSet.toImmutableSet;
      19             : import static com.google.common.flogger.LazyArgs.lazy;
      20             : import static com.google.gerrit.entities.RefNames.REFS_CONFIG;
      21             : import static java.util.stream.Collectors.toCollection;
      22             : 
      23             : import com.google.auto.value.AutoValue;
      24             : import com.google.common.base.Supplier;
      25             : import com.google.common.base.Suppliers;
      26             : import com.google.common.collect.ImmutableList;
      27             : import com.google.common.collect.ImmutableMap;
      28             : import com.google.common.collect.ImmutableSet;
      29             : import com.google.common.flogger.FluentLogger;
      30             : import com.google.gerrit.common.Nullable;
      31             : import com.google.gerrit.entities.BranchNameKey;
      32             : import com.google.gerrit.entities.Change;
      33             : import com.google.gerrit.entities.RefNames;
      34             : import com.google.gerrit.metrics.Counter0;
      35             : import com.google.gerrit.metrics.Description;
      36             : import com.google.gerrit.metrics.MetricMaker;
      37             : import com.google.gerrit.server.CurrentUser;
      38             : import com.google.gerrit.server.config.GerritServerConfig;
      39             : import com.google.gerrit.server.git.SearchingChangeCacheImpl;
      40             : import com.google.gerrit.server.git.TagCache;
      41             : import com.google.gerrit.server.git.TagMatcher;
      42             : import com.google.gerrit.server.logging.TraceContext;
      43             : import com.google.gerrit.server.logging.TraceContext.TraceTimer;
      44             : import com.google.gerrit.server.notedb.ChangeNotes;
      45             : import com.google.gerrit.server.permissions.PermissionBackend.RefFilterOptions;
      46             : import com.google.gerrit.server.project.ProjectState;
      47             : import com.google.gerrit.server.query.change.ChangeData;
      48             : import com.google.inject.Inject;
      49             : import com.google.inject.assistedinject.Assisted;
      50             : import java.io.IOException;
      51             : import java.util.ArrayList;
      52             : import java.util.Collection;
      53             : import java.util.List;
      54             : import java.util.Objects;
      55             : import java.util.stream.Collectors;
      56             : import org.eclipse.jgit.lib.Config;
      57             : import org.eclipse.jgit.lib.Constants;
      58             : import org.eclipse.jgit.lib.Ref;
      59             : import org.eclipse.jgit.lib.Repository;
      60             : 
      61             : class DefaultRefFilter {
      62         135 :   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
      63             : 
      64             :   interface Factory {
      65             :     DefaultRefFilter create(ProjectControl projectControl);
      66             :   }
      67             : 
      68             :   private final TagCache tagCache;
      69             :   private final PermissionBackend permissionBackend;
      70             :   private final RefVisibilityControl refVisibilityControl;
      71             :   private final ProjectControl projectControl;
      72             :   private final CurrentUser user;
      73             :   private final ProjectState projectState;
      74             :   private final PermissionBackend.ForProject permissionBackendForProject;
      75             :   private final @Nullable SearchingChangeCacheImpl searchingChangeDataProvider;
      76             :   private final ChangeData.Factory changeDataFactory;
      77             :   private final ChangeNotes.Factory changeNotesFactory;
      78             :   private final Counter0 fullFilterCount;
      79             :   private final Counter0 skipFilterCount;
      80             :   private final boolean skipFullRefEvaluationIfAllRefsAreVisible;
      81             : 
      82             :   @Inject
      83             :   DefaultRefFilter(
      84             :       TagCache tagCache,
      85             :       PermissionBackend permissionBackend,
      86             :       RefVisibilityControl refVisibilityControl,
      87             :       @GerritServerConfig Config config,
      88             :       MetricMaker metricMaker,
      89             :       @Nullable SearchingChangeCacheImpl searchingChangeDataProvider,
      90             :       ChangeData.Factory changeDataFactory,
      91             :       ChangeNotes.Factory changeNotesFactory,
      92         135 :       @Assisted ProjectControl projectControl) {
      93         135 :     this.tagCache = tagCache;
      94         135 :     this.permissionBackend = permissionBackend;
      95         135 :     this.refVisibilityControl = refVisibilityControl;
      96         135 :     this.searchingChangeDataProvider = searchingChangeDataProvider;
      97         135 :     this.changeDataFactory = changeDataFactory;
      98         135 :     this.changeNotesFactory = changeNotesFactory;
      99         135 :     this.skipFullRefEvaluationIfAllRefsAreVisible =
     100         135 :         config.getBoolean("auth", "skipFullRefEvaluationIfAllRefsAreVisible", true);
     101         135 :     this.projectControl = projectControl;
     102             : 
     103         135 :     this.user = projectControl.getUser();
     104         135 :     this.projectState = projectControl.getProjectState();
     105         135 :     this.permissionBackendForProject =
     106         135 :         permissionBackend.user(user).project(projectState.getNameKey());
     107         135 :     this.fullFilterCount =
     108         135 :         metricMaker.newCounter(
     109             :             "permissions/ref_filter/full_filter_count",
     110         135 :             new Description("Rate of full ref filter operations").setRate());
     111         135 :     this.skipFilterCount =
     112         135 :         metricMaker.newCounter(
     113             :             "permissions/ref_filter/skip_filter_count",
     114             :             new Description(
     115             :                     "Rate of ref filter operations where we skip full evaluation"
     116             :                         + " because the user can read all refs")
     117         135 :                 .setRate());
     118         135 :   }
     119             : 
     120             :   /** Filters given refs and tags by visibility. */
     121             :   ImmutableList<Ref> filter(Collection<Ref> refs, Repository repo, RefFilterOptions opts)
     122             :       throws PermissionBackendException {
     123         135 :     logger.atFinest().log(
     124             :         "Filter refs for repository %s by visibility (options = %s, refs = %s)",
     125         135 :         projectState.getNameKey(), opts, refs);
     126         135 :     logger.atFinest().log("Calling user: %s", user.getLoggableName());
     127         135 :     logger.atFinest().log("Groups: %s", lazy(() -> user.getEffectiveGroups().getKnownGroups()));
     128         135 :     logger.atFinest().log(
     129             :         "auth.skipFullRefEvaluationIfAllRefsAreVisible = %s",
     130         135 :         skipFullRefEvaluationIfAllRefsAreVisible);
     131         135 :     logger.atFinest().log(
     132             :         "Project state %s permits read = %s",
     133         135 :         projectState.getProject().getState(), projectState.statePermitsRead());
     134             : 
     135             :     // Perform an initial ref filtering with all the refs the caller asked for. If we find tags that
     136             :     // we have to investigate separately (deferred tags) then perform a reachability check starting
     137             :     // from all visible branches (refs/heads/*).
     138         135 :     Supplier<ImmutableMap<Change.Id, ChangeData>> visibleChanges =
     139         135 :         Suppliers.memoize(
     140             :             () ->
     141          19 :                 GitVisibleChangeFilter.getVisibleChanges(
     142             :                     searchingChangeDataProvider,
     143             :                     changeNotesFactory,
     144             :                     changeDataFactory,
     145          19 :                     projectState.getNameKey(),
     146             :                     permissionBackendForProject,
     147             :                     repo,
     148          19 :                     changes(refs)));
     149         135 :     Result initialRefFilter = filterRefs(new ArrayList<>(refs), opts, visibleChanges);
     150         135 :     ImmutableList.Builder<Ref> visibleRefs = ImmutableList.builder();
     151         135 :     visibleRefs.addAll(initialRefFilter.visibleRefs());
     152         135 :     if (!initialRefFilter.deferredTags().isEmpty()) {
     153           9 :       try (TraceTimer traceTimer = TraceContext.newTimer("Check visibility of deferred tags")) {
     154           9 :         Result allVisibleBranches = filterRefs(getTaggableRefs(repo), opts, visibleChanges);
     155           9 :         checkState(
     156           9 :             allVisibleBranches.deferredTags().isEmpty(),
     157             :             "unexpected tags found when filtering refs/heads/* "
     158           9 :                 + allVisibleBranches.deferredTags());
     159             : 
     160           9 :         TagMatcher tags =
     161             :             tagCache
     162           9 :                 .get(projectState.getNameKey())
     163           9 :                 .matcher(tagCache, repo, allVisibleBranches.visibleRefs());
     164           9 :         for (Ref tag : initialRefFilter.deferredTags()) {
     165             :           try {
     166           9 :             if (tags.isReachable(tag)) {
     167           9 :               logger.atFinest().log("Include reachable tag %s", tag.getName());
     168           9 :               visibleRefs.add(tag);
     169             :             } else {
     170           8 :               logger.atFinest().log("Filter out non-reachable tag %s", tag.getName());
     171             :             }
     172           0 :           } catch (IOException e) {
     173           0 :             throw new PermissionBackendException(e);
     174           9 :           }
     175           9 :         }
     176             :       }
     177             :     }
     178             : 
     179         135 :     ImmutableList<Ref> visibleRefList = visibleRefs.build();
     180         135 :     logger.atFinest().log("visible refs = %s", visibleRefList);
     181         135 :     return visibleRefList;
     182             :   }
     183             : 
     184             :   /**
     185             :    * Filters refs by visibility. Returns tags where visibility can't be trivially computed
     186             :    * separately for later rev-walk-based visibility computation. Tags where visibility is trivial to
     187             :    * compute will be returned as part of {@link Result#visibleRefs()}.
     188             :    */
     189             :   Result filterRefs(
     190             :       List<Ref> refs,
     191             :       RefFilterOptions opts,
     192             :       Supplier<ImmutableMap<Change.Id, ChangeData>> visibleChanges)
     193             :       throws PermissionBackendException {
     194         135 :     logger.atFinest().log("Filter refs (refs = %s)", refs);
     195         135 :     if (!projectState.statePermitsRead()) {
     196           0 :       return new AutoValue_DefaultRefFilter_Result(ImmutableList.of(), ImmutableList.of());
     197             :     }
     198             : 
     199             :     // TODO(hiesel): Remove when optimization is done.
     200         135 :     boolean hasReadOnRefsStar =
     201         135 :         checkProjectPermission(permissionBackendForProject, ProjectPermission.READ);
     202         135 :     logger.atFinest().log("User has READ on refs/* = %s", hasReadOnRefsStar);
     203         135 :     if (skipFullRefEvaluationIfAllRefsAreVisible && !projectState.isAllUsers()) {
     204         134 :       if (hasReadOnRefsStar) {
     205         133 :         skipFilterCount.increment();
     206         133 :         logger.atFinest().log(
     207             :             "Fast path, all refs are visible because user has READ on refs/*: %s", refs);
     208         133 :         return new AutoValue_DefaultRefFilter_Result(
     209         133 :             ImmutableList.copyOf(refs), ImmutableList.of());
     210          31 :       } else if (projectControl.allRefsAreVisible(ImmutableSet.of(RefNames.REFS_CONFIG))) {
     211           7 :         skipFilterCount.increment();
     212           7 :         refs = fastHideRefsMetaConfig(refs);
     213           7 :         logger.atFinest().log(
     214             :             "Fast path, all refs except %s are visible: %s", RefNames.REFS_CONFIG, refs);
     215           7 :         return new AutoValue_DefaultRefFilter_Result(
     216           7 :             ImmutableList.copyOf(refs), ImmutableList.of());
     217             :       }
     218             :     }
     219          36 :     logger.atFinest().log("Doing full ref filtering");
     220          36 :     fullFilterCount.increment();
     221             : 
     222          36 :     boolean hasAccessDatabase =
     223             :         permissionBackend
     224          36 :             .user(projectControl.getUser())
     225          36 :             .testOrFalse(GlobalPermission.ACCESS_DATABASE);
     226          36 :     ImmutableList.Builder<Ref> resultRefs = ImmutableList.builderWithExpectedSize(refs.size());
     227          36 :     ImmutableList.Builder<Ref> deferredTags = ImmutableList.builder();
     228          36 :     for (Ref ref : refs) {
     229          36 :       String refName = ref.getName();
     230             :       Change.Id changeId;
     231          36 :       if (opts.filterMeta() && isMetadata(refName)) {
     232           1 :         logger.atFinest().log("Filter out metadata ref %s", refName);
     233          36 :       } else if (isTag(ref)) {
     234           9 :         if (hasReadOnRefsStar) {
     235             :           // The user has READ on refs/* with no effective block permission. This is the broadest
     236             :           // permission one can assign. There is no way to grant access to (specific) tags in
     237             :           // Gerrit,
     238             :           // so we have to assume that these users can see all tags because there could be tags that
     239             :           // aren't reachable by any visible ref while the user can see all non-Gerrit refs. This
     240             :           // matches Gerrit's historic behavior.
     241             :           // This makes it so that these users could see commits that they can't see otherwise
     242             :           // (e.g. a private change ref) if a tag was attached to it. Tags are meant to be used on
     243             :           // the regular Git tree that users interact with, not on any of the Gerrit trees, so this
     244             :           // is a negligible risk.
     245           6 :           logger.atFinest().log("Include tag ref %s because user has read on refs/*", refName);
     246           6 :           resultRefs.add(ref);
     247             :         } else {
     248             :           // If its a tag, consider it later.
     249           9 :           if (ref.getObjectId() != null) {
     250           9 :             logger.atFinest().log("Defer tag ref %s", refName);
     251           9 :             deferredTags.add(ref);
     252             :           } else {
     253           0 :             logger.atFinest().log("Filter out tag ref %s that is not a tag", refName);
     254             :           }
     255             :         }
     256          36 :       } else if ((changeId = Change.Id.fromRef(refName)) != null) {
     257             :         // This is a mere performance optimization. RefVisibilityControl could determine the
     258             :         // visibility of these refs just fine. But instead, we use highly-optimized logic that
     259             :         // looks only on the available changes in the change index and cache (which are the
     260             :         // most recent changes).
     261          19 :         if (hasAccessDatabase) {
     262           3 :           resultRefs.add(ref);
     263          19 :         } else if (!visibleChanges.get().containsKey(changeId)) {
     264           9 :           logger.atFinest().log("Filter out invisible change ref %s", refName);
     265          18 :         } else if (RefNames.isRefsEdit(refName) && !visibleEdit(refName, visibleChanges.get())) {
     266           1 :           logger.atFinest().log("Filter out invisible change edit ref %s", refName);
     267             :         } else {
     268             :           // Change is visible
     269          18 :           resultRefs.add(ref);
     270             :         }
     271          36 :       } else if (refVisibilityControl.isVisible(projectControl, ref.getLeaf().getName())) {
     272          34 :         resultRefs.add(ref);
     273             :       }
     274          36 :     }
     275          36 :     Result result = new AutoValue_DefaultRefFilter_Result(resultRefs.build(), deferredTags.build());
     276          36 :     logger.atFinest().log("Result of ref filtering = %s", result);
     277          36 :     return result;
     278             :   }
     279             : 
     280             :   /**
     281             :    * Returns all refs tag we regard as starting points for reachability computation for tags. In
     282             :    * general, these are all refs not managed by Gerrit excluding symbolic refs and tags.
     283             :    *
     284             :    * <p>We exclude symbolic refs because their target will be included and this will suffice for
     285             :    * computing reachability.
     286             :    */
     287             :   private static List<Ref> getTaggableRefs(Repository repo) throws PermissionBackendException {
     288             :     try {
     289           9 :       List<Ref> allRefs = repo.getRefDatabase().getRefs();
     290           9 :       return allRefs.stream()
     291           9 :           .filter(
     292             :               r ->
     293           9 :                   !RefNames.isGerritRef(r.getName())
     294           9 :                       && !r.getName().startsWith(RefNames.REFS_TAGS)
     295           9 :                       && !r.isSymbolic()
     296           9 :                       && !r.getName().equals(RefNames.REFS_CONFIG))
     297           9 :           .collect(Collectors.toList());
     298           0 :     } catch (IOException e) {
     299           0 :       throw new PermissionBackendException(e);
     300             :     }
     301             :   }
     302             : 
     303             :   /**
     304             :    * Returns the number of changes contained in {@code refs}. A change has one meta ref and many
     305             :    * patch set refs. We count over the meta refs to make sure we get the number of unique changes in
     306             :    * the provided refs.
     307             :    */
     308             :   private static ImmutableSet<Change.Id> changes(Collection<Ref> refs) {
     309          19 :     return refs.stream()
     310          19 :         .map(Ref::getName)
     311          19 :         .map(Change.Id::fromRef)
     312          19 :         .filter(Objects::nonNull)
     313          19 :         .collect(toImmutableSet());
     314             :   }
     315             : 
     316             :   private List<Ref> fastHideRefsMetaConfig(List<Ref> refs) throws PermissionBackendException {
     317           7 :     if (!canReadRef(REFS_CONFIG)) {
     318           7 :       return refs.stream()
     319           7 :           .filter(r -> !r.getName().equals(REFS_CONFIG))
     320           7 :           .collect(toCollection(() -> new ArrayList<>(refs.size())));
     321             :     }
     322           0 :     return refs;
     323             :   }
     324             : 
     325             :   private boolean visibleEdit(String name, ImmutableMap<Change.Id, ChangeData> visibleChanges)
     326             :       throws PermissionBackendException {
     327           1 :     Change.Id id = Change.Id.fromEditRefPart(name);
     328           1 :     if (id == null) {
     329           0 :       logger.atWarning().log("Couldn't extract change ID from edit ref %s", name);
     330           0 :       return false;
     331             :     }
     332             : 
     333           1 :     if (user.isIdentifiedUser()
     334           1 :         && name.startsWith(RefNames.refsEditPrefix(user.asIdentifiedUser().getAccountId()))
     335           1 :         && visibleChanges.containsKey(id)) {
     336           1 :       logger.atFinest().log("Own change edit ref is visible: %s", name);
     337           1 :       return true;
     338             :     }
     339             : 
     340           1 :     if (visibleChanges.containsKey(id)) {
     341             :       // Default to READ_PRIVATE_CHANGES as there is no special permission for reading edits.
     342           1 :       BranchNameKey dest = visibleChanges.get(id).change().getDest();
     343           1 :       boolean canRead =
     344           1 :           permissionBackendForProject.ref(dest.branch()).test(RefPermission.READ_PRIVATE_CHANGES);
     345           1 :       logger.atFinest().log(
     346           1 :           "Foreign change edit ref is " + (canRead ? "visible" : "invisible") + ": %s", name);
     347           1 :       return canRead;
     348             :     }
     349             : 
     350           0 :     logger.atFinest().log("Change %d of change edit ref %s is not visible", id.get(), name);
     351           0 :     return false;
     352             :   }
     353             : 
     354             :   private boolean isMetadata(String name) {
     355           4 :     boolean isMetaData = RefNames.isRefsChanges(name) || RefNames.isRefsEdit(name);
     356           4 :     logger.atFinest().log("ref %s is " + (isMetaData ? "" : "not ") + "a metadata ref", name);
     357           4 :     return isMetaData;
     358             :   }
     359             : 
     360             :   private static boolean isTag(Ref ref) {
     361          36 :     return ref.getLeaf().getName().startsWith(Constants.R_TAGS);
     362             :   }
     363             : 
     364             :   private boolean canReadRef(String ref) throws PermissionBackendException {
     365           7 :     return permissionBackendForProject.ref(ref).test(RefPermission.READ);
     366             :   }
     367             : 
     368             :   private boolean checkProjectPermission(
     369             :       PermissionBackend.ForProject forProject, ProjectPermission perm)
     370             :       throws PermissionBackendException {
     371         135 :     return forProject.test(perm);
     372             :   }
     373             : 
     374             :   @AutoValue
     375         135 :   abstract static class Result {
     376             :     /** Subset of the refs passed into the computation that is visible to the user. */
     377             :     abstract ImmutableList<Ref> visibleRefs();
     378             : 
     379             :     /**
     380             :      * List of tags where we couldn't figure out visibility in the first pass and need to do an
     381             :      * expensive ref walk.
     382             :      */
     383             :     abstract ImmutableList<Ref> deferredTags();
     384             :   }
     385             : }

Generated by: LCOV version 1.16+git.20220603.dfeb750