summaryrefslogtreecommitdiffstats
path: root/java/com/google/gerrit/server/permissions/DefaultRefFilter.java
diff options
context:
space:
mode:
Diffstat (limited to 'java/com/google/gerrit/server/permissions/DefaultRefFilter.java')
-rw-r--r--java/com/google/gerrit/server/permissions/DefaultRefFilter.java426
1 files changed, 426 insertions, 0 deletions
diff --git a/java/com/google/gerrit/server/permissions/DefaultRefFilter.java b/java/com/google/gerrit/server/permissions/DefaultRefFilter.java
new file mode 100644
index 0000000000..55d7c6c62f
--- /dev/null
+++ b/java/com/google/gerrit/server/permissions/DefaultRefFilter.java
@@ -0,0 +1,426 @@
+// 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_CACHE_AUTOMERGE;
+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.Objects.requireNonNull;
+
+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.Account;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+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.IdentifiedUser;
+import com.google.gerrit.server.account.GroupCache;
+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.group.InternalGroup;
+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 GroupCache groupCache;
+ private final PermissionBackend permissionBackend;
+ 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,
+ GroupCache groupCache,
+ PermissionBackend permissionBackend,
+ @GerritServerConfig Config config,
+ MetricMaker metricMaker,
+ @Assisted ProjectControl projectControl) {
+ this.tagCache = tagCache;
+ this.changeNotesFactory = changeNotesFactory;
+ this.changeCache = changeCache;
+ this.db = db;
+ this.groupCache = groupCache;
+ this.permissionBackend = permissionBackend;
+ 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 viewMetadata;
+ boolean isAdmin;
+ Account.Id userId;
+ IdentifiedUser identifiedUser;
+ PermissionBackend.WithUser withUser = permissionBackend.user(user);
+ if (user.isIdentifiedUser()) {
+ viewMetadata = withUser.testOrFalse(GlobalPermission.ACCESS_DATABASE);
+ isAdmin = withUser.testOrFalse(GlobalPermission.ADMINISTRATE_SERVER);
+ identifiedUser = user.asIdentifiedUser();
+ userId = identifiedUser.getAccountId();
+ } else {
+ viewMetadata = false;
+ isAdmin = false;
+ userId = null;
+ identifiedUser = null;
+ }
+
+ Map<String, Ref> result = new HashMap<>();
+ List<Ref> deferredTags = new ArrayList<>();
+
+ for (Ref ref : refs.values()) {
+ String name = ref.getName();
+ Change.Id changeId;
+ Account.Id accountId;
+ AccountGroup.UUID accountGroupUuid;
+ if (name.startsWith(REFS_CACHE_AUTOMERGE) || (opts.filterMeta() && isMetadata(name))) {
+ continue;
+ } else if (RefNames.isRefsEdit(name)) {
+ // Edits are visible only to the owning user, if change is visible.
+ if (viewMetadata || visibleEdit(repo, name)) {
+ result.put(name, ref);
+ }
+ } else if ((changeId = Change.Id.fromRef(name)) != null) {
+ // Change ref is visible only if the change is visible.
+ if (viewMetadata || visible(repo, changeId)) {
+ result.put(name, ref);
+ }
+ } else if ((accountId = Account.Id.fromRef(name)) != null) {
+ // Account ref is visible only to the corresponding account.
+ if (viewMetadata || (accountId.equals(userId) && canReadRef(name))) {
+ result.put(name, ref);
+ }
+ } else if ((accountGroupUuid = AccountGroup.UUID.fromRef(name)) != null) {
+ // Group ref is visible only to the corresponding owner group.
+ InternalGroup group = groupCache.get(accountGroupUuid).orElse(null);
+ if (viewMetadata
+ || (group != null
+ && isGroupOwner(group, identifiedUser, isAdmin)
+ && canReadRef(name))) {
+ result.put(name, ref);
+ }
+ } else if (isTag(ref)) {
+ // If its a tag, consider it later.
+ if (ref.getObjectId() != null) {
+ deferredTags.add(ref);
+ }
+ } else if (name.startsWith(RefNames.REFS_SEQUENCES)) {
+ // Sequences are internal database implementation details.
+ if (viewMetadata) {
+ result.put(name, ref);
+ }
+ } else if (projectState.isAllUsers()
+ && (name.equals(RefNames.REFS_EXTERNAL_IDS) || name.equals(RefNames.REFS_GROUPNAMES))) {
+ // The notes branches with the external IDs / group names must not be exposed to normal
+ // users.
+ if (viewMetadata) {
+ result.put(name, ref);
+ }
+ } else if (canReadRef(ref.getLeaf().getName())) {
+ // Use the leaf to lookup the control data. If the reference is
+ // symbolic we want the control around the final target. If its
+ // not symbolic then getLeaf() is a no-op returning ref itself.
+ result.put(name, ref);
+ } else if (isRefsUsersSelf(ref)) {
+ // viewMetadata allows to see all account refs, hence refs/users/self should be included as
+ // well
+ if (viewMetadata) {
+ result.put(name, 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() && (!result.isEmpty() || opts.filterTagsSeparately())) {
+ TagMatcher tags =
+ tagCache
+ .get(projectState.getNameKey())
+ .matcher(
+ tagCache,
+ repo,
+ opts.filterTagsSeparately()
+ ? filter(
+ repo.getAllRefs(),
+ repo,
+ opts.toBuilder().setFilterTagsSeparately(false).build())
+ .values()
+ : result.values());
+ for (Ref tag : deferredTags) {
+ if (tags.isReachable(tag)) {
+ result.put(tag.getName(), tag);
+ }
+ }
+ }
+
+ return result;
+ }
+
+ 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 static boolean isRefsUsersSelf(Ref ref) {
+ return ref.getName().startsWith(REFS_USERS_SELF);
+ }
+
+ 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;
+ }
+
+ private boolean isGroupOwner(
+ InternalGroup group, @Nullable IdentifiedUser user, boolean isAdmin) {
+ requireNonNull(group);
+
+ // Keep this logic in sync with GroupControl#isOwner().
+ return isAdmin
+ || (user != null && user.getEffectiveGroups().contains(group.getOwnerGroupUUID()));
+ }
+}