| // 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.collect.ImmutableList.toImmutableList; |
| import static com.google.gerrit.reviewdb.client.RefNames.REFS_CHANGES; |
| import static com.google.gerrit.reviewdb.client.RefNames.REFS_CONFIG; |
| import static com.google.gerrit.reviewdb.client.RefNames.REFS_USERS_SELF; |
| import static java.util.stream.Collectors.toMap; |
| |
| import com.google.common.collect.ImmutableList; |
| import com.google.common.collect.ImmutableSet; |
| import com.google.common.collect.Maps; |
| import com.google.common.flogger.FluentLogger; |
| import com.google.gerrit.common.Nullable; |
| import com.google.gerrit.extensions.restapi.AuthException; |
| import com.google.gerrit.metrics.Counter0; |
| import com.google.gerrit.metrics.Description; |
| import com.google.gerrit.metrics.MetricMaker; |
| import com.google.gerrit.reviewdb.client.Branch; |
| import com.google.gerrit.reviewdb.client.Change; |
| import com.google.gerrit.reviewdb.client.Project; |
| import com.google.gerrit.reviewdb.client.RefNames; |
| import com.google.gerrit.reviewdb.server.ReviewDb; |
| import com.google.gerrit.server.CurrentUser; |
| import com.google.gerrit.server.config.GerritServerConfig; |
| import com.google.gerrit.server.git.SearchingChangeCacheImpl; |
| import com.google.gerrit.server.git.TagCache; |
| import com.google.gerrit.server.git.TagMatcher; |
| import com.google.gerrit.server.notedb.ChangeNotes; |
| import com.google.gerrit.server.notedb.ChangeNotes.Factory.ChangeNotesResult; |
| 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.gwtorm.server.OrmException; |
| import com.google.inject.Inject; |
| import com.google.inject.Provider; |
| import com.google.inject.assistedinject.Assisted; |
| import java.io.IOException; |
| import java.util.ArrayList; |
| import java.util.Collections; |
| import java.util.HashMap; |
| import java.util.List; |
| import java.util.Map; |
| import org.eclipse.jgit.lib.Config; |
| import org.eclipse.jgit.lib.Constants; |
| import org.eclipse.jgit.lib.Ref; |
| import org.eclipse.jgit.lib.Repository; |
| import org.eclipse.jgit.lib.SymbolicRef; |
| |
| class DefaultRefFilter { |
| private static final FluentLogger logger = FluentLogger.forEnclosingClass(); |
| |
| interface Factory { |
| DefaultRefFilter create(ProjectControl projectControl); |
| } |
| |
| private final TagCache tagCache; |
| private final ChangeNotes.Factory changeNotesFactory; |
| @Nullable private final SearchingChangeCacheImpl changeCache; |
| private final Provider<ReviewDb> db; |
| 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 Counter0 fullFilterCount; |
| private final Counter0 skipFilterCount; |
| private final boolean skipFullRefEvaluationIfAllRefsAreVisible; |
| |
| private Map<Change.Id, Branch.NameKey> visibleChanges; |
| |
| @Inject |
| DefaultRefFilter( |
| TagCache tagCache, |
| ChangeNotes.Factory changeNotesFactory, |
| @Nullable SearchingChangeCacheImpl changeCache, |
| Provider<ReviewDb> db, |
| PermissionBackend permissionBackend, |
| RefVisibilityControl refVisibilityControl, |
| @GerritServerConfig Config config, |
| MetricMaker metricMaker, |
| @Assisted ProjectControl projectControl) { |
| this.tagCache = tagCache; |
| this.changeNotesFactory = changeNotesFactory; |
| this.changeCache = changeCache; |
| this.db = db; |
| this.permissionBackend = permissionBackend; |
| this.refVisibilityControl = refVisibilityControl; |
| this.skipFullRefEvaluationIfAllRefsAreVisible = |
| config.getBoolean("auth", "skipFullRefEvaluationIfAllRefsAreVisible", true); |
| this.projectControl = projectControl; |
| |
| this.user = projectControl.getUser(); |
| this.projectState = projectControl.getProjectState(); |
| this.permissionBackendForProject = |
| permissionBackend.user(user).database(db).project(projectState.getNameKey()); |
| this.fullFilterCount = |
| metricMaker.newCounter( |
| "permissions/ref_filter/full_filter_count", |
| new Description("Rate of full ref filter operations").setRate()); |
| this.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()); |
| } |
| |
| Map<String, Ref> filter(Map<String, Ref> refs, Repository repo, RefFilterOptions opts) |
| throws PermissionBackendException { |
| if (projectState.isAllUsers()) { |
| refs = addUsersSelfSymref(refs); |
| } |
| |
| if (skipFullRefEvaluationIfAllRefsAreVisible && !projectState.isAllUsers()) { |
| if (projectState.statePermitsRead() |
| && checkProjectPermission(permissionBackendForProject, ProjectPermission.READ)) { |
| skipFilterCount.increment(); |
| return refs; |
| } else if (projectControl.allRefsAreVisible(ImmutableSet.of(RefNames.REFS_CONFIG))) { |
| skipFilterCount.increment(); |
| return fastHideRefsMetaConfig(refs); |
| } |
| } |
| fullFilterCount.increment(); |
| |
| boolean hasAccessDatabase = |
| permissionBackend |
| .user(projectControl.getUser()) |
| .testOrFalse(GlobalPermission.ACCESS_DATABASE); |
| Map<String, Ref> resultRefs = Maps.newHashMapWithExpectedSize(refs.size()); |
| List<Ref> deferredTags = new ArrayList<>(); |
| |
| boolean hasReadOnRefsStar = |
| checkProjectPermission(permissionBackendForProject, ProjectPermission.READ); |
| for (Ref ref : refs.values()) { |
| 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.put(refName, 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 last 10k most recent changes using the change index and a cache. |
| if (hasAccessDatabase) { |
| resultRefs.put(refName, ref); |
| } else if (!visible(repo, changeId)) { |
| logger.atFinest().log("Filter out invisible change ref %s", refName); |
| } else if (RefNames.isRefsEdit(refName) && !visibleEdit(repo, refName)) { |
| logger.atFinest().log("Filter out invisible change edit ref %s", refName); |
| } else { |
| // Change is visible |
| resultRefs.put(refName, ref); |
| } |
| } else if (refVisibilityControl.isVisible(projectControl, ref.getLeaf().getName())) { |
| resultRefs.put(refName, ref); |
| } |
| } |
| |
| // If we have tags that were deferred, we need to do a revision walk |
| // to identify what tags we can actually reach, and what we cannot. |
| // |
| if (!deferredTags.isEmpty() && (!resultRefs.isEmpty() || opts.filterTagsSeparately())) { |
| TagMatcher tags = |
| tagCache |
| .get(projectState.getNameKey()) |
| .matcher( |
| tagCache, |
| repo, |
| opts.filterTagsSeparately() |
| ? filter( |
| getTaggableRefsMap(repo), |
| repo, |
| opts.toBuilder().setFilterTagsSeparately(false).build()) |
| .values() |
| : resultRefs.values()); |
| for (Ref tag : deferredTags) { |
| if (tags.isReachable(tag)) { |
| resultRefs.put(tag.getName(), tag); |
| } |
| } |
| } |
| return resultRefs; |
| } |
| |
| /** |
| * Returns all refs tag we regard as starting points for reachability computation for tags. In |
| * general, these are all refs not managed by Gerrit. |
| */ |
| private static Map<String, Ref> getTaggableRefsMap(Repository repo) |
| throws PermissionBackendException { |
| try { |
| return repo.getRefDatabase().getRefs().stream() |
| .filter( |
| r -> |
| !RefNames.isGerritRef(r.getName()) |
| && !r.getName().startsWith(RefNames.REFS_TAGS) |
| && !r.isSymbolic() |
| && !REFS_CONFIG.equals(r.getName())) |
| .collect(toMap(Ref::getName, r -> r)); |
| } catch (IOException e) { |
| throw new PermissionBackendException(e); |
| } |
| } |
| |
| private Map<String, Ref> fastHideRefsMetaConfig(Map<String, Ref> refs) |
| throws PermissionBackendException { |
| if (refs.containsKey(REFS_CONFIG) && !canReadRef(REFS_CONFIG)) { |
| Map<String, Ref> r = new HashMap<>(refs); |
| r.remove(REFS_CONFIG); |
| return r; |
| } |
| return refs; |
| } |
| |
| private Map<String, Ref> addUsersSelfSymref(Map<String, Ref> refs) { |
| if (user.isIdentifiedUser()) { |
| Ref r = refs.get(RefNames.refsUsers(user.getAccountId())); |
| if (r != null) { |
| SymbolicRef s = new SymbolicRef(REFS_USERS_SELF, r); |
| refs = new HashMap<>(refs); |
| refs.put(s.getName(), s); |
| } |
| } |
| return refs; |
| } |
| |
| private boolean visible(Repository repo, Change.Id changeId) throws PermissionBackendException { |
| if (visibleChanges == null) { |
| if (changeCache == null) { |
| visibleChanges = visibleChangesByScan(repo); |
| } else { |
| visibleChanges = visibleChangesBySearch(); |
| } |
| } |
| return visibleChanges.containsKey(changeId); |
| } |
| |
| private boolean visibleEdit(Repository repo, String name) throws PermissionBackendException { |
| Change.Id id = Change.Id.fromEditRefPart(name); |
| // Initialize if it wasn't yet |
| if (visibleChanges == null) { |
| visible(repo, id); |
| } |
| if (id == null) { |
| return false; |
| } |
| if (user.isIdentifiedUser() |
| && name.startsWith(RefNames.refsEditPrefix(user.asIdentifiedUser().getAccountId())) |
| && visible(repo, id)) { |
| return true; |
| } |
| if (visibleChanges.containsKey(id)) { |
| try { |
| // Default to READ_PRIVATE_CHANGES as there is no special permission for reading edits. |
| permissionBackendForProject |
| .ref(visibleChanges.get(id).get()) |
| .check(RefPermission.READ_PRIVATE_CHANGES); |
| return true; |
| } catch (AuthException e) { |
| return false; |
| } |
| } |
| return false; |
| } |
| |
| private Map<Change.Id, Branch.NameKey> visibleChangesBySearch() |
| throws PermissionBackendException { |
| Project.NameKey project = projectState.getNameKey(); |
| try { |
| Map<Change.Id, Branch.NameKey> visibleChanges = new HashMap<>(); |
| for (ChangeData cd : changeCache.getChangeData(db.get(), project)) { |
| ChangeNotes notes = changeNotesFactory.createFromIndexedChange(cd.change()); |
| if (!projectState.statePermitsRead()) { |
| continue; |
| } |
| try { |
| permissionBackendForProject.indexedChange(cd, notes).check(ChangePermission.READ); |
| visibleChanges.put(cd.getId(), cd.change().getDest()); |
| } catch (AuthException e) { |
| // Do nothing. |
| } |
| } |
| return visibleChanges; |
| } catch (OrmException e) { |
| logger.atSevere().withCause(e).log( |
| "Cannot load changes for project %s, assuming no changes are visible", project); |
| return Collections.emptyMap(); |
| } |
| } |
| |
| private Map<Change.Id, Branch.NameKey> visibleChangesByScan(Repository repo) |
| throws PermissionBackendException { |
| Project.NameKey p = projectState.getNameKey(); |
| ImmutableList<ChangeNotesResult> changes; |
| try { |
| changes = changeNotesFactory.scan(repo, db.get(), p).collect(toImmutableList()); |
| } catch (IOException e) { |
| logger.atSevere().withCause(e).log( |
| "Cannot load changes for project %s, assuming no changes are visible", p); |
| return Collections.emptyMap(); |
| } |
| |
| Map<Change.Id, Branch.NameKey> result = Maps.newHashMapWithExpectedSize(changes.size()); |
| for (ChangeNotesResult notesResult : changes) { |
| ChangeNotes notes = toNotes(notesResult); |
| if (notes != null) { |
| result.put(notes.getChangeId(), notes.getChange().getDest()); |
| } |
| } |
| return result; |
| } |
| |
| @Nullable |
| private ChangeNotes toNotes(ChangeNotesResult r) throws PermissionBackendException { |
| if (r.error().isPresent()) { |
| logger.atWarning().withCause(r.error().get()).log( |
| "Failed to load change %s in %s", r.id(), projectState.getName()); |
| return null; |
| } |
| |
| if (!projectState.statePermitsRead()) { |
| return null; |
| } |
| |
| try { |
| permissionBackendForProject.change(r.notes()).check(ChangePermission.READ); |
| return r.notes(); |
| } catch (AuthException e) { |
| // Skip. |
| } |
| return null; |
| } |
| |
| private boolean isMetadata(String name) { |
| return name.startsWith(REFS_CHANGES) || RefNames.isRefsEdit(name); |
| } |
| |
| private static boolean isTag(Ref ref) { |
| return ref.getLeaf().getName().startsWith(Constants.R_TAGS); |
| } |
| |
| private boolean canReadRef(String ref) throws PermissionBackendException { |
| try { |
| permissionBackendForProject.ref(ref).check(RefPermission.READ); |
| } catch (AuthException e) { |
| return false; |
| } |
| return projectState.statePermitsRead(); |
| } |
| |
| private boolean checkProjectPermission( |
| PermissionBackend.ForProject forProject, ProjectPermission perm) |
| throws PermissionBackendException { |
| try { |
| forProject.check(perm); |
| } catch (AuthException e) { |
| return false; |
| } |
| return true; |
| } |
| } |