diff options
author | Luca Milanesio <luca.milanesio@gmail.com> | 2020-11-19 00:43:40 +0000 |
---|---|---|
committer | Luca Milanesio <luca.milanesio@gmail.com> | 2020-11-19 00:43:40 +0000 |
commit | d62c6f611e2fd536e9d7d1742224a768899a2651 (patch) | |
tree | 0a932896a70172610dcbf14b0abb089fd4b56703 | |
parent | fd266bf32ba071e7a7a0d25e17ad2524e6e02d89 (diff) | |
parent | db8c3be3ff84dd93c38083c6018e5bbd10a2a2a1 (diff) |
Merge branch 'stable-2.16-2020-11.notedb-refs-tags' into stable-2.16
* stable-2.16-2020-11.notedb-refs-tags:
Set version to 2.16.26-SNAPSHOT
Set version to 2.16.25
Workaround Gitiles bug on All-Users visibility
Set version to 2.16.25-SNAPSHOT
Set version to 2.16.24
Make PermissionBackend#ForRef authoritative
Change-Id: Idec7d52fa1ef663240b4e3ca3900427b87d8d003
18 files changed, 1045 insertions, 132 deletions
diff --git a/java/com/google/gerrit/reviewdb/client/RefNames.java b/java/com/google/gerrit/reviewdb/client/RefNames.java index fd2fb56b0e..994a299ef1 100644 --- a/java/com/google/gerrit/reviewdb/client/RefNames.java +++ b/java/com/google/gerrit/reviewdb/client/RefNames.java @@ -118,6 +118,11 @@ public class RefNames { return shard(id.get(), r).append(META_SUFFIX).toString(); } + public static String patchSetRef(PatchSet.Id id) { + StringBuilder r = newStringBuilder().append(REFS_CHANGES); + return shard(id.changeId.get(), r).append('/').append(id.get()).toString(); + } + public static String robotCommentsRef(Change.Id id) { StringBuilder r = newStringBuilder().append(REFS_CHANGES); return shard(id.get(), r).append(ROBOT_COMMENTS_SUFFIX).toString(); @@ -263,6 +268,31 @@ public class RefNames { return REFS_CONFIG.equals(ref); } + /** + * Whether the ref is managed by Gerrit. Covers all Gerrit-internal refs like refs/cache-automerge + * and refs/meta as well as refs/changes. Does not cover user-created refs like branches or custom + * ref namespaces like refs/my-company. + * + * <p>Any ref for which this method evaluates to true will be served to users who have the {@code + * ACCESS_DATABASE} capability. + * + * <p><b>Caution</b>Any ref not in this list will be served if the user was granted a READ + * permission on it using Gerrit's permission model. + */ + public static boolean isGerritRef(String ref) { + return ref.startsWith(REFS_CHANGES) + || ref.startsWith(REFS_EXTERNAL_IDS) + || ref.startsWith(REFS_CACHE_AUTOMERGE) + || ref.startsWith(REFS_DRAFT_COMMENTS) + || ref.startsWith(REFS_DELETED_GROUPS) + || ref.startsWith(REFS_SEQUENCES) + || ref.startsWith(REFS_GROUPS) + || ref.startsWith(REFS_GROUPNAMES) + || ref.startsWith(REFS_USERS) + || ref.startsWith(REFS_STARRED_CHANGES) + || ref.startsWith(REFS_REJECT_COMMITS); + } + static Integer parseShardedRefPart(String name) { if (name == null) { return null; diff --git a/java/com/google/gerrit/server/permissions/ChangeControl.java b/java/com/google/gerrit/server/permissions/ChangeControl.java index f4e659eb7a..4d88f241b3 100644 --- a/java/com/google/gerrit/server/permissions/ChangeControl.java +++ b/java/com/google/gerrit/server/permissions/ChangeControl.java @@ -70,6 +70,8 @@ class ChangeControl { private final RefControl refControl; private final ChangeNotes notes; + private ChangeData cd; + private ChangeControl( ChangeData.Factory changeDataFactory, RefControl refControl, ChangeNotes notes) { this.changeDataFactory = changeDataFactory; @@ -78,7 +80,8 @@ class ChangeControl { } ForChange asForChange(@Nullable ChangeData cd, @Nullable Provider<ReviewDb> db) { - return new ForChangeImpl(cd, db); + setChangeData(cd); + return new ForChangeImpl(db); } private CurrentUser getUser() { @@ -93,12 +96,27 @@ class ChangeControl { return notes.getChange(); } + private ChangeData changeData(ReviewDb db) { + if (cd == null) { + cd = changeDataFactory.create(db, notes); + } + return cd; + } + + ChangeControl setChangeData(@Nullable ChangeData cd) { + if (cd != null) { + this.cd = cd; + } + return this; + } + /** Can this user see this change? */ - private boolean isVisible(ReviewDb db, @Nullable ChangeData cd) throws OrmException { + boolean isVisible(ReviewDb db) throws OrmException { if (getChange().isPrivate() && !isPrivateVisible(db, cd)) { return false; } - return refControl.isVisible(); + // Does the user have READ permission on the destination? + return refControl.asForRef().testOrFalse(RefPermission.READ); } /** Can this user abandon this change? */ @@ -215,13 +233,12 @@ class ChangeControl { || getUser().isInternalUser(); } - private class ForChangeImpl extends ForChange { - private ChangeData cd; + class ForChangeImpl extends ForChange { + private Map<String, PermissionRange> labels; private String resourcePath; - ForChangeImpl(@Nullable ChangeData cd, @Nullable Provider<ReviewDb> db) { - this.cd = cd; + ForChangeImpl(@Nullable Provider<ReviewDb> db) { this.db = db; } @@ -295,7 +312,7 @@ class ChangeControl { try { switch (perm) { case READ: - return isVisible(db(), changeData()); + return isVisible(db()); case ABANDON: return canAbandon(); case DELETE: diff --git a/java/com/google/gerrit/server/permissions/DefaultRefFilter.java b/java/com/google/gerrit/server/permissions/DefaultRefFilter.java index 55d7c6c62f..2a0e41c848 100644 --- a/java/com/google/gerrit/server/permissions/DefaultRefFilter.java +++ b/java/com/google/gerrit/server/permissions/DefaultRefFilter.java @@ -15,11 +15,10 @@ 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 static java.util.stream.Collectors.toMap; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; @@ -30,21 +29,16 @@ 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; @@ -77,8 +71,8 @@ class DefaultRefFilter { 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 RefVisibilityControl refVisibilityControl; private final ProjectControl projectControl; private final CurrentUser user; private final ProjectState projectState; @@ -95,8 +89,8 @@ class DefaultRefFilter { ChangeNotes.Factory changeNotesFactory, @Nullable SearchingChangeCacheImpl changeCache, Provider<ReviewDb> db, - GroupCache groupCache, PermissionBackend permissionBackend, + RefVisibilityControl refVisibilityControl, @GerritServerConfig Config config, MetricMaker metricMaker, @Assisted ProjectControl projectControl) { @@ -104,8 +98,8 @@ class DefaultRefFilter { this.changeNotesFactory = changeNotesFactory; this.changeCache = changeCache; this.db = db; - this.groupCache = groupCache; this.permissionBackend = permissionBackend; + this.refVisibilityControl = refVisibilityControl; this.skipFullRefEvaluationIfAllRefsAreVisible = config.getBoolean("auth", "skipFullRefEvaluationIfAllRefsAreVisible", true); this.projectControl = projectControl; @@ -145,92 +139,66 @@ class DefaultRefFilter { } 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<>(); + 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 name = ref.getName(); + String refName = 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); - } + if (opts.filterMeta() && isMetadata(refName)) { + logger.atFinest().log("Filter out metadata ref %s", refName); } 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); + 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 (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); + } 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() && (!result.isEmpty() || opts.filterTagsSeparately())) { + if (!deferredTags.isEmpty() && (!resultRefs.isEmpty() || opts.filterTagsSeparately())) { TagMatcher tags = tagCache .get(projectState.getNameKey()) @@ -239,19 +207,38 @@ class DefaultRefFilter { repo, opts.filterTagsSeparately() ? filter( - repo.getAllRefs(), + getTaggableRefsMap(repo), repo, opts.toBuilder().setFilterTagsSeparately(false).build()) .values() - : result.values()); + : resultRefs.values()); for (Ref tag : deferredTags) { if (tags.isReachable(tag)) { - result.put(tag.getName(), tag); + resultRefs.put(tag.getName(), tag); } } } + return resultRefs; + } - 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. + */ + 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) @@ -391,10 +378,6 @@ class DefaultRefFilter { 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); @@ -414,13 +397,4 @@ class DefaultRefFilter { } 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())); - } } diff --git a/java/com/google/gerrit/server/permissions/ProjectControl.java b/java/com/google/gerrit/server/permissions/ProjectControl.java index 0094979732..85e7fb16b4 100644 --- a/java/com/google/gerrit/server/permissions/ProjectControl.java +++ b/java/com/google/gerrit/server/permissions/ProjectControl.java @@ -33,8 +33,10 @@ 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.account.GroupMembership; +import com.google.gerrit.server.config.AllUsersName; import com.google.gerrit.server.config.GitReceivePackGroups; import com.google.gerrit.server.config.GitUploadPackGroups; +import com.google.gerrit.server.git.GitRepositoryManager; import com.google.gerrit.server.group.SystemGroupBackend; import com.google.gerrit.server.notedb.ChangeNotes; import com.google.gerrit.server.permissions.PermissionBackend.ForChange; @@ -67,11 +69,14 @@ class ProjectControl { private final Set<AccountGroup.UUID> uploadGroups; private final Set<AccountGroup.UUID> receiveGroups; private final PermissionBackend permissionBackend; + private final RefVisibilityControl refVisibilityControl; + private final GitRepositoryManager repositoryManager; private final CurrentUser user; private final ProjectState state; private final ChangeControl.Factory changeControlFactory; private final PermissionCollection.Factory permissionFilter; private final DefaultRefFilter.Factory refFilterFactory; + private final AllUsersName allUsersName; private List<SectionMatcher> allSections; private Map<String, RefControl> refControls; @@ -84,7 +89,10 @@ class ProjectControl { PermissionCollection.Factory permissionFilter, ChangeControl.Factory changeControlFactory, PermissionBackend permissionBackend, + RefVisibilityControl refVisibilityControl, + GitRepositoryManager repositoryManager, DefaultRefFilter.Factory refFilterFactory, + AllUsersName allUsersName, @Assisted CurrentUser who, @Assisted ProjectState ps) { this.changeControlFactory = changeControlFactory; @@ -92,7 +100,10 @@ class ProjectControl { this.receiveGroups = receiveGroups; this.permissionFilter = permissionFilter; this.permissionBackend = permissionBackend; + this.refVisibilityControl = refVisibilityControl; + this.repositoryManager = repositoryManager; this.refFilterFactory = refFilterFactory; + this.allUsersName = allUsersName; user = who; state = ps; } @@ -121,7 +132,7 @@ class ProjectControl { RefControl ctl = refControls.get(refName); if (ctl == null) { PermissionCollection relevant = permissionFilter.filter(access(), refName, user); - ctl = new RefControl(this, refName, relevant); + ctl = new RefControl(refVisibilityControl, this, repositoryManager, refName, relevant); refControls.put(refName, ctl); } return ctl; @@ -168,7 +179,9 @@ class ProjectControl { } boolean allRefsAreVisible(Set<String> ignore) { - return user.isInternalUser() || canPerformOnAllRefs(Permission.READ, ignore); + return user.isInternalUser() + || (!getProject().getNameKey().equals(allUsersName) + && canPerformOnAllRefs(Permission.READ, ignore)); } /** Can the user run upload pack? */ @@ -434,7 +447,7 @@ class ProjectControl { return canPushToAtLeastOneRef(); case READ_CONFIG: - return controlForRef(RefNames.REFS_CONFIG).isVisible(); + return controlForRef(RefNames.REFS_CONFIG).hasReadPermissionOnRef(false); case BAN_COMMIT: case READ_REFLOG: diff --git a/java/com/google/gerrit/server/permissions/RefControl.java b/java/com/google/gerrit/server/permissions/RefControl.java index 74b04a32bf..5d6910a68d 100644 --- a/java/com/google/gerrit/server/permissions/RefControl.java +++ b/java/com/google/gerrit/server/permissions/RefControl.java @@ -16,6 +16,7 @@ package com.google.gerrit.server.permissions; import static com.google.common.base.Preconditions.checkArgument; +import com.google.common.collect.ImmutableMap; import com.google.common.flogger.FluentLogger; import com.google.gerrit.common.data.Permission; import com.google.gerrit.common.data.PermissionRange; @@ -27,6 +28,7 @@ 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.server.CurrentUser; +import com.google.gerrit.server.git.GitRepositoryManager; import com.google.gerrit.server.logging.CallerFinder; import com.google.gerrit.server.notedb.ChangeNotes; import com.google.gerrit.server.permissions.PermissionBackend.ForChange; @@ -35,16 +37,23 @@ import com.google.gerrit.server.query.change.ChangeData; import com.google.gerrit.server.util.MagicBranch; import com.google.gwtorm.server.OrmException; import com.google.inject.util.Providers; +import java.io.IOException; import java.util.Collection; import java.util.EnumSet; import java.util.List; import java.util.Set; +import java.util.concurrent.TimeUnit; +import org.eclipse.jgit.lib.Constants; +import org.eclipse.jgit.lib.Ref; +import org.eclipse.jgit.lib.Repository; /** Manages access control for Git references (aka branches, tags). */ class RefControl { private static final FluentLogger logger = FluentLogger.forEnclosingClass(); + private final RefVisibilityControl refVisibilityControl; private final ProjectControl projectControl; + private final GitRepositoryManager repositoryManager; private final String refName; /** All permissions that apply to this reference. */ @@ -57,10 +66,17 @@ class RefControl { private Boolean owner; private Boolean canForgeAuthor; private Boolean canForgeCommitter; - private Boolean isVisible; - - RefControl(ProjectControl projectControl, String ref, PermissionCollection relevant) { + private Boolean hasReadPermissionOnRef; + + RefControl( + RefVisibilityControl refVisibilityControl, + ProjectControl projectControl, + GitRepositoryManager repositoryManager, + String ref, + PermissionCollection relevant) { + this.refVisibilityControl = refVisibilityControl; this.projectControl = projectControl; + this.repositoryManager = repositoryManager; this.refName = ref; this.relevant = relevant; this.callerFinder = @@ -93,12 +109,27 @@ class RefControl { return owner; } - /** Can this user see this reference exists? */ - boolean isVisible() { - if (isVisible == null) { - isVisible = getUser().isInternalUser() || canPerform(Permission.READ); + /** + * Returns {@code true} if the user has permission to read the ref. This method evaluates {@link + * RefPermission#READ} only. Hence, it is not authoritative. For example, it does not tell if the + * user can see NoteDb refs such as {@code refs/meta/external-ids} which requires {@link + * GlobalPermission#ACCESS_DATABASE} and deny access in this case. + */ + boolean hasReadPermissionOnRef(boolean allowNoteDbRefs) { + // Don't allow checking for NoteDb refs unless instructed otherwise. + if (!allowNoteDbRefs + && (refName.startsWith(Constants.R_TAGS) || RefNames.isGerritRef(refName))) { + logger.atWarning().atMostEvery(30, TimeUnit.SECONDS).log( + "%s: Can't determine visibility of %s in RefControl. Denying access. " + + "This case should have been handled before.", + projectControl.getProject().getName(), refName); + return false; } - return isVisible; + + if (hasReadPermissionOnRef == null) { + hasReadPermissionOnRef = getUser().isInternalUser() || canPerform(Permission.READ); + } + return hasReadPermissionOnRef; } /** @return true if this user can add a new patch set to this ref */ @@ -577,7 +608,10 @@ class RefControl { private boolean can(RefPermission perm) throws PermissionBackendException { switch (perm) { case READ: - return isVisible(); + if (refName.startsWith(Constants.R_TAGS)) { + return isTagVisible(); + } + return refVisibilityControl.isVisible(projectControl, refName); case CREATE: // TODO This isn't an accurate test. return canPerform(refPermissionName(perm)); @@ -627,6 +661,43 @@ class RefControl { } throw new PermissionBackendException(perm + " unsupported"); } + + private boolean isTagVisible() throws PermissionBackendException { + if (projectControl.asForProject().test(ProjectPermission.READ)) { + // 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. + return true; + } + + try (Repository repo = + repositoryManager.openRepository(projectControl.getProject().getNameKey())) { + // Tag visibility requires going through RefFilter because it entails loading all taggable + // refs and filtering them all by visibility. + Ref resolvedRef = repo.getRefDatabase().exactRef(refName); + if (resolvedRef == null) { + return false; + } + return projectControl.asForProject() + .filter( + ImmutableMap.of(resolvedRef.getName(), resolvedRef), + repo, + PermissionBackend.RefFilterOptions.builder() + .setFilterMeta(true) + .setFilterTagsSeparately(true) + .build()) + .values().stream() + .anyMatch(r -> refName.equals(r.getName())); + } catch (IOException e) { + throw new PermissionBackendException(e); + } + } } private static String refPermissionName(RefPermission refPermission) { diff --git a/java/com/google/gerrit/server/permissions/RefVisibilityControl.java b/java/com/google/gerrit/server/permissions/RefVisibilityControl.java new file mode 100644 index 0000000000..7af94dc78a --- /dev/null +++ b/java/com/google/gerrit/server/permissions/RefVisibilityControl.java @@ -0,0 +1,248 @@ +// Copyright (C) 2020 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 com.google.common.base.Optional; +import com.google.common.base.Throwables; +import com.google.common.flogger.FluentLogger; +import com.google.gerrit.common.Nullable; +import com.google.gerrit.common.errors.NoSuchGroupException; +import com.google.gerrit.extensions.restapi.AuthException; +import com.google.gerrit.reviewdb.client.Account; +import com.google.gerrit.reviewdb.client.AccountGroup; +import com.google.gerrit.reviewdb.client.Change; +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.account.GroupControl; +import com.google.gerrit.server.notedb.ChangeNotes; +import com.google.gerrit.server.project.NoSuchChangeException; +import com.google.gerrit.server.query.change.ChangeData; +import com.google.gerrit.server.util.ManualRequestContext; +import com.google.gerrit.server.util.OneOffRequestContext; +import com.google.gwtorm.server.OrmException; +import javax.inject.Inject; +import javax.inject.Provider; +import javax.inject.Singleton; +import org.eclipse.jgit.lib.Constants; + +/** + * This class is a component that is internal to {@link DefaultPermissionBackend}. It can + * authoritatively tell if a ref is accessible by a user. + */ +@Singleton +class RefVisibilityControl { + private static final FluentLogger logger = FluentLogger.forEnclosingClass(); + + private final Provider<ReviewDb> dbProvider; + private final PermissionBackend permissionBackend; + private final GroupControl.GenericFactory groupControlFactory; + private final ChangeData.Factory changeDataFactory; + private final OneOffRequestContext oneOffRequestContext; + + @Inject + RefVisibilityControl( + Provider<ReviewDb> dbProvider, + PermissionBackend permissionBackend, + GroupControl.GenericFactory groupControlFactory, + ChangeData.Factory changeDataFactory, + OneOffRequestContext oneOffRequestContext) { + this.dbProvider = dbProvider; + this.permissionBackend = permissionBackend; + this.groupControlFactory = groupControlFactory; + this.changeDataFactory = changeDataFactory; + this.oneOffRequestContext = oneOffRequestContext; + } + + /** + * Returns an authoritative answer if the ref is visible to the user. Does not have support for + * tags and will throw a {@link PermissionBackendException} if asked for tags visibility. + */ + boolean isVisible(ProjectControl projectControl, String refName) + throws PermissionBackendException { + if (refName.startsWith(Constants.R_TAGS)) { + throw new PermissionBackendException( + "can't check tags through RefVisibilityControl. Use PermissionBackend#filter instead."); + } + if (!RefNames.isGerritRef(refName)) { + // This is not a special Gerrit ref and not a NoteDb ref. Likely, it's just a ref under + // refs/heads or another ref the user created. Apply the regular permissions with inheritance. + return projectControl.controlForRef(refName).hasReadPermissionOnRef(false); + } + + if (refName.startsWith(RefNames.REFS_CACHE_AUTOMERGE)) { + // Internal cache state that is accessible to no one. + return false; + } + + boolean hasAccessDatabase = + permissionBackend + .user(projectControl.getUser()) + .testOrFalse(GlobalPermission.ACCESS_DATABASE); + if (hasAccessDatabase) { + return true; + } + + // Change and change edit visibility + Change.Id changeId; + if ((changeId = Change.Id.fromRef(refName)) != null) { + // Change ref is visible only if the change is visible. + try (CloseableOneTimeReviewDb ignored = new CloseableOneTimeReviewDb()) { + ChangeData cd; + try { + cd = + changeDataFactory.create( + dbProvider.get(), projectControl.getProject().getNameKey(), changeId); + checkState(cd.change().getId().equals(changeId)); + } catch (OrmException e) { + if (Throwables.getCausalChain(e).stream() + .anyMatch(e2 -> e2 instanceof NoSuchChangeException)) { + // The change was deleted or is otherwise not accessible anymore. + // If the caller can see all refs and is allowed to see private changes on refs/, allow + // access. This is an escape hatch for receivers of "ref deleted" events. + PermissionBackend.ForProject forProject = projectControl.asForProject(); + return forProject.test(ProjectPermission.READ) + && forProject.ref("refs/").test(RefPermission.READ_PRIVATE_CHANGES); + } + throw new PermissionBackendException(e); + } + if (RefNames.isRefsEdit(refName)) { + // Edits are visible only to the owning user, if change is visible. + return visibleEdit(refName, projectControl, cd); + } + return isVisible(projectControl.controlFor(getNotes(cd)).setChangeData(cd)); + } + } + + // Account visibility + CurrentUser user = projectControl.getUser(); + Account.Id currentUserAccountId = user.isIdentifiedUser() ? user.getAccountId() : null; + Account.Id accountId; + if ((accountId = Account.Id.fromRef(refName)) != null) { + // Account ref is visible only to the corresponding account. + if (accountId.equals(currentUserAccountId) + && projectControl.controlForRef(refName).hasReadPermissionOnRef(true)) { + return true; + } + return false; + } + + // Group visibility + AccountGroup.UUID accountGroupUuid; + if ((accountGroupUuid = AccountGroup.UUID.fromRef(refName)) != null) { + // Group ref is visible only to the corresponding owner group. + try { + return projectControl.controlForRef(refName).hasReadPermissionOnRef(true) + && groupControlFactory.controlFor(user, accountGroupUuid).isOwner(); + } catch (NoSuchGroupException e) { + // The group is broken, but the ref is still around. Pretend the ref is not visible. + logger.atWarning().withCause(e).log("Found group ref %s but group isn't parsable", refName); + return false; + } + } + + // We are done checking all cases where we would allow access to Gerrit-managed refs. Deny + // access in case we got this far. + logger.atFine().log( + "Denying access to %s because user doesn't have access to this Gerrit ref", refName); + return false; + } + + private boolean visibleEdit(String refName, ProjectControl projectControl, ChangeData cd) + throws PermissionBackendException { + Change.Id id = Change.Id.fromEditRefPart(refName); + if (id == null) { + throw new IllegalStateException("unable to parse change id from edit ref " + refName); + } + + if (!isVisible(projectControl.controlFor(getNotes(cd)).setChangeData(cd))) { + // The user can't see the change so they can't see any edits. + return false; + } + + if (projectControl.getUser().isIdentifiedUser() + && refName.startsWith( + RefNames.refsEditPrefix(projectControl.getUser().asIdentifiedUser().getAccountId()))) { + logger.atFinest().log("Own change edit ref is visible: %s", refName); + return true; + } + + try { + // Default to READ_PRIVATE_CHANGES as there is no special permission for reading edits. + projectControl + .asForProject() + .ref(cd.change().getDest().get()) + .check(RefPermission.READ_PRIVATE_CHANGES); + logger.atFinest().log("Foreign change edit ref is visible: %s", refName); + return true; + } catch (AuthException e) { + logger.atFinest().log("Foreign change edit ref is not visible: %s", refName); + return false; + } catch (OrmException e) { + throw new PermissionBackendException(e); + } + } + + private ChangeNotes getNotes(ChangeData cd) throws PermissionBackendException { + try { + return cd.notes(); + } catch (OrmException e) { + throw new PermissionBackendException(e); + } + } + + private boolean isVisible(ChangeControl changeControl) throws PermissionBackendException { + try { + Optional<ReviewDb> db = getReviewDb(); + return changeControl.isVisible(dbProvider.get()); + } catch (OrmException e) { + throw new PermissionBackendException(e); + } + } + + private Optional<ReviewDb> getReviewDb() { + try { + return Optional.of(dbProvider.get()); + } catch (Exception e) { + return Optional.absent(); + } + } + + /** Helper to establish a database connection. */ + private class CloseableOneTimeReviewDb implements AutoCloseable { + @Nullable private final ManualRequestContext ctx; + + CloseableOneTimeReviewDb() throws PermissionBackendException { + if (!getReviewDb().isPresent()) { + try { + ctx = oneOffRequestContext.open(); + } catch (OrmException e) { + throw new PermissionBackendException(e); + } + } else { + ctx = null; + } + } + + @Override + public void close() { + if (ctx != null) { + ctx.close(); + } + } + } +} diff --git a/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java b/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java index e6dfa1d865..99d14d5a83 100644 --- a/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java +++ b/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java @@ -1117,6 +1117,7 @@ public class ChangeIT extends AbstractDaemonTest { throws Exception { try { setApiUser(owner); + allow(projectName, "refs/*", Permission.VIEW_PRIVATE_CHANGES, ANONYMOUS_USERS); ChangeInput in = new ChangeInput(); in.project = projectName.get(); in.branch = "refs/heads/master"; diff --git a/javatests/com/google/gerrit/acceptance/api/group/GroupsIT.java b/javatests/com/google/gerrit/acceptance/api/group/GroupsIT.java index e2d390e6fd..c6da810896 100644 --- a/javatests/com/google/gerrit/acceptance/api/group/GroupsIT.java +++ b/javatests/com/google/gerrit/acceptance/api/group/GroupsIT.java @@ -1018,6 +1018,8 @@ public class GroupsIT extends AbstractDaemonTest { @Test public void pushToDeletedGroupBranchIsRejectedForAllUsersRepo() throws Exception { + // refs/deleted-groups is only visible with ACCESS_DATABASE + allowGlobalCapabilities(REGISTERED_USERS, GlobalCapability.ACCESS_DATABASE); String groupRef = RefNames.refsDeletedGroups( new AccountGroup.UUID(gApi.groups().create(name("foo")).get().id)); @@ -1196,6 +1198,8 @@ public class GroupsIT extends AbstractDaemonTest { @Test public void cannotDeleteDeletedGroupBranch() throws Exception { + // refs/deleted-groups is only visible with ACCESS_DATABASE + allowGlobalCapabilities(REGISTERED_USERS, GlobalCapability.ACCESS_DATABASE); String groupRef = RefNames.refsDeletedGroups(new AccountGroup.UUID(name("foo"))); createBranch(allUsers, groupRef); testCannotDeleteGroupBranch(RefNames.REFS_DELETED_GROUPS + "*", groupRef); diff --git a/javatests/com/google/gerrit/acceptance/rest/project/AbstractPushTag.java b/javatests/com/google/gerrit/acceptance/rest/project/AbstractPushTag.java index 12468c3734..f095ae106b 100644 --- a/javatests/com/google/gerrit/acceptance/rest/project/AbstractPushTag.java +++ b/javatests/com/google/gerrit/acceptance/rest/project/AbstractPushTag.java @@ -31,6 +31,8 @@ import com.google.gerrit.common.data.Permission; import com.google.gerrit.reviewdb.client.AccountGroup; import com.google.gerrit.reviewdb.client.RefNames; import com.google.gerrit.server.project.testing.Util; +import com.google.gerrit.testing.ConfigSuite; +import org.eclipse.jgit.lib.Config; import org.eclipse.jgit.lib.PersonIdent; import org.eclipse.jgit.revwalk.RevCommit; import org.eclipse.jgit.transport.PushResult; @@ -51,6 +53,13 @@ public abstract class AbstractPushTag extends AbstractDaemonTest { } } + @ConfigSuite.Config + public static Config skipFalse() { + Config config = new Config(); + config.setBoolean("auth", null, "skipFullRefEvaluationIfAllRefsAreVisible", false); + return config; + } + private RevCommit initialHead; private TagType tagType; @@ -90,6 +99,20 @@ public abstract class AbstractPushTag extends AbstractDaemonTest { } @Test + public void createTagForExistingCommit_withoutGlobalReadPermissions() throws Exception { + removeReadAccessOnRefsStar(); + grantReadAccessOnRefsHeadsStar(); + createTagForExistingCommit(); + } + + @Test + public void createTagForNewCommit_withoutGlobalReadPermissions() throws Exception { + removeReadAccessOnRefsStar(); + grantReadAccessOnRefsHeadsStar(); + createTagForNewCommit(); + } + + @Test public void fastForward() throws Exception { allowTagCreation(); String tagName = pushTagForExistingCommit(Status.OK); @@ -106,6 +129,15 @@ public abstract class AbstractPushTag extends AbstractDaemonTest { fastForwardTagToExistingCommit(tagName, expectedStatus); fastForwardTagToNewCommit(tagName, expectedStatus); + // Above we just fast-forwarded the tag to a new commit which is not part of any branch. By + // default this tag is not visible, as users can only see tags that point to commits that are + // part of visible branches, which is not the case for this tag. It's odd that we allow the user + // to create such a tag that is then not visible to the creator. Below we want to fast-forward + // this tag, but this is only possible if the tag is visible. To make it visible we must allow + // the user to read all tags, regardless of whether it points to a commit that is part of a + // visible branch. + allowReadingAllTag(); + allowForcePushOnRefsTags(); fastForwardTagToExistingCommit(tagName, Status.OK); fastForwardTagToNewCommit(tagName, Status.OK); @@ -227,6 +259,33 @@ public abstract class AbstractPushTag extends AbstractDaemonTest { assertThat(refUpdate.getStatus()).named(tagType.name()).isEqualTo(expectedStatus); } + private void removeReadAccessOnRefsStar() throws Exception { + removePermission(allProjects, "refs/heads/*", Permission.READ); + removePermission(project, "refs/heads/*", Permission.READ); + } + + private void grantReadAccessOnRefsHeadsStar() throws Exception { + grant(project, "refs/heads/*", Permission.READ, false, REGISTERED_USERS); + } + + private void allowReadingAllTag() throws Exception { + // Tags are only visible if the commits to which they point are part of a visible branch. + // To make all tags visible, including tags that point to commits that are not part of a visible + // branch, either auth.skipFullRefEvaluationIfAllRefsAreVisible in gerrit.config needs to be + // true, or the user must have READ access for all refs in the repository. + + if (cfg.getBoolean("auth", "skipFullRefEvaluationIfAllRefsAreVisible", true)) { + return; + } + + // By default READ access in the All-Projects project is granted to registered users on refs/*, + // which makes all refs, except refs/meta/config, visible to them. refs/meta/config is not + // visible since by default READ access to it is exclusively granted to the project owners only. + // This means to make all refs, and thus all tags, visible, we must allow registered users to + // see the refs/meta/config branch. + allow(project, "refs/meta/config", Permission.READ, REGISTERED_USERS); + } + private void allowTagCreation() throws Exception { grant(project, "refs/tags/*", tagType.createPermission, false, REGISTERED_USERS); } diff --git a/javatests/com/google/gerrit/acceptance/rest/project/GetBranchIT.java b/javatests/com/google/gerrit/acceptance/rest/project/GetBranchIT.java new file mode 100644 index 0000000000..c6e2484416 --- /dev/null +++ b/javatests/com/google/gerrit/acceptance/rest/project/GetBranchIT.java @@ -0,0 +1,454 @@ +// Copyright (C) 2020 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.acceptance.rest.project; + +import static com.google.common.truth.Truth.assertThat; +import static com.google.gerrit.server.group.SystemGroupBackend.ANONYMOUS_USERS; +import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS; +import static com.google.gerrit.testing.GerritJUnit.assertThrows; + +import com.google.gerrit.acceptance.AbstractDaemonTest; +import com.google.gerrit.acceptance.TestAccount; +import com.google.gerrit.acceptance.testsuite.group.GroupOperations; +import com.google.gerrit.common.data.GlobalCapability; +import com.google.gerrit.common.data.Permission; +import com.google.gerrit.extensions.api.changes.DraftInput; +import com.google.gerrit.extensions.api.projects.BranchInfo; +import com.google.gerrit.extensions.api.projects.TagInfo; +import com.google.gerrit.extensions.api.projects.TagInput; +import com.google.gerrit.extensions.restapi.ResourceNotFoundException; +import com.google.gerrit.extensions.restapi.RestApiException; +import com.google.gerrit.reviewdb.client.AccountGroup; +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.client.RefNames; +import com.google.gerrit.server.Sequences; +import com.google.gerrit.testing.ConfigSuite; +import com.google.gerrit.testing.NoteDbMode; +import com.google.inject.Inject; +import org.eclipse.jgit.junit.TestRepository; +import org.eclipse.jgit.lib.Config; +import org.eclipse.jgit.lib.Repository; +import org.junit.Test; + +public class GetBranchIT extends AbstractDaemonTest { + @Inject private GroupOperations groupOperations; + + @ConfigSuite.Config + public static Config skipFalse() { + Config config = new Config(); + config.setBoolean("auth", null, "skipFullRefEvaluationIfAllRefsAreVisible", false); + return config; + } + + @Test + public void cannotGetNonExistingBranch() { + assertBranchNotFound(project, RefNames.fullName("non-existing")); + } + + @Test + public void getBranch() throws Exception { + setApiUser(user); + assertBranchFound(project, RefNames.fullName("master")); + } + + @Test + public void getBranchByShortName() throws Exception { + setApiUser(user); + assertBranchFound(project, "master"); + } + + @Test + public void cannotGetNonVisibleBranch() throws Exception { + String branchName = "master"; + + // block read access to the branch + block(project, RefNames.fullName(branchName), Permission.READ, ANONYMOUS_USERS); + + setApiUser(user); + assertBranchNotFound(project, RefNames.fullName(branchName)); + } + + @Test + public void cannotGetNonVisibleBranchByShortName() throws Exception { + String branchName = "master"; + + // block read access to the branch + block(project, RefNames.fullName(branchName), Permission.READ, ANONYMOUS_USERS); + + setApiUser(user); + assertBranchNotFound(project, branchName); + } + + @Test + public void getChangeRef() throws Exception { + // create a change + Change.Id changeId = createChange("refs/for/master").getPatchSetId().changeId; + + // a user without the 'Access Database' capability can see the change ref + setApiUser(user); + String changeRef = RefNames.patchSetRef(new PatchSet.Id(changeId, 1)); + assertBranchFound(project, changeRef); + } + + @Test + public void getChangeRefOfNonVisibleChange() throws Exception { + // create a change + String branchName = "master"; + Change.Id changeId = createChange("refs/for/" + branchName).getPatchSetId().changeId; + + // block read access to the branch + block(project, RefNames.fullName(branchName), Permission.READ, ANONYMOUS_USERS); + + // a user without the 'Access Database' capability cannot see the change ref + setApiUser(user); + String changeRef = RefNames.patchSetRef(new PatchSet.Id(changeId, 1)); + assertBranchNotFound(project, changeRef); + + // a user with the 'Access Database' capability can see the change ref + testGetRefWithAccessDatabase(project, changeRef); + } + + @Test + public void getChangeEditRef() throws Exception { + TestAccount user2 = accountCreator.user2(); + + // create a change + Change.Id changeId = createChange("refs/for/master").getPatchSetId().changeId; + + // create a change edit by 'user' + setApiUser(user); + gApi.changes().id(changeId.get()).edit().create(); + + // every user can see their own change edit refs + String changeEditRef = RefNames.refsEdit(user.id, changeId, new PatchSet.Id(changeId, 1)); + assertBranchFound(project, changeEditRef); + + // a user without the 'Access Database' capability cannot see the change edit ref of another + // user + setApiUser(user2); + assertBranchNotFound(project, changeEditRef); + + // a user with the 'Access Database' capability can see the change edit ref of another user + testGetRefWithAccessDatabase(project, changeEditRef); + } + + @Test + public void cannotGetChangeEditRefOfNonVisibleChange() throws Exception { + // create a change + String branchName = "master"; + Change.Id changeId = createChange("refs/for/" + branchName).getPatchSetId().changeId; + + // create a change edit by 'user' + setApiUser(user); + gApi.changes().id(changeId.get()).edit().create(); + + // make the change non-visible by blocking read access on the destination + block(project, RefNames.fullName(branchName), Permission.READ, ANONYMOUS_USERS); + + // user cannot see their own change edit refs if the change is no longer visible + String changeEditRef = RefNames.refsEdit(user.id, changeId, new PatchSet.Id(changeId, 1)); + assertBranchNotFound(project, changeEditRef); + + // a user with the 'Access Database' capability can see the change edit ref + testGetRefWithAccessDatabase(project, changeEditRef); + } + + @Test + public void getChangeMetaRef() throws Exception { + // create a change + Change.Id changeId = createChange("refs/for/master").getPatchSetId().changeId; + + // A user without the 'Access Database' capability can see the change meta ref. + // This may be surprising, as 'Access Database' guards access to meta refs and the change meta + // ref is a meta ref, however change meta refs have been always visible to all users that can + // see the change and some tools rely on seeing these refs, so we have to keep the current + // behaviour. + setApiUser(user); + String changeMetaRef = RefNames.changeMetaRef(changeId); + if (NoteDbMode.get().ordinal() >= NoteDbMode.WRITE.ordinal()) { + // Branch is there when we write to NoteDb + assertBranchFound(project, changeMetaRef); + } + } + + @Test + public void getRefsMetaConfig() throws Exception { + // a non-project owner cannot get the refs/meta/config branch + setApiUser(user); + assertBranchNotFound(project, RefNames.REFS_CONFIG); + + // a non-project owner cannot get the refs/meta/config branch even with the 'Access Database' + // capability + allowGlobalCapabilities(REGISTERED_USERS, GlobalCapability.ACCESS_DATABASE); + try { + assertBranchNotFound(project, RefNames.REFS_CONFIG); + } finally { + removeGlobalCapabilities(REGISTERED_USERS, GlobalCapability.ACCESS_DATABASE); + } + + setApiUser(user); + + // a project owner can get the refs/meta/config branch + allow(project, "refs/*", Permission.OWNER, REGISTERED_USERS); + assertBranchFound(project, RefNames.REFS_CONFIG); + } + + @Test + public void getUserRefOfOtherUser() throws Exception { + String userRef = RefNames.refsUsers(admin.id); + + // a user without the 'Access Database' capability cannot see the user ref of another user + setApiUser(user); + assertBranchNotFound(allUsers, userRef); + + // a user with the 'Access Database' capability can see the user ref of another user + testGetRefWithAccessDatabase(allUsers, userRef); + } + + @Test + public void getOwnUserRef() throws Exception { + // every user can see the own user ref + setApiUser(user); + assertBranchFound(allUsers, RefNames.refsUsers(user.id)); + + // TODO: every user can see the own user ref via the magic ref/users/self ref + // setApiUser(user); + // assertBranchFound(allUsers, RefNames.REFS_USERS_SELF); + } + + @Test + public void getExternalIdsRefs() throws Exception { + // a user without the 'Access Database' capability cannot see the refs/meta/external-ids ref + setApiUser(user); + assertBranchNotFound(allUsers, RefNames.REFS_EXTERNAL_IDS); + + // a user with the 'Access Database' capability can see the refs/meta/external-ids ref + testGetRefWithAccessDatabase(allUsers, RefNames.REFS_EXTERNAL_IDS); + } + + @Test + public void getGroupRef() throws Exception { + // create a group + AccountGroup.UUID ownerGroupUuid = + groupOperations.newGroup().name("owner-group").addMember(admin.id).create(); + AccountGroup.UUID testGroupUuid = + groupOperations.newGroup().name("test-group").ownerGroupUuid(ownerGroupUuid).create(); + + // a non-group owner without the 'Access Database' capability cannot see the group ref + setApiUser(user); + String groupRef = RefNames.refsGroups(testGroupUuid); + assertBranchNotFound(allUsers, groupRef); + + // a non-group owner with the 'Access Database' capability can see the group ref + testGetRefWithAccessDatabase(allUsers, groupRef); + + // a group owner can see the group ref if the group ref is visible + groupOperations.group(ownerGroupUuid).forUpdate().addMember(user.id).update(); + assertBranchFound(allUsers, groupRef); + + // A group owner cannot see the group ref if the group ref is not visible. + // The READ access for refs/groups/* must be blocked on All-Projects rather than All-Users. + // This is because READ access for refs/groups/* on All-Users is by default granted to + // REGISTERED_USERS, and if an ALLOW rule and a BLOCK rule are on the same project and ref, + // the ALLOW rule takes precedence. + block(allProjects, "refs/groups/*", Permission.READ, ANONYMOUS_USERS); + assertBranchNotFound(allUsers, groupRef); + } + + @Test + public void getGroupNamesRef() throws Exception { + // a user without the 'Access Database' capability cannot see the refs/meta/group-names ref + setApiUser(user); + assertBranchNotFound(allUsers, RefNames.REFS_GROUPNAMES); + + // a user with the 'Access Database' capability can see the refs/meta/group-names ref + testGetRefWithAccessDatabase(allUsers, RefNames.REFS_GROUPNAMES); + } + + @Test + public void getDeletedGroupRef() throws Exception { + // Create a deleted group ref. We must create a directly in the repo, since group deletion is + // not supported yet. + String deletedGroupRef = RefNames.refsDeletedGroups(new AccountGroup.UUID("deleted-group")); + TestRepository<Repository> testRepo = + new TestRepository<>(repoManager.openRepository(allUsers)); + testRepo + .branch(deletedGroupRef) + .commit() + .message("Some Message") + .add("group.config", "content") + .create(); + + setApiUser(user); + assertBranchNotFound(allUsers, deletedGroupRef); + + // a user with the 'Access Database' capability can see the deleted group ref + testGetRefWithAccessDatabase(allUsers, deletedGroupRef); + } + + @Test + public void getDraftCommentsRef() throws Exception { + TestAccount user2 = accountCreator.user2(); + + // create a change + String fileName = "a.txt"; + Change change = createChange("A Change", fileName, "content").getChange().change(); + + // create a draft comment by the by 'user' + setApiUser(user); + DraftInput draftInput = new DraftInput(); + draftInput.path = fileName; + draftInput.line = 0; + draftInput.message = "Some Comment"; + gApi.changes().id(change.getChangeId()).current().createDraft(draftInput); + + // every user can see their own draft comments refs + // TODO: is this a bug? + String draftCommentsRef = RefNames.refsDraftComments(change.getId(), user.id); + if (NoteDbMode.get().ordinal() >= NoteDbMode.WRITE.ordinal()) { + // Branch is there when we write to NoteDb + assertBranchFound(allUsers, draftCommentsRef); + } + + // a user without the 'Access Database' capability cannot see the draft comments ref of another + // user + setApiUser(user2); + assertBranchNotFound(allUsers, draftCommentsRef); + + // a user with the 'Access Database' capability can see the draft comments ref of another user + if (NoteDbMode.get().ordinal() >= NoteDbMode.WRITE.ordinal()) { + // Branch is there when we write to NoteDb + testGetRefWithAccessDatabase(allUsers, draftCommentsRef); + } + } + + @Test + public void getStarredChangesRef() throws Exception { + TestAccount user2 = accountCreator.user2(); + + // create a change + Change change = createChange().getChange().change(); + + // let user star the change + setApiUser(user); + gApi.accounts().self().starChange(Integer.toString(change.getChangeId())); + + // every user can see their own starred changes refs + // TODO: is this a bug? + String starredChangesRef = RefNames.refsStarredChanges(change.getId(), user.id); + assertBranchFound(allUsers, starredChangesRef); + + // a user without the 'Access Database' capability cannot see the starred changes ref of another + // user + setApiUser(user2); + assertBranchNotFound(allUsers, starredChangesRef); + + // a user with the 'Access Database' capability can see the starred changes ref of another user + testGetRefWithAccessDatabase(allUsers, starredChangesRef); + } + + @Test + public void getTagRef() throws Exception { + // create a tag + TagInput input = new TagInput(); + input.message = "My Tag"; + input.revision = gApi.projects().name(project.get()).head(); + TagInfo tagInfo = gApi.projects().name(project.get()).tag("my-tag").create(input).get(); + + // any user who can see the project, can see the tag + setApiUser(user); + assertBranchFound(project, tagInfo.ref); + } + + @Test + public void cannotGetTagRefThatPointsToNonVisibleBranch() throws Exception { + // create a tag + TagInput input = new TagInput(); + input.message = "My Tag"; + input.revision = gApi.projects().name(project.get()).head(); + TagInfo tagInfo = gApi.projects().name(project.get()).tag("my-tag").create(input).get(); + + // block read access to the branch + block(allProjects, RefNames.fullName("master"), Permission.READ, ANONYMOUS_USERS); + + // if the user cannot see the project, the tag is not visible + setApiUser(user); + assertBranchNotFound(project, tagInfo.ref); + } + + @Test + public void getSymbolicRef() throws Exception { + // 'HEAD' is visible since it points to 'master' that is visible + setApiUser(user); + assertBranchFound(project, "HEAD"); + } + + @Test + public void cannotGetSymbolicRefThatPointsToNonVisibleBranch() throws Exception { + // block read access to the branch to which HEAD points by default + block(allProjects, RefNames.fullName("master"), Permission.READ, ANONYMOUS_USERS); + + // since 'master' is not visible, 'HEAD' which points to 'master' is also not visible + setApiUser(user); + assertBranchNotFound(project, "HEAD"); + } + + @Test + public void getAccountSequenceRef() throws Exception { + // a user without the 'Access Database' capability cannot see the refs/sequences/accounts ref + setApiUser(user); + String accountSequenceRef = RefNames.REFS_SEQUENCES + Sequences.NAME_ACCOUNTS; + assertBranchNotFound(allUsers, accountSequenceRef); + + // a user with the 'Access Database' capability can see the refs/sequences/accounts ref + testGetRefWithAccessDatabase(allUsers, accountSequenceRef); + } + + @Test + public void getGroupSequenceRef() throws Exception { + // a user without the 'Access Database' capability cannot see the refs/sequences/groups ref + setApiUser(user); + String groupSequenceRef = RefNames.REFS_SEQUENCES + Sequences.NAME_GROUPS; + assertBranchNotFound(allUsers, groupSequenceRef); + + // a user with the 'Access Database' capability can see the refs/sequences/groups ref + testGetRefWithAccessDatabase(allUsers, groupSequenceRef); + } + + private void testGetRefWithAccessDatabase(Project.NameKey project, String ref) throws Exception { + allowGlobalCapabilities(REGISTERED_USERS, GlobalCapability.ACCESS_DATABASE); + try { + setApiUser(user); + assertBranchFound(project, ref); + } finally { + removeGlobalCapabilities(REGISTERED_USERS, GlobalCapability.ACCESS_DATABASE); + } + } + + private void assertBranchNotFound(Project.NameKey project, String ref) { + ResourceNotFoundException exception = + assertThrows( + ResourceNotFoundException.class, + () -> gApi.projects().name(project.get()).branch(ref).get()); + assertThat(exception).hasMessageThat().isEqualTo("Not found: " + ref); + } + + private void assertBranchFound(Project.NameKey project, String ref) throws RestApiException { + BranchInfo branchInfo = gApi.projects().name(project.get()).branch(ref).get(); + assertThat(branchInfo.ref).isEqualTo(RefNames.fullName(ref)); + } +} diff --git a/javatests/com/google/gerrit/server/permissions/RefControlTest.java b/javatests/com/google/gerrit/server/permissions/RefControlTest.java index 0d19984427..0f0f1c3ec1 100644 --- a/javatests/com/google/gerrit/server/permissions/RefControlTest.java +++ b/javatests/com/google/gerrit/server/permissions/RefControlTest.java @@ -106,6 +106,14 @@ public class RefControlTest { assertThat(u.isOwner()).named("not owner").isFalse(); } + private void assertAllRefsAreVisible(ProjectControl u) { + assertThat(u.allRefsAreVisible(Collections.emptySet())).named("all refs visible").isTrue(); + } + + private void assertAllRefsAreNotVisible(ProjectControl u) { + assertThat(u.allRefsAreVisible(Collections.emptySet())).named("all refs NOT visible").isFalse(); + } + private void assertNotOwner(String ref, ProjectControl u) { assertThat(u.controlForRef(ref).isOwner()).named("NOT OWN " + ref).isFalse(); } @@ -121,11 +129,17 @@ public class RefControlTest { } private void assertCanRead(String ref, ProjectControl u) { - assertThat(u.controlForRef(ref).isVisible()).named("can read " + ref).isTrue(); + assertThat(u.controlForRef(ref).hasReadPermissionOnRef(true)) + // This should be false but the test relies on inheritance into refs/tags + .named("can read " + ref) + .isTrue(); } private void assertCannotRead(String ref, ProjectControl u) { - assertThat(u.controlForRef(ref).isVisible()).named("cannot read " + ref).isFalse(); + assertThat(u.controlForRef(ref).hasReadPermissionOnRef(true)) + // This should be false but the test relies on inheritance into refs/tags + .named("cannot read " + ref) + .isFalse(); } private void assertCanSubmit(String ref, ProjectControl u) { @@ -189,6 +203,7 @@ public class RefControlTest { private final Map<Project.NameKey, ProjectState> all = new HashMap<>(); private Project.NameKey localKey = new Project.NameKey("local"); private ProjectConfig local; + private ProjectConfig allUsers; private Project.NameKey parentKey = new Project.NameKey("parent"); private ProjectConfig parent; private InMemoryRepositoryManager repoManager; @@ -206,6 +221,7 @@ public class RefControlTest { @Inject private DefaultRefFilter.Factory refFilterFactory; @Inject private TransferConfig transferConfig; @Inject private MetricMaker metricMaker; + @Inject private RefVisibilityControl refVisibilityControl; @Before public void setUp() throws Exception { @@ -219,7 +235,7 @@ public class RefControlTest { @Override public ProjectState getAllUsers() { - return null; + return get(allUsersName); } @Override @@ -273,12 +289,17 @@ public class RefControlTest { injector.injectMembers(this); try { - Repository repo = repoManager.createRepository(allProjectsName); + Repository allProjectsRepo = repoManager.createRepository(allProjectsName); ProjectConfig allProjects = new ProjectConfig(new Project.NameKey(allProjectsName.get())); - allProjects.load(repo); + allProjects.load(allProjectsRepo); LabelType cr = Util.codeReview(); allProjects.getLabelSections().put(cr.getName(), cr); add(allProjects); + + Repository allUsersRepo = repoManager.createRepository(allUsersName); + allUsers = new ProjectConfig(new Project.NameKey(allUsersName.get())); + allUsers.load(allUsersRepo); + add(allUsers); } catch (IOException | ConfigInvalidException e) { throw new RuntimeException(e); } @@ -353,6 +374,24 @@ public class RefControlTest { } @Test + public void allRefsAreVisibleForRegularProject() throws Exception { + allow(local, READ, DEVS, "refs/*"); + allow(local, READ, DEVS, "refs/groups/*"); + allow(local, READ, DEVS, "refs/users/default"); + + assertAllRefsAreVisible(user(local, DEVS)); + } + + @Test + public void allRefsAreNotVisibleForAllUsers() throws Exception { + allow(allUsers, READ, DEVS, "refs/*"); + allow(allUsers, READ, DEVS, "refs/groups/*"); + allow(allUsers, READ, DEVS, "refs/users/default"); + + assertAllRefsAreNotVisible(user(allUsers, DEVS)); + } + + @Test public void branchDelegation1() throws Exception { allow(local, OWNER, ADMIN, "refs/*"); allow(local, OWNER, DEVS, "refs/heads/x/*"); @@ -1012,7 +1051,10 @@ public class RefControlTest { sectionSorter, changeControlFactory, permissionBackend, + refVisibilityControl, + repoManager, refFilterFactory, + allUsersName, new MockUser(name, memberOf), newProjectState(local)); } diff --git a/tools/BUILD b/tools/BUILD index 9a53c8b6c6..c246c57a4f 100644 --- a/tools/BUILD +++ b/tools/BUILD @@ -39,7 +39,7 @@ java_package_configuration( "-Xep:ElementsCountedInLoop:WARN", "-Xep:EqualsHashCode:WARN", "-Xep:EqualsIncompatibleType:WARN", - "-Xep:ExpectedExceptionChecker:ERROR", + "-Xep:ExpectedExceptionChecker:WARN", "-Xep:Finally:WARN", "-Xep:FloatingPointLiteralPrecision:WARN", "-Xep:FragmentInjection:WARN", diff --git a/tools/maven/gerrit-acceptance-framework_pom.xml b/tools/maven/gerrit-acceptance-framework_pom.xml index d3c5f35813..8c0d0d1d44 100644 --- a/tools/maven/gerrit-acceptance-framework_pom.xml +++ b/tools/maven/gerrit-acceptance-framework_pom.xml @@ -2,7 +2,7 @@ <modelVersion>4.0.0</modelVersion> <groupId>com.google.gerrit</groupId> <artifactId>gerrit-acceptance-framework</artifactId> - <version>2.16.24-SNAPSHOT</version> + <version>2.16.26-SNAPSHOT</version> <packaging>jar</packaging> <name>Gerrit Code Review - Acceptance Test Framework</name> <description>Framework for Gerrit's acceptance tests</description> diff --git a/tools/maven/gerrit-extension-api_pom.xml b/tools/maven/gerrit-extension-api_pom.xml index 7ab1abfc0a..9c82d72b42 100644 --- a/tools/maven/gerrit-extension-api_pom.xml +++ b/tools/maven/gerrit-extension-api_pom.xml @@ -2,7 +2,7 @@ <modelVersion>4.0.0</modelVersion> <groupId>com.google.gerrit</groupId> <artifactId>gerrit-extension-api</artifactId> - <version>2.16.24-SNAPSHOT</version> + <version>2.16.26-SNAPSHOT</version> <packaging>jar</packaging> <name>Gerrit Code Review - Extension API</name> <description>API for Gerrit Extensions</description> diff --git a/tools/maven/gerrit-plugin-api_pom.xml b/tools/maven/gerrit-plugin-api_pom.xml index e783e366a6..1343db2a98 100644 --- a/tools/maven/gerrit-plugin-api_pom.xml +++ b/tools/maven/gerrit-plugin-api_pom.xml @@ -2,7 +2,7 @@ <modelVersion>4.0.0</modelVersion> <groupId>com.google.gerrit</groupId> <artifactId>gerrit-plugin-api</artifactId> - <version>2.16.24-SNAPSHOT</version> + <version>2.16.26-SNAPSHOT</version> <packaging>jar</packaging> <name>Gerrit Code Review - Plugin API</name> <description>API for Gerrit Plugins</description> diff --git a/tools/maven/gerrit-plugin-gwtui_pom.xml b/tools/maven/gerrit-plugin-gwtui_pom.xml index 61adfae4b5..a86de7260f 100644 --- a/tools/maven/gerrit-plugin-gwtui_pom.xml +++ b/tools/maven/gerrit-plugin-gwtui_pom.xml @@ -2,7 +2,7 @@ <modelVersion>4.0.0</modelVersion> <groupId>com.google.gerrit</groupId> <artifactId>gerrit-plugin-gwtui</artifactId> - <version>2.16.24-SNAPSHOT</version> + <version>2.16.26-SNAPSHOT</version> <packaging>jar</packaging> <name>Gerrit Code Review - Plugin GWT UI</name> <description>Common Classes for Gerrit GWT UI Plugins</description> diff --git a/tools/maven/gerrit-war_pom.xml b/tools/maven/gerrit-war_pom.xml index ff8f63f824..333dd05bf0 100644 --- a/tools/maven/gerrit-war_pom.xml +++ b/tools/maven/gerrit-war_pom.xml @@ -2,7 +2,7 @@ <modelVersion>4.0.0</modelVersion> <groupId>com.google.gerrit</groupId> <artifactId>gerrit-war</artifactId> - <version>2.16.24-SNAPSHOT</version> + <version>2.16.26-SNAPSHOT</version> <packaging>war</packaging> <name>Gerrit Code Review - WAR</name> <description>Gerrit WAR</description> diff --git a/version.bzl b/version.bzl index 63e650ea46..3a35896dbd 100644 --- a/version.bzl +++ b/version.bzl @@ -2,4 +2,4 @@ # Used by :api_install and :api_deploy targets # when talking to the destination repository. # -GERRIT_VERSION = "2.16.24-SNAPSHOT" +GERRIT_VERSION = "2.16.26-SNAPSHOT" |