SEC-1171: Allow multiple http elements and add pattern attribute to specify filter chain mapping.

This commit is contained in:
Luke Taylor 2010-04-06 20:50:47 +01:00
parent b0758dd8de
commit 7d74b7c87e
7 changed files with 153 additions and 73 deletions

View File

@ -22,21 +22,18 @@ import org.springframework.security.web.authentication.www.BasicAuthenticationFi
import org.springframework.security.web.context.SecurityContextPersistenceFilter;
import org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter;
import org.springframework.security.web.session.SessionManagementFilter;
import org.springframework.security.web.util.AnyRequestMatcher;
public class DefaultFilterChainValidator implements FilterChainProxy.FilterChainValidator {
private Log logger = LogFactory.getLog(getClass());
public void validate(FilterChainProxy fcp) {
for(List<Filter> filters : fcp.getFilterChainMap().values()) {
checkLoginPageIsntProtected(fcp, filters);
checkFilterStack(filters);
}
checkLoginPageIsntProtected(fcp);
}
private Object getFilter(Class<?> type, List<Filter> filters) {
for (Filter f : filters) {
if (type.isAssignableFrom(f.getClass())) {
return f;
@ -77,59 +74,60 @@ public class DefaultFilterChainValidator implements FilterChainProxy.FilterChain
}
/* Checks for the common error of having a login page URL protected by the security interceptor */
private void checkLoginPageIsntProtected(FilterChainProxy fcp) {
List<Filter> defaultFilters = fcp.getFilterChainMap().get(new AnyRequestMatcher());
ExceptionTranslationFilter etf = (ExceptionTranslationFilter)getFilter(ExceptionTranslationFilter.class, defaultFilters);
private void checkLoginPageIsntProtected(FilterChainProxy fcp, List<Filter> filterStack) {
ExceptionTranslationFilter etf = (ExceptionTranslationFilter)getFilter(ExceptionTranslationFilter.class, filterStack);
if (etf.getAuthenticationEntryPoint() instanceof LoginUrlAuthenticationEntryPoint) {
String loginPage =
((LoginUrlAuthenticationEntryPoint)etf.getAuthenticationEntryPoint()).getLoginFormUrl();
FilterInvocation loginRequest = new FilterInvocation(loginPage, "POST");
List<Filter> filters = fcp.getFilters(loginPage);
logger.info("Checking whether login URL '" + loginPage + "' is accessible with your configuration");
if(etf == null || !(etf.getAuthenticationEntryPoint() instanceof LoginUrlAuthenticationEntryPoint)) {
return;
}
if (filters == null || filters.isEmpty()) {
logger.debug("Filter chain is empty for the login page");
return;
String loginPage = ((LoginUrlAuthenticationEntryPoint)etf.getAuthenticationEntryPoint()).getLoginFormUrl();
FilterInvocation loginRequest = new FilterInvocation(loginPage, "POST");
List<Filter> filters = fcp.getFilters(loginPage);
logger.info("Checking whether login URL '" + loginPage + "' is accessible with your configuration");
if (filters == null || filters.isEmpty()) {
logger.debug("Filter chain is empty for the login page");
return;
}
if (getFilter(DefaultLoginPageGeneratingFilter.class, filters) != null) {
logger.debug("Default generated login page is in use");
return;
}
FilterSecurityInterceptor fsi = (FilterSecurityInterceptor) getFilter(FilterSecurityInterceptor.class, filters);
DefaultFilterInvocationSecurityMetadataSource fids =
(DefaultFilterInvocationSecurityMetadataSource) fsi.getSecurityMetadataSource();
Collection<ConfigAttribute> attributes = fids.getAttributes(loginRequest);
if (attributes == null) {
logger.debug("No access attributes defined for login page URL");
if (fsi.isRejectPublicInvocations()) {
logger.warn("FilterSecurityInterceptor is configured to reject public invocations." +
" Your login page may not be accessible.");
}
return;
}
if (getFilter(DefaultLoginPageGeneratingFilter.class, filters) != null) {
logger.debug("Default generated login page is in use");
return;
}
AnonymousAuthenticationFilter anonPF = (AnonymousAuthenticationFilter) getFilter(AnonymousAuthenticationFilter.class, filters);
if (anonPF == null) {
logger.warn("The login page is being protected by the filter chain, but you don't appear to have" +
" anonymous authentication enabled. This is almost certainly an error.");
return;
}
FilterSecurityInterceptor fsi = (FilterSecurityInterceptor) getFilter(FilterSecurityInterceptor.class, filters);
DefaultFilterInvocationSecurityMetadataSource fids =
(DefaultFilterInvocationSecurityMetadataSource) fsi.getSecurityMetadataSource();
Collection<ConfigAttribute> attributes = fids.getAttributes(loginRequest);
if (attributes == null) {
logger.debug("No access attributes defined for login page URL");
if (fsi.isRejectPublicInvocations()) {
logger.warn("FilterSecurityInterceptor is configured to reject public invocations." +
" Your login page may not be accessible.");
}
return;
}
AnonymousAuthenticationFilter anonPF = (AnonymousAuthenticationFilter) getFilter(AnonymousAuthenticationFilter.class, filters);
if (anonPF == null) {
logger.warn("The login page is being protected by the filter chain, but you don't appear to have" +
" anonymous authentication enabled. This is almost certainly an error.");
return;
}
// Simulate an anonymous access with the supplied attributes.
AnonymousAuthenticationToken token = new AnonymousAuthenticationToken("key", anonPF.getUserAttribute().getPassword(),
anonPF.getUserAttribute().getAuthorities());
try {
fsi.getAccessDecisionManager().decide(token, new Object(), attributes);
} catch (Exception e) {
logger.warn("Anonymous access to the login page doesn't appear to be enabled. This is almost certainly " +
"an error. Please check your configuration allows unauthenticated access to the configured " +
"login page. (Simulated access was rejected: " + e + ")");
}
// Simulate an anonymous access with the supplied attributes.
AnonymousAuthenticationToken token = new AnonymousAuthenticationToken("key", anonPF.getUserAttribute().getPassword(),
anonPF.getUserAttribute().getAuthorities());
try {
fsi.getAccessDecisionManager().decide(token, new Object(), attributes);
} catch (Exception e) {
logger.warn("Anonymous access to the login page doesn't appear to be enabled. This is almost certainly " +
"an error. Please check your configuration allows unauthenticated access to the configured " +
"login page. (Simulated access was rejected: " + e + ")");
}
}
}

View File

@ -11,6 +11,7 @@ import org.springframework.beans.BeanMetadataElement;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.BeanReference;
import org.springframework.beans.factory.config.RuntimeBeanReference;
import org.springframework.beans.factory.config.ConstructorArgumentValues.ValueHolder;
import org.springframework.beans.factory.parsing.BeanComponentDefinition;
import org.springframework.beans.factory.parsing.CompositeComponentDefinition;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
@ -104,9 +105,20 @@ public class HttpSecurityBeanDefinitionParser implements BeanDefinitionParser {
filterChain.add(od.bean);
}
// Get the map of empty filter chains (if any)
ManagedMap<BeanDefinition, List<BeanMetadataElement>> filterChainMap = httpBldr.getFilterChainMap();
BeanDefinition universalMatch = new RootBeanDefinition(AnyRequestMatcher.class);
filterChainMap.put(universalMatch, filterChain);
String filterChainPattern = element.getAttribute(ATT_PATH_PATTERN);
BeanDefinition filterChainMatcher;
if (StringUtils.hasText(filterChainPattern)) {
filterChainMatcher = matcherType.createMatcher(filterChainPattern, null);
} else {
filterChainMatcher = new RootBeanDefinition(AnyRequestMatcher.class);
}
filterChainMap.put(filterChainMatcher, filterChain);
registerFilterChainProxy(pc, filterChainMap, source);
@ -215,18 +227,33 @@ public class HttpSecurityBeanDefinitionParser implements BeanDefinitionParser {
return customFilters;
}
@SuppressWarnings("unchecked")
private void registerFilterChainProxy(ParserContext pc, Map<BeanDefinition, List<BeanMetadataElement>> filterChainMap, Object source) {
if (pc.getRegistry().containsBeanDefinition(BeanIds.FILTER_CHAIN_PROXY)) {
pc.getReaderContext().error("Duplicate <http> element detected", source);
}
// Already registered. Obtain the filter chain map and add the new entries to it
// pc.getReaderContext().error("Duplicate <http> element detected", source);
BeanDefinitionBuilder fcpBldr = BeanDefinitionBuilder.rootBeanDefinition(FilterChainProxy.class);
fcpBldr.getRawBeanDefinition().setSource(source);
// fcpBldr.addPropertyValue("stripQueryStringFromUrls", Boolean.valueOf(matcher instanceof AntUrlPathMatcher));
fcpBldr.addPropertyValue("filterChainMap", filterChainMap);
BeanDefinition fcpBean = fcpBldr.getBeanDefinition();
pc.registerBeanComponent(new BeanComponentDefinition(fcpBean, BeanIds.FILTER_CHAIN_PROXY));
pc.getRegistry().registerAlias(BeanIds.FILTER_CHAIN_PROXY, BeanIds.SPRING_SECURITY_FILTER_CHAIN);
BeanDefinition fcp = pc.getRegistry().getBeanDefinition(BeanIds.FILTER_CHAIN_PROXY);
Map existingFilterChainMap = (Map) fcp.getPropertyValues().getPropertyValue("filterChainMap").getValue();
for (BeanDefinition matcherBean : filterChainMap.keySet()) {
if (existingFilterChainMap.containsKey(matcherBean)) {
Map<Integer,ValueHolder> args = matcherBean.getConstructorArgumentValues().getIndexedArgumentValues();
pc.getReaderContext().error("The filter chain map already contains this request matcher ["
+ args.get(0).getValue() + ", " +args.get(1).getValue() + "]", source);
}
}
existingFilterChainMap.putAll(filterChainMap);
} else {
// Not already registered, so register it
BeanDefinitionBuilder fcpBldr = BeanDefinitionBuilder.rootBeanDefinition(FilterChainProxy.class);
fcpBldr.getRawBeanDefinition().setSource(source);
fcpBldr.addPropertyValue("filterChainMap", filterChainMap);
fcpBldr.addPropertyValue("filterChainValidator", new RootBeanDefinition(DefaultFilterChainValidator.class));
BeanDefinition fcpBean = fcpBldr.getBeanDefinition();
pc.registerBeanComponent(new BeanComponentDefinition(fcpBean, BeanIds.FILTER_CHAIN_PROXY));
pc.getRegistry().registerAlias(BeanIds.FILTER_CHAIN_PROXY, BeanIds.SPRING_SECURITY_FILTER_CHAIN);
}
}
}

View File

@ -257,6 +257,9 @@ protect-pointcut.attlist &=
http =
## Container element for HTTP security configuration
element http {http.attlist, (intercept-url+ & access-denied-handler? & form-login? & openid-login? & x509? & http-basic? & logout? & session-management & remember-me? & anonymous? & port-mappings & custom-filter* & request-cache?) }
http.attlist &=
## The request URL pattern which will be mapped to the filter chain created by this <http> element. If omitted, the filter chain will match all requests.
attribute pattern {xsd:token}?
http.attlist &=
## Automatically registers a login form, BASIC authentication, anonymous authentication, logout services, remember-me and servlet-api-integration. If set to "true", all of these capabilities are added (although you can still customize the configuration of each by providing the respective element). If unspecified, defaults to "false".
attribute auto-config {boolean}?

View File

@ -687,6 +687,11 @@
<xs:attributeGroup ref="security:http.attlist"/>
</xs:complexType></xs:element>
<xs:attributeGroup name="http.attlist">
<xs:attribute name="pattern" type="xs:token">
<xs:annotation>
<xs:documentation>The request URL pattern which will be mapped to the filter chain created by this &lt;http&gt; element. If omitted, the filter chain will match all requests.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="auto-config" type="security:boolean">
<xs:annotation>
<xs:documentation>Automatically registers a login form, BASIC authentication, anonymous authentication, logout services, remember-me and servlet-api-integration. If set to "true", all of these capabilities are added (although you can still customize the configuration of each by providing the respective element). If unspecified, defaults to "false".</xs:documentation>

View File

@ -82,16 +82,6 @@ class MiscHttpConfigTests extends AbstractHttpConfigTests {
assert fsi.isObserveOncePerRequest()
}
def duplicateElementCausesError() {
when: "Two http blocks are defined"
xml.http('auto-config': 'true')
xml.http('auto-config': 'true')
createAppContext()
then:
BeanDefinitionParsingException e = thrown();
}
def filterListShouldBeEmptyForPatternWithNoFilters() {
httpAutoConfig() {
interceptUrlNoFilters('/unprotected')

View File

@ -0,0 +1,47 @@
package org.springframework.security.config.http
import java.util.Map;
import java.util.List;
import org.springframework.security.web.FilterChainProxy;
import org.springframework.security.config.BeanIds;
import org.springframework.beans.factory.parsing.BeanDefinitionParsingException
/**
* Tests scenarios with multiple &lt;http&gt; elements.
*
* @author Luke Taylor
*/
class MultiHttpBlockConfigTests extends AbstractHttpConfigTests {
def multipleHttpElementsAreSupported () {
when: "Two <http> elements are used"
xml.http(pattern: '/stateless/**', 'create-session': 'stateless') {
'http-basic'()
}
xml.http(pattern: '/stateful/**') {
'form-login'()
}
createAppContext()
FilterChainProxy fcp = appContext.getBean(BeanIds.FILTER_CHAIN_PROXY)
Map filterChains = fcp.getFilterChainMap();
then:
filterChains.size() == 2
(filterChains.keySet() as List)[0].pattern == '/stateless/**'
}
def duplicatePatternsAreRejected () {
when: "Two <http> elements are used"
xml.http(pattern: '/stateless/**', 'create-session': 'stateless') {
'http-basic'()
}
xml.http(pattern: '/stateless/**') {
'form-login'()
}
createAppContext()
then:
thrown(BeanDefinitionParsingException)
}
}

View File

@ -80,6 +80,16 @@ public final class AntPathRequestMatcher implements RequestMatcher {
return pattern;
}
@Override
public boolean equals(Object obj) {
if (!(obj instanceof AntPathRequestMatcher)) {
return false;
}
AntPathRequestMatcher other = (AntPathRequestMatcher)obj;
return this.pattern.equals(other.pattern) &&
this.httpMethod == other.httpMethod;
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();