Re-add support for projects with anonymous HTTP access

This was originally added by go-import#190630, but was reverted by the
merging of go-import#241958 into master.

Enforcing authentication in all cases is problematic for those who wish
to make their public Gerrit hosted Go projects easily installable via
the standard Go toolchain, either as standalone applications or as
libraries.

Anonymous repo URLs are now used as long as the anonymous user is
granted read access to the project's `refs/heads/*`.

Change-Id: I15aa7251483877aae6552df0d11032d63af4b9e2
diff --git a/src/main/java/com/ericsson/gerrit/plugins/goimport/GoImportFilter.java b/src/main/java/com/ericsson/gerrit/plugins/goimport/GoImportFilter.java
index ea8a4dc..d321774 100644
--- a/src/main/java/com/ericsson/gerrit/plugins/goimport/GoImportFilter.java
+++ b/src/main/java/com/ericsson/gerrit/plugins/goimport/GoImportFilter.java
@@ -18,13 +18,19 @@
 import com.google.common.base.Strings;
 import com.google.gerrit.httpd.AllRequestFilter;
 import com.google.gerrit.httpd.HtmlDomUtil;
+import com.google.gerrit.reviewdb.client.Branch;
 import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.client.RefNames;
+import com.google.gerrit.server.AnonymousUser;
 import com.google.gerrit.server.config.CanonicalWebUrl;
+import com.google.gerrit.server.permissions.PermissionBackend;
+import com.google.gerrit.server.permissions.RefPermission;
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.project.ProjectState;
 import com.google.gerrit.util.http.CacheHeaders;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
+import com.google.inject.Provider;
 import java.io.IOException;
 import java.io.OutputStream;
 import java.net.URI;
@@ -74,13 +80,21 @@
           + "</html>";
 
   private static final Pattern AUTHENTICATED_REQ = Pattern.compile("^/a/.*");
+  private final Provider<AnonymousUser> anonProvider;
+  private final PermissionBackend permissions;
   private final ProjectCache projectCache;
   final String webUrl;
   final String projectPrefix;
 
   @Inject
-  GoImportFilter(ProjectCache projectCache, @CanonicalWebUrl String webUrl)
+  GoImportFilter(
+      Provider<AnonymousUser> anonProvider,
+      PermissionBackend permissions,
+      ProjectCache projectCache,
+      @CanonicalWebUrl String webUrl)
       throws URISyntaxException {
+    this.anonProvider = anonProvider;
+    this.permissions = permissions;
     this.projectCache = projectCache;
     this.webUrl = webUrl.replaceFirst("/?$", "/");
     this.projectPrefix = generateProjectPrefix();
@@ -160,9 +174,22 @@
         + (authenticated ? "a/" : "")
         + projectName
         + " git "
-        + webUrl
-        + "a/"
-        + projectName;
+	+ getRepoRoot(projectName, authenticated);
+  }
+
+  private String getRepoRoot(String projectName, boolean authenticated) {
+    if (allowsAnonymousAccess(projectName) && !authenticated) {
+      return webUrl + projectName;
+    }
+    return webUrl + "a/" + projectName;
+  }
+
+  private boolean allowsAnonymousAccess(String projectName) {
+    AnonymousUser anonymous = anonProvider.get();
+    Branch.NameKey heads =
+        new Branch.NameKey(new Project.NameKey(projectName), RefNames.REFS_HEADS);
+
+    return permissions.user(anonymous).ref(heads).testOrFalse(RefPermission.READ);
   }
 
   private boolean projectExists(String projectName) {
diff --git a/src/test/java/com/ericsson/gerrit/plugins/goimport/GoImportFilterTest.java b/src/test/java/com/ericsson/gerrit/plugins/goimport/GoImportFilterTest.java
index 4ecd9e9..b659ba5 100644
--- a/src/test/java/com/ericsson/gerrit/plugins/goimport/GoImportFilterTest.java
+++ b/src/test/java/com/ericsson/gerrit/plugins/goimport/GoImportFilterTest.java
@@ -25,8 +25,12 @@
 import static org.mockito.Mockito.when;
 
 import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.server.AnonymousUser;
+import com.google.gerrit.server.permissions.PermissionBackend;
+import com.google.gerrit.server.permissions.RefPermission;
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.project.ProjectState;
+import com.google.inject.Provider;
 import java.io.IOException;
 import java.net.URISyntaxException;
 import javax.servlet.FilterChain;
@@ -48,6 +52,8 @@
   private static final String CONTENT_FORMAT = "%1$s/%3$s git %2$s/%3$s";
   private static final String CONTENT =
       String.format(CONTENT_FORMAT, PROD_FQDN, auth(PROD_URL), PROJECT_NAME);
+  private static final String ANON_CONTENT =
+      String.format(CONTENT_FORMAT, PROD_FQDN, PROD_URL, PROJECT_NAME);
   private static final String AUTH_CONTENT =
       String.format(CONTENT_FORMAT, auth(PROD_FQDN), auth(PROD_URL), PROJECT_NAME);
 
@@ -56,14 +62,22 @@
     return baseUrl + "/a";
   }
 
