/** * Copyright 2015 Google Inc. All Rights Reserved. * * 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.apphosting.vmruntime.jetty9; import static javax.servlet.http.HttpServletResponse.SC_FORBIDDEN; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import com.google.appengine.api.users.UserServiceFactory; import com.google.appengine.tools.development.testing.LocalServiceTestHelper; import com.google.appengine.tools.development.testing.LocalUserServiceTestConfig; import junit.framework.TestCase; import org.eclipse.jetty.http.HttpMethod; import org.eclipse.jetty.security.ConstraintMapping; import org.eclipse.jetty.security.ConstraintSecurityHandler; import org.eclipse.jetty.server.Request; import org.eclipse.jetty.server.Response; import org.eclipse.jetty.servlet.ServletHandler; import org.eclipse.jetty.servlet.ServletHolder; import org.eclipse.jetty.util.security.Constraint; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.PrintWriter; import javax.servlet.DispatcherType; import javax.servlet.ServletException; import org.eclipse.jetty.http.HttpFields; import org.eclipse.jetty.http.HttpURI; import org.eclipse.jetty.http.HttpVersion; import org.eclipse.jetty.http.MetaData; import org.eclipse.jetty.util.MultiMap; /** * Tests for AppEngineAuthentication. * * Test plan: Create and configure a ConstraintSecurityHandler to use our AppEngineAuthentication * classes, then configure some paths to require users to be logged in. We then fire requests at the * security handler by calling its handle() method and verify that unauthenticated requests are * redirected to a login url, while authenticated requests are allowed through. */ public class AppEngineAuthenticationTest extends TestCase { private static final String SERVER_NAME = "testapp.appspot.com"; private static final String USER_EMAIL = "user@gmail.com"; private static final String ADMIN_EMAIL = "isdal@gmail.com"; private static final String TEST_ENV_DOMAIN = "gmail.com"; private class MockAddressChecker implements VmRuntimeTrustedAddressChecker { @Override public boolean isTrustedRemoteAddr(String remoteAddr) { return true; } } /** * Version of ConstraintSecurityHandler that allows doStart to be called from outside of package. */ private static class TestConstraintSecurityHandler extends ConstraintSecurityHandler { // Override so we can call doStart to initialize the mapping; @Override public void doStart() throws Exception { super.doStart(); } } private void addConstraint( ConstraintSecurityHandler handler, String path, String name, String... roles) { Constraint constraint = new Constraint(); constraint.setName(name); constraint.setRoles(roles); constraint.setAuthenticate(true); ConstraintMapping mapping = new ConstraintMapping(); mapping.setMethod("GET"); mapping.setPathSpec(path); mapping.setConstraint(constraint); handler.addConstraintMapping(mapping); } private LocalServiceTestHelper helper; private TestConstraintSecurityHandler securityHandler; @Override public void setUp() throws Exception { // Initialize a Security handler and install our authenticatior. ServletHandler servletHandler = new ServletHandler(); servletHandler.addServletWithMapping(new ServletHolder(new AuthServlet()) {}, "/*"); securityHandler = new TestConstraintSecurityHandler(); securityHandler.setHandler(servletHandler); AppEngineAuthentication.configureSecurityHandler(securityHandler, new MockAddressChecker()); // Add authenticated paths to the security handler. Requests for those paths will be forwarded // to the authenticator with "mandatory=true". addConstraint(securityHandler, "/admin/*", "adminOnly", "admin"); addConstraint(securityHandler, "/user/*", "userOnly", "*"); addConstraint(securityHandler, "/_ah/login", "reserved", "*", "admin"); securityHandler.doStart(); // Start the handler so the constraint map is compiled. // Use a local test version of the UserService to allow us to control login state. helper = new LocalServiceTestHelper(new LocalUserServiceTestConfig()).setUp(); } @Override public void tearDown() throws Exception { helper.tearDown(); } /** * Fire one request at the security handler (and by extension to the AuthServlet behind it). * * @param path The path to hit. * @param request The request object to use. * @param response The response object to use. Must be created by Mockito.mock() * @return Any data written to response.getWriter() * @throws IOException * @throws ServletException */ private String runRequest(String path, Request request, Response response) throws IOException, ServletException { //request.setMethod(/*HttpMethod.GET,*/ "GET"); HttpURI uri =new HttpURI("http", SERVER_NAME,9999, path); HttpFields httpf = new HttpFields(); MetaData.Request metadata = new MetaData.Request("GET", uri, HttpVersion.HTTP_2, httpf); request.setMetaData(metadata); // request.setServerName(SERVER_NAME); // request.setAuthority(SERVER_NAME,9999); //// request.setPathInfo(path); //// request.setURIPathQuery(path); request.setDispatcherType(DispatcherType.REQUEST); doReturn(response).when(request).getResponse(); ByteArrayOutputStream output = new ByteArrayOutputStream(); try (PrintWriter writer = new PrintWriter(output)) { when(response.getWriter()).thenReturn(writer); securityHandler.handle(path, request, request, response); } return new String(output.toByteArray()); } private String runRequest2(String path, Request request, Response response) throws IOException, ServletException { //request.setMethod(/*HttpMethod.GET,*/ "GET"); HttpURI uri =new HttpURI("http", SERVER_NAME,9999, path); HttpFields httpf = new HttpFields(); MetaData.Request metadata = new MetaData.Request("GET", uri, HttpVersion.HTTP_2, httpf); // request.setMetaData(metadata); // request.setServerName(SERVER_NAME); // request.setAuthority(SERVER_NAME,9999); //// request.setPathInfo(path); //// request.setURIPathQuery(path); request.setDispatcherType(DispatcherType.REQUEST); doReturn(response).when(request).getResponse(); ByteArrayOutputStream output = new ByteArrayOutputStream(); try (PrintWriter writer = new PrintWriter(output)) { when(response.getWriter()).thenReturn(writer); securityHandler.handle(path, request, request, response); } return new String(output.toByteArray()); } public void testUserNotRequired() throws Exception { Request request = spy(new Request(null, null)); Response response = mock(Response.class); String path = "/"; String output = runRequest(path, request, response); assertEquals("null: null", output); } public void testUserNotRequired_WithUser() throws Exception { // Add a logged in user. helper.setEnvIsLoggedIn(true).setEnvEmail(USER_EMAIL).setEnvAuthDomain(TEST_ENV_DOMAIN).setUp(); Request request = spy(new Request(null, null)); Response response = mock(Response.class); String path = "/"; String output = runRequest(path, request, response); assertEquals(String.format("%s: %s", USER_EMAIL, USER_EMAIL), output); } public void testUserNotRequired_WithAdmin() throws Exception { // Add a logged in admin. helper.setEnvIsLoggedIn(true) .setEnvIsAdmin(true) .setEnvEmail(ADMIN_EMAIL) .setEnvAuthDomain(TEST_ENV_DOMAIN) .setUp(); Request request = spy(new Request(null, null)); Response response = mock(Response.class); String path = "/"; String output = runRequest(path, request, response); assertEquals(String.format("%s: %s", ADMIN_EMAIL, ADMIN_EMAIL), output); } public void testUserRequired_NoUser() throws Exception { String path = "/user/blah"; Request request = spy(new Request(null, null)); //request.setServerPort(9999); HttpURI uri =new HttpURI("http", SERVER_NAME,9999, path); HttpFields httpf = new HttpFields(); MetaData.Request metadata = new MetaData.Request("GET", uri, HttpVersion.HTTP_2, httpf); request.setMetaData(metadata); // request.setAuthority(SERVER_NAME,9999); Response response = mock(Response.class); String output = runRequest(path, request, response); // Verify that the servlet never was run (there is no output). assertEquals("", output); // Verify that the request was redirected to the login url. String loginUrl = UserServiceFactory.getUserService() .createLoginURL(String.format("http://%s%s", SERVER_NAME + ":9999", path)); verify(response).sendRedirect(loginUrl); } public void testUserRequired_NoUserButLoginUrl() throws Exception { String path = "/_ah/login"; Request request = spy(new Request(null, null)); Response response = mock(Response.class); String output = runRequest(path, request, response); // Verify that the request made it to the login url. assertEquals("null: null", output); } public void testUserRequired_PreserveQueryParams() throws Exception { String path = "/user/blah"; Request request = new Request(null, null); // request.setServerPort(9999); HttpURI uri =new HttpURI("http", SERVER_NAME,9999, path,"foo=baqr","foo=bar","foo=barff"); HttpFields httpf = new HttpFields(); MetaData.Request metadata = new MetaData.Request("GET", uri, HttpVersion.HTTP_2, httpf); request.setMetaData(metadata); MultiMap<String> queryParameters = new MultiMap<> (); queryParameters.add("ffo", "bar"); request.setQueryParameters(queryParameters); request = spy(request); /// request.setAuthority(SERVER_NAME,9999); request.setQueryString("foo=bar"); Response response = mock(Response.class); String output = runRequest2(path, request, response); // Verify that the servlet never was run (there is no output). assertEquals("", output); // Verify that the request was redirected to the login url. String loginUrl = UserServiceFactory.getUserService() .createLoginURL(String.format("http://%s%s?foo=bar", SERVER_NAME + ":9999", path)); verify(response).sendRedirect(loginUrl); } public void testUserRequired_WithUser() throws Exception { // Add a logged in user. helper.setEnvIsLoggedIn(true).setEnvEmail(USER_EMAIL).setEnvAuthDomain(TEST_ENV_DOMAIN).setUp(); String path = "/user/blah"; Request request = spy(new Request(null, null)); Response response = mock(Response.class); String output = runRequest(path, request, response); assertEquals(String.format("%s: %s", USER_EMAIL, USER_EMAIL), output); } public void testAdminRequired_NonAdmin() throws Exception { // Add a logged in user. helper.setEnvIsLoggedIn(true).setEnvEmail(USER_EMAIL).setEnvAuthDomain(TEST_ENV_DOMAIN).setUp(); String path = "/admin/blah"; Request request = spy(new Request(null, null)); Response response = mock(Response.class); String output = runRequest(path, request, response); // Verify that the servlet never was run (there is no output). assertEquals("", output); // Verify that the user got a 403 back. verify(response).sendError(SC_FORBIDDEN, "!role"); } public void testAdminRequired_NoUser() throws Exception { String path = "/admin/blah"; Request request = spy(new Request(null, null)); //request.setServerPort(9999); HttpURI uri =new HttpURI("http", SERVER_NAME,9999, path); HttpFields httpf = new HttpFields(); MetaData.Request metadata = new MetaData.Request("GET", uri, HttpVersion.HTTP_2, httpf); request.setMetaData(metadata); // request.setAuthority(SERVER_NAME,9999); Response response = mock(Response.class); String output = runRequest(path, request, response); // Verify that the servlet never was run (there is no output). assertEquals("", output); // Verify that the request was redirected to the login url. String loginUrl = UserServiceFactory.getUserService() .createLoginURL(String.format("http://%s%s", SERVER_NAME + ":9999", path)); verify(response).sendRedirect(loginUrl); } public void testAdminRequired_WithAdmin() throws Exception { // Add a logged in admin. helper.setEnvIsLoggedIn(true) .setEnvIsAdmin(true) .setEnvEmail(ADMIN_EMAIL) .setEnvAuthDomain(TEST_ENV_DOMAIN) .setUp(); String path = "/admin/blah"; Request request = spy(new Request(null, null)); Response response = mock(Response.class); String output = runRequest(path, request, response); assertEquals(String.format("%s: %s", ADMIN_EMAIL, ADMIN_EMAIL), output); } public void testUserRequired_NoUserSkipAdminCheck() throws Exception { String path = "/user/blah"; Request request = spy(new Request(null, null)); doReturn("true").when(request).getAttribute("com.google.apphosting.internal.SkipAdminCheck"); Response response = mock(Response.class); String output = runRequest(path, request, response); assertEquals("null: null", output); } public void testAdminRequired_NoUserSkipAdminCheck() throws Exception { String path = "/admin/blah"; Request request = spy(new Request(null, null)); doReturn("true").when(request).getAttribute("com.google.apphosting.internal.SkipAdminCheck"); Response response = mock(Response.class); String output = runRequest(path, request, response); assertEquals("null: null", output); } }