Hide repository name and description when user has no access

Gitiles did not respect Gerrit's permission and would show the
repo name and description to any user who could guess the name.
The repositories list as well as the conent APIs were not affected.

This commit adds integration tests and fixes the problem.

Bug: Issue 7282
Change-Id: Icdca15b8f5e2b15e4a9f174a6ffdf366d1ff22a9
diff --git a/BUILD b/BUILD
index e45eca3..08741ba 100644
--- a/BUILD
+++ b/BUILD
@@ -1,5 +1,6 @@
 load("//tools/bzl:genrule2.bzl", "genrule2")
-load("//tools/bzl:plugin.bzl", "gerrit_plugin")
+load("//tools/bzl:plugin.bzl", "PLUGIN_DEPS", "PLUGIN_TEST_DEPS", "gerrit_plugin")
+load("//tools/bzl:junit.bzl", "junit_tests")
 
 gerrit_plugin(
     name = "gitiles",
@@ -30,3 +31,16 @@
         "zip -qr $$ROOT/$@ .",
     ]),
 )
+
+junit_tests(
+    name = "gitiles_tests",
+    srcs = glob(["src/test/java/**/*Test.java"]),
+    tags = ["gitiles"],
+    visibility = ["//visibility:public"],
+    runtime_deps = [":gitiles__plugin"],
+    deps = PLUGIN_TEST_DEPS + PLUGIN_DEPS + [
+        ":gitiles__plugin",
+        "//javatests/com/google/gerrit/util/http/testutil",
+        "//lib/gitiles",
+    ],
+)
diff --git a/src/main/java/com/googlesource/gerrit/plugins/gitiles/FilteredRepository.java b/src/main/java/com/googlesource/gerrit/plugins/gitiles/FilteredRepository.java
index 58f2b0f..2b19d95 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/gitiles/FilteredRepository.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/gitiles/FilteredRepository.java
@@ -43,6 +43,7 @@
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.lib.RepositoryBuilder;
 import org.eclipse.jgit.lib.StoredConfig;
