summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorLuca Milanesio <luca.milanesio@gmail.com>2020-11-19 00:42:49 +0000
committerLuca Milanesio <luca.milanesio@gmail.com>2020-11-19 00:42:49 +0000
commita0eeeb5cb6346d97c36bb72f60d35c0690009148 (patch)
treeaf587b6b477c1fe427f27606fad0eae583b90ec3
parenteb09fdaa338c8bc3ffde8ad3b7dfc56781293215 (diff)
parentb3b98a35c4a60a81d6fdb0144902ecb2763af63c (diff)
Merge branch 'stable-2.15-2020-11.notedb-refs-tags' into stable-2.15
* stable-2.15-2020-11.notedb-refs-tags: Set version to 2.15.22-SNAPSHOT Set version to 2.15.21 Workaround Gitiles bug on All-Users visibility Set version to 2.15.21-SNAPSHOT Set version to 2.15.20 Fetch JGit documentation from the archive site Remove generation for c.g.gwtexpui.* JavaDoc Make PermissionBackend#ForRef authoritative Validate Gerrit changes on stable-2.15 with Jenkins Fix tests for stable-2.15 branch Change-Id: I91db12c2c627550b2e897ccb4d7e27ee760cd32d
-rw-r--r--Jenkinsfile1
-rw-r--r--gerrit-acceptance-framework/pom.xml2
-rw-r--r--gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/AbstractPushTag.java23
-rw-r--r--gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/GetBranchIT.java384
-rw-r--r--gerrit-extension-api/pom.xml2
-rw-r--r--gerrit-plugin-api/pom.xml2
-rw-r--r--gerrit-plugin-gwtui/BUILD4
-rw-r--r--gerrit-plugin-gwtui/pom.xml2
-rw-r--r--gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/RefNames.java21
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/git/VisibleRefFilter.java89
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/permissions/RefVisibilityControl.java215
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/project/ChangeControl.java37
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectControl.java33
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/project/RefControl.java90
-rw-r--r--gerrit-server/src/test/java/com/google/gerrit/server/project/RefControlTest.java53
-rw-r--r--gerrit-war/pom.xml2
-rw-r--r--lib/jgit/jgit.bzl2
-rw-r--r--polygerrit-ui/app/BUILD1
-rw-r--r--tools/BUILD2
-rw-r--r--version.bzl2
20 files changed, 871 insertions, 96 deletions
diff --git a/Jenkinsfile b/Jenkinsfile
new file mode 100644
index 0000000000..565dd45586
--- /dev/null
+++ b/Jenkinsfile
@@ -0,0 +1 @@
+gerritPipeline()
diff --git a/gerrit-acceptance-framework/pom.xml b/gerrit-acceptance-framework/pom.xml
index 82199d88e4..34dd6a3a1f 100644
--- a/gerrit-acceptance-framework/pom.xml
+++ b/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.15.20-SNAPSHOT</version>
+ <version>2.15.22-SNAPSHOT</version>
<packaging>jar</packaging>
<name>Gerrit Code Review - Acceptance Test Framework</name>
<description>Framework for Gerrit's acceptance tests</description>
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/AbstractPushTag.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/AbstractPushTag.java
index cea907d941..101a9affe4 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/AbstractPushTag.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/AbstractPushTag.java
@@ -157,6 +157,29 @@ public abstract class AbstractPushTag extends AbstractDaemonTest {
pushTagDeletion(tagName, Status.OK);
}
+ @Test
+ public void createTagForExistingCommit_withoutGlobalReadPermissions() throws Exception {
+ removeReadAccessOnRefsStar();
+ grantReadAccessOnRefsHeadsStar();
+ createTagForExistingCommit();
+ }
+
+ @Test
+ public void createTagForNewCommit_withoutGlobalReadPermissions() throws Exception {
+ removeReadAccessOnRefsStar();
+ grantReadAccessOnRefsHeadsStar();
+ createTagForNewCommit();
+ }
+
+ 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 String pushTagForExistingCommit(Status expectedStatus) throws Exception {
return pushTag(null, false, false, expectedStatus);
}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/GetBranchIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/GetBranchIT.java
new file mode 100644
index 0000000000..bd93a44150
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/GetBranchIT.java
@@ -0,0 +1,384 @@
+// 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.testutil.GerritJUnit.assertThrows;
+
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.TestAccount;
+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.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.config.AllUsersName;
+import com.google.gerrit.testutil.NoteDbMode;
+import com.google.inject.Inject;
+import org.junit.Test;
+
+public class GetBranchIT extends AbstractDaemonTest {
+ @Inject protected AllUsersName allUsers;
+
+ @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 {
+ // Groups were not yet in NoteDb in this release.
+ }
+
+ @Test
+ public void getGroupNamesRef() throws Exception {
+ // Groups were not yet in NoteDb in this release.
+ }
+
+ @Test
+ public void getDeletedGroupRef() throws Exception {
+ // Groups were not yet in NoteDb in this release.
+ }
+
+ @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(project, 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(project, 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 {
+ // Sequences were not yet in NoteDb in this release.
+ }
+
+ @Test
+ public void getGroupSequenceRef() throws Exception {
+ // Sequences were not yet in NoteDb in this release.
+ }
+
+ 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/gerrit-extension-api/pom.xml b/gerrit-extension-api/pom.xml
index fefe81d734..dabc892678 100644
--- a/gerrit-extension-api/pom.xml
+++ b/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.15.20-SNAPSHOT</version>
+ <version>2.15.22-SNAPSHOT</version>
<packaging>jar</packaging>
<name>Gerrit Code Review - Extension API</name>
<description>API for Gerrit Extensions</description>
diff --git a/gerrit-plugin-api/pom.xml b/gerrit-plugin-api/pom.xml
index 9c24d1163f..701c788de4 100644
--- a/gerrit-plugin-api/pom.xml
+++ b/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.15.20-SNAPSHOT</version>
+ <version>2.15.22-SNAPSHOT</version>
<packaging>jar</packaging>
<name>Gerrit Code Review - Plugin API</name>
<description>API for Gerrit Plugins</description>
diff --git a/gerrit-plugin-gwtui/BUILD b/gerrit-plugin-gwtui/BUILD
index 86f24bbc8f..1f75912f90 100644
--- a/gerrit-plugin-gwtui/BUILD
+++ b/gerrit-plugin-gwtui/BUILD
@@ -74,10 +74,6 @@ java_doc(
],
pkgs = [
"com.google.gerrit.plugin",
- "com.google.gwtexpui.clippy",
- "com.google.gwtexpui.globalkey",
- "com.google.gwtexpui.safehtml",
- "com.google.gwtexpui.user",
],
title = "Gerrit Review GWT Extension API Documentation",
)
diff --git a/gerrit-plugin-gwtui/pom.xml b/gerrit-plugin-gwtui/pom.xml
index ccca22a5de..addc365519 100644
--- a/gerrit-plugin-gwtui/pom.xml
+++ b/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.15.20-SNAPSHOT</version>
+ <version>2.15.22-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/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/RefNames.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/RefNames.java
index 89de9dcd64..16896aab96 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/RefNames.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/RefNames.java
@@ -101,11 +101,32 @@ public class RefNames {
return 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.
+ */
+ 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_SEQUENCES)
+ || ref.startsWith(REFS_USERS)
+ || ref.startsWith(REFS_STARRED_CHANGES)
+ || ref.startsWith(REFS_REJECT_COMMITS);
+ }
+
public static String changeMetaRef(Change.Id id) {
StringBuilder r = newStringBuilder().append(REFS_CHANGES);
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();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/VisibleRefFilter.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/VisibleRefFilter.java
index c16c195d3a..9b368beeae 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/VisibleRefFilter.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/VisibleRefFilter.java
@@ -14,7 +14,6 @@
package com.google.gerrit.server.git;
-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;
@@ -38,6 +37,7 @@ import com.google.gerrit.server.permissions.PermissionBackend;
import com.google.gerrit.server.permissions.PermissionBackendException;
import com.google.gerrit.server.permissions.ProjectPermission;
import com.google.gerrit.server.permissions.RefPermission;
+import com.google.gerrit.server.permissions.RefVisibilityControl;
import com.google.gerrit.server.project.ProjectControl;
import com.google.gerrit.server.project.ProjectState;
import com.google.gerrit.server.query.change.ChangeData;
@@ -80,6 +80,7 @@ public class VisibleRefFilter extends AbstractAdvertiseRefsHook {
private final PermissionBackend.ForProject perm;
private final ProjectState projectState;
private final Repository git;
+ private final RefVisibilityControl refVisibilityControl;
private ProjectControl projectCtl;
private boolean showMetadata = true;
private String userEditPrefix;
@@ -93,6 +94,7 @@ public class VisibleRefFilter extends AbstractAdvertiseRefsHook {
Provider<ReviewDb> db,
Provider<CurrentUser> user,
PermissionBackend permissionBackend,
+ RefVisibilityControl refVisibilityControl,
@Assisted ProjectState projectState,
@Assisted Repository git) {
this.tagCache = tagCache;
@@ -105,6 +107,7 @@ public class VisibleRefFilter extends AbstractAdvertiseRefsHook {
permissionBackend.user(user).database(db).project(projectState.getProject().getNameKey());
this.projectState = projectState;
this.git = git;
+ this.refVisibilityControl = refVisibilityControl;
}
/** Show change references. Default is {@code true}. */
@@ -128,68 +131,54 @@ public class VisibleRefFilter extends AbstractAdvertiseRefsHook {
}
}
- Account.Id userId;
- boolean viewMetadata;
+ boolean hasAccessDatabase;
if (user.get().isIdentifiedUser()) {
- viewMetadata = withUser.testOrFalse(GlobalPermission.ACCESS_DATABASE);
+ hasAccessDatabase = withUser.testOrFalse(GlobalPermission.ACCESS_DATABASE);
IdentifiedUser u = user.get().asIdentifiedUser();
- userId = u.getAccountId();
+ Account.Id userId = u.getAccountId();
userEditPrefix = RefNames.refsEditPrefix(userId);
} else {
- userId = null;
- viewMetadata = false;
+ hasAccessDatabase = false;
}
- Map<String, Ref> result = new HashMap<>();
+ Map<String, Ref> resultRefs = new HashMap<>();
List<Ref> deferredTags = new ArrayList<>();
projectCtl = projectState.controlFor(user.get());
for (Ref ref : refs.values()) {
- String name = ref.getName();
+ String refName = ref.getName();
Change.Id changeId;
- Account.Id accountId;
- if (name.startsWith(REFS_CACHE_AUTOMERGE) || (!showMetadata && isMetadata(name))) {
- continue;
- } else if (RefNames.isRefsEdit(name)) {
- // Edits are visible only to the owning user, if change is visible.
- if (viewMetadata || visibleEdit(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(changeId)) {
- result.put(name, ref);
- }
- } else if ((accountId = Account.Id.fromRef(name)) != null) {
- // Account ref is visible only to corresponding account.
- if (viewMetadata || (accountId.equals(userId) && canReadRef(name))) {
- result.put(name, ref);
- }
+ if (!showMetadata && isMetadata(refName)) {
+ log.debug("Filter out metadata ref %s", refName);
} else if (isTag(ref)) {
// If its a tag, consider it later.
if (ref.getObjectId() != null) {
+ log.debug("Defer tag ref %s", refName);
deferredTags.add(ref);
+ } else {
+ log.debug("Filter out tag ref %s that is not a tag", refName);
}
- } else if (name.startsWith(RefNames.REFS_SEQUENCES)) {
- // Sequences are internal database implementation details.
- 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(changeId)) {
+ log.debug("Filter out invisible change ref %s", refName);
+ } else if (RefNames.isRefsEdit(refName) && !visibleEdit(refName)) {
+ log.debug("Filter out invisible change edit ref %s", refName);
+ } else {
+ // Change is visible
+ resultRefs.put(refName, ref);
}
- } else if (projectState.isAllUsers() && name.equals(RefNames.REFS_EXTERNAL_IDS)) {
- // The notes branch with the external IDs of all users 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 {
+ try {
+ if (refVisibilityControl.isVisible(projectCtl, ref.getLeaf().getName())) {
+ resultRefs.put(refName, ref);
+ }
+ } catch (PermissionBackendException e) {
+ log.warn("could not evaluate ref permission", e);
}
}
}
@@ -197,22 +186,22 @@ public class VisibleRefFilter extends AbstractAdvertiseRefsHook {
// 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() || filterTagsSeparately)) {
+ if (!deferredTags.isEmpty() && (!resultRefs.isEmpty() || filterTagsSeparately)) {
TagMatcher tags =
tagCache
.get(projectState.getNameKey())
.matcher(
tagCache,
git,
- filterTagsSeparately ? filter(git.getAllRefs()).values() : result.values());
+ filterTagsSeparately ? filter(git.getAllRefs()).values() : resultRefs.values());
for (Ref tag : deferredTags) {
if (tags.isReachable(tag)) {
- result.put(tag.getName(), tag);
+ resultRefs.put(tag.getName(), tag);
}
}
}
- return result;
+ return resultRefs;
}
private Map<String, Ref> fastHideRefsMetaConfig(Map<String, Ref> refs) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/permissions/RefVisibilityControl.java b/gerrit-server/src/main/java/com/google/gerrit/server/permissions/RefVisibilityControl.java
new file mode 100644
index 0000000000..d75bc7dbc1
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/permissions/RefVisibilityControl.java
@@ -0,0 +1,215 @@
+// 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.gerrit.common.Nullable;
+import com.google.gerrit.reviewdb.client.Account;
+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.notedb.ChangeNotes;
+import com.google.gerrit.server.project.ChangeControl;
+import com.google.gerrit.server.project.NoSuchChangeException;
+import com.google.gerrit.server.project.ProjectControl;
+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;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * This class is a component that is internal to {@link DefaultPermissionBackend}. It can
+ * authoritatively tell if a ref is accessible by a user.
+ */
+@Singleton
+public class RefVisibilityControl {
+ private static final Logger logger = LoggerFactory.getLogger(RefVisibilityControl.class);
+
+ private final Provider<ReviewDb> dbProvider;
+ private final OneOffRequestContext oneOffRequestContext;
+ private final PermissionBackend permissionBackend;
+ private final ChangeData.Factory changeDataFactory;
+
+ @Inject
+ RefVisibilityControl(
+ Provider<ReviewDb> dbProvider,
+ OneOffRequestContext oneOffRequestContext,
+ PermissionBackend permissionBackend,
+ ChangeData.Factory changeDataFactory) {
+ this.dbProvider = dbProvider;
+ this.oneOffRequestContext = oneOffRequestContext;
+ this.permissionBackend = permissionBackend;
+ this.changeDataFactory = changeDataFactory;
+ }
+
+ /**
+ * 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.
+ */
+ public 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);
+ }
+ 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;
+ }
+
+ // We are done checking all cases where we would allow access to Gerrit-managed refs. Deny
+ // access in case we got this far.
+ logger.debug(
+ "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.debug("Own change edit ref is visible: %s", refName);
+ return true;
+ }
+
+ return false;
+ }
+
+ 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 {
+ 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/gerrit-server/src/main/java/com/google/gerrit/server/project/ChangeControl.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ChangeControl.java
index 1582d43958..a605a7d240 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/ChangeControl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ChangeControl.java
@@ -50,7 +50,7 @@ import java.util.Map;
import java.util.Set;
/** Access control management for a user accessing a single change. */
-class ChangeControl {
+public class ChangeControl {
@Singleton
static class Factory {
private final ChangeData.Factory changeDataFactory;
@@ -87,6 +87,8 @@ class ChangeControl {
private final ChangeNotes notes;
private final PatchSetUtil patchSetUtil;
+ private ChangeData cd;
+
ChangeControl(
ChangeData.Factory changeDataFactory,
ApprovalsUtil approvalsUtil,
@@ -128,17 +130,20 @@ class ChangeControl {
return notes;
}
- /** Can this user see this change? */
- private boolean isVisible(ReviewDb db, @Nullable ChangeData cd) throws OrmException {
- if (getChange().isPrivate() && !isPrivateVisible(db, cd)) {
- return false;
+ public ChangeControl setChangeData(@Nullable ChangeData cd) {
+ if (cd != null) {
+ this.cd = cd;
}
- return isRefVisible();
+ return this;
}
- /** Can the user see this change? Does not account for draft status */
- private boolean isRefVisible() {
- return getRefControl().isVisible();
+ /** Can this user see this change? */
+ public boolean isVisible(ReviewDb db) throws OrmException {
+ if (getChange().isPrivate() && !isPrivateVisible(db, changeData(db))) {
+ return false;
+ }
+ // Does the user have READ permission on the destination?
+ return refControl.asForRef().testOrFalse(RefPermission.READ);
}
/** Can this user abandon this change? */
@@ -237,7 +242,7 @@ class ChangeControl {
/** Is this user a reviewer for the change? */
private boolean isReviewer(ReviewDb db, @Nullable ChangeData cd) throws OrmException {
if (getUser().isIdentifiedUser()) {
- Collection<Account.Id> results = changeData(db, cd).reviewers().all();
+ Collection<Account.Id> results = setChangeData(cd).changeData(db).reviewers().all();
return results.contains(getUser().getAccountId());
}
return false;
@@ -282,8 +287,8 @@ class ChangeControl {
|| getProjectControl().isAdmin();
}
- private ChangeData changeData(ReviewDb db, @Nullable ChangeData cd) {
- return cd != null ? cd : changeDataFactory.create(db, getNotes());
+ private ChangeData changeData(ReviewDb db) {
+ return this.cd != null ? cd : changeDataFactory.create(db, getNotes());
}
private boolean isPrivateVisible(ReviewDb db, ChangeData cd) throws OrmException {
@@ -294,15 +299,13 @@ class ChangeControl {
}
ForChange asForChange(@Nullable ChangeData cd, @Nullable Provider<ReviewDb> db) {
- return new ForChangeImpl(cd, db);
+ return new ForChangeImpl(db);
}
private class ForChangeImpl extends ForChange {
- private ChangeData cd;
private Map<String, PermissionRange> labels;
- ForChangeImpl(@Nullable ChangeData cd, @Nullable Provider<ReviewDb> db) {
- this.cd = cd;
+ ForChangeImpl(@Nullable Provider<ReviewDb> db) {
this.db = db;
}
@@ -370,7 +373,7 @@ class ChangeControl {
try {
switch (perm) {
case READ:
- return isVisible(db(), changeData());
+ return isVisible(db());
case ABANDON:
return canAbandon(db());
case DELETE:
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectControl.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectControl.java
index 76e9e9816f..2dd960fe4d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectControl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectControl.java
@@ -39,8 +39,11 @@ 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.git.VisibleRefFilter;
import com.google.gerrit.server.group.SystemGroupBackend;
import com.google.gerrit.server.notedb.ChangeNotes;
import com.google.gerrit.server.permissions.FailedPermissionBackend;
@@ -51,6 +54,7 @@ import com.google.gerrit.server.permissions.PermissionBackend.ForProject;
import com.google.gerrit.server.permissions.PermissionBackend.ForRef;
import com.google.gerrit.server.permissions.PermissionBackendException;
import com.google.gerrit.server.permissions.ProjectPermission;
+import com.google.gerrit.server.permissions.RefVisibilityControl;
import com.google.gerrit.server.query.change.ChangeData;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
@@ -134,6 +138,10 @@ public class ProjectControl {
private final CommitsCollection commits;
private final ChangeControl.Factory changeControlFactory;
private final PermissionCollection.Factory permissionFilter;
+ private final RefVisibilityControl refVisibilityControl;
+ private final VisibleRefFilter.Factory visibleRefFilterFactory;
+ private final GitRepositoryManager gitRepositoryManager;
+ private final AllUsersName allUsersName;
private List<SectionMatcher> allSections;
private Map<String, RefControl> refControls;
@@ -147,6 +155,10 @@ public class ProjectControl {
CommitsCollection commits,
ChangeControl.Factory changeControlFactory,
PermissionBackend permissionBackend,
+ RefVisibilityControl refVisibilityControl,
+ GitRepositoryManager gitRepositoryManager,
+ VisibleRefFilter.Factory visibleRefFilterFactory,
+ AllUsersName allUsersName,
@Assisted CurrentUser who,
@Assisted ProjectState ps) {
this.changeControlFactory = changeControlFactory;
@@ -155,6 +167,10 @@ public class ProjectControl {
this.permissionFilter = permissionFilter;
this.commits = commits;
this.perm = permissionBackend.user(who);
+ this.refVisibilityControl = refVisibilityControl;
+ this.gitRepositoryManager = gitRepositoryManager;
+ this.visibleRefFilterFactory = visibleRefFilterFactory;
+ this.allUsersName = allUsersName;
user = who;
state = ps;
}
@@ -186,7 +202,14 @@ public 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(
+ visibleRefFilterFactory,
+ refVisibilityControl,
+ this,
+ gitRepositoryManager,
+ refName,
+ relevant);
refControls.put(refName, ctl);
}
return ctl;
@@ -243,7 +266,9 @@ public class ProjectControl {
}
private boolean allRefsAreVisible(Set<String> ignore) {
- return user.isInternalUser() || canPerformOnAllRefs(Permission.READ, ignore);
+ return user.isInternalUser()
+ || (!getProject().getNameKey().equals(allUsersName)
+ && canPerformOnAllRefs(Permission.READ, ignore));
}
/** Returns whether the project is hidden. */
@@ -428,11 +453,11 @@ public class ProjectControl {
}
}
- ForProject asForProject() {
+ public ForProject asForProject() {
return new ForProjectImpl();
}
- private class ForProjectImpl extends ForProject {
+ public class ForProjectImpl extends ForProject {
@Override
public ForProject user(CurrentUser user) {
return forUser(user).asForProject().database(db);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/RefControl.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/RefControl.java
index cde2c8098e..2ba5e0f6d2 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/RefControl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/RefControl.java
@@ -16,6 +16,7 @@ package com.google.gerrit.server.project;
import static com.google.common.base.Preconditions.checkArgument;
+import com.google.common.collect.ImmutableMap;
import com.google.gerrit.common.data.Permission;
import com.google.gerrit.common.data.PermissionRange;
import com.google.gerrit.common.data.PermissionRule;
@@ -24,15 +25,20 @@ 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.git.VisibleRefFilter;
import com.google.gerrit.server.notedb.ChangeNotes;
import com.google.gerrit.server.permissions.FailedPermissionBackend;
import com.google.gerrit.server.permissions.PermissionBackend.ForChange;
import com.google.gerrit.server.permissions.PermissionBackend.ForRef;
import com.google.gerrit.server.permissions.PermissionBackendException;
+import com.google.gerrit.server.permissions.ProjectPermission;
import com.google.gerrit.server.permissions.RefPermission;
+import com.google.gerrit.server.permissions.RefVisibilityControl;
import com.google.gerrit.server.query.change.ChangeData;
import com.google.gwtorm.server.OrmException;
import com.google.inject.util.Providers;
+import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
@@ -42,10 +48,16 @@ import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
+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). */
public class RefControl {
+ private final VisibleRefFilter.Factory visibleRefFilterFactory;
+ 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 +69,19 @@ public 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(
+ VisibleRefFilter.Factory visibleRefFilterFactory,
+ RefVisibilityControl refVisibilityControl,
+ ProjectControl projectControl,
+ GitRepositoryManager repositoryManager,
+ String ref,
+ PermissionCollection relevant) {
+ this.visibleRefFilterFactory = visibleRefFilterFactory;
+ this.refVisibilityControl = refVisibilityControl;
this.projectControl = projectControl;
+ this.repositoryManager = repositoryManager;
this.refName = ref;
this.relevant = relevant;
this.effective = new HashMap<>();
@@ -83,7 +104,13 @@ public class RefControl {
if (relevant.isUserSpecific()) {
return newCtl.controlForRef(getRefName());
}
- return new RefControl(newCtl, getRefName(), relevant);
+ return new RefControl(
+ visibleRefFilterFactory,
+ refVisibilityControl,
+ newCtl,
+ repositoryManager,
+ getRefName(),
+ relevant);
}
/** Is this user a ref owner? */
@@ -99,14 +126,24 @@ public class RefControl {
return owner;
}
- /** Can this user see this reference exists? */
- boolean isVisible() {
- if (isVisible == null) {
- isVisible =
+ /**
+ * 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.
+ */
+ public boolean hasReadPermissionOnRef(boolean allowNoteDbRefs) {
+ // Don't allow checking for NoteDb refs unless instructed otherwise.
+ if (!allowNoteDbRefs
+ && (refName.startsWith(Constants.R_TAGS) || RefNames.isGerritRef(refName))) {
+ return false;
+ }
+ if (hasReadPermissionOnRef == null) {
+ hasReadPermissionOnRef =
(getUser().isInternalUser() || canPerform(Permission.READ))
&& isProjectStatePermittingRead();
}
- return isVisible;
+ return hasReadPermissionOnRef;
}
/** Can this user see other users change edits? */
@@ -552,7 +589,10 @@ public 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(perm.permissionName().get());
@@ -588,4 +628,34 @@ public 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 visibleRefFilterFactory.create(projectControl.getProjectState(), repo)
+ .filter(ImmutableMap.of(resolvedRef.getName(), resolvedRef), true).values().stream()
+ .anyMatch(r -> refName.equals(r.getName()));
+ } catch (IOException e) {
+ throw new PermissionBackendException(e);
+ }
+ }
}
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/project/RefControlTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/project/RefControlTest.java
index 1fc95c18bc..364013fcba 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/project/RefControlTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/project/RefControlTest.java
@@ -55,12 +55,16 @@ import com.google.gerrit.server.config.AllProjectsNameProvider;
import com.google.gerrit.server.config.AllUsersName;
import com.google.gerrit.server.config.AllUsersNameProvider;
import com.google.gerrit.server.config.SitePaths;
+import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.git.ProjectConfig;
import com.google.gerrit.server.git.TransferConfig;
+import com.google.gerrit.server.git.VisibleRefFilter;
import com.google.gerrit.server.index.SingleVersionModule.SingleVersionListener;
import com.google.gerrit.server.permissions.PermissionBackend;
+import com.google.gerrit.server.permissions.PermissionBackendException;
import com.google.gerrit.server.permissions.ProjectPermission;
import com.google.gerrit.server.permissions.RefPermission;
+import com.google.gerrit.server.permissions.RefVisibilityControl;
import com.google.gerrit.server.schema.SchemaCreator;
import com.google.gerrit.server.util.RequestContext;
import com.google.gerrit.server.util.ThreadLocalRequestContext;
@@ -107,6 +111,16 @@ public class RefControlTest {
assertThat(u.controlForRef(ref).isOwner()).named("NOT OWN " + ref).isFalse();
}
+ private void assertAllRefsAreVisible(ProjectControl u) throws PermissionBackendException {
+ assertThat(u.asForProject().test(ProjectPermission.READ)).named("all refs visible").isTrue();
+ }
+
+ private void assertAllRefsAreNotVisible(ProjectControl u) throws PermissionBackendException {
+ assertThat(u.asForProject().test(ProjectPermission.READ))
+ .named("all refs NOT visible")
+ .isFalse();
+ }
+
private void assertCanAccess(ProjectControl u) {
boolean access = u.asForProject().testOrFalse(ProjectPermission.ACCESS);
assertThat(access).named("can access").isTrue();
@@ -118,11 +132,13 @@ 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)).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))
+ .named("cannot read " + ref)
+ .isFalse();
}
private void assertCanSubmit(String ref, ProjectControl u) {
@@ -194,6 +210,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;
@@ -209,6 +226,9 @@ public class RefControlTest {
@Inject private InMemoryDatabase schemaFactory;
@Inject private ThreadLocalRequestContext requestContext;
@Inject private TransferConfig transferConfig;
+ @Inject private RefVisibilityControl refVisibilityControl;
+ @Inject private GitRepositoryManager gitRepositoryManager;
+ @Inject private VisibleRefFilter.Factory visibleRefFilterFactory;
@Before
public void setUp() throws Exception {
@@ -222,7 +242,7 @@ public class RefControlTest {
@Override
public ProjectState getAllUsers() {
- return null;
+ return get(allUsersName);
}
@Override
@@ -282,6 +302,11 @@ public class RefControlTest {
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);
}
@@ -356,6 +381,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() {
allow(local, OWNER, ADMIN, "refs/*");
allow(local, OWNER, DEVS, "refs/heads/x/*");
@@ -886,6 +929,10 @@ public class RefControlTest {
null, // commitsCollection
changeControlFactory,
permissionBackend,
+ refVisibilityControl,
+ gitRepositoryManager,
+ visibleRefFilterFactory,
+ allUsersName,
new MockUser(name, memberOf),
newProjectState(local));
}
diff --git a/gerrit-war/pom.xml b/gerrit-war/pom.xml
index 568dddd158..7fcdb2d774 100644
--- a/gerrit-war/pom.xml
+++ b/gerrit-war/pom.xml
@@ -2,7 +2,7 @@
<modelVersion>4.0.0</modelVersion>
<groupId>com.google.gerrit</groupId>
<artifactId>gerrit-war</artifactId>
- <version>2.15.20-SNAPSHOT</version>
+ <version>2.15.22-SNAPSHOT</version>
<packaging>war</packaging>
<name>Gerrit Code Review - WAR</name>
<description>Gerrit WAR</description>
diff --git a/lib/jgit/jgit.bzl b/lib/jgit/jgit.bzl
index 141b293b66..74e63cbd13 100644
--- a/lib/jgit/jgit.bzl
+++ b/lib/jgit/jgit.bzl
@@ -4,7 +4,7 @@ _JGIT_VERS = "4.11.9.201909030838-r"
_DOC_VERS = _JGIT_VERS # Set to _JGIT_VERS unless using a snapshot
-JGIT_DOC_URL = "https://download.eclipse.org/jgit/site/" + _DOC_VERS + "/apidocs"
+JGIT_DOC_URL = "https://archive.eclipse.org/jgit/site/" + _DOC_VERS + "/apidocs"
_JGIT_REPO = MAVEN_CENTRAL # Leave here even if set to MAVEN_CENTRAL.
diff --git a/polygerrit-ui/app/BUILD b/polygerrit-ui/app/BUILD
index 8012c20897..39533d6b92 100644
--- a/polygerrit-ui/app/BUILD
+++ b/polygerrit-ui/app/BUILD
@@ -146,6 +146,7 @@ DIRECTORIES = [
tags = [
# Should not run sandboxed.
"local",
+ "manual",
"template",
],
) for directory in DIRECTORIES]
diff --git a/tools/BUILD b/tools/BUILD
index 646414037a..094cb0149f 100644
--- a/tools/BUILD
+++ b/tools/BUILD
@@ -48,7 +48,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/version.bzl b/version.bzl
index 4b922458ff..b5879e4b80 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.15.20-SNAPSHOT"
+GERRIT_VERSION = "2.15.22-SNAPSHOT"