From 807eccf459613461dff5889dd8ab356884305372 Mon Sep 17 00:00:00 2001 From: Sebastian Sdorra Date: Sat, 5 Nov 2016 19:46:32 +0100 Subject: [PATCH] added unit tests for security filters --- .../sonia/scm/filter/AdminSecurityFilter.java | 18 +- .../java/sonia/scm/filter/SecurityFilter.java | 11 +- .../scm/filter/AdminSecurityFilterTest.java | 85 ++++++ .../sonia/scm/filter/SecurityFilterTest.java | 242 ++++++++++++++++++ .../test/resources/sonia/scm/shiro-001.ini | 1 + 5 files changed, 342 insertions(+), 15 deletions(-) create mode 100644 scm-webapp/src/test/java/sonia/scm/filter/AdminSecurityFilterTest.java create mode 100644 scm-webapp/src/test/java/sonia/scm/filter/SecurityFilterTest.java diff --git a/scm-webapp/src/main/java/sonia/scm/filter/AdminSecurityFilter.java b/scm-webapp/src/main/java/sonia/scm/filter/AdminSecurityFilter.java index a458138910..a85f3a54f9 100644 --- a/scm-webapp/src/main/java/sonia/scm/filter/AdminSecurityFilter.java +++ b/scm-webapp/src/main/java/sonia/scm/filter/AdminSecurityFilter.java @@ -44,7 +44,9 @@ import sonia.scm.config.ScmConfiguration; import sonia.scm.security.Role; /** - * + * Security filter which allow only administrators to access the underlying + * resources. + * * @author Sebastian Sdorra */ @Singleton @@ -52,10 +54,9 @@ public class AdminSecurityFilter extends SecurityFilter { /** - * Constructs ... + * Constructs a new instance. * - * - * @param configuration + * @param configuration scm-manager main configuration */ @Inject public AdminSecurityFilter(ScmConfiguration configuration) @@ -66,14 +67,11 @@ public class AdminSecurityFilter extends SecurityFilter //~--- get methods ---------------------------------------------------------- /** - * Method description + * Returns {@code true} if the subject has the admin role. * + * @param subject subject * - * @param securityContext - * - * @param subject - * - * @return + * @return {@code true} if the subject has the admin role */ @Override protected boolean hasPermission(Subject subject) diff --git a/scm-webapp/src/main/java/sonia/scm/filter/SecurityFilter.java b/scm-webapp/src/main/java/sonia/scm/filter/SecurityFilter.java index 61dd73ea81..45f0a3ac8a 100644 --- a/scm-webapp/src/main/java/sonia/scm/filter/SecurityFilter.java +++ b/scm-webapp/src/main/java/sonia/scm/filter/SecurityFilter.java @@ -35,6 +35,7 @@ package sonia.scm.filter; //~--- non-JDK imports -------------------------------------------------------- +import com.google.common.annotations.VisibleForTesting; import com.google.inject.Inject; import com.google.inject.Singleton; @@ -65,7 +66,8 @@ public class SecurityFilter extends HttpFilter { /** name of request attribute for the primary principal */ - private static final String ATTRIBUTE_REMOTE_USER = "principal"; + @VisibleForTesting + static final String ATTRIBUTE_REMOTE_USER = "principal"; /** Field description */ public static final String URL_AUTHENTICATION = "/api/rest/authentication"; @@ -102,13 +104,12 @@ public class SecurityFilter extends HttpFilter HttpServletResponse response, FilterChain chain) throws IOException, ServletException { - Subject subject = SecurityUtils.getSubject(); - String uri = request.getRequestURI().substring(request.getContextPath().length()); if (!uri.startsWith(URL_AUTHENTICATION)) { + Subject subject = SecurityUtils.getSubject(); if (hasPermission(subject)) { // add primary principal as request attribute @@ -164,7 +165,7 @@ public class SecurityFilter extends HttpFilter */ private User getUser(Subject subject) { - User user = null; + User user; if (subject.isAuthenticated() || subject.isRemembered()) { @@ -181,5 +182,5 @@ public class SecurityFilter extends HttpFilter //~--- fields --------------------------------------------------------------- /** Field description */ - private ScmConfiguration configuration; + private final ScmConfiguration configuration; } diff --git a/scm-webapp/src/test/java/sonia/scm/filter/AdminSecurityFilterTest.java b/scm-webapp/src/test/java/sonia/scm/filter/AdminSecurityFilterTest.java new file mode 100644 index 0000000000..b6a9c02a8d --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/filter/AdminSecurityFilterTest.java @@ -0,0 +1,85 @@ +/** + * Copyright (c) 2014, Sebastian Sdorra + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * 3. Neither the name of SCM-Manager; nor the names of its + * contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * http://bitbucket.org/sdorra/scm-manager + * + */ + +package sonia.scm.filter; + +import com.github.sdorra.shiro.ShiroRule; +import com.github.sdorra.shiro.SubjectAware; +import org.apache.shiro.SecurityUtils; +import org.junit.Test; +import static org.junit.Assert.*; +import org.junit.Before; +import org.junit.Rule; +import org.junit.runner.RunWith; +import org.mockito.runners.MockitoJUnitRunner; +import sonia.scm.config.ScmConfiguration; + +/** + * Unit tests for {@link AdminSecurityFilter}. + * + * @author Sebastian Sdorra + */ +@RunWith(MockitoJUnitRunner.class) +@SubjectAware(configuration = "classpath:sonia/scm/shiro-001.ini") +public class AdminSecurityFilterTest { + + private AdminSecurityFilter securityFilter; + + @Rule + public ShiroRule shiro = new ShiroRule(); + + /** + * Prepare object under test and mocks. + */ + @Before + public void setUp(){ + this.securityFilter = new AdminSecurityFilter(new ScmConfiguration()); + } + + /** + * Tests {@link AdminSecurityFilter#hasPermission(org.apache.shiro.subject.Subject)} as administrator. + */ + @Test + @SubjectAware(username = "dent", password = "secret") + public void testHasPermissionAsAdministrator() { + assertTrue(securityFilter.hasPermission(SecurityUtils.getSubject())); + } + + /** + * Tests {@link AdminSecurityFilter#hasPermission(org.apache.shiro.subject.Subject)} as user. + */ + @Test + @SubjectAware(username = "trillian", password = "secret") + public void testHasPermissionAsUser() { + assertFalse(securityFilter.hasPermission(SecurityUtils.getSubject())); + } + +} \ No newline at end of file diff --git a/scm-webapp/src/test/java/sonia/scm/filter/SecurityFilterTest.java b/scm-webapp/src/test/java/sonia/scm/filter/SecurityFilterTest.java new file mode 100644 index 0000000000..9a87d63151 --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/filter/SecurityFilterTest.java @@ -0,0 +1,242 @@ +/** + * Copyright (c) 2014, Sebastian Sdorra + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * 3. Neither the name of SCM-Manager; nor the names of its + * contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * http://bitbucket.org/sdorra/scm-manager + * + */ + +package sonia.scm.filter; + +import com.github.sdorra.shiro.ShiroRule; +import com.github.sdorra.shiro.SubjectAware; +import java.io.IOException; +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import org.apache.shiro.subject.SimplePrincipalCollection; +import org.apache.shiro.subject.Subject; +import org.junit.Test; +import static org.junit.Assert.*; +import static org.hamcrest.Matchers.*; +import org.junit.Before; +import org.junit.Rule; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Mock; +import org.mockito.Mockito; +import static org.mockito.Mockito.*; +import org.mockito.runners.MockitoJUnitRunner; +import sonia.scm.SCMContext; +import sonia.scm.config.ScmConfiguration; +import sonia.scm.user.User; +import sonia.scm.user.UserTestData; + +/** + * Unit tests for {@link SecurityFilter}. + * + * @author Sebastian Sdorra + */ +@RunWith(MockitoJUnitRunner.class) +@SubjectAware(configuration = "classpath:sonia/scm/shiro-001.ini") +public class SecurityFilterTest { + + @Mock + private HttpServletRequest request; + + @Captor + private ArgumentCaptor requestCaptor; + + @Mock + private HttpServletResponse response; + + @Captor + private ArgumentCaptor responseCaptor; + + @Mock + private FilterChain chain; + + private ScmConfiguration configuration; + + private SecurityFilter securityFilter; + + @Rule + public ShiroRule shiro = new ShiroRule(); + + /** + * Prepare object under test and mocks. + */ + @Before + public void setUp(){ + this.configuration = new ScmConfiguration(); + this.securityFilter = new SecurityFilter(configuration); + + when(request.getContextPath()).thenReturn("/scm"); + } + + /** + * Tests filter on authentication endpoint. + * + * @throws IOException + * @throws ServletException + */ + @Test + public void testDoOnAuthenticationUrl() throws IOException, ServletException { + when(request.getRequestURI()).thenReturn("/scm/api/rest/authentication"); + securityFilter.doFilter(request, response, chain); + verify(request, never()).setAttribute(Mockito.anyString(), Mockito.any()); + verify(chain).doFilter(request, response); + } + + /** + * Tests filter without prior authentication. + * + * @throws IOException + * @throws ServletException + */ + @Test + public void testAnonymous() throws IOException, ServletException { + when(request.getRequestURI()).thenReturn("/scm/api"); + securityFilter.doFilter(request, response, chain); + response.sendError(HttpServletResponse.SC_FORBIDDEN); + } + + /** + * Tests filter without prior authentication and enabled anonymous access. + * + * @throws IOException + * @throws ServletException + */ + @Test + public void testAnonymousWithAccessEnabled() throws IOException, ServletException { + when(request.getRequestURI()).thenReturn("/scm/api"); + configuration.setAnonymousAccessEnabled(true); + + // execute + securityFilter.doFilter(request, response, chain); + + // verify and capture + verify(request).setAttribute(SecurityFilter.ATTRIBUTE_REMOTE_USER, SCMContext.USER_ANONYMOUS); + verify(chain).doFilter(requestCaptor.capture(), responseCaptor.capture()); + + // assert + HttpServletRequest captured = requestCaptor.getValue(); + assertEquals(SCMContext.USER_ANONYMOUS, captured.getRemoteUser()); + assertEquals(SCMContext.ANONYMOUS, captured.getUserPrincipal()); + } + + /** + * Tests filter with prior authentication. + * + * @throws IOException + * @throws ServletException + */ + @Test + public void testAuthenticated() throws IOException, ServletException { + authenticateUser(UserTestData.createTrillian()); + when(request.getRequestURI()).thenReturn("/scm/api"); + + // execute + securityFilter.doFilter(request, response, chain); + + // verify and capture + verify(request).setAttribute(SecurityFilter.ATTRIBUTE_REMOTE_USER, "trillian"); + verify(chain).doFilter(requestCaptor.capture(), responseCaptor.capture()); + + // assert + HttpServletRequest captured = requestCaptor.getValue(); + assertEquals(HttpServletRequest.BASIC_AUTH, captured.getAuthType()); + assertEquals("trillian", captured.getRemoteUser()); + assertThat(captured.getUserPrincipal(), instanceOf(User.class)); + assertEquals("trillian", captured.getUserPrincipal().getName()); + } + + /** + * Tests filter without permissions. + * + * @throws IOException + * @throws ServletException + */ + @Test + public void testForbidden() throws IOException, ServletException { + authenticateUser(UserTestData.createTrillian()); + when(request.getRequestURI()).thenReturn("/scm/api"); + + // execute + securityFilter = new AccessForbiddenSecurityFilter(configuration); + securityFilter.doFilter(request, response, chain); + + // assert + verify(response).sendError(HttpServletResponse.SC_FORBIDDEN); + } + + /** + * Tests filter unauthenticated and without permissions. + * + * @throws IOException + * @throws ServletException + */ + @Test + public void testUnauthorized() throws IOException, ServletException { + when(request.getRequestURI()).thenReturn("/scm/api"); + + // execute + securityFilter = new AccessForbiddenSecurityFilter(configuration); + securityFilter.doFilter(request, response, chain); + + // assert + verify(response).sendError(HttpServletResponse.SC_UNAUTHORIZED); + } + + private void authenticateUser(User user) { + SimplePrincipalCollection spc = new SimplePrincipalCollection(); + spc.add(user.getName(), "unit-test"); + spc.add(user, "unit-test"); + + Subject subject = new Subject.Builder() + .authenticated(true) + .principals(spc) + .buildSubject(); + + shiro.setSubject(subject); + } + + private static class AccessForbiddenSecurityFilter extends SecurityFilter { + + private AccessForbiddenSecurityFilter(ScmConfiguration configuration) { + super(configuration); + } + + @Override + protected boolean hasPermission(Subject subject) { + return false; + } + + } + +} \ No newline at end of file diff --git a/scm-webapp/src/test/resources/sonia/scm/shiro-001.ini b/scm-webapp/src/test/resources/sonia/scm/shiro-001.ini index 0202162d00..54741bcf4d 100644 --- a/scm-webapp/src/test/resources/sonia/scm/shiro-001.ini +++ b/scm-webapp/src/test/resources/sonia/scm/shiro-001.ini @@ -1,5 +1,6 @@ [users] trillian = secret, user +dent = secret, admin [roles] admin = *