Redirect anonymous users without read access to login Trigger login redirection for anonymous users who attempt to access a project they cannot read, including requests for non-existent projects to avoid leaking project existence. Change-Id: I5b53ad4ee03882c122c8bf59ba8f6dd9c674d440
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 f1b8b36..6e320b3 100644 --- a/src/main/java/com/googlesource/gerrit/plugins/gitiles/FilteredRepository.java +++ b/src/main/java/com/googlesource/gerrit/plugins/gitiles/FilteredRepository.java
@@ -69,17 +69,30 @@ } FilteredRepository create(Project.NameKey name) - throws NoSuchProjectException, IOException, PermissionBackendException { + throws NoSuchProjectException, + LoginRedirectRequiredException, + IOException, + PermissionBackendException { Optional<ProjectState> projectState = projectCache.get(name); - if (!projectState.isPresent() || !projectState.get().statePermitsRead()) { - 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); + if (userProvider.get() == null || !userProvider.get().isIdentifiedUser()) { + // If the user is an anonymous user and the project is not visible to anonymous + // users, redirect the user to login. + try { + permissionBackend.currentUser().project(name).check(ProjectPermission.ACCESS); + } catch (Exception e) { + throw new LoginRedirectRequiredException(e); + } + } else { + if (!projectState.isPresent() || !projectState.get().statePermitsRead()) { + 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.get(),
diff --git a/src/main/java/com/googlesource/gerrit/plugins/gitiles/HttpModule.java b/src/main/java/com/googlesource/gerrit/plugins/gitiles/HttpModule.java index 3d8d8da..7e8ce3f 100644 --- a/src/main/java/com/googlesource/gerrit/plugins/gitiles/HttpModule.java +++ b/src/main/java/com/googlesource/gerrit/plugins/gitiles/HttpModule.java
@@ -89,6 +89,9 @@ @Override protected void configureServlets() { + // Filter unauthenticated users to login page as needed + filter("/*").through(LoginRedirectErrorHandler.class); + // Filter all paths so we can decode escaped entities in the URI filter("/*").through(createPathFilter()); filter("/*").through(new MenuFilter(userProvider, urls));
diff --git a/src/main/java/com/googlesource/gerrit/plugins/gitiles/LoginRedirectErrorHandler.java b/src/main/java/com/googlesource/gerrit/plugins/gitiles/LoginRedirectErrorHandler.java new file mode 100644 index 0000000..b2bb99f --- /dev/null +++ b/src/main/java/com/googlesource/gerrit/plugins/gitiles/LoginRedirectErrorHandler.java
@@ -0,0 +1,58 @@ +// Copyright (C) 2025 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 com.google.gitiles.GitilesUrls; +import com.google.inject.Inject; +import com.google.inject.Singleton; +import java.io.IOException; +import javax.servlet.Filter; +import javax.servlet.FilterChain; +import javax.servlet.FilterConfig; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +@Singleton +public class LoginRedirectErrorHandler implements Filter { + private final GitilesUrls urls; + + @Inject + LoginRedirectErrorHandler(GitilesUrls urls) { + this.urls = urls; + } + + @Override + public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) + throws IOException, ServletException { + HttpServletRequest httpReq = (HttpServletRequest) req; + HttpServletResponse httpRes = (HttpServletResponse) res; + + try { + chain.doFilter(req, res); + } catch (LoginRedirectRequiredException e) { + String loginUrl = LoginRedirectUtil.getLoginRedirectUrl(httpReq, urls); + httpRes.sendRedirect(loginUrl); + } + } + + @Override + public void init(FilterConfig cfg) throws ServletException {} + + @Override + public void destroy() {} +}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/gitiles/LoginRedirectRequiredException.java b/src/main/java/com/googlesource/gerrit/plugins/gitiles/LoginRedirectRequiredException.java new file mode 100644 index 0000000..0315eb6 --- /dev/null +++ b/src/main/java/com/googlesource/gerrit/plugins/gitiles/LoginRedirectRequiredException.java
@@ -0,0 +1,23 @@ +// Copyright (C) 2025 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; + +public class LoginRedirectRequiredException extends RuntimeException { + public LoginRedirectRequiredException() {} + + public LoginRedirectRequiredException(Throwable cause) { + super(cause); + } +}
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 825cc60..cb49d10 100644 --- a/src/main/java/com/googlesource/gerrit/plugins/gitiles/Resolver.java +++ b/src/main/java/com/googlesource/gerrit/plugins/gitiles/Resolver.java
@@ -66,9 +66,6 @@ if (userProvider.get().isIdentifiedUser()) { throw new RepositoryNotFoundException(name, e); } - // Allow anonymous users a chance to login. - // Avoid leaking information by not distinguishing between - // project not existing and no access rights. throw new ServiceNotAuthorizedException(); } catch (IOException | PermissionBackendException e) { ServiceMayNotContinueException err =
diff --git a/src/test/java/com/googlesource/gerrit/plugins/gitiles/RepositoryResolverAccessTest.java b/src/test/java/com/googlesource/gerrit/plugins/gitiles/RepositoryResolverAccessTest.java index a1cf436..855ffb8 100644 --- a/src/test/java/com/googlesource/gerrit/plugins/gitiles/RepositoryResolverAccessTest.java +++ b/src/test/java/com/googlesource/gerrit/plugins/gitiles/RepositoryResolverAccessTest.java
@@ -79,7 +79,6 @@ 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() @@ -93,8 +92,19 @@ RepositoryNotFoundException ex = assertThrows( RepositoryNotFoundException.class, - () -> resolver().open(new FakeHttpServletRequest(), "invisible")); - assertThat(ex).hasMessageThat().contains("repository not found: invisible"); + () -> resolver().open(new FakeHttpServletRequest(), "nonexistent")); + assertThat(ex).hasMessageThat().contains("repository not found: nonexistent"); + } + + @Test + public void resolveRepository_anonymousUserRedirectedToLogin() throws Exception { + projectOperations.allProjectsForUpdate().removeAllAccessSections().update(); + projectOperations.newProject().name("invisible").create(); + requestScopeOperations.setApiUserAnonymous(); + + assertThrows( + LoginRedirectRequiredException.class, + () -> resolver().open(new FakeHttpServletRequest(), project.get())); } private Resolver resolver() {