From 901c7d4752cfadab966bf4e64b593b25c18f0e14 Mon Sep 17 00:00:00 2001 From: Ben Alex Date: Tue, 27 Apr 2004 06:21:00 +0000 Subject: [PATCH] Significantly enhance channel processing filter. --- changelog.txt | 4 +- .../securechannel/ChannelDecisionManager.java | 7 +- .../ChannelDecisionManagerImpl.java | 63 +++- .../securechannel/ChannelEntryPoint.java | 15 +- .../ChannelProcessingFilter.java | 63 +++- .../InsecureChannelRequiredException.java | 50 +++ .../RetryWithHttpEntryPoint.java | 120 +++++++ .../RetryWithHttpsEntryPoint.java | 112 ++---- .../ChannelDecisionManagerImplTests.java | 182 ++++++++++ .../ChannelProcessingFilterTests.java | 339 ++++++++++++++++++ .../RetryWithHttpEntryPointTests.java | 171 +++++++++ .../RetryWithHttpsEntryPointTests.java | 171 +++++++++ docs/reference/src/index.xml | 294 +++++++++++++-- .../contacts/etc/cas/applicationContext.xml | 37 ++ samples/contacts/etc/cas/web.xml | 14 + .../etc/filter/applicationContext.xml | 31 +- samples/contacts/etc/filter/web.xml | 14 +- 17 files changed, 1542 insertions(+), 145 deletions(-) create mode 100644 core/src/main/java/org/acegisecurity/securechannel/InsecureChannelRequiredException.java create mode 100644 core/src/main/java/org/acegisecurity/securechannel/RetryWithHttpEntryPoint.java create mode 100644 core/src/test/java/org/acegisecurity/securechannel/ChannelDecisionManagerImplTests.java create mode 100644 core/src/test/java/org/acegisecurity/securechannel/ChannelProcessingFilterTests.java create mode 100644 core/src/test/java/org/acegisecurity/securechannel/RetryWithHttpEntryPointTests.java create mode 100644 core/src/test/java/org/acegisecurity/securechannel/RetryWithHttpsEntryPointTests.java diff --git a/changelog.txt b/changelog.txt index 3a7448d617..634de7cf23 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,4 +1,4 @@ -Changes in version 0.5 (2004-xx-xx) +Changes in version 0.5 (2004-04-28) ----------------------------------- * Added single sign on support via Yale Central Authentication Service (CAS) @@ -13,7 +13,9 @@ Changes in version 0.5 (2004-xx-xx) * Added definable prefixes to avoid expectation of "ROLE_" GrantedAuthoritys * Added pluggable AuthenticationEntryPoints to SecurityEnforcementFilter * Added Apache Ant path syntax support to SecurityEnforcementFilter +* Added filter to automate entry into secure channels, such as HTTPS * Updated JAR to Spring 1.0.1 +* Updated several classes to use absolute (not relative) redirection URLs * Refactored filters to use Spring application context lifecycle support * Improved constructor detection of nulls in User and other key objects * Fixed FilterInvocation.getRequestUrl() to also include getPathInfo() diff --git a/core/src/main/java/org/acegisecurity/securechannel/ChannelDecisionManager.java b/core/src/main/java/org/acegisecurity/securechannel/ChannelDecisionManager.java index 2b01df8f68..6547af3193 100644 --- a/core/src/main/java/org/acegisecurity/securechannel/ChannelDecisionManager.java +++ b/core/src/main/java/org/acegisecurity/securechannel/ChannelDecisionManager.java @@ -29,10 +29,11 @@ public interface ChannelDecisionManager { //~ Methods ================================================================ /** - * Decided whether the presented {@link FilterInvocation} provides - * sufficient security based on the requested {@link + * Decided whether the presented {@link FilterInvocation} provides the + * appropriate level of channel security based on the requested {@link * ConfigAttributeDefinition}. */ public void decide(FilterInvocation invocation, - ConfigAttributeDefinition config) throws SecureChannelRequiredException; + ConfigAttributeDefinition config) + throws InsecureChannelRequiredException, SecureChannelRequiredException; } diff --git a/core/src/main/java/org/acegisecurity/securechannel/ChannelDecisionManagerImpl.java b/core/src/main/java/org/acegisecurity/securechannel/ChannelDecisionManagerImpl.java index 749ddcc50c..a2a93d0dce 100644 --- a/core/src/main/java/org/acegisecurity/securechannel/ChannelDecisionManagerImpl.java +++ b/core/src/main/java/org/acegisecurity/securechannel/ChannelDecisionManagerImpl.java @@ -26,14 +26,31 @@ import java.util.Iterator; /** *

- * Requires a secure channel for a web request if a {@link - * ConfigAttribute#getAttribute()} keyword is detected. + * Ensures configuration attribute requested channel security is present by + * review of HttpServletRequest.isSecure() responses. *

* *

- * The default keyword string is REQUIRES_SECURE_CHANNEL, but this - * may be overriden to any value. The ConfigAttribute must - * exactly match the case of the keyword string. + * The class responds to two and only two case-sensitive keywords: {@link + * #getInsecureKeyword()} and {@link #getSecureKeyword}. If either of these + * keywords are detected, HttpServletRequest.isSecure() is used + * to determine the channel security offered. If the channel security differs + * from that requested by the keyword, the relevant exception is thrown. + *

+ * + *

+ * If both the secureKeyword and insecureKeyword + * configuration attributes are detected, the request will be deemed to be + * requesting a secure channel. This is a reasonable approach, as when in + * doubt, the decision manager assumes the most secure outcome is desired. Of + * course, you should indicate one configuration attribute or the other + * (not both). + *

+ * + *

+ * The default secureKeyword and insecureKeyword is + * REQUIRES_SECURE_CHANNEL and + * REQUIRES_INSECURE_CHANNEL respectively. *

* * @author Ben Alex @@ -43,21 +60,34 @@ public class ChannelDecisionManagerImpl implements InitializingBean, ChannelDecisionManager { //~ Instance fields ======================================================== - private String keyword = "REQUIRES_SECURE_CHANNEL"; + private String insecureKeyword = "REQUIRES_INSECURE_CHANNEL"; + private String secureKeyword = "REQUIRES_SECURE_CHANNEL"; //~ Methods ================================================================ - public void setKeyword(String keyword) { - this.keyword = keyword; + public void setInsecureKeyword(String insecureKeyword) { + this.insecureKeyword = insecureKeyword; } - public String getKeyword() { - return keyword; + public String getInsecureKeyword() { + return insecureKeyword; + } + + public void setSecureKeyword(String secureKeyword) { + this.secureKeyword = secureKeyword; + } + + public String getSecureKeyword() { + return secureKeyword; } public void afterPropertiesSet() throws Exception { - if ((keyword == null) || "".equals(keyword)) { - throw new IllegalArgumentException("keyword required"); + if ((secureKeyword == null) || "".equals(secureKeyword)) { + throw new IllegalArgumentException("secureKeyword required"); + } + + if ((insecureKeyword == null) || "".equals(insecureKeyword)) { + throw new IllegalArgumentException("insecureKeyword required"); } } @@ -72,12 +102,19 @@ public class ChannelDecisionManagerImpl implements InitializingBean, while (iter.hasNext()) { ConfigAttribute attribute = (ConfigAttribute) iter.next(); - if (attribute.equals(keyword)) { + if (attribute.equals(secureKeyword)) { if (!invocation.getHttpRequest().isSecure()) { throw new SecureChannelRequiredException( "Request is not being made over a secure channel"); } } + + if (attribute.equals(insecureKeyword)) { + if (invocation.getHttpRequest().isSecure()) { + throw new InsecureChannelRequiredException( + "Request is being made over a secure channel when an insecure channel is required"); + } + } } } } diff --git a/core/src/main/java/org/acegisecurity/securechannel/ChannelEntryPoint.java b/core/src/main/java/org/acegisecurity/securechannel/ChannelEntryPoint.java index 0187b2e822..5dfc4da6a7 100644 --- a/core/src/main/java/org/acegisecurity/securechannel/ChannelEntryPoint.java +++ b/core/src/main/java/org/acegisecurity/securechannel/ChannelEntryPoint.java @@ -23,7 +23,12 @@ import javax.servlet.ServletResponse; /** - * Used by {@link ChannelProcessingFilter} to launch a secure web channel. + * Used by {@link ChannelProcessingFilter} to launch a web channel. + * + *

+ * Depending on the implementation, a secure or insecure channel will be + * launched. + *

* * @author Ben Alex * @version $Id$ @@ -37,12 +42,14 @@ public interface ChannelEntryPoint { *

* Implementations should modify the headers on the * ServletResponse as necessary to commence the user agent - * using the secure channel. + * using the implementation's supported channel type (ie secure or + * insecure). *

* * @param request that resulted in a - * SecureChannelRequiredException - * @param response so that the user agent can begin using a secure channel + * SecureChannelRequiredException or + * InsecureChannelRequiredException + * @param response so that the user agent can begin using a new channel */ public void commence(ServletRequest request, ServletResponse response) throws IOException, ServletException; diff --git a/core/src/main/java/org/acegisecurity/securechannel/ChannelProcessingFilter.java b/core/src/main/java/org/acegisecurity/securechannel/ChannelProcessingFilter.java index 3e33b9e80f..8efef64f4c 100644 --- a/core/src/main/java/org/acegisecurity/securechannel/ChannelProcessingFilter.java +++ b/core/src/main/java/org/acegisecurity/securechannel/ChannelProcessingFilter.java @@ -37,7 +37,7 @@ import javax.servlet.http.HttpServletResponse; /** - * Ensures a request is delivered over a secure channel. + * Ensures a web request is delivered over the required channel. * *

* Internally uses a {@link FilterInvocation} to represent the request, so that @@ -62,7 +62,8 @@ public class ChannelProcessingFilter implements InitializingBean, Filter { //~ Instance fields ======================================================== private ChannelDecisionManager channelDecisionManager; - private ChannelEntryPoint channelEntryPoint; + private ChannelEntryPoint insecureChannelEntryPoint; + private ChannelEntryPoint secureChannelEntryPoint; private FilterInvocationDefinitionSource filterInvocationDefinitionSource; //~ Methods ================================================================ @@ -76,14 +77,6 @@ public class ChannelProcessingFilter implements InitializingBean, Filter { return channelDecisionManager; } - public void setChannelEntryPoint(ChannelEntryPoint channelEntryPoint) { - this.channelEntryPoint = channelEntryPoint; - } - - public ChannelEntryPoint getChannelEntryPoint() { - return channelEntryPoint; - } - public void setFilterInvocationDefinitionSource( FilterInvocationDefinitionSource filterInvocationDefinitionSource) { this.filterInvocationDefinitionSource = filterInvocationDefinitionSource; @@ -93,6 +86,23 @@ public class ChannelProcessingFilter implements InitializingBean, Filter { return filterInvocationDefinitionSource; } + public void setInsecureChannelEntryPoint( + ChannelEntryPoint insecureChannelEntryPoint) { + this.insecureChannelEntryPoint = insecureChannelEntryPoint; + } + + public ChannelEntryPoint getInsecureChannelEntryPoint() { + return insecureChannelEntryPoint; + } + + public void setSecureChannelEntryPoint(ChannelEntryPoint channelEntryPoint) { + this.secureChannelEntryPoint = channelEntryPoint; + } + + public ChannelEntryPoint getSecureChannelEntryPoint() { + return secureChannelEntryPoint; + } + public void afterPropertiesSet() throws Exception { if (filterInvocationDefinitionSource == null) { throw new IllegalArgumentException( @@ -104,9 +114,14 @@ public class ChannelProcessingFilter implements InitializingBean, Filter { "channelDecisionManager must be specified"); } - if (channelEntryPoint == null) { + if (secureChannelEntryPoint == null) { throw new IllegalArgumentException( - "channelEntryPoint must be specified"); + "secureChannelEntryPoint must be specified"); + } + + if (insecureChannelEntryPoint == null) { + throw new IllegalArgumentException( + "insecureChannelEntryPoint must be specified"); } } @@ -128,20 +143,32 @@ public class ChannelProcessingFilter implements InitializingBean, Filter { if (attr != null) { if (logger.isDebugEnabled()) { - logger.debug("Request : " + request.toString() + logger.debug("Request: " + fi.getFullRequestUrl() + "; ConfigAttributes: " + attr.toString()); } try { channelDecisionManager.decide(fi, attr); - } catch (SecureChannelRequiredException channelException) { + } catch (SecureChannelRequiredException secureException) { if (logger.isDebugEnabled()) { - logger.debug("Channel insufficient (" - + channelException.getMessage() - + "); delegating to channelEntryPoint"); + logger.debug("Channel insufficient security (" + + secureException.getMessage() + + "); delegating to secureChannelEntryPoint"); } - channelEntryPoint.commence(request, response); + secureChannelEntryPoint.commence(request, response); + + return; + } catch (InsecureChannelRequiredException insecureException) { + if (logger.isDebugEnabled()) { + logger.debug("Channel too much security (" + + insecureException.getMessage() + + "); delegating to insecureChannelEntryPoint"); + } + + insecureChannelEntryPoint.commence(request, response); + + return; } } diff --git a/core/src/main/java/org/acegisecurity/securechannel/InsecureChannelRequiredException.java b/core/src/main/java/org/acegisecurity/securechannel/InsecureChannelRequiredException.java new file mode 100644 index 0000000000..16838d6f49 --- /dev/null +++ b/core/src/main/java/org/acegisecurity/securechannel/InsecureChannelRequiredException.java @@ -0,0 +1,50 @@ +/* Copyright 2004 Acegi Technology Pty Limited + * + * 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 net.sf.acegisecurity.securechannel; + +import net.sf.acegisecurity.AccessDeniedException; + + +/** + * Thrown if a secure web channel is detected, but is not required. + * + * @author Ben Alex + * @version $Id$ + */ +public class InsecureChannelRequiredException extends AccessDeniedException { + //~ Constructors =========================================================== + + /** + * Constructs an InsecureChannelRequiredException with the + * specified message. + * + * @param msg the detail message. + */ + public InsecureChannelRequiredException(String msg) { + super(msg); + } + + /** + * Constructs an InsecureChannelRequiredException with the + * specified message and root cause. + * + * @param msg the detail message. + * @param t root cause + */ + public InsecureChannelRequiredException(String msg, Throwable t) { + super(msg, t); + } +} diff --git a/core/src/main/java/org/acegisecurity/securechannel/RetryWithHttpEntryPoint.java b/core/src/main/java/org/acegisecurity/securechannel/RetryWithHttpEntryPoint.java new file mode 100644 index 0000000000..133aec7b09 --- /dev/null +++ b/core/src/main/java/org/acegisecurity/securechannel/RetryWithHttpEntryPoint.java @@ -0,0 +1,120 @@ +/* Copyright 2004 Acegi Technology Pty Limited + * + * 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 net.sf.acegisecurity.securechannel; + +import net.sf.acegisecurity.util.PortMapper; +import net.sf.acegisecurity.util.PortResolver; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.beans.factory.InitializingBean; + +import java.io.IOException; + +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + + +/** + * Commences an insecure channel by retrying the original request using HTTP. + * + *

+ * This entry point should suffice in most circumstances. However, it is not + * intended to properly handle HTTP POSTs or other usage where a standard + * redirect would cause an issue. + *

+ * + * @author Ben Alex + * @version $Id$ + */ +public class RetryWithHttpEntryPoint implements InitializingBean, + ChannelEntryPoint { + //~ Static fields/initializers ============================================= + + private static final Log logger = LogFactory.getLog(RetryWithHttpEntryPoint.class); + + //~ Instance fields ======================================================== + + private PortMapper portMapper; + private PortResolver portResolver; + + //~ Methods ================================================================ + + public void setPortMapper(PortMapper portMapper) { + this.portMapper = portMapper; + } + + public PortMapper getPortMapper() { + return portMapper; + } + + public void setPortResolver(PortResolver portResolver) { + this.portResolver = portResolver; + } + + public PortResolver getPortResolver() { + return portResolver; + } + + public void afterPropertiesSet() throws Exception { + if (portMapper == null) { + throw new IllegalArgumentException("portMapper is required"); + } + + if (portResolver == null) { + throw new IllegalArgumentException("portResolver is required"); + } + } + + public void commence(ServletRequest request, ServletResponse response) + throws IOException, ServletException { + HttpServletRequest req = (HttpServletRequest) request; + + String pathInfo = req.getPathInfo(); + String queryString = req.getQueryString(); + String contextPath = req.getContextPath(); + String destination = req.getServletPath() + + ((pathInfo == null) ? "" : pathInfo) + + ((queryString == null) ? "" : ("?" + queryString)); + + String redirectUrl = contextPath; + + Integer httpsPort = new Integer(portResolver.getServerPort(req)); + Integer httpPort = portMapper.lookupHttpPort(httpsPort); + + if (httpPort != null) { + boolean includePort = true; + + if (httpPort.intValue() == 80) { + includePort = false; + } + + redirectUrl = "http://" + req.getServerName() + + ((includePort) ? (":" + httpPort) : "") + contextPath + + destination; + } + + if (logger.isDebugEnabled()) { + logger.debug("Redirecting to: " + redirectUrl); + } + + ((HttpServletResponse) response).sendRedirect(redirectUrl); + } +} diff --git a/core/src/main/java/org/acegisecurity/securechannel/RetryWithHttpsEntryPoint.java b/core/src/main/java/org/acegisecurity/securechannel/RetryWithHttpsEntryPoint.java index 27c5bb5fa1..f2ff31af28 100644 --- a/core/src/main/java/org/acegisecurity/securechannel/RetryWithHttpsEntryPoint.java +++ b/core/src/main/java/org/acegisecurity/securechannel/RetryWithHttpsEntryPoint.java @@ -15,6 +15,9 @@ package net.sf.acegisecurity.securechannel; +import net.sf.acegisecurity.util.PortMapper; +import net.sf.acegisecurity.util.PortResolver; + import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -22,10 +25,6 @@ import org.springframework.beans.factory.InitializingBean; import java.io.IOException; -import java.util.HashMap; -import java.util.Iterator; -import java.util.Map; - import javax.servlet.ServletException; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; @@ -53,71 +52,34 @@ public class RetryWithHttpsEntryPoint implements InitializingBean, //~ Instance fields ======================================================== - private Map httpsPortMappings; - - //~ Constructors =========================================================== - - public RetryWithHttpsEntryPoint() { - httpsPortMappings = new HashMap(); - httpsPortMappings.put(new Integer(80), new Integer(443)); - httpsPortMappings.put(new Integer(8080), new Integer(8443)); - } + private PortMapper portMapper; + private PortResolver portResolver; //~ Methods ================================================================ - /** - *

- * Set to override the default http port to https port mappings of 80:443, - * and 8080:8443. - *

- * In a Spring XML ApplicationContext, a definition would look something - * like this: - *
-     *   <property name="httpsPortMapping">
-     *     <map>
-     *       <entry key="80"><value>443</value></entry>
-     *       <entry key="8080"><value>8443</value></entry>
-     *     </map>
-     *   </property>
-     * 
- * - * @param newMappings A Map consisting of String keys and String values, - * where for each entry the key is the string representation of an - * integer http port number, and the value is the string - * representation of the corresponding integer https port number. - * - * @throws IllegalArgumentException if input map does not consist of String - * keys and values, each representing an integer port number in - * the range 1-65535 for that mapping. - */ - public void setHttpsPortMappings(HashMap newMappings) { - httpsPortMappings.clear(); + public void setPortMapper(PortMapper portMapper) { + this.portMapper = portMapper; + } - Iterator it = newMappings.entrySet().iterator(); + public PortMapper getPortMapper() { + return portMapper; + } - while (it.hasNext()) { - Map.Entry entry = (Map.Entry) it.next(); - Integer httpPort = new Integer((String) entry.getKey()); - Integer httpsPort = new Integer((String) entry.getValue()); + public void setPortResolver(PortResolver portResolver) { + this.portResolver = portResolver; + } - if ((httpPort.intValue() < 1) || (httpPort.intValue() > 65535) - || (httpsPort.intValue() < 1) || (httpsPort.intValue() > 65535)) { - throw new IllegalArgumentException( - "one or both ports out of legal range: " + httpPort + ", " - + httpsPort); - } - - httpsPortMappings.put(httpPort, httpsPort); - - if (httpsPortMappings.size() < 1) { - throw new IllegalArgumentException("must map at least one port"); - } - } + public PortResolver getPortResolver() { + return portResolver; } public void afterPropertiesSet() throws Exception { - if (httpsPortMappings == null) { - throw new IllegalArgumentException("httpsPortMappings required"); + if (portMapper == null) { + throw new IllegalArgumentException("portMapper is required"); + } + + if (portResolver == null) { + throw new IllegalArgumentException("portResolver is required"); } } @@ -134,25 +96,25 @@ public class RetryWithHttpsEntryPoint implements InitializingBean, String redirectUrl = contextPath; - Integer httpPort = new Integer(req.getServerPort()); - Integer httpsPort = (Integer) httpsPortMappings.get(httpPort); + Integer httpPort = new Integer(portResolver.getServerPort(req)); + Integer httpsPort = portMapper.lookupHttpsPort(httpPort); if (httpsPort != null) { - String serverName = req.getServerName(); - redirectUrl = "https://" + serverName + ":" + httpsPort - + contextPath + destination; + boolean includePort = true; + + if (httpsPort.intValue() == 443) { + includePort = false; + } + + redirectUrl = "https://" + req.getServerName() + + ((includePort) ? (":" + httpsPort) : "") + contextPath + + destination; + } + + if (logger.isDebugEnabled()) { + logger.debug("Redirecting to: " + redirectUrl); } ((HttpServletResponse) response).sendRedirect(redirectUrl); } - - /** - * Returns the translated (Integer -> Integer) version of the original port - * mapping specified via setHttpsPortMapping() - * - * @return DOCUMENT ME! - */ - protected Map getTranslatedHttpsPortMappings() { - return httpsPortMappings; - } } diff --git a/core/src/test/java/org/acegisecurity/securechannel/ChannelDecisionManagerImplTests.java b/core/src/test/java/org/acegisecurity/securechannel/ChannelDecisionManagerImplTests.java new file mode 100644 index 0000000000..96f748ea39 --- /dev/null +++ b/core/src/test/java/org/acegisecurity/securechannel/ChannelDecisionManagerImplTests.java @@ -0,0 +1,182 @@ +/* Copyright 2004 Acegi Technology Pty Limited + * + * 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 net.sf.acegisecurity.securechannel; + +import junit.framework.TestCase; + +import net.sf.acegisecurity.ConfigAttributeDefinition; +import net.sf.acegisecurity.MockFilterChain; +import net.sf.acegisecurity.MockHttpServletRequest; +import net.sf.acegisecurity.MockHttpServletResponse; +import net.sf.acegisecurity.SecurityConfig; +import net.sf.acegisecurity.intercept.web.FilterInvocation; + + +/** + * Tests {@link ChannelDecisionManagerImpl}. + * + * @author Ben Alex + * @version $Id$ + */ +public class ChannelDecisionManagerImplTests extends TestCase { + //~ Methods ================================================================ + + public final void setUp() throws Exception { + super.setUp(); + } + + public static void main(String[] args) { + junit.textui.TestRunner.run(ChannelDecisionManagerImplTests.class); + } + + public void testDetectsInvalidInsecureKeyword() throws Exception { + ChannelDecisionManagerImpl cdm = new ChannelDecisionManagerImpl(); + cdm.setInsecureKeyword(""); + + try { + cdm.afterPropertiesSet(); + fail("Should have thrown IllegalArgumentException"); + } catch (IllegalArgumentException expected) { + assertEquals("insecureKeyword required", expected.getMessage()); + } + + cdm.setInsecureKeyword(null); + + try { + cdm.afterPropertiesSet(); + fail("Should have thrown IllegalArgumentException"); + } catch (IllegalArgumentException expected) { + assertEquals("insecureKeyword required", expected.getMessage()); + } + } + + public void testDetectsInvalidSecureKeyword() throws Exception { + ChannelDecisionManagerImpl cdm = new ChannelDecisionManagerImpl(); + cdm.setSecureKeyword(""); + + try { + cdm.afterPropertiesSet(); + fail("Should have thrown IllegalArgumentException"); + } catch (IllegalArgumentException expected) { + assertEquals("secureKeyword required", expected.getMessage()); + } + + cdm.setSecureKeyword(null); + + try { + cdm.afterPropertiesSet(); + fail("Should have thrown IllegalArgumentException"); + } catch (IllegalArgumentException expected) { + assertEquals("secureKeyword required", expected.getMessage()); + } + } + + public void testDetectsNullsPassedToMainMethod() { + ChannelDecisionManagerImpl cdm = new ChannelDecisionManagerImpl(); + + try { + cdm.decide(null, new ConfigAttributeDefinition()); + fail("Should have thrown IllegalArgumentException"); + } catch (IllegalArgumentException expected) { + assertEquals("Nulls cannot be provided", expected.getMessage()); + } + + try { + cdm.decide(new FilterInvocation(new MockHttpServletRequest("x"), + new MockHttpServletResponse(), new MockFilterChain()), null); + fail("Should have thrown IllegalArgumentException"); + } catch (IllegalArgumentException expected) { + assertEquals("Nulls cannot be provided", expected.getMessage()); + } + } + + public void testDetectsWhenInsecureChannelNeededAndInsecureSchemeUsed() { + ConfigAttributeDefinition attr = new ConfigAttributeDefinition(); + attr.addConfigAttribute(new SecurityConfig( + "SOME_CONFIG_ATTRIBUTE_TO_IGNORE")); + attr.addConfigAttribute(new SecurityConfig("REQUIRES_INSECURE_CHANNEL")); + + MockHttpServletRequest request = new MockHttpServletRequest("foo=bar"); + request.setScheme("http"); + + ChannelDecisionManagerImpl cdm = new ChannelDecisionManagerImpl(); + cdm.decide(new FilterInvocation(request, new MockHttpServletResponse(), + new MockFilterChain()), attr); + assertTrue(true); + } + + public void testDetectsWhenInsecureChannelNeededAndSecureSchemeUsed() { + ConfigAttributeDefinition attr = new ConfigAttributeDefinition(); + attr.addConfigAttribute(new SecurityConfig( + "SOME_CONFIG_ATTRIBUTE_TO_IGNORE")); + attr.addConfigAttribute(new SecurityConfig("REQUIRES_INSECURE_CHANNEL")); + + MockHttpServletRequest request = new MockHttpServletRequest("foo=bar"); + request.setScheme("https"); + + ChannelDecisionManagerImpl cdm = new ChannelDecisionManagerImpl(); + + try { + cdm.decide(new FilterInvocation(request, + new MockHttpServletResponse(), new MockFilterChain()), attr); + } catch (InsecureChannelRequiredException expected) { + assertTrue(true); + } + } + + public void testDetectsWhenSecureChannelNeeded() { + ConfigAttributeDefinition attr = new ConfigAttributeDefinition(); + attr.addConfigAttribute(new SecurityConfig( + "SOME_CONFIG_ATTRIBUTE_TO_IGNORE")); + attr.addConfigAttribute(new SecurityConfig("REQUIRES_SECURE_CHANNEL")); + + MockHttpServletRequest request = new MockHttpServletRequest("foo=bar"); + request.setScheme("http"); + + ChannelDecisionManagerImpl cdm = new ChannelDecisionManagerImpl(); + + try { + cdm.decide(new FilterInvocation(request, + new MockHttpServletResponse(), new MockFilterChain()), attr); + } catch (SecureChannelRequiredException expected) { + assertTrue(true); + } + } + + public void testGetterSetters() throws Exception { + ChannelDecisionManagerImpl cdm = new ChannelDecisionManagerImpl(); + cdm.afterPropertiesSet(); + assertEquals("REQUIRES_INSECURE_CHANNEL", cdm.getInsecureKeyword()); + assertEquals("REQUIRES_SECURE_CHANNEL", cdm.getSecureKeyword()); + + cdm.setInsecureKeyword("MY_INSECURE"); + cdm.setSecureKeyword("MY_SECURE"); + + assertEquals("MY_INSECURE", cdm.getInsecureKeyword()); + assertEquals("MY_SECURE", cdm.getSecureKeyword()); + } + + public void testIgnoresOtherConfigAttributes() { + ConfigAttributeDefinition attr = new ConfigAttributeDefinition(); + attr.addConfigAttribute(new SecurityConfig("XYZ")); + + ChannelDecisionManagerImpl cdm = new ChannelDecisionManagerImpl(); + cdm.decide(new FilterInvocation(new MockHttpServletRequest("x"), + new MockHttpServletResponse(), new MockFilterChain()), attr); + assertTrue(true); + } +} +; diff --git a/core/src/test/java/org/acegisecurity/securechannel/ChannelProcessingFilterTests.java b/core/src/test/java/org/acegisecurity/securechannel/ChannelProcessingFilterTests.java new file mode 100644 index 0000000000..0f962b615d --- /dev/null +++ b/core/src/test/java/org/acegisecurity/securechannel/ChannelProcessingFilterTests.java @@ -0,0 +1,339 @@ +/* Copyright 2004 Acegi Technology Pty Limited + * + * 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 net.sf.acegisecurity.securechannel; + +import junit.framework.TestCase; + +import net.sf.acegisecurity.ConfigAttributeDefinition; +import net.sf.acegisecurity.MockFilterConfig; +import net.sf.acegisecurity.MockHttpServletRequest; +import net.sf.acegisecurity.MockHttpServletResponse; +import net.sf.acegisecurity.SecurityConfig; +import net.sf.acegisecurity.intercept.web.FilterInvocation; +import net.sf.acegisecurity.intercept.web.FilterInvocationDefinitionSource; +import net.sf.acegisecurity.intercept.web.RegExpBasedFilterInvocationDefinitionMap; + +import java.io.IOException; + +import java.util.Iterator; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; + + +/** + * Tests {@link ChannelProcessingFilter}. + * + * @author Ben Alex + * @version $Id$ + */ +public class ChannelProcessingFilterTests extends TestCase { + //~ Methods ================================================================ + + public final void setUp() throws Exception { + super.setUp(); + } + + public static void main(String[] args) { + junit.textui.TestRunner.run(ChannelProcessingFilterTests.class); + } + + public void testCallsInsecureEntryPointWhenTooMuchChannelSecurity() + throws Exception { + ConfigAttributeDefinition attr = new ConfigAttributeDefinition(); + attr.addConfigAttribute(new SecurityConfig("REQUIRES_INSECURE_CHANNEL")); + + MockFilterInvocationDefinitionMap fids = new MockFilterInvocationDefinitionMap("/path", + attr); + + ChannelProcessingFilter filter = new ChannelProcessingFilter(); + filter.setInsecureChannelEntryPoint(new MockEntryPoint(true)); + filter.setSecureChannelEntryPoint(new MockEntryPoint(false)); + filter.setFilterInvocationDefinitionSource(fids); + filter.setChannelDecisionManager(new ChannelDecisionManagerImpl()); + + MockHttpServletRequest request = new MockHttpServletRequest("info=now"); + request.setServletPath("/path"); + request.setScheme("https"); + + MockHttpServletResponse response = new MockHttpServletResponse(); + MockFilterChain chain = new MockFilterChain(false); + + filter.doFilter(request, response, chain); + assertTrue(true); + } + + public void testCallsSecureEntryPointWhenTooLittleChannelSecurity() + throws Exception { + ConfigAttributeDefinition attr = new ConfigAttributeDefinition(); + attr.addConfigAttribute(new SecurityConfig("REQUIRES_SECURE_CHANNEL")); + + MockFilterInvocationDefinitionMap fids = new MockFilterInvocationDefinitionMap("/path", + attr); + + ChannelProcessingFilter filter = new ChannelProcessingFilter(); + filter.setInsecureChannelEntryPoint(new MockEntryPoint(false)); + filter.setSecureChannelEntryPoint(new MockEntryPoint(true)); + filter.setFilterInvocationDefinitionSource(fids); + filter.setChannelDecisionManager(new ChannelDecisionManagerImpl()); + + MockHttpServletRequest request = new MockHttpServletRequest("info=now"); + request.setServletPath("/path"); + request.setScheme("http"); + + MockHttpServletResponse response = new MockHttpServletResponse(); + MockFilterChain chain = new MockFilterChain(false); + + filter.doFilter(request, response, chain); + assertTrue(true); + } + + public void testDetectsMissingChannelDecisionManager() + throws Exception { + ChannelProcessingFilter filter = new ChannelProcessingFilter(); + filter.setSecureChannelEntryPoint(new RetryWithHttpsEntryPoint()); + filter.setFilterInvocationDefinitionSource(new RegExpBasedFilterInvocationDefinitionMap()); + + try { + filter.afterPropertiesSet(); + fail("Should have thrown IllegalArgumentException"); + } catch (IllegalArgumentException expected) { + assertEquals("channelDecisionManager must be specified", + expected.getMessage()); + } + } + + public void testDetectsMissingFilterInvocationDefinitionMap() + throws Exception { + ChannelProcessingFilter filter = new ChannelProcessingFilter(); + filter.setInsecureChannelEntryPoint(new RetryWithHttpEntryPoint()); + filter.setSecureChannelEntryPoint(new RetryWithHttpsEntryPoint()); + filter.setChannelDecisionManager(new ChannelDecisionManagerImpl()); + + try { + filter.afterPropertiesSet(); + fail("Should have thrown IllegalArgumentException"); + } catch (IllegalArgumentException expected) { + assertEquals("filterInvocationDefinitionSource must be specified", + expected.getMessage()); + } + } + + public void testDetectsMissingInsecureChannelEntryPoint() + throws Exception { + ChannelProcessingFilter filter = new ChannelProcessingFilter(); + filter.setSecureChannelEntryPoint(new RetryWithHttpsEntryPoint()); + filter.setFilterInvocationDefinitionSource(new RegExpBasedFilterInvocationDefinitionMap()); + filter.setChannelDecisionManager(new ChannelDecisionManagerImpl()); + + try { + filter.afterPropertiesSet(); + fail("Should have thrown IllegalArgumentException"); + } catch (IllegalArgumentException expected) { + assertEquals("insecureChannelEntryPoint must be specified", + expected.getMessage()); + } + } + + public void testDetectsMissingSecureChannelEntryPoint() + throws Exception { + ChannelProcessingFilter filter = new ChannelProcessingFilter(); + filter.setInsecureChannelEntryPoint(new RetryWithHttpEntryPoint()); + filter.setFilterInvocationDefinitionSource(new RegExpBasedFilterInvocationDefinitionMap()); + filter.setChannelDecisionManager(new ChannelDecisionManagerImpl()); + + try { + filter.afterPropertiesSet(); + fail("Should have thrown IllegalArgumentException"); + } catch (IllegalArgumentException expected) { + assertEquals("secureChannelEntryPoint must be specified", + expected.getMessage()); + } + } + + public void testDoFilterWithNonHttpServletRequestDetected() + throws Exception { + ChannelProcessingFilter filter = new ChannelProcessingFilter(); + + try { + filter.doFilter(null, new MockHttpServletResponse(), + new MockFilterChain()); + fail("Should have thrown ServletException"); + } catch (ServletException expected) { + assertEquals("HttpServletRequest required", expected.getMessage()); + } + } + + public void testDoFilterWithNonHttpServletResponseDetected() + throws Exception { + ChannelProcessingFilter filter = new ChannelProcessingFilter(); + + try { + filter.doFilter(new MockHttpServletRequest(null, null), null, + new MockFilterChain()); + fail("Should have thrown ServletException"); + } catch (ServletException expected) { + assertEquals("HttpServletResponse required", expected.getMessage()); + } + } + + public void testDoesNotInterruptRequestsWithCorrectChannelSecurity() + throws Exception { + ConfigAttributeDefinition attr = new ConfigAttributeDefinition(); + attr.addConfigAttribute(new SecurityConfig("REQUIRES_SECURE_CHANNEL")); + + MockFilterInvocationDefinitionMap fids = new MockFilterInvocationDefinitionMap("/path", + attr); + + ChannelProcessingFilter filter = new ChannelProcessingFilter(); + filter.setInsecureChannelEntryPoint(new RetryWithHttpEntryPoint()); + filter.setSecureChannelEntryPoint(new RetryWithHttpsEntryPoint()); + filter.setFilterInvocationDefinitionSource(fids); + filter.setChannelDecisionManager(new ChannelDecisionManagerImpl()); + + MockHttpServletRequest request = new MockHttpServletRequest("info=now"); + request.setServletPath("/path"); + request.setScheme("https"); + + MockHttpServletResponse response = new MockHttpServletResponse(); + MockFilterChain chain = new MockFilterChain(true); + + filter.doFilter(request, response, chain); + assertTrue(true); + } + + public void testDoesNotInterruptRequestsWithNoConfigAttribute() + throws Exception { + ChannelProcessingFilter filter = new ChannelProcessingFilter(); + filter.setInsecureChannelEntryPoint(new RetryWithHttpEntryPoint()); + filter.setSecureChannelEntryPoint(new RetryWithHttpsEntryPoint()); + filter.setFilterInvocationDefinitionSource(new RegExpBasedFilterInvocationDefinitionMap()); + filter.setChannelDecisionManager(new ChannelDecisionManagerImpl()); + + MockHttpServletRequest request = new MockHttpServletRequest("info=now"); + MockHttpServletResponse response = new MockHttpServletResponse(); + MockFilterChain chain = new MockFilterChain(true); + + filter.doFilter(request, response, chain); + assertTrue(true); + } + + public void testGetterSetters() { + ChannelProcessingFilter filter = new ChannelProcessingFilter(); + filter.setInsecureChannelEntryPoint(new RetryWithHttpEntryPoint()); + filter.setSecureChannelEntryPoint(new RetryWithHttpsEntryPoint()); + filter.setFilterInvocationDefinitionSource(new RegExpBasedFilterInvocationDefinitionMap()); + filter.setChannelDecisionManager(new ChannelDecisionManagerImpl()); + + assertTrue(filter.getInsecureChannelEntryPoint() != null); + assertTrue(filter.getSecureChannelEntryPoint() != null); + assertTrue(filter.getFilterInvocationDefinitionSource() != null); + assertTrue(filter.getChannelDecisionManager() != null); + } + + public void testLifecycle() throws Exception { + ChannelProcessingFilter filter = new ChannelProcessingFilter(); + filter.setInsecureChannelEntryPoint(new RetryWithHttpEntryPoint()); + filter.setSecureChannelEntryPoint(new RetryWithHttpsEntryPoint()); + filter.setFilterInvocationDefinitionSource(new RegExpBasedFilterInvocationDefinitionMap()); + filter.setChannelDecisionManager(new ChannelDecisionManagerImpl()); + filter.afterPropertiesSet(); + + filter.init(new MockFilterConfig()); + filter.destroy(); + } + + //~ Inner Classes ========================================================== + + private class MockEntryPoint implements ChannelEntryPoint { + private boolean expectToBeCalled; + + public MockEntryPoint(boolean expectToBeCalled) { + this.expectToBeCalled = expectToBeCalled; + } + + private MockEntryPoint() { + super(); + } + + public void commence(ServletRequest request, ServletResponse response) + throws IOException, ServletException { + if (expectToBeCalled) { + assertTrue(true); + } else { + fail("Did not expect this ChannelEntryPoint to be called"); + } + } + } + + private class MockFilterChain implements FilterChain { + private boolean expectToProceed; + + public MockFilterChain(boolean expectToProceed) { + this.expectToProceed = expectToProceed; + } + + private MockFilterChain() { + super(); + } + + public void doFilter(ServletRequest request, ServletResponse response) + throws IOException, ServletException { + if (expectToProceed) { + assertTrue(true); + } else { + fail("Did not expect filter chain to proceed"); + } + } + } + + private class MockFilterInvocationDefinitionMap + implements FilterInvocationDefinitionSource { + private ConfigAttributeDefinition toReturn; + private String servletPath; + + public MockFilterInvocationDefinitionMap(String servletPath, + ConfigAttributeDefinition toReturn) { + this.servletPath = servletPath; + this.toReturn = toReturn; + } + + private MockFilterInvocationDefinitionMap() { + super(); + } + + public ConfigAttributeDefinition getAttributes(Object object) + throws IllegalArgumentException { + FilterInvocation fi = (FilterInvocation) object; + + if (servletPath.equals(fi.getHttpRequest().getServletPath())) { + return toReturn; + } else { + return null; + } + } + + public Iterator getConfigAttributeDefinitions() { + return null; + } + + public boolean supports(Class clazz) { + return true; + } + } +} diff --git a/core/src/test/java/org/acegisecurity/securechannel/RetryWithHttpEntryPointTests.java b/core/src/test/java/org/acegisecurity/securechannel/RetryWithHttpEntryPointTests.java new file mode 100644 index 0000000000..b0bf002fe9 --- /dev/null +++ b/core/src/test/java/org/acegisecurity/securechannel/RetryWithHttpEntryPointTests.java @@ -0,0 +1,171 @@ +/* Copyright 2004 Acegi Technology Pty Limited + * + * 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 net.sf.acegisecurity.securechannel; + +import junit.framework.TestCase; + +import net.sf.acegisecurity.MockHttpServletRequest; +import net.sf.acegisecurity.MockHttpServletResponse; +import net.sf.acegisecurity.MockPortResolver; +import net.sf.acegisecurity.util.PortMapperImpl; + +import java.util.HashMap; +import java.util.Map; + + +/** + * Tests {@link RetryWithHttpEntryPoint}. + * + * @author Ben Alex + * @version $Id$ + */ +public class RetryWithHttpEntryPointTests extends TestCase { + //~ Methods ================================================================ + + public final void setUp() throws Exception { + super.setUp(); + } + + public static void main(String[] args) { + junit.textui.TestRunner.run(RetryWithHttpEntryPointTests.class); + } + + public void testDetectsMissingPortMapper() throws Exception { + RetryWithHttpEntryPoint ep = new RetryWithHttpEntryPoint(); + ep.setPortResolver(new MockPortResolver(80, 443)); + + try { + ep.afterPropertiesSet(); + fail("Should have thrown IllegalArgumentException"); + } catch (IllegalArgumentException expected) { + assertEquals("portMapper is required", expected.getMessage()); + } + } + + public void testDetectsMissingPortResolver() throws Exception { + RetryWithHttpEntryPoint ep = new RetryWithHttpEntryPoint(); + ep.setPortMapper(new PortMapperImpl()); + + try { + ep.afterPropertiesSet(); + fail("Should have thrown IllegalArgumentException"); + } catch (IllegalArgumentException expected) { + assertEquals("portResolver is required", expected.getMessage()); + } + } + + public void testGettersSetters() { + RetryWithHttpEntryPoint ep = new RetryWithHttpEntryPoint(); + ep.setPortMapper(new PortMapperImpl()); + ep.setPortResolver(new MockPortResolver(8080, 8443)); + assertTrue(ep.getPortMapper() != null); + assertTrue(ep.getPortResolver() != null); + } + + public void testNormalOperation() throws Exception { + MockHttpServletRequest request = new MockHttpServletRequest("open=true"); + request.setScheme("https"); + request.setServerName("www.example.com"); + request.setContextPath("/bigWebApp"); + request.setServletPath("/hello"); + request.setPathInfo("/pathInfo.html"); + request.setServerPort(443); + + MockHttpServletResponse response = new MockHttpServletResponse(); + + RetryWithHttpEntryPoint ep = new RetryWithHttpEntryPoint(); + ep.setPortMapper(new PortMapperImpl()); + ep.setPortResolver(new MockPortResolver(80, 443)); + ep.afterPropertiesSet(); + + ep.commence(request, response); + System.out.println(response.getRedirect()); + assertEquals("http://www.example.com/bigWebApp/hello/pathInfo.html?open=true", + response.getRedirect()); + } + + public void testNormalOperationWithNullPathInfoAndNullQueryString() + throws Exception { + MockHttpServletRequest request = new MockHttpServletRequest(null); + request.setScheme("https"); + request.setServerName("www.example.com"); + request.setContextPath("/bigWebApp"); + request.setServletPath("/hello"); + request.setPathInfo(null); + request.setServerPort(443); + + MockHttpServletResponse response = new MockHttpServletResponse(); + + RetryWithHttpEntryPoint ep = new RetryWithHttpEntryPoint(); + ep.setPortMapper(new PortMapperImpl()); + ep.setPortResolver(new MockPortResolver(80, 443)); + ep.afterPropertiesSet(); + + ep.commence(request, response); + System.out.println(response.getRedirect()); + assertEquals("http://www.example.com/bigWebApp/hello", + response.getRedirect()); + } + + public void testOperationWhenTargetPortIsUnknown() + throws Exception { + MockHttpServletRequest request = new MockHttpServletRequest("open=true"); + request.setScheme("https"); + request.setServerName("www.example.com"); + request.setContextPath("/bigWebApp"); + request.setServletPath("/hello"); + request.setPathInfo("/pathInfo.html"); + request.setServerPort(8768); + + MockHttpServletResponse response = new MockHttpServletResponse(); + + RetryWithHttpEntryPoint ep = new RetryWithHttpEntryPoint(); + ep.setPortMapper(new PortMapperImpl()); + ep.setPortResolver(new MockPortResolver(8768, 1234)); + ep.afterPropertiesSet(); + + ep.commence(request, response); + System.out.println(response.getRedirect()); + assertEquals("/bigWebApp", response.getRedirect()); + } + + public void testOperationWithNonStandardPort() throws Exception { + MockHttpServletRequest request = new MockHttpServletRequest("open=true"); + request.setScheme("https"); + request.setServerName("www.example.com"); + request.setContextPath("/bigWebApp"); + request.setServletPath("/hello"); + request.setPathInfo("/pathInfo.html"); + request.setServerPort(9999); + + MockHttpServletResponse response = new MockHttpServletResponse(); + + PortMapperImpl portMapper = new PortMapperImpl(); + Map map = new HashMap(); + map.put("8888", "9999"); + portMapper.setPortMappings(map); + + RetryWithHttpEntryPoint ep = new RetryWithHttpEntryPoint(); + ep.setPortResolver(new MockPortResolver(8888, 9999)); + ep.setPortMapper(portMapper); + ep.afterPropertiesSet(); + + ep.commence(request, response); + System.out.println(response.getRedirect()); + assertEquals("http://www.example.com:8888/bigWebApp/hello/pathInfo.html?open=true", + response.getRedirect()); + } +} diff --git a/core/src/test/java/org/acegisecurity/securechannel/RetryWithHttpsEntryPointTests.java b/core/src/test/java/org/acegisecurity/securechannel/RetryWithHttpsEntryPointTests.java new file mode 100644 index 0000000000..85048908a7 --- /dev/null +++ b/core/src/test/java/org/acegisecurity/securechannel/RetryWithHttpsEntryPointTests.java @@ -0,0 +1,171 @@ +/* Copyright 2004 Acegi Technology Pty Limited + * + * 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 net.sf.acegisecurity.securechannel; + +import junit.framework.TestCase; + +import net.sf.acegisecurity.MockHttpServletRequest; +import net.sf.acegisecurity.MockHttpServletResponse; +import net.sf.acegisecurity.MockPortResolver; +import net.sf.acegisecurity.util.PortMapperImpl; + +import java.util.HashMap; +import java.util.Map; + + +/** + * Tests {@link RetryWithHttpsEntryPoint}. + * + * @author Ben Alex + * @version $Id$ + */ +public class RetryWithHttpsEntryPointTests extends TestCase { + //~ Methods ================================================================ + + public final void setUp() throws Exception { + super.setUp(); + } + + public static void main(String[] args) { + junit.textui.TestRunner.run(RetryWithHttpsEntryPointTests.class); + } + + public void testDetectsMissingPortMapper() throws Exception { + RetryWithHttpsEntryPoint ep = new RetryWithHttpsEntryPoint(); + ep.setPortResolver(new MockPortResolver(80, 443)); + + try { + ep.afterPropertiesSet(); + fail("Should have thrown IllegalArgumentException"); + } catch (IllegalArgumentException expected) { + assertEquals("portMapper is required", expected.getMessage()); + } + } + + public void testDetectsMissingPortResolver() throws Exception { + RetryWithHttpsEntryPoint ep = new RetryWithHttpsEntryPoint(); + ep.setPortMapper(new PortMapperImpl()); + + try { + ep.afterPropertiesSet(); + fail("Should have thrown IllegalArgumentException"); + } catch (IllegalArgumentException expected) { + assertEquals("portResolver is required", expected.getMessage()); + } + } + + public void testGettersSetters() { + RetryWithHttpsEntryPoint ep = new RetryWithHttpsEntryPoint(); + ep.setPortMapper(new PortMapperImpl()); + ep.setPortResolver(new MockPortResolver(8080, 8443)); + assertTrue(ep.getPortMapper() != null); + assertTrue(ep.getPortResolver() != null); + } + + public void testNormalOperation() throws Exception { + MockHttpServletRequest request = new MockHttpServletRequest("open=true"); + request.setScheme("http"); + request.setServerName("www.example.com"); + request.setContextPath("/bigWebApp"); + request.setServletPath("/hello"); + request.setPathInfo("/pathInfo.html"); + request.setServerPort(80); + + MockHttpServletResponse response = new MockHttpServletResponse(); + + RetryWithHttpsEntryPoint ep = new RetryWithHttpsEntryPoint(); + ep.setPortMapper(new PortMapperImpl()); + ep.setPortResolver(new MockPortResolver(80, 443)); + ep.afterPropertiesSet(); + + ep.commence(request, response); + System.out.println(response.getRedirect()); + assertEquals("https://www.example.com/bigWebApp/hello/pathInfo.html?open=true", + response.getRedirect()); + } + + public void testNormalOperationWithNullPathInfoAndNullQueryString() + throws Exception { + MockHttpServletRequest request = new MockHttpServletRequest(null); + request.setScheme("http"); + request.setServerName("www.example.com"); + request.setContextPath("/bigWebApp"); + request.setServletPath("/hello"); + request.setPathInfo(null); + request.setServerPort(80); + + MockHttpServletResponse response = new MockHttpServletResponse(); + + RetryWithHttpsEntryPoint ep = new RetryWithHttpsEntryPoint(); + ep.setPortMapper(new PortMapperImpl()); + ep.setPortResolver(new MockPortResolver(80, 443)); + ep.afterPropertiesSet(); + + ep.commence(request, response); + System.out.println(response.getRedirect()); + assertEquals("https://www.example.com/bigWebApp/hello", + response.getRedirect()); + } + + public void testOperationWhenTargetPortIsUnknown() + throws Exception { + MockHttpServletRequest request = new MockHttpServletRequest("open=true"); + request.setScheme("http"); + request.setServerName("www.example.com"); + request.setContextPath("/bigWebApp"); + request.setServletPath("/hello"); + request.setPathInfo("/pathInfo.html"); + request.setServerPort(8768); + + MockHttpServletResponse response = new MockHttpServletResponse(); + + RetryWithHttpsEntryPoint ep = new RetryWithHttpsEntryPoint(); + ep.setPortMapper(new PortMapperImpl()); + ep.setPortResolver(new MockPortResolver(8768, 1234)); + ep.afterPropertiesSet(); + + ep.commence(request, response); + System.out.println(response.getRedirect()); + assertEquals("/bigWebApp", response.getRedirect()); + } + + public void testOperationWithNonStandardPort() throws Exception { + MockHttpServletRequest request = new MockHttpServletRequest("open=true"); + request.setScheme("http"); + request.setServerName("www.example.com"); + request.setContextPath("/bigWebApp"); + request.setServletPath("/hello"); + request.setPathInfo("/pathInfo.html"); + request.setServerPort(8888); + + MockHttpServletResponse response = new MockHttpServletResponse(); + + PortMapperImpl portMapper = new PortMapperImpl(); + Map map = new HashMap(); + map.put("8888", "9999"); + portMapper.setPortMappings(map); + + RetryWithHttpsEntryPoint ep = new RetryWithHttpsEntryPoint(); + ep.setPortResolver(new MockPortResolver(8888, 9999)); + ep.setPortMapper(portMapper); + ep.afterPropertiesSet(); + + ep.commence(request, response); + System.out.println(response.getRedirect()); + assertEquals("https://www.example.com:9999/bigWebApp/hello/pathInfo.html?open=true", + response.getRedirect()); + } +} diff --git a/docs/reference/src/index.xml b/docs/reference/src/index.xml index 8396b05fbb..3f2590ec33 100644 --- a/docs/reference/src/index.xml +++ b/docs/reference/src/index.xml @@ -519,33 +519,23 @@ Notice that the filter is actually a FilterToBeanProxy. Most of the filters used by the - Acegi Security System for Spring use this class . What it does is - delegate the Filter's methods through to a bean - which is obtained from the Spring application context. This enables - the bean to benefit from the Spring application context lifecycle - support and configuration flexibility. - FilterToBeanProxy only requires a single - initialization parameter, targetClass or - targetBean. The targetClass - parameter locates the first object in the application context of the - specified class, whilst targetBean locates the - object by bean name. Like standard Spring web applications, the - FilterToBeanProxy accesses the application context - via - WebApplicationContextUtils.getWebApplicationContext(ServletContext), - so you should configure a ContextLoaderListener in - web.xml. + Acegi Security System for Spring use this class. Refer to the Filters + section to learn more about this bean. - In the application context you will need to configure three + In the application context you will need to configure four beans: <bean id="securityEnforcementFilter" class="net.sf.acegisecurity.intercept.web.SecurityEnforcementFilter"> <property name="filterSecurityInterceptor"><ref bean="filterInvocationInterceptor"/></property> <property name="authenticationEntryPoint"><ref bean="authenticationEntryPoint"/></property> + <property name="portResolver"><ref bean="portResolver"/></property> </bean> <bean id="authenticationEntryPoint" class="net.sf.acegisecurity.ui.webapp.AuthenticationProcessingFilterEntryPoint"> <property name="loginFormUrl"><value>/acegilogin.jsp</value></property> + <property name="forceHttps"><value>false</value></property> + <property name="portResolver"><ref bean="portResolver"/></property> + <property name="portMapper"><ref bean="portMapper"/></property> </bean> <bean id="filterInvocationInterceptor" class="net.sf.acegisecurity.intercept.web.FilterSecurityInterceptor"> @@ -559,6 +549,12 @@ \A/secure/.*\Z=ROLE_SUPERVISOR,ROLE_TELLER </value> </property> +</bean> + +<!-- Comment the always[Scheme]Port properties to use ServletRequest.getServerPort() --> +<bean id="portResolver" class="net.sf.acegisecurity.util.PortResolverImpl"> + <property name="alwaysHttpPort"><value>8080</value></property> + <property name="alwaysHttpsPort"><value>8443</value></property> </bean> The AuthenticationEntryPoint will be called @@ -577,6 +573,21 @@ properties related to forcing the use of HTTPS, so please refer to the JavaDocs if you require this. + The PortResolver is used to inspect a HTTP + request and determine the server port it was received on. Generally + this means using ServletRequest.getServerPort(), + although implementations can be forced to always return particular + ports (based on the transport protocol), as shown in the example + above. + + The PortMapper provides information on which + HTTPS ports correspond to which HTTP ports. This is used by the + AuthenticationProcessingFilterEntryPoint and + several other beans. The default implementation, + PortMapperImpl, knows the common HTTP ports 80 and + 8080 map to HTTPS ports 443 and 8443 respectively. You can customise + this mapping if desired. + The SecurityEnforcementFilter primarily provides session management support and initiates authentication when required. It delegates actual FilterInvocation @@ -1585,9 +1596,8 @@ public boolean supports(Class clazz); </filter-mapping> For a discussion of FilterToBeanProxy, please - refer to the FilterInvocation Security Interceptor section. The - application context will need to define the - AuthenticationProcessingFilter: + refer to the Filters section. The application context will need to + define the AuthenticationProcessingFilter: <bean id="authenticationProcessingFilter" class="net.sf.acegisecurity.ui.webapp.AuthenticationProcessingFilter"> <property name="authenticationManager"><ref bean="authenticationManager"/></property> @@ -1661,9 +1671,8 @@ public boolean supports(Class clazz); </filter-mapping> For a discussion of FilterToBeanProxy, please - refer to the FilterInvocation Security Interceptor section. The - application context will need to define the - BasicProcessingFilter and its required + refer to the Filters section. The application context will need to + define the BasicProcessingFilter and its required collaborator: <bean id="basicProcessingFilter" class="net.sf.acegisecurity.ui.basicauth.BasicProcessingFilter"> @@ -2739,6 +2748,245 @@ $CATALINA_HOME/bin/startup.sh + + Channel Security + + + Overview + + In addition to coordinating the authentication and authorization + requirements of your application, the Acegi Security System for Spring + is also able to ensure web requests are received using an appropriate + transport. If your application has many security requirements, you'll + probably want to use HTTPS as the transport, whilst less secure pages + can use the unencrypted HTTP transport. + + An important issue in considering transport security is that of + session hijacking. Your web container manages a + HttpSession by reference to a + jsessionid that is sent to user agents either via a + cookie or URL rewriting. If the jsessionid is ever + sent over HTTP, there is a possibility that session identifier can be + intercepted and used to impersonate the user after they complete the + authentication process. This is because most web containers maintain + the same session identifier for a given user, even after they switch + from HTTP to HTTPS pages. + + If session hijacking is considered too significant a risk for + your particular application, the only option is to use HTTPS for every + request. This means the jsessionid is never sent + across an insecure channel. You will need to ensure your + web.xml-defined + <welcome-file> points to a HTTPS location, + and the application never directs the user to a HTTP location. The + Acegi Security System for Spring provides a solution to assist with + the latter. + + + + Configuration + + To utilise Acegi Security's channel security services, add the + following lines to web.xml: + + <filter> + <filter-name>Acegi Channel Processing Filter</filter-name> + <filter-class>net.sf.acegisecurity.util.FilterToBeanProxy</filter-class> + <init-param> + <param-name>targetClass</param-name> + <param-value>net.sf.acegisecurity.securechannel.ChannelProcessingFilter</param-value> + </init-param> +</filter> + +<filter-mapping> + <filter-name>Acegi Channel Processing Filter</filter-name> + <url-pattern>/*</url-pattern> +</filter-mapping> + + As usual when running FilterToBeanProxy, you + will also need to configure the filter in your application + context: + + <bean id="channelProcessingFilter" class="net.sf.acegisecurity.securechannel.ChannelProcessingFilter"> + <property name="channelDecisionManager"><ref bean="channelDecisionManager"/></property> + <property name="secureChannelEntryPoint"><ref bean="secureChannelEntryPoint"/></property> + <property name="insecureChannelEntryPoint"><ref bean="insecureChannelEntryPoint"/></property> + <property name="filterInvocationDefinitionSource"> + <value> + CONVERT_URL_TO_LOWERCASE_BEFORE_COMPARISON + \A/secure/.*\Z=REQUIRES_SECURE_CHANNEL + \A/acegilogin.jsp.*\Z=REQUIRES_SECURE_CHANNEL + \A/j_acegi_security_check.*\Z=REQUIRES_SECURE_CHANNEL + \A.*\Z=REQUIRES_INSECURE_CHANNEL + </value> + </property> +</bean> + +<bean id="channelDecisionManager" class="net.sf.acegisecurity.securechannel.ChannelDecisionManagerImpl"/> + +<bean id="secureChannelEntryPoint" class="net.sf.acegisecurity.securechannel.RetryWithHttpsEntryPoint"> + <property name="portMapper"><ref bean="portMapper"/></property> + <property name="portResolver"><ref bean="portResolver"/></property> +</bean> + +<bean id="insecureChannelEntryPoint" class="net.sf.acegisecurity.securechannel.RetryWithHttpEntryPoint"> + <property name="portMapper"><ref bean="portMapper"/></property> + <property name="portResolver"><ref bean="portResolver"/></property> +</bean> + + Like FilterSecurityInterceptor, Apache Ant + style paths are also supported by the + ChannelProcessingFilter. + + The ChannelProcessingFilter operates by + filtering all web requests and determining the configuration + attributes that apply. It then delegates to the + ChannelDecisionManager. The default implementation, + ChannelDecisionManagerImpl, should suffice in most + cases. It simply throws a + SecureChannelRequiredException or + InsecureChannelRequiredException if the request's + transport channel carries too little or too much security + respectively. + + The ChannelProcessingFilter will detect the + SecureChannelRequiredException or + InsecureChannelRequiredException and delegate to + the secureChannelEntryPoint or + insecureChannelEntryPoint respectively. These entry + points implement the ChannelEntryPoint interface, + which allows the implementation to perform a redirect or take similar + action. The included RetryWithHttpsEntryPoint and + RetryWithHttpEntryPoint implementations simply + perform a redirect. + + Note that the redirections are absolute (eg + http://www.company.com:8080/app/page), not relative (eg /app/page). + During testing it was discovered that Internet Explorer 6 Service Pack + 1 appears to have a bug whereby it does not respond correctly to a + redirection instruction which also changes the port to use. + Accordingly, absolute URLs are used in conjunction with the + PortResolver interface to overcome this issue. The + PortResolverImpl is the included implementation, + and is capable of determining the port a request was received on + either from the ServletRequest.getServerPort() + method or from properties defined in the application context. Please + refer to the JavaDocs for PortResolverImpl for + further details. + + + + Usage + + Once configured, using the channel security filter is very easy. + Simply request pages without regard to the protocol (ie HTTP or HTTPS) + or port (eg 80, 8080, 443, 8443 etc). Obviously you'll still need a + way of making the initial request (probably via the + web.xml <welcome-file> or + a well-known home page URL), but once this is done the filter will + perform redirects as defined by your application context. + + + + + Filters + + + Overview + + The Acegi Security System for Spring uses filters extensively. + Each filter is covered in detail in a respective section of this + document. This section includes information that applies to all + filters. + + + + FilterToBeanProxy + + Most filters are configured using the + FilterToBeanProxy. An example configuration from + web.xml follows: + + <filter> + <filter-name>Acegi HTTP Request Security Filter</filter-name> + <filter-class>net.sf.acegisecurity.util.FilterToBeanProxy</filter-class> + <init-param> + <param-name>targetClass</param-name> + <param-value>net.sf.acegisecurity.ClassThatImplementsFilter</param-value> + </init-param> +</filter> + + Notice that the filter in web.xml is actually + a FilterToBeanProxy, and not the filter that will + actually implements the logic of the filter. What + FilterToBeanProxy does is delegate the + Filter's methods through to a bean which is + obtained from the Spring application context. This enables the bean to + benefit from the Spring application context lifecycle support and + configuration flexibility. The bean must implement + javax.servlet.Filter. + + The FilterToBeanProxy only requires a single + initialization parameter, targetClass or + targetBean. The targetClass + parameter locates the first object in the application context of the + specified class, whilst targetBean locates the + object by bean name. Like standard Spring web applications, the + FilterToBeanProxy accesses the application context + via + WebApplicationContextUtils.getWebApplicationContext(ServletContext), + so you should configure a ContextLoaderListener in + web.xml. + + + + Filter Ordering + + The order that filters are defined in web.xml + is important. + + Irrespective of which filters you are actually using, the order + of the <filter-mapping>s should be as + follows: + + + + Acegi Channel Processing Filter + (ChannelProcessingFilter) + + + + Acegi Authentication Processing Filter + (AuthenticationProcessingFilter) + + + + Acegi CAS Processing Filter + (CasProcessingFilter) + + + + Acegi HTTP BASIC Authorization Filter + (BasicProcessingFilter) + + + + Acegi Security System for Spring Auto Integration Filter + (AutoIntegrationFilter) + + + + Acegi HTTP Request Security Filter + (SecurityEnforcementFilter) + + + + All of the above filters use + FilterToBeanProxy, which is discussed in the + previous section. + + + Contacts Sample Application diff --git a/samples/contacts/etc/cas/applicationContext.xml b/samples/contacts/etc/cas/applicationContext.xml index d3528c8445..2c2d71064e 100644 --- a/samples/contacts/etc/cas/applicationContext.xml +++ b/samples/contacts/etc/cas/applicationContext.xml @@ -159,6 +159,34 @@ + + + + + + + + + CONVERT_URL_TO_LOWERCASE_BEFORE_COMPARISON + \A/secure/.*\Z=REQUIRES_SECURE_CHANNEL + \A/j_acegi_cas_security_check.*\Z=REQUIRES_SECURE_CHANNEL + \A.*\Z=REQUIRES_INSECURE_CHANNEL + + + + + + + + + + + + + + + + @@ -171,6 +199,7 @@ + @@ -178,6 +207,14 @@ + + + + + 8080 + 8443 + + false diff --git a/samples/contacts/etc/cas/web.xml b/samples/contacts/etc/cas/web.xml index fe02e4c524..8474ca5842 100644 --- a/samples/contacts/etc/cas/web.xml +++ b/samples/contacts/etc/cas/web.xml @@ -33,6 +33,15 @@ /WEB-INF/applicationContext.xml + + Acegi Channel Processing Filter + net.sf.acegisecurity.util.FilterToBeanProxy + + targetClass + net.sf.acegisecurity.securechannel.ChannelProcessingFilter + + + Acegi CAS Processing Filter net.sf.acegisecurity.util.FilterToBeanProxy @@ -65,6 +74,11 @@ + + Acegi Channel Processing Filter + /* + + Acegi CAS Processing Filter /* diff --git a/samples/contacts/etc/filter/applicationContext.xml b/samples/contacts/etc/filter/applicationContext.xml index a89199cdc8..c18458718c 100644 --- a/samples/contacts/etc/filter/applicationContext.xml +++ b/samples/contacts/etc/filter/applicationContext.xml @@ -134,23 +134,36 @@ + + - + + CONVERT_URL_TO_LOWERCASE_BEFORE_COMPARISON \A/secure/.*\Z=REQUIRES_SECURE_CHANNEL - \A/info/.*\Z=REQUIRES_SECURE_CHANNEL \A/acegilogin.jsp.*\Z=REQUIRES_SECURE_CHANNEL + \A/j_acegi_security_check.*\Z=REQUIRES_SECURE_CHANNEL + \A.*\Z=REQUIRES_INSECURE_CHANNEL - + + + + + + + + + @@ -164,10 +177,22 @@ + /acegilogin.jsp + false + + + + + + + + + 8080 + 8443 diff --git a/samples/contacts/etc/filter/web.xml b/samples/contacts/etc/filter/web.xml index 86ae3db0f4..c3b851e85c 100644 --- a/samples/contacts/etc/filter/web.xml +++ b/samples/contacts/etc/filter/web.xml @@ -23,8 +23,8 @@ contextConfigLocation /WEB-INF/applicationContext.xml - - + + Acegi Channel Processing Filter net.sf.acegisecurity.util.FilterToBeanProxy @@ -64,12 +64,16 @@ net.sf.acegisecurity.intercept.web.SecurityEnforcementFilter - + + + + Acegi Authentication Processing Filter /* @@ -99,7 +103,7 @@ org.springframework.web.context.ContextLoaderListener - +