-  private static byte[] response200(boolean authenticated) {
-    return authenticated
-        ? PAGE_200.replace(CONTENT_PLH, AUTH_CONTENT).getBytes()
-        : PAGE_200.replace(CONTENT_PLH, CONTENT).getBytes();
+  private static byte[] response200(boolean allowAnon, boolean authenticated) {
+    if (authenticated) {
+       return PAGE_200.replace(CONTENT_PLH, AUTH_CONTENT).getBytes();
+    } else if (allowAnon) {
+       return PAGE_200.replace(CONTENT_PLH, ANON_CONTENT).getBytes();
+    }
+    return PAGE_200.replace(CONTENT_PLH, CONTENT).getBytes();
   }
 
   private GoImportFilter unitUnderTest;
 
+  @Mock private Provider<AnonymousUser> mockAnonProvider;
+  @Mock private AnonymousUser mockAnon;
+  @Mock private PermissionBackend mockPerms;
+  @Mock private PermissionBackend.WithUser mockPermsWithUser;
+  @Mock private PermissionBackend.ForRef mockPermsForRef;
   @Mock private ProjectCache mockProjectCache;
   @Mock private HttpServletRequest mockRequest;
   @Mock private HttpServletResponse mockResponse;
@@ -73,23 +87,34 @@
 
   @Before
   public void setUp() throws Exception {
-    unitUnderTest = new GoImportFilter(mockProjectCache, PROD_URL);
+    unitUnderTest = new GoImportFilter(mockAnonProvider, mockPerms, mockProjectCache, PROD_URL);
     assertThat(unitUnderTest).isNotNull();
     when(mockResponse.getOutputStream()).thenReturn(mockOutputStream);
+
+    when(mockAnonProvider.get()).thenReturn(mockAnon);
+    when(mockPerms.user(mockAnon)).thenReturn(mockPermsWithUser);
+    when(mockPermsWithUser.ref(any())).thenReturn(mockPermsForRef);
   }
 
   @Test
   public void testConstructor() throws Exception {
     assertThat(unitUnderTest.webUrl.endsWith("/")).isTrue();
-    unitUnderTest =
-        new GoImportFilter(mockProjectCache, "http://gerrit-review.googlesource.com:8080/");
+    unitUnderTest = new GoImportFilter(
+        mockAnonProvider,
+        mockPerms,
+        mockProjectCache,
+        "http://gerrit-review.googlesource.com:8080/");
     assertThat(unitUnderTest.webUrl.endsWith("/")).isTrue();
     assertThat(unitUnderTest.projectPrefix).isNotNull();
   }
 
   @Test(expected = URISyntaxException.class)
   public void testConstructorWithURISyntaxException() throws Exception {
-    unitUnderTest = new GoImportFilter(mockProjectCache, "\\\\");
+    unitUnderTest = new GoImportFilter(
+        mockAnonProvider,
+        mockPerms,
+        mockProjectCache,
+        "\\\\");
   }
 
   @Test
@@ -119,11 +144,13 @@
     when(mockRequest.getServletPath()).thenReturn("/" + PROJECT_NAME);
     when(mockRequest.getParameter("go-get")).thenReturn("1");
     when(mockProjectCache.get(new Project.NameKey(PROJECT_NAME))).thenReturn(mockProjectState);
+    when(mockPermsForRef.testOrFalse(RefPermission.READ)).thenReturn(false);
     unitUnderTest.doFilter(mockRequest, mockResponse, mockChain);
-    verify(mockOutputStream, times(1)).write(response200(false));
+    verify(mockOutputStream, times(1)).write(response200(false, false));
     verify(mockChain, times(0)).doFilter(mockRequest, mockResponse);
     verify(mockProjectCache, times(1)).get(any(Project.NameKey.class));
     verify(mockResponse, times(1)).setStatus(200);
+    verify(mockPermsForRef, times(1)).testOrFalse(RefPermission.READ);
   }
 
   @Test