+import org.eclipse.jgit.transport.ServiceMayNotContinueException;
 
 class FilteredRepository extends Repository {
   static class Factory {
@@ -69,6 +70,13 @@
       if (projectState == null || !projectState.getProject().getState().permitsRead()) {
         throw new NoSuchProjectException(name);
       }
+      try {
+        permissionBackend.currentUser().project(name).check(ProjectPermission.ACCESS);
+      } catch (AuthException e) {
+        throw new NoSuchProjectException(name, e);
+      } catch (PermissionBackendException e) {
+        throw new ServiceMayNotContinueException(e);
+      }
       return new FilteredRepository(
           projectState, userProvider.get(), repoManager.openRepository(name), permissionBackend);
     }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/gitiles/Resolver.java b/src/main/java/com/googlesource/gerrit/plugins/gitiles/Resolver.java
index 217dafc..71f604c 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/gitiles/Resolver.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/gitiles/Resolver.java
@@ -30,7 +30,7 @@
 import org.eclipse.jgit.transport.resolver.RepositoryResolver;
 import org.eclipse.jgit.transport.resolver.ServiceNotAuthorizedException;
 
-class Resolver implements RepositoryResolver<HttpServletRequest> {
+public class Resolver implements RepositoryResolver<HttpServletRequest> {
   private static final String NAME_KEY_ATTRIBUTE = Resolver.class.getName() + "/NameKey";
   private final Provider<CurrentUser> userProvider;
 
diff --git a/src/test/java/com/googlesource/gerrit/plugins/gitiles/ListProjectsAccessTest.java b/src/test/java/com/googlesource/gerrit/plugins/gitiles/ListProjectsAccessTest.java
new file mode 100644
index 0000000..4bdbca6
--- /dev/null
+++ b/src/test/java/com/googlesource/gerrit/plugins/gitiles/ListProjectsAccessTest.java
@@ -0,0 +1,101 @@
+// Copyright (C) 2021 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.googlesource.gerrit.plugins.gitiles;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allow;
+import static com.google.gerrit.server.group.SystemGroupBackend.ANONYMOUS_USERS;
+
+import com.google.common.collect.ImmutableSet;
+import com.google.gerrit.acceptance.LightweightPluginDaemonTest;
+import com.google.gerrit.acceptance.TestPlugin;
+import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
+import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
+import com.google.gerrit.common.data.Permission;
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.extensions.api.projects.BranchInput;
+import com.google.gerrit.testing.ConfigSuite;
+import com.google.gerrit.util.http.testutil.FakeHttpServletRequest;
+import java.util.stream.Collectors;
+import javax.inject.Inject;
+import org.eclipse.jgit.lib.Config;
+import org.junit.After;
+import org.junit.Test;
+
+@TestPlugin(name = "gitiles", sysModule = "com.googlesource.gerrit.plugins.gitiles.Module")
+public class ListProjectsAccessTest extends LightweightPluginDaemonTest {
+  @ConfigSuite.Default
+  public static Config defaultConfig() {
+    // Required config parameters to run Gitiles.
+    Config cfg = new Config();
+    cfg.setString("gerrit", null, "gitHttpUrl", "http://example.com/gitiles");
+    return cfg;
+  }
+
+  @Inject private RequestScopeOperations requestScopeOperations;
+  @Inject private ProjectOperations projectOperations;
+
+  @After
+  public void cleanUp() {
+    projectOperations.project(project).forUpdate().removeAllAccessSections().update();
+  }
+
+  @Test
+  public void listRepositories_repositoryVisibilityIsRespected() throws Exception {
+    projectOperations.allProjectsForUpdate().removeAllAccessSections().update();
+    projectOperations.newProject().name("visible").create();
+    projectOperations.newProject().name("invisible").create();
+    projectOperations
+        .project(Project.nameKey("visible"))
+        .forUpdate()
+        .add(allow(Permission.READ).ref("refs/heads/main").group(ANONYMOUS_USERS))
+        .update();
+    requestScopeOperations.setApiUserAnonymous();
+
+    assertThat(access().listRepositories(null, ImmutableSet.of()).keySet())
+        .containsExactly("visible");
+  }
+
+  @Test
+  public void listBranches_branchVisiblityIsRespected() throws Exception {
+    projectOperations.allProjectsForUpdate().removeAllAccessSections().update();
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.READ).ref("refs/heads/visible").group(ANONYMOUS_USERS))
+        .add(allow(Permission.CREATE).ref("refs/*").group(ANONYMOUS_USERS))
+        .add(allow(Permission.PUSH).ref("refs/*").group(ANONYMOUS_USERS))
+        .update();
+    gApi.projects().name(project.get()).branch("refs/heads/visible").create(new BranchInput());
+    gApi.projects().name(project.get()).branch("refs/heads/invisible").create(new BranchInput());
+    requestScopeOperations.setApiUserAnonymous();
+
+    assertThat(
+            access()
+                .listRepositories(
+                    null, ImmutableSet.of("refs/heads/visible", "refs/heads/invisible"))
+                .values().stream()
+                .map(r -> r.branches.keySet())
+                .collect(Collectors.toList()))
+        .containsExactly(ImmutableSet.of("refs/heads/visible"));
+  }
+
+  private GerritGitilesAccess access() {
+    return plugin
+        .getSysInjector()
+        .getInstance(GerritGitilesAccess.Factory.class)
+        .forRequest(new FakeHttpServletRequest());
+  }
+}
diff --git a/src/test/java/com/googlesource/gerrit/plugins/gitiles/RepositoryResolverAccessTest.java b/src/test/java/com/googlesource/gerrit/plugins/gitiles/RepositoryResolverAccessTest.java
new file mode 100644
index 0000000..31e4b47
--- /dev/null
+++ b/src/test/java/com/googlesource/gerrit/plugins/gitiles/RepositoryResolverAccessTest.java
@@ -0,0 +1,97 @@
+// Copyright (C) 2021 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.googlesource.gerrit.plugins.gitiles;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allow;
+import static com.google.gerrit.server.group.SystemGroupBackend.ANONYMOUS_USERS;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
+
+import com.google.gerrit.acceptance.LightweightPluginDaemonTest;
+import com.google.gerrit.acceptance.TestPlugin;
+import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
+import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
+import com.google.gerrit.common.data.Permission;
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.extensions.api.projects.BranchInput;
+import com.google.gerrit.testing.ConfigSuite;
+import com.google.gerrit.util.http.testutil.FakeHttpServletRequest;
+import javax.inject.Inject;
+import org.eclipse.jgit.errors.RepositoryNotFoundException;
+import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.lib.Repository;
+import org.junit.After;
+import org.junit.Test;
+
+@TestPlugin(name = "gitiles", sysModule = "com.googlesource.gerrit.plugins.gitiles.Module")
+public class RepositoryResolverAccessTest extends LightweightPluginDaemonTest {
+  @ConfigSuite.Default
+  public static Config defaultConfig() {
+    // Required config parameters to run Gitiles.
+    Config cfg = new Config();
+    cfg.setString("gerrit", null, "gitHttpUrl", "http://example.com/gitiles");
+    return cfg;
+  }
+
+  @Inject private RequestScopeOperations requestScopeOperations;
+  @Inject private ProjectOperations projectOperations;
+
+  @After
+  public void cleanUp() {
+    projectOperations.project(project).forUpdate().removeAllAccessSections().update();
+  }
+
+  @Test
+  public void resolveRepository_branchVisibilityIsRespected() throws Exception {
+    projectOperations.allProjectsForUpdate().removeAllAccessSections().update();
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.READ).ref("refs/heads/visible").group(ANONYMOUS_USERS))
+        .add(allow(Permission.CREATE).ref("refs/*").group(ANONYMOUS_USERS))
+        .add(allow(Permission.PUSH).ref("refs/*").group(ANONYMOUS_USERS))
+        .update();
+    gApi.projects().name(project.get()).branch("refs/heads/visible").create(new BranchInput());
+    gApi.projects().name(project.get()).branch("refs/heads/invisible").create(new BranchInput());
+
+    Repository repo = resolver().open(new FakeHttpServletRequest(), project.get());
+    assertThat(repo.exactRef("refs/heads/visible")).isNotNull();
+    assertThat(repo.exactRef("refs/heads/invisible")).isNull();
+  }
+
+  @Test
+  public void resolveRepository_repositoryVisibilityIsRespected() throws Exception {
+    projectOperations.allProjectsForUpdate().removeAllAccessSections().update();
+    projectOperations.newProject().name("visible").create();
+    projectOperations.newProject().name("invisible").create();
+    projectOperations
+        .project(Project.nameKey("visible"))
+        .forUpdate()
+        .add(allow(Permission.READ).ref("refs/heads/master").group(ANONYMOUS_USERS))
+        .update();
+
+    requestScopeOperations.setApiUser(user.id());
+    assertThat(resolver().open(new FakeHttpServletRequest(), "visible")).isNotNull();
+    RepositoryNotFoundException ex =
+        assertThrows(
+            RepositoryNotFoundException.class,
+            () -> resolver().open(new FakeHttpServletRequest(), "invisible"));
+    assertThat(ex).hasMessageThat().contains("repository not found: invisible");
+  }
+
+  private Resolver resolver() {
+    return plugin.getSysInjector().getInstance(Resolver.class);
+  }
+}