@@ -131,11 +158,13 @@
     when(mockRequest.getServletPath()).thenReturn("/a/" + PROJECT_NAME);
     when(mockRequest.getParameter("go-get")).thenReturn("1");
     when(mockProjectCache.get(new Project.NameKey(PROJECT_NAME))).thenReturn(mockProjectState);
+    when(mockPermsForRef.testOrFalse(RefPermission.READ)).thenReturn(false);
     unitUnderTest.doFilter(mockRequest, mockResponse, mockChain);
-    verify(mockOutputStream, times(1)).write(response200(true));
+    verify(mockOutputStream, times(1)).write(response200(false, true));
     verify(mockChain, times(0)).doFilter(mockRequest, mockResponse);
     verify(mockProjectCache, times(1)).get(any(Project.NameKey.class));
     verify(mockResponse, times(1)).setStatus(200);
+    verify(mockPermsForRef, times(1)).testOrFalse(RefPermission.READ);
   }
 
   @Test
@@ -143,11 +172,27 @@
     when(mockRequest.getServletPath()).thenReturn("/" + PROJECT_NAME + "/my/package");
     when(mockRequest.getParameter("go-get")).thenReturn("1");
     when(mockProjectCache.get(new Project.NameKey(PROJECT_NAME))).thenReturn(mockProjectState);
+    when(mockPermsForRef.testOrFalse(RefPermission.READ)).thenReturn(false);
     unitUnderTest.doFilter(mockRequest, mockResponse, mockChain);
-    verify(mockOutputStream, times(1)).write(response200(false));
+    verify(mockOutputStream, times(1)).write(response200(false, false));
     verify(mockChain, times(0)).doFilter(mockRequest, mockResponse);
     verify(mockProjectCache, times(3)).get(any(Project.NameKey.class));
     verify(mockResponse, times(1)).setStatus(200);
+    verify(mockPermsForRef, times(1)).testOrFalse(RefPermission.READ);
+  }
+
+  @Test
+  public void testDoFilterWithAnonymousAccessibleProject() throws Exception {
+    when(mockRequest.getServletPath()).thenReturn("/projectName");
+    when(mockRequest.getParameter("go-get")).thenReturn("1");
+    when(mockProjectCache.get(new Project.NameKey("projectName"))).thenReturn(mockProjectState);
+    when(mockPermsForRef.testOrFalse(RefPermission.READ)).thenReturn(true);
+    unitUnderTest.doFilter(mockRequest, mockResponse, mockChain);
+    verify(mockOutputStream, times(1)).write(response200(true, false));
+    verify(mockChain, times(0)).doFilter(mockRequest, mockResponse);
+    verify(mockProjectCache, times(1)).get(any(Project.NameKey.class));
+    verify(mockResponse, times(1)).setStatus(200);
+    verify(mockPermsForRef, times(1)).testOrFalse(RefPermission.READ);
   }
 
   @Test
diff --git a/src/test/java/com/ericsson/gerrit/plugins/goimport/HttpModuleTest.java b/src/test/java/com/ericsson/gerrit/plugins/goimport/HttpModuleTest.java
index 547c159..a0a107d 100644
--- a/src/test/java/com/ericsson/gerrit/plugins/goimport/HttpModuleTest.java
+++ b/src/test/java/com/ericsson/gerrit/plugins/goimport/HttpModuleTest.java
@@ -16,7 +16,9 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
+import com.google.gerrit.server.AnonymousUser;
 import com.google.gerrit.server.config.CanonicalWebUrl;
+import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.inject.AbstractModule;
 import com.google.inject.Guice;
@@ -32,6 +34,8 @@
 public class HttpModuleTest {
 
   private HttpModule unitUnderTest;
+  @Mock private AnonymousUser mockAnon;
+  @Mock private PermissionBackend mockPerms;
   @Mock private ProjectCache mockProjectCache;
 
   @Before
@@ -53,6 +57,8 @@
   public class TestModule extends AbstractModule {
     @Override
     protected void configure() {
+      bind(AnonymousUser.class).toInstance(mockAnon);
+      bind(PermissionBackend.class).toInstance(mockPerms);
       bind(ProjectCache.class).toInstance(mockProjectCache);
     }