diff --git a/config/config.gradle b/config/config.gradle index 4e4689e758..6d8a34e0de 100644 --- a/config/config.gradle +++ b/config/config.gradle @@ -15,12 +15,22 @@ dependencies { // NB: Don't add other compile time dependencies to the config module as this breaks tooling compile project(':spring-security-core'), project(':spring-security-web'), + project(':spring-security-openid'), + project(':spring-security-ldap'), "org.aspectj:aspectjweaver:$aspectjVersion", 'aopalliance:aopalliance:1.0', "org.springframework:spring-aop:$springVersion", "org.springframework:spring-context:$springVersion", "org.springframework:spring-web:$springVersion", - "org.springframework:spring-beans:$springVersion" + "org.springframework:spring-beans:$springVersion", + "org.springframework:spring-jdbc:$springVersion", + "org.springframework:spring-tx:$springVersion", + "org.springframework.ldap:spring-ldap-core:$springLdapVersion" + compile('org.openid4java:openid4java-nodeps:0.9.6') { + exclude group: 'com.google.code.guice', module: 'guice' + } + compile 'com.google.inject:guice:2.0' + compile apacheds_libs provided "org.apache.tomcat:tomcat-servlet-api:$servletApiVersion" @@ -28,6 +38,7 @@ dependencies { testCompile project(':spring-security-ldap'), project(':spring-security-openid'), + project(':spring-security-cas'), project(':spring-security-core').sourceSets.test.output, 'javax.annotation:jsr250-api:1.0', "org.springframework.ldap:spring-ldap-core:$springLdapVersion", diff --git a/config/src/integration-test/java/org/springframework/security/config/annotation/authentication/ldap/LdapAuthenticationProviderBuilderSecurityBuilderTests.groovy b/config/src/integration-test/java/org/springframework/security/config/annotation/authentication/ldap/LdapAuthenticationProviderBuilderSecurityBuilderTests.groovy new file mode 100644 index 0000000000..e47a834571 --- /dev/null +++ b/config/src/integration-test/java/org/springframework/security/config/annotation/authentication/ldap/LdapAuthenticationProviderBuilderSecurityBuilderTests.groovy @@ -0,0 +1,177 @@ +/* + * Copyright 2002-2013 the original author or authors. + * + * 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 org.springframework.security.config.annotation.authentication.ldap + +import org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.ldap.core.ContextSource; +import org.springframework.ldap.core.support.BaseLdapPathContextSource; +import org.springframework.security.authentication.AuthenticationManager +import org.springframework.security.authentication.AuthenticationProvider +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken +import org.springframework.security.config.annotation.BaseSpringSpec +import org.springframework.security.config.annotation.SecurityBuilder; +import org.springframework.security.config.annotation.authentication.AuthenticationManagerBuilder +import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; +import org.springframework.security.ldap.DefaultSpringSecurityContextSource; +import org.springframework.security.ldap.authentication.LdapAuthenticationProvider; +import org.springframework.security.ldap.server.ApacheDSContainer; +import org.springframework.test.util.ReflectionTestUtils; + +/** + * + * @author Rob Winch + * + */ +class LdapAuthenticationProviderBuilderSecurityBuilderTests extends BaseSpringSpec { + def "default configuration"() { + when: + loadConfig(DefaultLdapConfig) + LdapAuthenticationProvider provider = ldapProvider() + then: + provider.authoritiesPopulator.groupRoleAttribute == "cn" + provider.authoritiesPopulator.groupSearchBase == "" + provider.authoritiesPopulator.groupSearchFilter == "(uniqueMember={0})" + ReflectionTestUtils.getField(provider,"authoritiesMapper").prefix == "ROLE_" + + } + + @Configuration + static class DefaultLdapConfig extends BaseLdapProviderConfig { + protected void registerAuthentication( + AuthenticationManagerBuilder auth) throws Exception { + auth + .ldapAuthentication() + .contextSource(contextSource()) + } + } + + def "group roles custom"() { + when: + loadConfig(GroupRolesConfig) + LdapAuthenticationProvider provider = ldapProvider() + then: + provider.authoritiesPopulator.groupRoleAttribute == "group" + } + + @Configuration + static class GroupRolesConfig extends BaseLdapProviderConfig { + protected void registerAuthentication(AuthenticationManagerBuilder auth) throws Exception { + auth + .ldapAuthentication() + .contextSource(contextSource()) + .groupRoleAttribute("group") + } + } + + def "group search custom"() { + when: + loadConfig(GroupSearchConfig) + LdapAuthenticationProvider provider = ldapProvider() + then: + provider.authoritiesPopulator.groupSearchFilter == "ou=groupName" + } + + @Configuration + static class GroupSearchConfig extends BaseLdapProviderConfig { + protected void registerAuthentication( + AuthenticationManagerBuilder auth) throws Exception { + auth + .ldapAuthentication() + .contextSource(contextSource()) + .groupSearchFilter("ou=groupName"); + } + } + + def "role prefix custom"() { + when: + loadConfig(RolePrefixConfig) + LdapAuthenticationProvider provider = ldapProvider() + then: + ReflectionTestUtils.getField(provider,"authoritiesMapper").prefix == "role_" + } + + @Configuration + static class RolePrefixConfig extends BaseLdapProviderConfig { + protected void registerAuthentication( + AuthenticationManagerBuilder auth) throws Exception { + auth + .ldapAuthentication() + .contextSource(contextSource()) + .rolePrefix("role_") + } + } + + def "bind authentication"() { + when: + loadConfig(BindAuthenticationConfig) + AuthenticationManager auth = context.getBean(AuthenticationManager) + then: + auth + auth.authenticate(new UsernamePasswordAuthenticationToken("admin","password")).authorities.collect { it.authority }.sort() == ["ROLE_ADMIN","ROLE_USER"] + } + + @Configuration + static class BindAuthenticationConfig extends BaseLdapServerConfig { + protected void registerAuthentication( + AuthenticationManagerBuilder auth) throws Exception { + auth + .ldapAuthentication() + .contextSource(contextSource()) + .groupSearchBase("ou=groups") + .userDnPatterns("uid={0},ou=people"); + } + } + + def ldapProvider() { + context.getBean(AuthenticationManager).providers[0] + } + + @Configuration + static abstract class BaseLdapServerConfig extends BaseLdapProviderConfig { + @Bean + public ApacheDSContainer ldapServer() throws Exception { + ApacheDSContainer apacheDSContainer = new ApacheDSContainer("dc=springframework,dc=org", "classpath:/users.ldif"); + apacheDSContainer.setPort(33389); + return apacheDSContainer; + } + } + + @Configuration + static abstract class BaseLdapProviderConfig { + @Bean + public AuthenticationManager authenticationManager() { + AuthenticationManagerBuilder registry = new AuthenticationManagerBuilder(); + registerAuthentication(registry); + return registry.build(); + } + + protected abstract void registerAuthentication( + AuthenticationManagerBuilder auth) throws Exception; + + @Bean + public BaseLdapPathContextSource contextSource() throws Exception { + DefaultSpringSecurityContextSource contextSource = new DefaultSpringSecurityContextSource( + "ldap://127.0.0.1:33389/dc=springframework,dc=org") + contextSource.userDn = "uid=admin,ou=system" + contextSource.password = "secret" + contextSource.afterPropertiesSet(); + return contextSource; + } + } +} diff --git a/config/src/integration-test/java/org/springframework/security/config/annotation/authentication/ldap/NamespaceLdapAuthenticationProviderTests.groovy b/config/src/integration-test/java/org/springframework/security/config/annotation/authentication/ldap/NamespaceLdapAuthenticationProviderTests.groovy new file mode 100644 index 0000000000..6a8fd83b1d --- /dev/null +++ b/config/src/integration-test/java/org/springframework/security/config/annotation/authentication/ldap/NamespaceLdapAuthenticationProviderTests.groovy @@ -0,0 +1,68 @@ +/* + * Copyright 2002-2013 the original author or authors. + * + * 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 org.springframework.security.config.annotation.authentication.ldap + +import static org.springframework.security.config.annotation.authentication.ldap.NamespaceLdapAuthenticationProviderTestsConfigs.* + +import org.springframework.ldap.core.support.BaseLdapPathContextSource +import org.springframework.security.authentication.AuthenticationManager +import org.springframework.security.authentication.AuthenticationProvider +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken +import org.springframework.security.config.annotation.BaseSpringSpec +import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; +import org.springframework.security.config.annotation.authentication.ldap.NamespaceLdapAuthenticationProviderTestsConfigs.LdapAuthenticationProviderConfig; +import org.springframework.security.ldap.authentication.LdapAuthenticationProvider; +import org.springframework.security.ldap.authentication.PasswordComparisonAuthenticator; +import org.springframework.security.ldap.userdetails.PersonContextMapper; +import org.springframework.test.util.ReflectionTestUtils; + +/** + * + * @author Rob Winch + * + */ +class NamespaceLdapAuthenticationProviderTests extends BaseSpringSpec { + def "ldap-authentication-provider"() { + when: + loadConfig(LdapAuthenticationProviderConfig) + then: + authenticationManager.authenticate(new UsernamePasswordAuthenticationToken("user","password")).authorities*.authority.sort() == ['ROLE_USER'] + } + + def "ldap-authentication-provider custom"() { + when: + loadConfig(CustomLdapAuthenticationProviderConfig) + LdapAuthenticationProvider provider = findAuthenticationProvider(LdapAuthenticationProvider) + then: + provider.authoritiesPopulator.groupRoleAttribute == "cn" + provider.authoritiesPopulator.groupSearchBase == "ou=groups" + provider.authoritiesPopulator.groupSearchFilter == "(member={0})" + ReflectionTestUtils.getField(provider,"authoritiesMapper").prefix == "PREFIX_" + provider.userDetailsContextMapper instanceof PersonContextMapper + provider.authenticator.getUserDns("user") == ["uid=user,ou=people"] + provider.authenticator.userSearch.searchBase == "ou=users" + provider.authenticator.userSearch.searchFilter == "(uid={0})" + } + + def "ldap-authentication-provider password compare"() { + when: + loadConfig(PasswordCompareLdapConfig) + LdapAuthenticationProvider provider = findAuthenticationProvider(LdapAuthenticationProvider) + then: + provider.authenticator instanceof PasswordComparisonAuthenticator + authenticationManager.authenticate(new UsernamePasswordAuthenticationToken("user","password")).authorities*.authority.sort() == ['ROLE_USER'] + } +} diff --git a/config/src/integration-test/java/org/springframework/security/config/annotation/authentication/ldap/NamespaceLdapAuthenticationProviderTestsConfigs.java b/config/src/integration-test/java/org/springframework/security/config/annotation/authentication/ldap/NamespaceLdapAuthenticationProviderTestsConfigs.java new file mode 100644 index 0000000000..094a0b133c --- /dev/null +++ b/config/src/integration-test/java/org/springframework/security/config/annotation/authentication/ldap/NamespaceLdapAuthenticationProviderTestsConfigs.java @@ -0,0 +1,84 @@ +/* + * Copyright 2002-2013 the original author or authors. + * + * 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 org.springframework.security.config.annotation.authentication.ldap; + +import org.springframework.context.annotation.Configuration; +import org.springframework.security.authentication.encoding.PlaintextPasswordEncoder; +import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; +import org.springframework.security.ldap.userdetails.PersonContextMapper; + +/** + * @author Rob Winch + * + */ +public class NamespaceLdapAuthenticationProviderTestsConfigs { + + @Configuration + @EnableWebSecurity + static class LdapAuthenticationProviderConfig extends WebSecurityConfigurerAdapter { + protected void registerAuthentication( + AuthenticationManagerBuilder auth) throws Exception { + auth + .ldapAuthentication() + .groupSearchBase("ou=groups") + .userDnPatterns("uid={0},ou=people"); // ldap-server@user-dn-pattern + } + } + + @Configuration + @EnableWebSecurity + static class CustomLdapAuthenticationProviderConfig extends WebSecurityConfigurerAdapter { + protected void registerAuthentication( + AuthenticationManagerBuilder auth) throws Exception { + auth + .ldapAuthentication() + .groupRoleAttribute("cn") // ldap-authentication-provider@group-role-attribute + .groupSearchBase("ou=groups") // ldap-authentication-provider@group-search-base + .groupSearchFilter("(member={0})") // ldap-authentication-provider@group-search-filter + .rolePrefix("PREFIX_") // ldap-authentication-provider@group-search-filter + .userDetailsContextMapper(new PersonContextMapper()) // ldap-authentication-provider@user-context-mapper-ref / ldap-authentication-provider@user-details-class + .userDnPatterns("uid={0},ou=people") // ldap-authentication-provider@user-dn-pattern + .userSearchBase("ou=users") // ldap-authentication-provider@user-dn-pattern + .userSearchFilter("(uid={0})") // ldap-authentication-provider@user-search-filter + // .contextSource(contextSource) // ldap-authentication-provider@server-ref + .contextSource() + .ldif("classpath:user.ldif") // ldap-server@ldif + .managerDn("uid=admin,ou=system") // ldap-server@manager-dn + .managerPassword("secret") // ldap-server@manager-password + .port(33399) // ldap-server@port + .root("dc=springframework,dc=org") // ldap-server@root + // .url("ldap://localhost:33389/dc-springframework,dc=org") this overrides root and port and is used for external + ; + } + } + + @Configuration + @EnableWebSecurity + static class PasswordCompareLdapConfig extends WebSecurityConfigurerAdapter { + protected void registerAuthentication( + AuthenticationManagerBuilder auth) throws Exception { + auth + .ldapAuthentication() + .groupSearchBase("ou=groups") + .userSearchFilter("(uid={0})") + .passwordCompare() + .passwordEncoder(new PlaintextPasswordEncoder()) // ldap-authentication-provider/password-compare/password-encoder@ref + .passwordAttribute("userPassword"); // ldap-authentication-provider/password-compare@password-attribute + } + } +} diff --git a/config/src/integration-test/resources/users.ldif b/config/src/integration-test/resources/users.ldif new file mode 100644 index 0000000000..fde2456d46 --- /dev/null +++ b/config/src/integration-test/resources/users.ldif @@ -0,0 +1,42 @@ +dn: ou=groups,dc=springframework,dc=org +objectclass: top +objectclass: organizationalUnit +ou: groups + +dn: ou=people,dc=springframework,dc=org +objectclass: top +objectclass: organizationalUnit +ou: people + +dn: uid=admin,ou=people,dc=springframework,dc=org +objectclass: top +objectclass: person +objectclass: organizationalPerson +objectclass: inetOrgPerson +cn: Rod Johnson +sn: Johnson +uid: admin +userPassword: password + +dn: uid=user,ou=people,dc=springframework,dc=org +objectclass: top +objectclass: person +objectclass: organizationalPerson +objectclass: inetOrgPerson +cn: Dianne Emu +sn: Emu +uid: user +userPassword: password + +dn: cn=user,ou=groups,dc=springframework,dc=org +objectclass: top +objectclass: groupOfNames +cn: user +uniqueMember: uid=admin,ou=people,dc=springframework,dc=org +uniqueMember: uid=user,ou=people,dc=springframework,dc=org + +dn: cn=admin,ou=groups,dc=springframework,dc=org +objectclass: top +objectclass: groupOfNames +cn: admin +uniqueMember: uid=admin,ou=people,dc=springframework,dc=org \ No newline at end of file diff --git a/config/src/main/java/org/springframework/security/config/annotation/AbstractConfiguredSecurityBuilder.java b/config/src/main/java/org/springframework/security/config/annotation/AbstractConfiguredSecurityBuilder.java new file mode 100644 index 0000000000..a3d0202f08 --- /dev/null +++ b/config/src/main/java/org/springframework/security/config/annotation/AbstractConfiguredSecurityBuilder.java @@ -0,0 +1,432 @@ +/* + * Copyright 2002-2013 the original author or authors. + * + * 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 org.springframework.security.config.annotation; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import org.springframework.security.config.annotation.web.builders.WebSecurity; +import org.springframework.util.Assert; +import org.springframework.web.filter.DelegatingFilterProxy; + +import com.google.inject.internal.ImmutableList.Builder; + +/** + *

A base {@link SecurityBuilder} that allows {@link SecurityConfigurer} to be + * applied to it. This makes modifying the {@link SecurityBuilder} a strategy + * that can be customized and broken up into a number of + * {@link SecurityConfigurer} objects that have more specific goals than that + * of the {@link SecurityBuilder}.

+ * + *

For example, a {@link SecurityBuilder} may build an + * {@link DelegatingFilterProxy}, but a {@link SecurityConfigurer} might + * populate the {@link SecurityBuilder} with the filters necessary for session + * management, form based login, authorization, etc.

+ * + * @see WebSecurity + * + * @author Rob Winch + * + * @param + * The object that this builder returns + * @param + * The type of this builder (that is returned by the base class) + */ +public abstract class AbstractConfiguredSecurityBuilder> extends AbstractSecurityBuilder { + + private final LinkedHashMap>, List>> configurers = + new LinkedHashMap>, List>>(); + + private final Map,Object> sharedObjects = new HashMap,Object>(); + + private final boolean allowConfigurersOfSameType; + + private BuildState buildState = BuildState.UNBUILT; + + private ObjectPostProcessor objectPostProcessor; + + /** + * Creates a new instance without post processing + */ + protected AbstractConfiguredSecurityBuilder() { + this(ObjectPostProcessor.QUIESCENT_POSTPROCESSOR); + } + + /*** + * Creates a new instance with the provided {@link ObjectPostProcessor}. + * This post processor must support Object since there are many types of + * objects that may be post processed. + * + * @param objectPostProcessor the {@link ObjectPostProcessor} to use + */ + protected AbstractConfiguredSecurityBuilder(ObjectPostProcessor objectPostProcessor) { + this(objectPostProcessor,false); + } + + /*** + * Creates a new instance with the provided {@link ObjectPostProcessor}. + * This post processor must support Object since there are many types of + * objects that may be post processed. + * + * @param objectPostProcessor the {@link ObjectPostProcessor} to use + * @param allowConfigurersOfSameType if true, will not override other {@link SecurityConfigurer}'s when performing apply + */ + protected AbstractConfiguredSecurityBuilder(ObjectPostProcessor objectPostProcessor, boolean allowConfigurersOfSameType) { + Assert.notNull(objectPostProcessor, "objectPostProcessor cannot be null"); + this.objectPostProcessor = objectPostProcessor; + this.allowConfigurersOfSameType = allowConfigurersOfSameType; + } + + /** + * Applies a {@link SecurityConfigurerAdapter} to this + * {@link SecurityBuilder} and invokes + * {@link SecurityConfigurerAdapter#setBuilder(SecurityBuilder)}. + * + * @param configurer + * @return + * @throws Exception + */ + @SuppressWarnings("unchecked") + public > C apply(C configurer) + throws Exception { + add(configurer); + configurer.addObjectPostProcessor(objectPostProcessor); + configurer.setBuilder((B) this); + return configurer; + } + + /** + * Applies a {@link SecurityConfigurer} to this {@link SecurityBuilder} + * overriding any {@link SecurityConfigurer} of the exact same class. Note + * that object hierarchies are not considered. + * + * @param configurer + * @return + * @throws Exception + */ + public > C apply(C configurer) + throws Exception { + add(configurer); + return configurer; + } + + /** + * Sets an object that is shared by multiple {@link SecurityConfigurer}. + * + * @param sharedType the Class to key the shared object by. + * @param object the Object to store + */ + @SuppressWarnings("unchecked") + public void setSharedObject(Class sharedType, C object) { + this.sharedObjects.put((Class) sharedType, object); + } + + /** + * Gets a shared Object. Note that object heirarchies are not considered. + * + * @param sharedType the type of the shared Object + * @return the shared Object or null if it is not found + */ + @SuppressWarnings("unchecked") + public C getSharedObject(Class sharedType) { + return (C) this.sharedObjects.get(sharedType); + } + + /** + * Gets the shared objects + * @return + */ + public Map,Object> getSharedObjects() { + return Collections.unmodifiableMap(this.sharedObjects); + } + + /** + * Adds {@link SecurityConfigurer} ensuring that it is allowed and + * invoking {@link SecurityConfigurer#init(SecurityBuilder)} immediately + * if necessary. + * + * @param configurer the {@link SecurityConfigurer} to add + * @throws Exception if an error occurs + */ + @SuppressWarnings("unchecked") + private > void add(C configurer) throws Exception { + Assert.notNull(configurer, "configurer cannot be null"); + + Class> clazz = (Class>) configurer + .getClass(); + synchronized(configurers) { + if(buildState.isConfigured()) { + throw new IllegalStateException("Cannot apply "+configurer+" to already built object"); + } + List> configs = allowConfigurersOfSameType ? this.configurers.get(clazz) : null; + if(configs == null) { + configs = new ArrayList>(1); + } + configs.add(configurer); + this.configurers.put(clazz, configs); + if(buildState.isInitializing()) { + configurer.init((B)this); + } + } + } + + /** + * Gets all the {@link SecurityConfigurer} instances by its class name or an + * empty List if not found. Note that object hierarchies are not considered. + * + * @param clazz the {@link SecurityConfigurer} class to look for + * @return + */ + @SuppressWarnings("unchecked") + public > List getConfigurers( + Class clazz) { + List configs = (List) this.configurers.get(clazz); + if(configs == null) { + return new ArrayList(); + } + return new ArrayList(configs); + } + + /** + * Removes all the {@link SecurityConfigurer} instances by its class name or an + * empty List if not found. Note that object hierarchies are not considered. + * + * @param clazz the {@link SecurityConfigurer} class to look for + * @return + */ + @SuppressWarnings("unchecked") + public > List removeConfigurers( + Class clazz) { + List configs = (List) this.configurers.remove(clazz); + if(configs == null) { + return new ArrayList(); + } + return new ArrayList(configs); + } + + /** + * Gets the {@link SecurityConfigurer} by its class name or + * null if not found. Note that object hierarchies are not + * considered. + * + * @param clazz + * @return + */ + @SuppressWarnings("unchecked") + public > C getConfigurer( + Class clazz) { + List> configs = this.configurers.get(clazz); + if(configs == null) { + return null; + } + if(configs.size() != 1) { + throw new IllegalStateException("Only one configurer expected for type " + clazz + ", but got " + configs); + } + return (C) configs.get(0); + } + + /** + * Removes and returns the {@link SecurityConfigurer} by its class name or + * null if not found. Note that object hierarchies are not + * considered. + * + * @param clazz + * @return + */ + @SuppressWarnings("unchecked") + public > C removeConfigurer(Class clazz) { + List> configs = this.configurers.remove(clazz); + if(configs == null) { + return null; + } + if(configs.size() != 1) { + throw new IllegalStateException("Only one configurer expected for type " + clazz + ", but got " + configs); + } + return (C) configs.get(0); + } + + /** + * Specifies the {@link ObjectPostProcessor} to use. + * @param objectPostProcessor the {@link ObjectPostProcessor} to use. Cannot be null + * @return the {@link SecurityBuilder} for further customizations + */ + @SuppressWarnings("unchecked") + public O objectPostProcessor(ObjectPostProcessor objectPostProcessor) { + Assert.notNull(objectPostProcessor,"objectPostProcessor cannot be null"); + this.objectPostProcessor = objectPostProcessor; + return (O) this; + } + + /** + * Performs post processing of an object. The default is to delegate to the + * {@link ObjectPostProcessor}. + * + * @param object the Object to post process + * @return the possibly modified Object to use + */ + protected

P postProcess(P object) { + return (P) this.objectPostProcessor.postProcess(object); + } + + /** + * Executes the build using the {@link SecurityConfigurer}'s that have been applied using the following steps: + * + *

    + *
  • Invokes {@link #beforeInit()} for any subclass to hook into
  • + *
  • Invokes {@link SecurityConfigurer#init(SecurityBuilder)} for any {@link SecurityConfigurer} that was applied to this builder.
  • + *
  • Invokes {@link #beforeConfigure()} for any subclass to hook into
  • + *
  • Invokes {@link #performBuild()} which actually builds the Object
  • + *
+ */ + @Override + protected final O doBuild() throws Exception { + synchronized(configurers) { + buildState = BuildState.INITIALIZING; + + beforeInit(); + init(); + + buildState = BuildState.CONFIGURING; + + beforeConfigure(); + configure(); + + buildState = BuildState.BUILDING; + + O result = performBuild(); + + buildState = BuildState.BUILT; + + return result; + } + } + + /** + * Invoked prior to invoking each + * {@link SecurityConfigurer#init(SecurityBuilder)} method. Subclasses may + * override this method to hook into the lifecycle without using a + * {@link SecurityConfigurer}. + */ + protected void beforeInit() throws Exception { + } + + /** + * Invoked prior to invoking each + * {@link SecurityConfigurer#configure(SecurityBuilder)} method. + * Subclasses may override this method to hook into the lifecycle without + * using a {@link SecurityConfigurer}. + */ + protected void beforeConfigure() throws Exception { + } + + /** + * Subclasses must implement this method to build the object that is being returned. + * + * @return + */ + protected abstract O performBuild() throws Exception; + + @SuppressWarnings("unchecked") + private void init() throws Exception { + Collection> configurers = getConfigurers(); + + for(SecurityConfigurer configurer : configurers ) { + configurer.init((B) this); + } + } + + @SuppressWarnings("unchecked") + private void configure() throws Exception { + Collection> configurers = getConfigurers(); + + for(SecurityConfigurer configurer : configurers ) { + configurer.configure((B) this); + } + } + + private Collection> getConfigurers() { + List> result = new ArrayList>(); + for(List> configs : this.configurers.values()) { + result.addAll(configs); + } + return result; + } + + /** + * The build state for the application + * + * @author Rob Winch + * @since 3.2 + */ + private static enum BuildState { + /** + * This is the state before the {@link Builder#build()} is invoked + */ + UNBUILT(0), + + /** + * The state from when {@link Builder#build()} is first invoked until + * all the {@link SecurityConfigurer#init(SecurityBuilder)} methods + * have been invoked. + */ + INITIALIZING(1), + + /** + * The state from after all + * {@link SecurityConfigurer#init(SecurityBuilder)} have been invoked + * until after all the + * {@link SecurityConfigurer#configure(SecurityBuilder)} methods have + * been invoked. + */ + CONFIGURING(2), + + /** + * From the point after all the + * {@link SecurityConfigurer#configure(SecurityBuilder)} have + * completed to just after + * {@link AbstractConfiguredSecurityBuilder#performBuild()}. + */ + BUILDING(3), + + /** + * After the object has been completely built. + */ + BUILT(4); + + private final int order; + + BuildState(int order) { + this.order = order; + } + + public boolean isInitializing() { + return INITIALIZING.order == order; + } + + /** + * Determines if the state is CONFIGURING or later + * @return + */ + public boolean isConfigured() { + return order >= CONFIGURING.order; + } + } +} \ No newline at end of file diff --git a/config/src/main/java/org/springframework/security/config/annotation/AbstractSecurityBuilder.java b/config/src/main/java/org/springframework/security/config/annotation/AbstractSecurityBuilder.java new file mode 100644 index 0000000000..bb4f5a3a8f --- /dev/null +++ b/config/src/main/java/org/springframework/security/config/annotation/AbstractSecurityBuilder.java @@ -0,0 +1,67 @@ +/* + * Copyright 2002-2013 the original author or authors. + * + * 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 org.springframework.security.config.annotation; + +import java.util.concurrent.atomic.AtomicBoolean; + +/** + * A base {@link SecurityBuilder} that ensures the object being built is only + * built one time. + * + * @param the type of Object that is being built + * + * @author Rob Winch + * + */ +public abstract class AbstractSecurityBuilder implements SecurityBuilder { + private AtomicBoolean building = new AtomicBoolean(); + + private O object; + + /* (non-Javadoc) + * @see org.springframework.security.config.annotation.SecurityBuilder#build() + */ + @Override + public final O build() throws Exception { + if(building.compareAndSet(false, true)) { + object = doBuild(); + return object; + } + throw new IllegalStateException("This object has already been built"); + } + + /** + * Gets the object that was built. If it has not been built yet an Exception + * is thrown. + * + * @return the Object that was built + */ + public final O getObject() { + if(!building.get()) { + throw new IllegalStateException("This object has not been built"); + } + return object; + } + + /** + * Subclasses should implement this to perform the build. + * + * @return the object that should be returned by {@link #build()}. + * + * @throws Exception if an error occurs + */ + protected abstract O doBuild() throws Exception; +} diff --git a/config/src/main/java/org/springframework/security/config/annotation/ObjectPostProcessor.java b/config/src/main/java/org/springframework/security/config/annotation/ObjectPostProcessor.java new file mode 100644 index 0000000000..05e110b292 --- /dev/null +++ b/config/src/main/java/org/springframework/security/config/annotation/ObjectPostProcessor.java @@ -0,0 +1,52 @@ +/* + * Copyright 2002-2013 the original author or authors. + * + * 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 org.springframework.security.config.annotation; + +import org.springframework.beans.factory.Aware; +import org.springframework.beans.factory.DisposableBean; +import org.springframework.beans.factory.InitializingBean; + +/** + * Allows initialization of Objects. Typically this is used to call the + * {@link Aware} methods, {@link InitializingBean#afterPropertiesSet()}, and + * ensure that {@link DisposableBean#destroy()} has been invoked. + * + * @param the bound of the types of Objects this {@link ObjectPostProcessor} supports. + * + * @author Rob Winch + * @since 3.2 + */ +public interface ObjectPostProcessor { + + /** + * Initialize the object possibly returning a modified instance that should + * be used instead. + * + * @param object the object to initialize + * @return the initialized version of the object + */ + O postProcess(O object); + + /** + * A do nothing implementation of the {@link ObjectPostProcessor} + */ + ObjectPostProcessor QUIESCENT_POSTPROCESSOR = new ObjectPostProcessor() { + @Override + public T postProcess(T object) { + return object; + } + }; +} diff --git a/config/src/main/java/org/springframework/security/config/annotation/SecurityBuilder.java b/config/src/main/java/org/springframework/security/config/annotation/SecurityBuilder.java new file mode 100644 index 0000000000..36e103fa7c --- /dev/null +++ b/config/src/main/java/org/springframework/security/config/annotation/SecurityBuilder.java @@ -0,0 +1,35 @@ +/* + * Copyright 2002-2013 the original author or authors. + * + * 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 org.springframework.security.config.annotation; + +/** + * Interface for building an Object + * + * @author Rob Winch + * @since 3.2 + * + * @param The type of the Object being built + */ +public interface SecurityBuilder { + + /** + * Builds the object and returns it or null. + * + * @return the Object to be built or null if the implementation allows it. + * @throws Exception if an error occured when building the Object + */ + O build() throws Exception; +} diff --git a/config/src/main/java/org/springframework/security/config/annotation/SecurityConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/SecurityConfigurer.java new file mode 100644 index 0000000000..365b9ba953 --- /dev/null +++ b/config/src/main/java/org/springframework/security/config/annotation/SecurityConfigurer.java @@ -0,0 +1,55 @@ +/* + * Copyright 2002-2013 the original author or authors. + * + * 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 org.springframework.security.config.annotation; + +/** + * Allows for configuring a {@link SecurityBuilder}. All + * {@link SecurityConfigurer} first have their {@link #init(SecurityBuilder)} + * method invoked. After all {@link #init(SecurityBuilder)} methods have been + * invoked, each {@link #configure(SecurityBuilder)} method is invoked. + * + * @see AbstractConfiguredSecurityBuilder + * + * @author Rob Winch + * + * @param + * The object being built by the {@link SecurityBuilder} B + * @param + * The {@link SecurityBuilder} that builds objects of type O. This is + * also the {@link SecurityBuilder} that is being configured. + */ +public interface SecurityConfigurer> { + /** + * Initialize the {@link SecurityBuilder}. Here only shared state should be + * created and modified, but not properties on the {@link SecurityBuilder} + * used for building the object. This ensures that the + * {@link #configure(SecurityBuilder)} method uses the correct shared + * objects when building. + * + * @param builder + * @throws Exception + */ + void init(B builder) throws Exception; + + /** + * Configure the {@link SecurityBuilder} by setting the necessary properties + * on the {@link SecurityBuilder}. + * + * @param builder + * @throws Exception + */ + void configure(B builder) throws Exception; +} diff --git a/config/src/main/java/org/springframework/security/config/annotation/SecurityConfigurerAdapter.java b/config/src/main/java/org/springframework/security/config/annotation/SecurityConfigurerAdapter.java new file mode 100644 index 0000000000..3a948856c1 --- /dev/null +++ b/config/src/main/java/org/springframework/security/config/annotation/SecurityConfigurerAdapter.java @@ -0,0 +1,135 @@ +/* + * Copyright 2002-2013 the original author or authors. + * + * 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 org.springframework.security.config.annotation; + +import java.util.ArrayList; +import java.util.List; + +import org.springframework.core.GenericTypeResolver; + +/** + * A base class for {@link SecurityConfigurer} that allows subclasses to only + * implement the methods they are interested in. It also provides a mechanism + * for using the {@link SecurityConfigurer} and when done gaining access to the + * {@link SecurityBuilder} that is being configured. + * + * @author Rob Winch + * + * @param + * The Object being built by B + * @param + * The Builder that is building O and is configured by {@link SecurityConfigurerAdapter} + */ +public abstract class SecurityConfigurerAdapter> implements SecurityConfigurer { + private B securityBuilder; + + private CompositeObjectPostProcessor objectPostProcessor = new CompositeObjectPostProcessor(); + + @Override + public void init(B builder) throws Exception {} + + @Override + public void configure(B builder) throws Exception {} + + /** + * Return the {@link SecurityBuilder} when done using the + * {@link SecurityConfigurer}. This is useful for method chaining. + * + * @return + */ + public B and() { + return getBuilder(); + } + + /** + * Gets the {@link SecurityBuilder}. Cannot be null. + * + * @return the {@link SecurityBuilder} + * @throw {@link IllegalStateException} if {@link SecurityBuilder} is null + */ + protected final B getBuilder() { + if(securityBuilder == null) { + throw new IllegalStateException("securityBuilder cannot be null"); + } + return securityBuilder; + } + + /** + * Performs post processing of an object. The default is to delegate to the + * {@link ObjectPostProcessor}. + * + * @param object the Object to post process + * @return the possibly modified Object to use + */ + @SuppressWarnings("unchecked") + protected T postProcess(T object) { + return (T) this.objectPostProcessor.postProcess(object); + } + + /** + * Adds an {@link ObjectPostProcessor} to be used for this + * {@link SecurityConfigurerAdapter}. The default implementation does + * nothing to the object. + * + * @param objectPostProcessor the {@link ObjectPostProcessor} to use + */ + public void addObjectPostProcessor(ObjectPostProcessor objectPostProcessor) { + this.objectPostProcessor.addObjectPostProcessor(objectPostProcessor); + } + + /** + * Sets the {@link SecurityBuilder} to be used. This is automatically set + * when using + * {@link AbstractConfiguredSecurityBuilder#apply(SecurityConfigurerAdapter)} + * + * @param builder the {@link SecurityBuilder} to set + */ + public void setBuilder(B builder) { + this.securityBuilder = builder; + } + + /** + * An {@link ObjectPostProcessor} that delegates work to numerous + * {@link ObjectPostProcessor} implementations. + * + * @author Rob Winch + */ + private static final class CompositeObjectPostProcessor implements ObjectPostProcessor { + private List> postProcessors = new ArrayList>(); + + @Override + @SuppressWarnings({ "rawtypes", "unchecked" }) + public Object postProcess(Object object) { + for(ObjectPostProcessor opp : postProcessors) { + Class oppClass = opp.getClass(); + Class oppType = GenericTypeResolver.resolveTypeArgument(oppClass,ObjectPostProcessor.class); + if(oppType == null || oppType.isAssignableFrom(object.getClass())) { + object = opp.postProcess(object); + } + } + return object; + } + + /** + * Adds an {@link ObjectPostProcessor} to use + * @param objectPostProcessor the {@link ObjectPostProcessor} to add + * @return true if the {@link ObjectPostProcessor} was added, else false + */ + private boolean addObjectPostProcessor(ObjectPostProcessor objectPostProcessor) { + return this.postProcessors.add(objectPostProcessor); + } + } +} diff --git a/config/src/main/java/org/springframework/security/config/annotation/authentication/ProviderManagerBuilder.java b/config/src/main/java/org/springframework/security/config/annotation/authentication/ProviderManagerBuilder.java new file mode 100644 index 0000000000..bf8c10012b --- /dev/null +++ b/config/src/main/java/org/springframework/security/config/annotation/authentication/ProviderManagerBuilder.java @@ -0,0 +1,44 @@ +/* + * Copyright 2002-2013 the original author or authors. + * + * 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 org.springframework.security.config.annotation.authentication; + +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.AuthenticationProvider; +import org.springframework.security.authentication.ProviderManager; +import org.springframework.security.config.annotation.SecurityBuilder; + +/** + * Interface for operating on a SecurityBuilder that creates a {@link ProviderManager} + * + * @author Rob Winch + * + * @param the type of the {@link SecurityBuilder} + */ +public interface ProviderManagerBuilder> extends SecurityBuilder { + + /** + * Add authentication based upon the custom {@link AuthenticationProvider} + * that is passed in. Since the {@link AuthenticationProvider} + * implementation is unknown, all customizations must be done externally and + * the {@link ProviderManagerBuilder} is returned immediately. + * + * @return a {@link ProviderManagerBuilder} to allow further authentication + * to be provided to the {@link ProviderManagerBuilder} + * @throws Exception + * if an error occurs when adding the {@link AuthenticationProvider} + */ + B authenticationProvider(AuthenticationProvider authenticationProvider); +} diff --git a/config/src/main/java/org/springframework/security/config/annotation/authentication/builders/AuthenticationManagerBuilder.java b/config/src/main/java/org/springframework/security/config/annotation/authentication/builders/AuthenticationManagerBuilder.java new file mode 100644 index 0000000000..769fb44d45 --- /dev/null +++ b/config/src/main/java/org/springframework/security/config/annotation/authentication/builders/AuthenticationManagerBuilder.java @@ -0,0 +1,254 @@ +/* + * Copyright 2002-2013 the original author or authors. + * + * 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 org.springframework.security.config.annotation.authentication.builders; + +import java.util.ArrayList; +import java.util.List; + +import org.springframework.security.authentication.AuthenticationEventPublisher; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.AuthenticationProvider; +import org.springframework.security.authentication.ProviderManager; +import org.springframework.security.config.annotation.AbstractConfiguredSecurityBuilder; +import org.springframework.security.config.annotation.ObjectPostProcessor; +import org.springframework.security.config.annotation.SecurityBuilder; +import org.springframework.security.config.annotation.authentication.ProviderManagerBuilder; +import org.springframework.security.config.annotation.authentication.configurers.ldap.LdapAuthenticationProviderConfigurer; +import org.springframework.security.config.annotation.authentication.configurers.provisioning.InMemoryUserDetailsManagerConfigurer; +import org.springframework.security.config.annotation.authentication.configurers.provisioning.JdbcUserDetailsManagerConfigurer; +import org.springframework.security.config.annotation.authentication.configurers.userdetails.DaoAuthenticationConfigurer; +import org.springframework.security.config.annotation.authentication.configurers.userdetails.UserDetailsAwareConfigurer; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.util.Assert; + +/** + * {@link SecurityBuilder} used to create an {@link AuthenticationManager}. + * Allows for easily building in memory authentication, LDAP authentication, + * JDBC based authentication, adding {@link UserDetailsService}, and adding + * {@link AuthenticationProvider}'s. + * + * @author Rob Winch + * @since 3.2 + */ +public class AuthenticationManagerBuilder extends AbstractConfiguredSecurityBuilder implements ProviderManagerBuilder { + + private AuthenticationManager parentAuthenticationManager; + private List authenticationProviders = new ArrayList(); + private UserDetailsService defaultUserDetailsService; + private Boolean eraseCredentials; + private AuthenticationEventPublisher eventPublisher; + + /** + * Creates a new instance + */ + public AuthenticationManagerBuilder() { + super(ObjectPostProcessor.QUIESCENT_POSTPROCESSOR,true); + } + + /** + * Allows providing a parent {@link AuthenticationManager} that will be + * tried if this {@link AuthenticationManager} was unable to attempt to + * authenticate the provided {@link Authentication}. + * + * @param authenticationManager + * the {@link AuthenticationManager} that should be used if the + * current {@link AuthenticationManager} was unable to attempt to + * authenticate the provided {@link Authentication}. + * @return the {@link AuthenticationManagerBuilder} for further adding types + * of authentication + */ + public AuthenticationManagerBuilder parentAuthenticationManager( + AuthenticationManager authenticationManager) { + this.parentAuthenticationManager = authenticationManager; + return this; + } + + /** + * Sets the {@link AuthenticationEventPublisher} + * + * @param eventPublisher + * the {@link AuthenticationEventPublisher} to use + * @return the {@link AuthenticationManagerBuilder} for further + * customizations + */ + public AuthenticationManagerBuilder authenticationEventPublisher(AuthenticationEventPublisher eventPublisher) { + Assert.notNull(eventPublisher, "AuthenticationEventPublisher cannot be null"); + this.eventPublisher = eventPublisher; + return this; + } + + /** + * + * + * @param eraseCredentials + * true if {@link AuthenticationManager} should clear the + * credentials from the {@link Authentication} object after + * authenticating + * @return the {@link AuthenticationManagerBuilder} for further customizations + */ + public AuthenticationManagerBuilder eraseCredentials(boolean eraseCredentials) { + this.eraseCredentials = eraseCredentials; + return this; + } + + + /** + * Add in memory authentication to the {@link AuthenticationManagerBuilder} + * and return a {@link InMemoryUserDetailsManagerConfigurer} to + * allow customization of the in memory authentication. + * + *

+ * This method also ensure that a {@link UserDetailsService} is available + * for the {@link #getDefaultUserDetailsService()} method. Note that + * additional {@link UserDetailsService}'s may override this + * {@link UserDetailsService} as the default. + *

+ * + * @return a {@link InMemoryUserDetailsManagerConfigurer} to allow + * customization of the in memory authentication + * @throws Exception + * if an error occurs when adding the in memory authentication + */ + public InMemoryUserDetailsManagerConfigurer inMemoryAuthentication() + throws Exception { + return apply(new InMemoryUserDetailsManagerConfigurer()); + } + + /** + * Add JDBC authentication to the {@link AuthenticationManagerBuilder} and + * return a {@link JdbcUserDetailsManagerConfigurer} to allow customization of the + * JDBC authentication. + * + *

+ * This method also ensure that a {@link UserDetailsService} is available + * for the {@link #getDefaultUserDetailsService()} method. Note that + * additional {@link UserDetailsService}'s may override this + * {@link UserDetailsService} as the default. + *

+ * + * @return a {@link JdbcUserDetailsManagerConfigurer} to allow customization of the + * JDBC authentication + * @throws Exception if an error occurs when adding the JDBC authentication + */ + public JdbcUserDetailsManagerConfigurer jdbcAuthentication() + throws Exception { + return apply(new JdbcUserDetailsManagerConfigurer()); + } + + /** + * Add authentication based upon the custom {@link UserDetailsService} that + * is passed in. It then returns a {@link DaoAuthenticationConfigurer} to + * allow customization of the authentication. + * + *

+ * This method also ensure that the {@link UserDetailsService} is available + * for the {@link #getDefaultUserDetailsService()} method. Note that + * additional {@link UserDetailsService}'s may override this + * {@link UserDetailsService} as the default. + *

+ * + * @return a {@link DaoAuthenticationConfigurer} to allow customization + * of the DAO authentication + * @throws Exception + * if an error occurs when adding the {@link UserDetailsService} + * based authentication + */ + public DaoAuthenticationConfigurer userDetailsService( + T userDetailsService) throws Exception { + this.defaultUserDetailsService = userDetailsService; + return apply(new DaoAuthenticationConfigurer(userDetailsService)); + } + + /** + * Add LDAP authentication to the {@link AuthenticationManagerBuilder} and + * return a {@link LdapAuthenticationProviderConfigurer} to allow + * customization of the LDAP authentication. + * + *

+ * This method does NOT ensure that a {@link UserDetailsService} is + * available for the {@link #getDefaultUserDetailsService()} method. + *

+ * + * @return a {@link LdapAuthenticationProviderConfigurer} to allow + * customization of the LDAP authentication + * @throws Exception + * if an error occurs when adding the LDAP authentication + */ + public LdapAuthenticationProviderConfigurer ldapAuthentication() + throws Exception { + return apply(new LdapAuthenticationProviderConfigurer()); + } + + /** + * Add authentication based upon the custom {@link AuthenticationProvider} + * that is passed in. Since the {@link AuthenticationProvider} + * implementation is unknown, all customizations must be done externally and + * the {@link AuthenticationManagerBuilder} is returned immediately. + * + *

+ * This method does NOT ensure that the {@link UserDetailsService} is + * available for the {@link #getDefaultUserDetailsService()} method. + *

+ * + * @return a {@link AuthenticationManagerBuilder} to allow further authentication + * to be provided to the {@link AuthenticationManagerBuilder} + * @throws Exception + * if an error occurs when adding the {@link AuthenticationProvider} + */ + public AuthenticationManagerBuilder authenticationProvider( + AuthenticationProvider authenticationProvider) { + this.authenticationProviders.add(authenticationProvider); + return this; + } + + @Override + protected ProviderManager performBuild() throws Exception { + ProviderManager providerManager = new ProviderManager(authenticationProviders, parentAuthenticationManager); + if(eraseCredentials != null) { + providerManager.setEraseCredentialsAfterAuthentication(eraseCredentials); + } + if(eventPublisher != null) { + providerManager.setAuthenticationEventPublisher(eventPublisher); + } + providerManager = postProcess(providerManager); + return providerManager; + } + + /** + * Gets the default {@link UserDetailsService} for the + * {@link AuthenticationManagerBuilder}. The result may be null in some + * circumstances. + * + * @return the default {@link UserDetailsService} for the + * {@link AuthenticationManagerBuilder} + */ + public UserDetailsService getDefaultUserDetailsService() { + return this.defaultUserDetailsService; + } + + /** + * Captures the {@link UserDetailsService} from any {@link UserDetailsAwareConfigurer}. + * + * @param configurer the {@link UserDetailsAwareConfigurer} to capture the {@link UserDetailsService} from. + * @return the {@link UserDetailsAwareConfigurer} for further customizations + * @throws Exception if an error occurs + */ + private > C apply(C configurer) throws Exception { + this.defaultUserDetailsService = configurer.getUserDetailsService(); + return (C) super.apply(configurer); + } +} \ No newline at end of file diff --git a/config/src/main/java/org/springframework/security/config/annotation/authentication/configurers/ldap/LdapAuthenticationProviderConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/authentication/configurers/ldap/LdapAuthenticationProviderConfigurer.java new file mode 100644 index 0000000000..dec4fe9bc5 --- /dev/null +++ b/config/src/main/java/org/springframework/security/config/annotation/authentication/configurers/ldap/LdapAuthenticationProviderConfigurer.java @@ -0,0 +1,469 @@ +/* + * Copyright 2002-2013 the original author or authors. + * + * 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 org.springframework.security.config.annotation.authentication.configurers.ldap; + +import org.springframework.ldap.core.support.BaseLdapPathContextSource; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.AuthenticationProvider; +import org.springframework.security.authentication.encoding.PasswordEncoder; +import org.springframework.security.authentication.encoding.PlaintextPasswordEncoder; +import org.springframework.security.config.annotation.SecurityConfigurerAdapter; +import org.springframework.security.config.annotation.authentication.ProviderManagerBuilder; +import org.springframework.security.core.authority.mapping.SimpleAuthorityMapper; +import org.springframework.security.ldap.DefaultSpringSecurityContextSource; +import org.springframework.security.ldap.authentication.AbstractLdapAuthenticator; +import org.springframework.security.ldap.authentication.BindAuthenticator; +import org.springframework.security.ldap.authentication.LdapAuthenticationProvider; +import org.springframework.security.ldap.authentication.LdapAuthenticator; +import org.springframework.security.ldap.authentication.PasswordComparisonAuthenticator; +import org.springframework.security.ldap.search.FilterBasedLdapUserSearch; +import org.springframework.security.ldap.search.LdapUserSearch; +import org.springframework.security.ldap.server.ApacheDSContainer; +import org.springframework.security.ldap.userdetails.DefaultLdapAuthoritiesPopulator; +import org.springframework.security.ldap.userdetails.InetOrgPersonContextMapper; +import org.springframework.security.ldap.userdetails.LdapUserDetailsMapper; +import org.springframework.security.ldap.userdetails.PersonContextMapper; +import org.springframework.security.ldap.userdetails.UserDetailsContextMapper; + +/** + * Configures LDAP {@link AuthenticationProvider} in the {@link ProviderManagerBuilder}. + * + * @param the {@link ProviderManagerBuilder} type that this is configuring. + * + * @author Rob Winch + * @since 3.2 + */ +public class LdapAuthenticationProviderConfigurer> extends SecurityConfigurerAdapter { + private String groupRoleAttribute = "cn"; + private String groupSearchBase = ""; + private String groupSearchFilter = "(uniqueMember={0})"; + private String rolePrefix = "ROLE_"; + private String userSearchBase = ""; // only for search + private String userSearchFilter = null;//"uid={0}"; // only for search + private String[] userDnPatterns; + private BaseLdapPathContextSource contextSource; + private ContextSourceBuilder contextSourceBuilder = new ContextSourceBuilder(); + private UserDetailsContextMapper userDetailsContextMapper; + private PasswordEncoder passwordEncoder; + private String passwordAttribute; + + private LdapAuthenticationProvider build() throws Exception { + BaseLdapPathContextSource contextSource = getContextSource(); + LdapAuthenticator ldapAuthenticator = createLdapAuthenticator(contextSource); + + DefaultLdapAuthoritiesPopulator authoritiesPopulator = new DefaultLdapAuthoritiesPopulator( + contextSource, groupSearchBase); + authoritiesPopulator.setGroupRoleAttribute(groupRoleAttribute); + authoritiesPopulator.setGroupSearchFilter(groupSearchFilter); + + LdapAuthenticationProvider ldapAuthenticationProvider = new LdapAuthenticationProvider( + ldapAuthenticator, authoritiesPopulator); + SimpleAuthorityMapper simpleAuthorityMapper = new SimpleAuthorityMapper(); + simpleAuthorityMapper.setPrefix(rolePrefix); + simpleAuthorityMapper.afterPropertiesSet(); + ldapAuthenticationProvider.setAuthoritiesMapper(simpleAuthorityMapper); + if(userDetailsContextMapper != null) { + ldapAuthenticationProvider.setUserDetailsContextMapper(userDetailsContextMapper); + } + return ldapAuthenticationProvider; + } + + /** + * Creates the {@link LdapAuthenticator} to use + * + * @param contextSource the {@link BaseLdapPathContextSource} to use + * @return the {@link LdapAuthenticator} to use + */ + private LdapAuthenticator createLdapAuthenticator(BaseLdapPathContextSource contextSource) { + AbstractLdapAuthenticator ldapAuthenticator = passwordEncoder == null ? createBindAuthenticator(contextSource) : createPasswordCompareAuthenticator(contextSource); + LdapUserSearch userSearch = createUserSearch(); + if(userSearch != null) { + ldapAuthenticator.setUserSearch(userSearch); + } + if(userDnPatterns != null && userDnPatterns.length > 0) { + ldapAuthenticator.setUserDnPatterns(userDnPatterns); + } + return postProcess(ldapAuthenticator); + } + + /** + * Creates {@link PasswordComparisonAuthenticator} + * + * @param contextSource the {@link BaseLdapPathContextSource} to use + * @return + */ + private PasswordComparisonAuthenticator createPasswordCompareAuthenticator(BaseLdapPathContextSource contextSource) { + PasswordComparisonAuthenticator ldapAuthenticator = new PasswordComparisonAuthenticator(contextSource); + ldapAuthenticator.setPasswordAttributeName(passwordAttribute); + ldapAuthenticator.setPasswordEncoder(passwordEncoder); + return ldapAuthenticator; + } + + /** + * Creates a {@link BindAuthenticator} + * + * @param contextSource the {@link BaseLdapPathContextSource} to use + * @return the {@link BindAuthenticator} to use + */ + private BindAuthenticator createBindAuthenticator( + BaseLdapPathContextSource contextSource) { + return new BindAuthenticator(contextSource); + } + + private LdapUserSearch createUserSearch() { + if(userSearchFilter == null) { + return null; + } + return new FilterBasedLdapUserSearch(userSearchBase, userSearchFilter, contextSource); + } + + /** + * Specifies the {@link BaseLdapPathContextSource} to be used. If not + * specified, an embedded LDAP server will be created using + * {@link #contextSource()}. + * + * @param contextSource + * the {@link BaseLdapPathContextSource} to use + * @return the {@link LdapAuthenticationProviderConfigurer} for further + * customizations + * @see #contextSource() + */ + public LdapAuthenticationProviderConfigurer contextSource(BaseLdapPathContextSource contextSource) { + this.contextSource = contextSource; + return this; + } + + /** + * Allows easily configuring of a {@link BaseLdapPathContextSource} with + * defaults pointing to an embedded LDAP server that is created. + * + * @return the {@link ContextSourceBuilder} for further customizations + */ + public ContextSourceBuilder contextSource() { + return contextSourceBuilder; + } + + /** + * Specifies the {@link PasswordEncoder} to be used when authenticating with + * password comparison. + * + * @param passwordEncoder the {@link PasswordEncoder} to use + * @return the {@link LdapAuthenticationProviderConfigurer} for further customization + */ + public LdapAuthenticationProviderConfigurer passwordEncoder(PasswordEncoder passwordEncoder) { + this.passwordEncoder = passwordEncoder; + return this; + } + + /** + * If your users are at a fixed location in the directory (i.e. you can work + * out the DN directly from the username without doing a directory search), + * you can use this attribute to map directly to the DN. It maps directly to + * the userDnPatterns property of AbstractLdapAuthenticator. The value is a + * specific pattern used to build the user's DN, for example + * "uid={0},ou=people". The key "{0}" must be present and will be + * substituted with the username. + * + * @param userDnPatterns the LDAP patterns for finding the usernames + * @return the {@link LdapAuthenticationProviderConfigurer} for further customizations + */ + public LdapAuthenticationProviderConfigurer userDnPatterns(String...userDnPatterns) { + this.userDnPatterns = userDnPatterns; + return this; + } + + /** + * Allows explicit customization of the loaded user object by specifying a + * UserDetailsContextMapper bean which will be called with the context + * information from the user's directory entry. + * + * @param userDetailsContextMapper the {@link UserDetailsContextMapper} to use + * @return the {@link LdapAuthenticationProviderConfigurer} for further + * customizations + * + * @see PersonContextMapper + * @see InetOrgPersonContextMapper + * @see LdapUserDetailsMapper + */ + public LdapAuthenticationProviderConfigurer userDetailsContextMapper(UserDetailsContextMapper userDetailsContextMapper) { + this.userDetailsContextMapper = userDetailsContextMapper; + return this; + } + + /** + * Specifies the attribute name which contains the role name. Default is "cn". + * @param groupRoleAttribute the attribute name that maps a group to a role. + * @return + */ + public LdapAuthenticationProviderConfigurer groupRoleAttribute(String groupRoleAttribute) { + this.groupRoleAttribute = groupRoleAttribute; + return this; + } + + /** + * The search base for group membership searches. Defaults to "". + * @param groupSearchBase + * @return the {@link LdapAuthenticationProviderConfigurer} for further customizations + */ + public LdapAuthenticationProviderConfigurer groupSearchBase(String groupSearchBase) { + this.groupSearchBase = groupSearchBase; + return this; + } + + /** + * The LDAP filter to search for groups. Defaults to "(uniqueMember={0})". + * The substituted parameter is the DN of the user. + * + * @param groupSearchFilter the LDAP filter to search for groups + * @return the {@link LdapAuthenticationProviderConfigurer} for further customizations + */ + public LdapAuthenticationProviderConfigurer groupSearchFilter(String groupSearchFilter) { + this.groupSearchFilter = groupSearchFilter; + return this; + } + + /** + * A non-empty string prefix that will be added as a prefix to the existing + * roles. The default is "ROLE_". + * + * @param rolePrefix the prefix to be added to the roles that are loaded. + * @return the {@link LdapAuthenticationProviderConfigurer} for further customizations + * @see SimpleAuthorityMapper#setPrefix(String) + */ + public LdapAuthenticationProviderConfigurer rolePrefix(String rolePrefix) { + this.rolePrefix = rolePrefix; + return this; + } + /** + * Search base for user searches. Defaults to "". Only used with {@link #userSearchFilter(String)}. + * + * @param userSearchBase search base for user searches + * @return the {@link LdapAuthenticationProviderConfigurer} for further customizations + */ + public LdapAuthenticationProviderConfigurer userSearchBase(String userSearchBase) { + this.userSearchBase = userSearchBase; + return this; + } + + /** + * The LDAP filter used to search for users (optional). For example + * "(uid={0})". The substituted parameter is the user's login name. + * + * @param userSearchFilter + * the LDAP filter used to search for users + * @return the {@link LdapAuthenticationProviderConfigurer} for further + * customizations + */ + public LdapAuthenticationProviderConfigurer userSearchFilter(String userSearchFilter) { + this.userSearchFilter = userSearchFilter; + return this; + } + + @Override + public void configure(B builder) throws Exception { + LdapAuthenticationProvider provider = postProcess(build()); + builder.authenticationProvider(provider); + } + + /** + * Sets up Password based comparison + * + * @author Rob Winch + */ + public final class PasswordCompareConfigurer { + + /** + * Allows specifying the {@link PasswordEncoder} to use. The default is {@link PlaintextPasswordEncoder}. + * @param passwordEncoder the {@link PasswordEncoder} to use + * @return the {@link PasswordEncoder} to use + */ + public PasswordCompareConfigurer passwordEncoder(PasswordEncoder passwordEncoder) { + LdapAuthenticationProviderConfigurer.this.passwordEncoder = passwordEncoder; + return this; + } + + /** + * The attribute in the directory which contains the user password. Defaults to "userPassword". + * + * @param passwordAttribute the attribute in the directory which contains the user password + * @return the {@link PasswordCompareConfigurer} for further customizations + */ + public PasswordCompareConfigurer passwordAttribute(String passwordAttribute) { + LdapAuthenticationProviderConfigurer.this.passwordAttribute = passwordAttribute; + return this; + } + + /** + * Allows obtaining a reference to the + * {@link LdapAuthenticationProviderConfigurer} for further + * customizations + * + * @return attribute in the directory which contains the user password + */ + public LdapAuthenticationProviderConfigurer and() { + return LdapAuthenticationProviderConfigurer.this; + } + + private PasswordCompareConfigurer() {} + } + + /** + * Allows building a {@link BaseLdapPathContextSource} and optionally + * creating an embedded LDAP instance. + * + * @author Rob Winch + * @since 3.2 + */ + public final class ContextSourceBuilder { + private String ldif = "classpath*:*.ldif"; + private String managerPassword; + private String managerDn; + private int port = 33389; + private String root = "dc=springframework,dc=org"; + private String url; + + /** + * Specifies an ldif to load at startup for an embedded LDAP server. + * This only loads if using an embedded instance. The default is + * "classpath*:*.ldif". + * + * @param ldif + * the ldif to load at startup for an embedded LDAP server. + * @return the {@link ContextSourceBuilder} for further customization + */ + public ContextSourceBuilder ldif(String ldif) { + this.ldif = ldif; + return this; + } + + /** + * Username (DN) of the "manager" user identity (i.e. + * "uid=admin,ou=system") which will be used to authenticate to a + * (non-embedded) LDAP server. If omitted, anonymous access will be + * used. + * + * @param managerDn + * the username (DN) of the "manager" user identity used to + * authenticate to a LDAP server. + * @return the {@link ContextSourceBuilder} for further customization + */ + public ContextSourceBuilder managerDn(String managerDn) { + this.managerDn = managerDn; + return this; + } + + /** + * The password for the manager DN. This is required if the manager-dn is specified. + * @param managerPassword password for the manager DN + * @return the {@link ContextSourceBuilder} for further customization + */ + public ContextSourceBuilder managerPassword(String managerPassword) { + this.managerPassword = managerPassword; + return this; + } + + /** + * The port to connect to LDAP to (the default is 33389). + * @param port the port to connect to + * @return the {@link ContextSourceBuilder} for further customization + */ + public ContextSourceBuilder port(int port) { + this.port = port; + return this; + } + + /** + * Optional root suffix for the embedded LDAP server. Default is + * "dc=springframework,dc=org" + * + * @param root + * root suffix for the embedded LDAP server + * @return the {@link ContextSourceBuilder} for further customization + */ + public ContextSourceBuilder root(String root) { + this.root = root; + return this; + } + + /** + * Specifies the ldap server URL when not using the embedded LDAP + * server. For example, "ldaps://ldap.example.com:33389/dc=myco,dc=org". + * + * @param url + * the ldap server URL + * @return the {@link ContextSourceBuilder} for further customization + */ + public ContextSourceBuilder url(String url) { + this.url = url; + return this; + } + + /** + * Gets the {@link LdapAuthenticationProviderConfigurer} for further + * customizations + * + * @return the {@link LdapAuthenticationProviderConfigurer} for further + * customizations + */ + public LdapAuthenticationProviderConfigurer and() { + return LdapAuthenticationProviderConfigurer.this; + } + + private DefaultSpringSecurityContextSource build() throws Exception { + DefaultSpringSecurityContextSource contextSource = new DefaultSpringSecurityContextSource(getProviderUrl()); + if(managerDn != null) { + contextSource.setUserDn(managerDn); + if(managerPassword == null) { + throw new IllegalStateException("managerPassword is required if managerDn is supplied"); + } + contextSource.setPassword(managerPassword); + } + contextSource = postProcess(contextSource); + if(url != null) { + return contextSource; + } + ApacheDSContainer apacheDsContainer = new ApacheDSContainer(root, ldif); + apacheDsContainer.setPort(port); + postProcess(apacheDsContainer); + return contextSource; + } + + private String getProviderUrl() { + if(url == null) { + return "ldap://127.0.0.1:" + port + "/" + root; + } + return url; + } + + private ContextSourceBuilder() {} + } + + private BaseLdapPathContextSource getContextSource() throws Exception { + if(contextSource == null) { + contextSource = contextSourceBuilder.build(); + } + return contextSource; + } + + /** + * @return + */ + public PasswordCompareConfigurer passwordCompare() { + return new PasswordCompareConfigurer() + .passwordAttribute("password") + .passwordEncoder(new PlaintextPasswordEncoder()); + } +} \ No newline at end of file diff --git a/config/src/main/java/org/springframework/security/config/annotation/authentication/configurers/provisioning/InMemoryUserDetailsManagerConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/authentication/configurers/provisioning/InMemoryUserDetailsManagerConfigurer.java new file mode 100644 index 0000000000..f35804a27d --- /dev/null +++ b/config/src/main/java/org/springframework/security/config/annotation/authentication/configurers/provisioning/InMemoryUserDetailsManagerConfigurer.java @@ -0,0 +1,42 @@ +/* + * Copyright 2002-2013 the original author or authors. + * + * 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 org.springframework.security.config.annotation.authentication.configurers.provisioning; + +import java.util.ArrayList; + +import org.springframework.security.config.annotation.authentication.ProviderManagerBuilder; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.provisioning.InMemoryUserDetailsManager; + +/** + * Configures an {@link org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder} to + * have in memory authentication. It also allows easily adding users to the in memory authentication. + * + * @param the type of the {@link SecurityBuilder} that is being configured + * + * @author Rob Winch + * @since 3.2 + */ +public class InMemoryUserDetailsManagerConfigurer> extends + UserDetailsManagerConfigurer> { + + /** + * Creates a new instance + */ + public InMemoryUserDetailsManagerConfigurer() { + super(new InMemoryUserDetailsManager(new ArrayList())); + } +} diff --git a/config/src/main/java/org/springframework/security/config/annotation/authentication/configurers/provisioning/JdbcUserDetailsManagerConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/authentication/configurers/provisioning/JdbcUserDetailsManagerConfigurer.java new file mode 100644 index 0000000000..181317fb96 --- /dev/null +++ b/config/src/main/java/org/springframework/security/config/annotation/authentication/configurers/provisioning/JdbcUserDetailsManagerConfigurer.java @@ -0,0 +1,192 @@ +/* + * Copyright 2002-2013 the original author or authors. + * + * 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 org.springframework.security.config.annotation.authentication.configurers.provisioning; + +import java.util.ArrayList; +import java.util.List; + +import javax.sql.DataSource; + +import org.springframework.core.io.ClassPathResource; +import org.springframework.core.io.Resource; +import org.springframework.jdbc.datasource.init.DataSourceInitializer; +import org.springframework.jdbc.datasource.init.DatabasePopulator; +import org.springframework.jdbc.datasource.init.ResourceDatabasePopulator; +import org.springframework.security.config.annotation.authentication.ProviderManagerBuilder; +import org.springframework.security.core.userdetails.UserCache; +import org.springframework.security.provisioning.JdbcUserDetailsManager; + +/** + * Configures an {@link org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder} to + * have JDBC authentication. It also allows easily adding users to the database used for authentication and setting up + * the schema. + * + *

+ * The only required method is the {@link #dataSource(javax.sql.DataSource)} all other methods have reasonable defaults. + *

+ * + * @param the type of the {@link ProviderManagerBuilder} that is being configured + * + * @author Rob Winch + * @since 3.2 + */ +public class JdbcUserDetailsManagerConfigurer> extends + UserDetailsManagerConfigurer> { + + private DataSource dataSource; + + private List initScripts = new ArrayList(); + + public JdbcUserDetailsManagerConfigurer(JdbcUserDetailsManager manager) { + super(manager); + } + + public JdbcUserDetailsManagerConfigurer() { + this(new JdbcUserDetailsManager()); + } + + + /** + * Populates the {@link DataSource} to be used. This is the only required attribute. + * + * @param dataSource the {@link DataSource} to be used. Cannot be null. + * @return + * @throws Exception + */ + public JdbcUserDetailsManagerConfigurer dataSource(DataSource dataSource) throws Exception { + this.dataSource = dataSource; + getUserDetailsService().setDataSource(dataSource); + return this; + } + + /** + * Sets the query to be used for finding a user by their username. For example: + * + * + * select username,password,enabled from users where username = ? + * + * @param query The query to use for selecting the username, password, and if the user is enabled by username. + * Must contain a single parameter for the username. + * @return The {@link JdbcUserDetailsManagerRegistry} used for additional customizations + * @throws Exception + */ + public JdbcUserDetailsManagerConfigurer usersByUsernameQuery(String query) throws Exception { + getUserDetailsService().setUsersByUsernameQuery(query); + return this; + } + + /** + * Sets the query to be used for finding a user's authorities by their username. For example: + * + * + * select username,authority from authorities where username = ? + * + * + * @param query The query to use for selecting the username, authority by username. + * Must contain a single parameter for the username. + * @return The {@link JdbcUserDetailsManagerRegistry} used for additional customizations + * @throws Exception + */ + public JdbcUserDetailsManagerConfigurer authoritiesByUsernameQuery(String query) throws Exception { + getUserDetailsService().setAuthoritiesByUsernameQuery(query); + return this; + } + + /** + * An SQL statement to query user's group authorities given a username. For example: + * + * + * select + * g.id, g.group_name, ga.authority + * from + * groups g, group_members gm, group_authorities ga + * where + * gm.username = ? and g.id = ga.group_id and g.id = gm.group_id + * + * + * @param query The query to use for selecting the authorities by group. + * Must contain a single parameter for the username. + * @return The {@link JdbcUserDetailsManagerRegistry} used for additional customizations + * @throws Exception + */ + public JdbcUserDetailsManagerConfigurer groupAuthoritiesByUsername(String query) throws Exception { + JdbcUserDetailsManager userDetailsService = getUserDetailsService(); + userDetailsService.setEnableGroups(true); + userDetailsService.setGroupAuthoritiesByUsernameQuery(query); + return this; + } + + /** + * A non-empty string prefix that will be added to role strings loaded from persistent storage (default is ""). + * + * @param rolePrefix + * @return + * @throws Exception + */ + public JdbcUserDetailsManagerConfigurer rolePrefix(String rolePrefix) throws Exception { + getUserDetailsService().setRolePrefix(rolePrefix); + return this; + } + + + /** + * Defines the {@link UserCache} to use + * + * @param userCache the {@link UserCache} to use + * @return the {@link JdbcUserDetailsManagerConfigurer} for further customizations + * @throws Exception + */ + public JdbcUserDetailsManagerConfigurer userCache(UserCache userCache) throws Exception { + getUserDetailsService().setUserCache(userCache); + return this; + } + + @Override + protected void initUserDetailsService() throws Exception { + if(!initScripts.isEmpty()) { + getDataSourceInit().afterPropertiesSet(); + } + super.initUserDetailsService(); + } + + @Override + public JdbcUserDetailsManager getUserDetailsService() { + return (JdbcUserDetailsManager) super.getUserDetailsService(); + } + + /** + * Populates the default schema that allows users and authorities to be stored. + * + * @return The {@link JdbcUserDetailsManagerRegistry} used for additional customizations + */ + public JdbcUserDetailsManagerConfigurer withDefaultSchema() { + this.initScripts.add(new ClassPathResource("org/springframework/security/core/userdetails/jdbc/users.ddl")); + return this; + } + + protected DatabasePopulator getDatabasePopulator() { + ResourceDatabasePopulator dbp = new ResourceDatabasePopulator(); + dbp.setScripts(initScripts.toArray(new Resource[initScripts.size()])); + return dbp; + } + + private DataSourceInitializer getDataSourceInit() { + DataSourceInitializer dsi = new DataSourceInitializer(); + dsi.setDatabasePopulator(getDatabasePopulator()); + dsi.setDataSource(dataSource); + return dsi; + } +} diff --git a/config/src/main/java/org/springframework/security/config/annotation/authentication/configurers/provisioning/UserDetailsManagerConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/authentication/configurers/provisioning/UserDetailsManagerConfigurer.java new file mode 100644 index 0000000000..88be60e62d --- /dev/null +++ b/config/src/main/java/org/springframework/security/config/annotation/authentication/configurers/provisioning/UserDetailsManagerConfigurer.java @@ -0,0 +1,263 @@ +/* + * Copyright 2002-2013 the original author or authors. + * + * 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 org.springframework.security.config.annotation.authentication.configurers.provisioning; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import org.springframework.security.config.annotation.SecurityBuilder; +import org.springframework.security.config.annotation.authentication.ProviderManagerBuilder; +import org.springframework.security.config.annotation.authentication.configurers.userdetails.UserDetailsServiceConfigurer; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.AuthorityUtils; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.User; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.provisioning.UserDetailsManager; +import org.springframework.util.Assert; + +/** + * Base class for populating an + * {@link org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder} with a + * {@link UserDetailsManager}. + * + * @param the type of the {@link SecurityBuilder} that is being configured + * @param the type of {@link UserDetailsManagerConfigurer} + * + * @author Rob Winch + * @since 3.2 + */ +public class UserDetailsManagerConfigurer, C extends UserDetailsManagerConfigurer> extends + UserDetailsServiceConfigurer { + + private final List userBuilders = new ArrayList(); + + protected UserDetailsManagerConfigurer(UserDetailsManager userDetailsManager) { + super(userDetailsManager); + } + + /** + * Populates the users that have been added. + * + * @throws Exception + */ + @Override + protected void initUserDetailsService() throws Exception { + for(UserDetailsBuilder userBuilder : userBuilders) { + getUserDetailsService().createUser(userBuilder.build()); + } + } + + /** + * Allows adding a user to the {@link UserDetailsManager} that is being created. This method can be invoked + * multiple times to add multiple users. + * + * @param username the username for the user being added. Cannot be null. + * @return + */ + @SuppressWarnings("unchecked") + public final UserDetailsBuilder withUser(String username) { + UserDetailsBuilder userBuilder = new UserDetailsBuilder((C)this); + userBuilder.username(username); + this.userBuilders.add(userBuilder); + return userBuilder; + } + + /** + * Builds the user to be added. At minimum the username, password, and authorities should provided. The remaining + * attributes have reasonable defaults. + * + * @param the type of {@link UserDetailsManagerConfigurer} to return for chaining methods. + */ + public class UserDetailsBuilder { + private String username; + private String password; + private List authorities; + private boolean accountExpired; + private boolean accountLocked; + private boolean credentialsExpired; + private boolean disabled; + private final C builder; + + /** + * Creates a new instance + * @param builder the builder to return + */ + private UserDetailsBuilder(C builder) { + this.builder = builder; + } + + /** + * Returns the {@link UserDetailsManagerRegistry} for method chaining (i.e. to add another user) + * + * @return the {@link UserDetailsManagerRegistry} for method chaining (i.e. to add another user) + */ + public C and() { + return builder; + } + + /** + * Populates the username. This attribute is required. + * + * @param username the username. Cannot be null. + * @return the {@link UserDetailsBuilder} for method chaining (i.e. to populate additional attributes for this + * user) + */ + private UserDetailsBuilder username(String username) { + Assert.notNull(username, "username cannot be null"); + this.username = username; + return this; + } + + /** + * Populates the password. This attribute is required. + * + * @param password the password. Cannot be null. + * @return the {@link UserDetailsBuilder} for method chaining (i.e. to populate additional attributes for this + * user) + */ + public UserDetailsBuilder password(String password) { + Assert.notNull(password, "password cannot be null"); + this.password = password; + return this; + } + + /** + * Populates the roles. This method is a shortcut for calling {@link #authorities(String...)}, but automatically + * prefixes each entry with "ROLE_". This means the following: + * + * + * builder.roles("USER","ADMIN"); + * + * + * is equivalent to + * + * + * builder.authorities("ROLE_USER","ROLE_ADMIN"); + * + * + *

This attribute is required, but can also be populated with {@link #authorities(String...)}.

+ * + * @param roles the roles for this user (i.e. USER, ADMIN, etc). Cannot be null, contain null values or start + * with "ROLE_" + * @return the {@link UserDetailsBuilder} for method chaining (i.e. to populate additional attributes for this + * user) + */ + public UserDetailsBuilder roles(String... roles) { + List authorities = new ArrayList(roles.length); + for(String role : roles) { + Assert.isTrue(!role.startsWith("ROLE_"), role + " cannot start with ROLE_ (it is automatically added)"); + authorities.add(new SimpleGrantedAuthority("ROLE_"+role)); + } + return authorities(authorities); + } + + /** + * Populates the authorities. This attribute is required. + * + * @param authorities the authorities for this user. Cannot be null, or contain null + * values + * @return the {@link UserDetailsBuilder} for method chaining (i.e. to populate additional attributes for this + * user) + * @see #roles(String...) + */ + public UserDetailsBuilder authorities(GrantedAuthority...authorities) { + return authorities(Arrays.asList(authorities)); + } + + /** + * Populates the authorities. This attribute is required. + * + * @param authorities the authorities for this user. Cannot be null, or contain null + * values + * @return the {@link UserDetailsBuilder} for method chaining (i.e. to populate additional attributes for this + * user) + * @see #roles(String...) + */ + public UserDetailsBuilder authorities(List authorities) { + this.authorities = new ArrayList(authorities); + return this; + } + + /** + * Populates the authorities. This attribute is required. + * + * @param authorities the authorities for this user (i.e. ROLE_USER, ROLE_ADMIN, etc). Cannot be null, or contain null + * values + * @return the {@link UserDetailsBuilder} for method chaining (i.e. to populate additional attributes for this + * user) + * @see #roles(String...) + */ + public UserDetailsBuilder authorities(String... authorities) { + return authorities(AuthorityUtils.createAuthorityList(authorities)); + } + + /** + * Defines if the account is expired or not. Default is false. + * + * @param accountExpired true if the account is expired, false otherwise + * @return the {@link UserDetailsBuilder} for method chaining (i.e. to populate additional attributes for this + * user) + */ + public UserDetailsBuilder accountExpired(boolean accountExpired) { + this.accountExpired = accountExpired; + return this; + } + + /** + * Defines if the account is locked or not. Default is false. + * + * @param accountLocked true if the account is locked, false otherwise + * @return the {@link UserDetailsBuilder} for method chaining (i.e. to populate additional attributes for this + * user) + */ + public UserDetailsBuilder accountLocked(boolean accountLocked) { + this.accountLocked = accountLocked; + return this; + } + + /** + * Defines if the credentials are expired or not. Default is false. + * + * @param credentialsExpired true if the credentials are expired, false otherwise + * @return the {@link UserDetailsBuilder} for method chaining (i.e. to populate additional attributes for this + * user) + */ + public UserDetailsBuilder credentialsExpired(boolean credentialsExpired) { + this.credentialsExpired = credentialsExpired; + return this; + } + + + /** + * Defines if the account is disabled or not. Default is false. + * + * @param disabled true if the account is disabled, false otherwise + * @return the {@link UserDetailsBuilder} for method chaining (i.e. to populate additional attributes for this + * user) + */ + public UserDetailsBuilder disabled(boolean disabled) { + this.disabled = disabled; + return this; + } + + private UserDetails build() { + return new User(username, password, !disabled, !accountExpired, + !credentialsExpired, !accountLocked, authorities); + } + } +} diff --git a/config/src/main/java/org/springframework/security/config/annotation/authentication/configurers/userdetails/AbstractDaoAuthenticationConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/authentication/configurers/userdetails/AbstractDaoAuthenticationConfigurer.java new file mode 100644 index 0000000000..72b45963da --- /dev/null +++ b/config/src/main/java/org/springframework/security/config/annotation/authentication/configurers/userdetails/AbstractDaoAuthenticationConfigurer.java @@ -0,0 +1,95 @@ +/* + * Copyright 2002-2013 the original author or authors. + * + * 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 org.springframework.security.config.annotation.authentication.configurers.userdetails; + +import org.springframework.security.authentication.dao.DaoAuthenticationProvider; +import org.springframework.security.config.annotation.SecurityBuilder; +import org.springframework.security.config.annotation.SecurityConfigurer; +import org.springframework.security.config.annotation.authentication.ProviderManagerBuilder; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.crypto.password.PasswordEncoder; + +/** + * Allows configuring a {@link DaoAuthenticationProvider} + * + * @author Rob Winch + * @since 3.2 + * + * @param the type of the {@link SecurityBuilder} + * @param the type of {@link AbstractDaoAuthenticationConfigurer} this is + * @param The type of {@link UserDetailsService} that is being used + * + */ +abstract class AbstractDaoAuthenticationConfigurer, C extends AbstractDaoAuthenticationConfigurer,U extends UserDetailsService> extends UserDetailsAwareConfigurer { + private DaoAuthenticationProvider provider = new DaoAuthenticationProvider(); + private final U userDetailsService; + + /** + * Creates a new instance + * + * @param userDetailsService + */ + protected AbstractDaoAuthenticationConfigurer(U userDetailsService) { + this.userDetailsService = userDetailsService; + provider.setUserDetailsService(userDetailsService); + } + + /** + * Allows specifying the {@link PasswordEncoder} to use with the {@link DaoAuthenticationProvider}. The default is + * is to use plain text. + * + * @param passwordEncoder The {@link PasswordEncoder} to use. + * @return + */ + @SuppressWarnings("unchecked") + public C passwordEncoder(PasswordEncoder passwordEncoder) { + provider.setPasswordEncoder(passwordEncoder); + return (C) this; + } + + /** + * Allows specifying the + * {@link org.springframework.security.authentication.encoding.PasswordEncoder} + * to use with the {@link DaoAuthenticationProvider}. The default is is to + * use plain text. + * + * @param passwordEncoder + * The + * {@link org.springframework.security.authentication.encoding.PasswordEncoder} + * to use. + * @return the {@link SecurityConfigurer} for further customizations + */ + @SuppressWarnings("unchecked") + public C passwordEncoder(org.springframework.security.authentication.encoding.PasswordEncoder passwordEncoder) { + provider.setPasswordEncoder(passwordEncoder); + return (C) this; + } + + @Override + public void configure(B builder) throws Exception { + provider = postProcess(provider); + builder.authenticationProvider(provider); + } + + /** + * Gets the {@link UserDetailsService} that is used with the {@link DaoAuthenticationProvider} + * + * @return the {@link UserDetailsService} that is used with the {@link DaoAuthenticationProvider} + */ + public U getUserDetailsService() { + return userDetailsService; + } +} diff --git a/config/src/main/java/org/springframework/security/config/annotation/authentication/configurers/userdetails/DaoAuthenticationConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/authentication/configurers/userdetails/DaoAuthenticationConfigurer.java new file mode 100644 index 0000000000..241d7545b3 --- /dev/null +++ b/config/src/main/java/org/springframework/security/config/annotation/authentication/configurers/userdetails/DaoAuthenticationConfigurer.java @@ -0,0 +1,41 @@ +/* + * Copyright 2002-2013 the original author or authors. + * + * 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 org.springframework.security.config.annotation.authentication.configurers.userdetails; + +import org.springframework.security.authentication.dao.DaoAuthenticationProvider; +import org.springframework.security.config.annotation.authentication.ProviderManagerBuilder; +import org.springframework.security.core.userdetails.UserDetailsService; + +/** +* Allows configuring a {@link DaoAuthenticationProvider} +* +* @author Rob Winch +* @since 3.2 +* +* @param The type of {@link ProviderManagerBuilder} this is +* @param The type of {@link UserDetailsService} that is being used +* +*/ +public class DaoAuthenticationConfigurer, U extends UserDetailsService> extends AbstractDaoAuthenticationConfigurer, U>{ + + /** + * Creates a new instance + * @param userDetailsService + */ + public DaoAuthenticationConfigurer(U userDetailsService) { + super(userDetailsService); + } +} diff --git a/config/src/main/java/org/springframework/security/config/annotation/authentication/configurers/userdetails/UserDetailsAwareConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/authentication/configurers/userdetails/UserDetailsAwareConfigurer.java new file mode 100644 index 0000000000..df19842056 --- /dev/null +++ b/config/src/main/java/org/springframework/security/config/annotation/authentication/configurers/userdetails/UserDetailsAwareConfigurer.java @@ -0,0 +1,39 @@ +/* + * Copyright 2002-2013 the original author or authors. + * + * 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 org.springframework.security.config.annotation.authentication.configurers.userdetails; + +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.config.annotation.SecurityConfigurerAdapter; +import org.springframework.security.config.annotation.authentication.ProviderManagerBuilder; +import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; +import org.springframework.security.core.userdetails.UserDetailsService; + +/** + * Base class that allows access to the {@link UserDetailsService} for using as a default value with {@link AuthenticationManagerBuilder}. + * + * @author Rob Winch + * + * @param the type of the {@link ProviderManagerBuilder} + * @param the type of {@link UserDetailsService} + */ +public abstract class UserDetailsAwareConfigurer, U extends UserDetailsService> extends SecurityConfigurerAdapter { + + /** + * Gets the {@link UserDetailsService} or null if it is not available + * @return the {@link UserDetailsService} or null if it is not available + */ + public abstract U getUserDetailsService(); +} diff --git a/config/src/main/java/org/springframework/security/config/annotation/authentication/configurers/userdetails/UserDetailsServiceConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/authentication/configurers/userdetails/UserDetailsServiceConfigurer.java new file mode 100644 index 0000000000..c37c49bd03 --- /dev/null +++ b/config/src/main/java/org/springframework/security/config/annotation/authentication/configurers/userdetails/UserDetailsServiceConfigurer.java @@ -0,0 +1,58 @@ +/* + * Copyright 2002-2013 the original author or authors. + * + * 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 org.springframework.security.config.annotation.authentication.configurers.userdetails; + +import org.springframework.security.config.annotation.SecurityConfigurer; +import org.springframework.security.config.annotation.authentication.ProviderManagerBuilder; +import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; +import org.springframework.security.core.userdetails.UserDetailsService; + +/** + * Allows configuring a {@link UserDetailsService} within a {@link AuthenticationManagerBuilder}. + * + * @author Rob Winch + * @since 3.2 + * + * @param the type of the {@link SecurityBuilder} + * @param the {@link SecurityConfigurer} (or this) + * @param the type of UserDetailsService being used to allow for returning the concrete UserDetailsService. + */ +public class UserDetailsServiceConfigurer, + C extends UserDetailsServiceConfigurer, + U extends UserDetailsService> + extends AbstractDaoAuthenticationConfigurer { + + /** + * Creates a new instance + * @param userDetailsService the {@link UserDetailsService} that should be used + */ + public UserDetailsServiceConfigurer(U userDetailsService) { + super(userDetailsService); + } + + @Override + public void configure(B builder) throws Exception { + initUserDetailsService(); + + super.configure(builder); + } + + /** + * Allows subclasses to initialize the {@link UserDetailsService}. For example, it might add users, initialize + * schema, etc. + */ + protected void initUserDetailsService() throws Exception {} +} diff --git a/config/src/main/java/org/springframework/security/config/annotation/configuration/AutowireBeanFactoryObjectPostProcessor.java b/config/src/main/java/org/springframework/security/config/annotation/configuration/AutowireBeanFactoryObjectPostProcessor.java new file mode 100644 index 0000000000..11e7f4c941 --- /dev/null +++ b/config/src/main/java/org/springframework/security/config/annotation/configuration/AutowireBeanFactoryObjectPostProcessor.java @@ -0,0 +1,77 @@ +/* + * Copyright 2002-2013 the original author or authors. + * + * 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 org.springframework.security.config.annotation.configuration; + +import java.util.ArrayList; +import java.util.List; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.beans.factory.Aware; +import org.springframework.beans.factory.DisposableBean; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.config.AutowireCapableBeanFactory; +import org.springframework.security.config.annotation.ObjectPostProcessor; +import org.springframework.util.Assert; + +/** + * Allows registering Objects to participate with an + * {@link AutowireCapableBeanFactory}'s post processing of {@link Aware} + * methods, {@link InitializingBean#afterPropertiesSet()}, and + * {@link DisposableBean#destroy()}. + * + * @author Rob Winch + * @since 3.2 + */ +final class AutowireBeanFactoryObjectPostProcessor implements ObjectPostProcessor, DisposableBean { + private final Log logger = LogFactory.getLog(getClass()); + private final AutowireCapableBeanFactory autowireBeanFactory; + private final List disposableBeans = new ArrayList(); + + public AutowireBeanFactoryObjectPostProcessor( + AutowireCapableBeanFactory autowireBeanFactory) { + Assert.notNull(autowireBeanFactory, "autowireBeanFactory cannot be null"); + this.autowireBeanFactory = autowireBeanFactory; + } + + /* (non-Javadoc) + * @see org.springframework.security.config.annotation.web.Initializer#initialize(java.lang.Object) + */ + @SuppressWarnings("unchecked") + @Override + public T postProcess(T object) { + T result = (T) autowireBeanFactory.initializeBean(object, null); + if(result instanceof DisposableBean) { + disposableBeans.add((DisposableBean) result); + } + return result; + } + + /* (non-Javadoc) + * @see org.springframework.beans.factory.DisposableBean#destroy() + */ + @Override + public void destroy() throws Exception { + for(DisposableBean disposable : disposableBeans) { + try { + disposable.destroy(); + } catch(Exception error) { + logger.error(error); + } + } + } + +} diff --git a/config/src/main/java/org/springframework/security/config/annotation/configuration/ObjectPostProcessorConfiguration.java b/config/src/main/java/org/springframework/security/config/annotation/configuration/ObjectPostProcessorConfiguration.java new file mode 100644 index 0000000000..ee3f200e9c --- /dev/null +++ b/config/src/main/java/org/springframework/security/config/annotation/configuration/ObjectPostProcessorConfiguration.java @@ -0,0 +1,44 @@ +/* + * Copyright 2002-2013 the original author or authors. + * + * 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 org.springframework.security.config.annotation.configuration; + +import org.springframework.beans.factory.config.AutowireCapableBeanFactory; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.ObjectPostProcessor; +import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; + +/** + * Spring {@link Configuration} that exports the default + * {@link ObjectPostProcessor}. This class is not intended to be imported + * manually rather it is imported automatically when using + * {@link EnableWebSecurity} or {@link EnableGlobalMethodSecurity}. + * + * @see EnableWebSecurity + * @see EnableGlobalMethodSecurity + * + * @author Rob Winch + * @since 3.2 + */ +@Configuration +public class ObjectPostProcessorConfiguration { + + @Bean + public ObjectPostProcessor objectPostProcessor(AutowireCapableBeanFactory beanFactory) { + return new AutowireBeanFactoryObjectPostProcessor(beanFactory); + } +} diff --git a/config/src/main/java/org/springframework/security/config/annotation/method/configuration/EnableGlobalMethodSecurity.java b/config/src/main/java/org/springframework/security/config/annotation/method/configuration/EnableGlobalMethodSecurity.java new file mode 100644 index 0000000000..98cd28d1ea --- /dev/null +++ b/config/src/main/java/org/springframework/security/config/annotation/method/configuration/EnableGlobalMethodSecurity.java @@ -0,0 +1,101 @@ +/* + * Copyright 2002-2013 the original author or authors. + * + * 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 org.springframework.security.config.annotation.method.configuration; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import org.springframework.context.annotation.AdviceMode; +import org.springframework.context.annotation.Import; +import org.springframework.core.Ordered; +import org.springframework.security.access.annotation.Secured; +import org.springframework.security.config.annotation.configuration.ObjectPostProcessorConfiguration; + +/** + *

Enables Spring Security global method security similar to the + * xml support.

+ * + *

+ * More advanced configurations may wish to extend + * {@link GlobalMethodSecurityConfiguration} and override the protected methods + * to provide custom implementations. Note that + * {@link EnableGlobalMethodSecurity} still must be included on the class + * extending {@link GlobalMethodSecurityConfiguration} to determine the + * settings. + * + * @author Rob Winch + * @since 3.2 + */ +@Retention(value=java.lang.annotation.RetentionPolicy.RUNTIME) +@Target(value={java.lang.annotation.ElementType.TYPE}) +@Documented +@Import({GlobalMethodSecuritySelector.class,ObjectPostProcessorConfiguration.class}) +public @interface EnableGlobalMethodSecurity { + + /** + * Determines if Spring Security's pre post annotations should be enabled. Default is false. + * @return true if pre post annotations should be enabled false otherwise. + */ + boolean prePostEnabled() default false; + + /** + * Determines if Spring Security's {@link Secured} annotations should be enabled. + * @return true if {@link Secured} annotations should be enabled false otherwise. Default is false. + */ + boolean securedEnabled() default false; + + /** + * Determines if JSR-250 annotations should be enabled. Default is false. + * @return true if JSR-250 should be enabled false otherwise. + */ + boolean jsr250Enabled() default false; + + /** + * Indicate whether subclass-based (CGLIB) proxies are to be created ({@code true}) as + * opposed to standard Java interface-based proxies ({@code false}). The default is + * {@code false}. Applicable only if {@link #mode()} is set to + * {@link AdviceMode#PROXY}. + * + *

Note that setting this attribute to {@code true} will affect all + * Spring-managed beans requiring proxying, not just those marked with + * the Security annotations. For example, other beans marked with Spring's + * {@code @Transactional} annotation will be upgraded to subclass proxying at the same + * time. This approach has no negative impact in practice unless one is explicitly + * expecting one type of proxy vs another, e.g. in tests. + * + * @return true if CGILIB proxies should be created instead of interface based proxies, else false + */ + boolean proxyTargetClass() default false; + + /** + * Indicate how security advice should be applied. The default is + * {@link AdviceMode#PROXY}. + * @see AdviceMode + * + * @return the {@link AdviceMode} to use + */ + AdviceMode mode() default AdviceMode.PROXY; + + /** + * Indicate the ordering of the execution of the security advisor + * when multiple advices are applied at a specific joinpoint. + * The default is {@link Ordered#LOWEST_PRECEDENCE}. + * + * @return the order the security advisor should be applied + */ + int order() default Ordered.LOWEST_PRECEDENCE; +} diff --git a/config/src/main/java/org/springframework/security/config/annotation/method/configuration/GlobalMethodSecurityAspectJAutoProxyRegistrar.java b/config/src/main/java/org/springframework/security/config/annotation/method/configuration/GlobalMethodSecurityAspectJAutoProxyRegistrar.java new file mode 100644 index 0000000000..9efb6ba9fc --- /dev/null +++ b/config/src/main/java/org/springframework/security/config/annotation/method/configuration/GlobalMethodSecurityAspectJAutoProxyRegistrar.java @@ -0,0 +1,68 @@ +/* + * Copyright 2002-2013 the original author or authors. + * + * 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 org.springframework.security.config.annotation.method.configuration; + +import java.util.Map; + +import org.springframework.aop.config.AopConfigUtils; +import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.context.annotation.ImportBeanDefinitionRegistrar; +import org.springframework.core.annotation.AnnotationAttributes; +import org.springframework.core.type.AnnotationMetadata; + +/** + * Registers an + * {@link org.springframework.aop.aspectj.annotation.AnnotationAwareAspectJAutoProxyCreator + * AnnotationAwareAspectJAutoProxyCreator} against the current + * {@link BeanDefinitionRegistry} as appropriate based on a given @ + * {@link EnableGlobalMethodSecurity} annotation. + * + *

+ * Note: This class is necessary because AspectJAutoProxyRegistrar only supports + * EnableAspectJAutoProxy. + *

+ * + * @author Rob Winch + * @since 3.2 + */ +class GlobalMethodSecurityAspectJAutoProxyRegistrar implements + ImportBeanDefinitionRegistrar { + + /** + * Register, escalate, and configure the AspectJ auto proxy creator based on + * the value of the @{@link EnableGlobalMethodSecurity#proxyTargetClass()} + * attribute on the importing {@code @Configuration} class. + */ + @Override + public void registerBeanDefinitions( + AnnotationMetadata importingClassMetadata, + BeanDefinitionRegistry registry) { + + AopConfigUtils + .registerAspectJAnnotationAutoProxyCreatorIfNecessary(registry); + + Map annotationAttributes = importingClassMetadata + .getAnnotationAttributes(EnableGlobalMethodSecurity.class + .getName()); + AnnotationAttributes enableAJAutoProxy = AnnotationAttributes + .fromMap(annotationAttributes); + + if (enableAJAutoProxy.getBoolean("proxyTargetClass")) { + AopConfigUtils.forceAutoProxyCreatorToUseClassProxying(registry); + } + } + +} \ No newline at end of file diff --git a/config/src/main/java/org/springframework/security/config/annotation/method/configuration/GlobalMethodSecurityConfiguration.java b/config/src/main/java/org/springframework/security/config/annotation/method/configuration/GlobalMethodSecurityConfiguration.java new file mode 100644 index 0000000000..398fb71b14 --- /dev/null +++ b/config/src/main/java/org/springframework/security/config/annotation/method/configuration/GlobalMethodSecurityConfiguration.java @@ -0,0 +1,385 @@ +/* + * Copyright 2002-2013 the original author or authors. + * + * 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 org.springframework.security.config.annotation.method.configuration; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +import org.aopalliance.intercept.MethodInterceptor; +import org.springframework.aop.framework.ProxyFactoryBean; +import org.springframework.aop.target.LazyInitTargetSource; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.ImportAware; +import org.springframework.context.annotation.Role; +import org.springframework.core.annotation.AnnotationAttributes; +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.core.type.AnnotationMetadata; +import org.springframework.security.access.AccessDecisionManager; +import org.springframework.security.access.AccessDecisionVoter; +import org.springframework.security.access.AfterInvocationProvider; +import org.springframework.security.access.annotation.Jsr250MethodSecurityMetadataSource; +import org.springframework.security.access.annotation.SecuredAnnotationSecurityMetadataSource; +import org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler; +import org.springframework.security.access.expression.method.ExpressionBasedAnnotationAttributeFactory; +import org.springframework.security.access.expression.method.ExpressionBasedPostInvocationAdvice; +import org.springframework.security.access.expression.method.ExpressionBasedPreInvocationAdvice; +import org.springframework.security.access.expression.method.MethodSecurityExpressionHandler; +import org.springframework.security.access.intercept.AfterInvocationManager; +import org.springframework.security.access.intercept.AfterInvocationProviderManager; +import org.springframework.security.access.intercept.RunAsManager; +import org.springframework.security.access.intercept.aopalliance.MethodSecurityInterceptor; +import org.springframework.security.access.intercept.aopalliance.MethodSecurityMetadataSourceAdvisor; +import org.springframework.security.access.method.DelegatingMethodSecurityMetadataSource; +import org.springframework.security.access.method.MethodSecurityMetadataSource; +import org.springframework.security.access.prepost.PostInvocationAdviceProvider; +import org.springframework.security.access.prepost.PreInvocationAuthorizationAdvice; +import org.springframework.security.access.prepost.PreInvocationAuthorizationAdviceVoter; +import org.springframework.security.access.prepost.PrePostAnnotationSecurityMetadataSource; +import org.springframework.security.access.vote.AffirmativeBased; +import org.springframework.security.access.vote.AuthenticatedVoter; +import org.springframework.security.access.vote.RoleVoter; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.DefaultAuthenticationEventPublisher; +import org.springframework.security.config.annotation.ObjectPostProcessor; +import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; +import org.springframework.util.Assert; + +/** + * Base {@link Configuration} for enabling global method security. Classes may + * extend this class to customize the defaults, but must be sure to specify the + * {@link EnableGlobalMethodSecurity} annotation on the subclass. + * + * @author Rob Winch + * @since 3.2 + * @see EnableGlobalMethodSecurity + */ +@Configuration +public class GlobalMethodSecurityConfiguration implements ImportAware { + @Autowired + private ApplicationContext context; + @Autowired(required=false) + private ObjectPostProcessor objectPostProcessor = new ObjectPostProcessor() { + @Override + public T postProcess(T object) { + throw new IllegalStateException(ObjectPostProcessor.class.getName()+ " is a required bean. Ensure you have used @"+EnableGlobalMethodSecurity.class.getName()); + } + }; + private AuthenticationManager authenticationManager; + private AuthenticationManagerBuilder auth = new AuthenticationManagerBuilder(); + private boolean disableAuthenticationRegistry; + private AnnotationAttributes enableMethodSecurity; + private MethodSecurityExpressionHandler expressionHandler; + + /** + * Creates the default MethodInterceptor which is a MethodSecurityInterceptor using the following methods to + * construct it. + *
    + *
  • {@link #accessDecisionManager()}
  • + *
  • {@link #afterInvocationManager()}
  • + *
  • {@link #authenticationManager()}
  • + *
  • {@link #methodSecurityMetadataSource()}
  • + *
  • {@link #runAsManager()}
  • + * + *
+ * + *

+ * Subclasses can override this method to provide a different {@link MethodInterceptor}. + *

+ * + * @return + * @throws Exception + */ + @Bean + public MethodInterceptor methodSecurityInterceptor() throws Exception { + MethodSecurityInterceptor methodSecurityInterceptor = new MethodSecurityInterceptor(); + methodSecurityInterceptor + .setAccessDecisionManager(accessDecisionManager()); + methodSecurityInterceptor + .setAfterInvocationManager(afterInvocationManager()); + methodSecurityInterceptor + .setAuthenticationManager(authenticationManager()); + methodSecurityInterceptor + .setSecurityMetadataSource(methodSecurityMetadataSource()); + RunAsManager runAsManager = runAsManager(); + if (runAsManager != null) { + methodSecurityInterceptor.setRunAsManager(runAsManager); + } + return methodSecurityInterceptor; + } + + /** + * Provide a custom {@link AfterInvocationManager} for the default + * implementation of {@link #methodSecurityInterceptor()}. The default is + * null if pre post is not enabled. Otherwise, it returns a {@link AfterInvocationProviderManager}. + * + *

+ * Subclasses should override this method to provide a custom {@link AfterInvocationManager} + *

+ * + * @return + */ + protected AfterInvocationManager afterInvocationManager() { + if(prePostEnabled()) { + AfterInvocationProviderManager invocationProviderManager = new AfterInvocationProviderManager(); + ExpressionBasedPostInvocationAdvice postAdvice = new ExpressionBasedPostInvocationAdvice(getExpressionHandler()); + PostInvocationAdviceProvider postInvocationAdviceProvider = new PostInvocationAdviceProvider(postAdvice); + List afterInvocationProviders = new ArrayList(); + afterInvocationProviders.add(postInvocationAdviceProvider); + invocationProviderManager.setProviders(afterInvocationProviders); + return invocationProviderManager; + } + return null; + } + + /** + * Provide a custom {@link RunAsManager} for the default implementation of + * {@link #methodSecurityInterceptor()}. The default is null. + * + * @return + */ + protected RunAsManager runAsManager() { + return null; + } + + /** + * Allows subclasses to provide a custom {@link AccessDecisionManager}. The default is a {@link AffirmativeBased} + * with the following voters: + * + *
    + *
  • {@link PreInvocationAuthorizationAdviceVoter}
  • + *
  • {@link RoleVoter}
  • + *
  • {@link AuthenticatedVoter}
  • + *
+ * + * @return + */ + @SuppressWarnings("rawtypes") + protected AccessDecisionManager accessDecisionManager() { + List decisionVoters = new ArrayList(); + ExpressionBasedPreInvocationAdvice expressionAdvice = new ExpressionBasedPreInvocationAdvice(); + expressionAdvice.setExpressionHandler(getExpressionHandler()); + + decisionVoters.add(new PreInvocationAuthorizationAdviceVoter( + expressionAdvice)); + decisionVoters.add(new RoleVoter()); + decisionVoters.add(new AuthenticatedVoter()); + return new AffirmativeBased(decisionVoters); + } + + /** + * Provide a {@link MethodSecurityExpressionHandler} that is + * registered with the {@link ExpressionBasedPreInvocationAdvice}. The default is + * {@link DefaultMethodSecurityExpressionHandler} + * + *

Subclasses may override this method to provide a custom {@link MethodSecurityExpressionHandler}

+ * + * @return + */ + protected MethodSecurityExpressionHandler expressionHandler() { + return new DefaultMethodSecurityExpressionHandler(); + } + + /** + * Gets the {@link MethodSecurityExpressionHandler} or creates it using {@link #expressionHandler}. + * + * @return a non {@code null} {@link MethodSecurityExpressionHandler} + */ + protected final MethodSecurityExpressionHandler getExpressionHandler() { + if(expressionHandler == null) { + expressionHandler = expressionHandler(); + } + return expressionHandler; + } + + /** + * Provides a custom {@link MethodSecurityMetadataSource} that is registered + * with the {@link #methodSecurityMetadataSource()}. Default is null. + * + * @return a custom {@link MethodSecurityMetadataSource} that is registered + * with the {@link #methodSecurityMetadataSource()} + */ + protected MethodSecurityMetadataSource customMethodSecurityMetadataSource() { + return null; + } + + /** + * Allows providing a custom {@link AuthenticationManager}. The default is + * to use any authentication mechanisms registered by {@link #registerAuthentication(AuthenticationManagerBuilder)}. If + * {@link #registerAuthentication(AuthenticationManagerBuilder)} was not overriden, then an {@link AuthenticationManager} + * is attempted to be autowired by type. + * + * @return + */ + protected AuthenticationManager authenticationManager() throws Exception { + if(authenticationManager == null) { + DefaultAuthenticationEventPublisher eventPublisher = objectPostProcessor.postProcess(new DefaultAuthenticationEventPublisher()); + auth.authenticationEventPublisher(eventPublisher); + auth.objectPostProcessor(objectPostProcessor); + registerAuthentication(auth); + if(!disableAuthenticationRegistry) { + authenticationManager = auth.build(); + } + if(authenticationManager == null) { + authenticationManager = lazyBean(AuthenticationManager.class); + } + } + return authenticationManager; + } + + /** + * Sub classes can override this method to register different types of authentication. If not overridden, + * {@link #registerAuthentication(AuthenticationManagerBuilder)} will attempt to autowire by type. + * + * @param auth the {@link AuthenticationManagerBuilder} used to register different authentication mechanisms for the + * global method security. + * @throws Exception + */ + protected void registerAuthentication(AuthenticationManagerBuilder auth) throws Exception { + this.disableAuthenticationRegistry = true; + } + + /** + * Provides the default {@link MethodSecurityMetadataSource} that will be + * used. It creates a {@link DelegatingMethodSecurityMetadataSource} based + * upon {@link #customMethodSecurityMetadataSource()} and the attributes on + * {@link EnableGlobalMethodSecurity}. + * + * @return + */ + @Bean + public MethodSecurityMetadataSource methodSecurityMetadataSource() { + List sources = new ArrayList(); + ExpressionBasedAnnotationAttributeFactory attributeFactory = new ExpressionBasedAnnotationAttributeFactory( + methodExpressionHandler()); + MethodSecurityMetadataSource customMethodSecurityMetadataSource = customMethodSecurityMetadataSource(); + if (customMethodSecurityMetadataSource != null) { + sources.add(customMethodSecurityMetadataSource); + } + if (prePostEnabled()) { + sources.add(new PrePostAnnotationSecurityMetadataSource( + attributeFactory)); + } + if (securedEnabled()) { + sources.add(new SecuredAnnotationSecurityMetadataSource()); + } + if (jsr250Enabled()) { + sources.add(new Jsr250MethodSecurityMetadataSource()); + } + return new DelegatingMethodSecurityMetadataSource(sources); + } + + /** + * Creates the {@link MethodSecurityExpressionHandler} to be used. + * + * @return + */ + @Bean + public MethodSecurityExpressionHandler methodExpressionHandler() { + return new DefaultMethodSecurityExpressionHandler(); + } + + /** + * Creates the {@link PreInvocationAuthorizationAdvice} to be used. The + * default is {@link ExpressionBasedPreInvocationAdvice}. + * + * @return + */ + @Bean + public PreInvocationAuthorizationAdvice preInvocationAuthorizationAdvice() { + ExpressionBasedPreInvocationAdvice preInvocationAdvice = new ExpressionBasedPreInvocationAdvice(); + preInvocationAdvice.setExpressionHandler(methodExpressionHandler()); + return preInvocationAdvice; + } + + /** + * Obtains the {@link MethodSecurityMetadataSourceAdvisor} to be used. + * + * @return + */ + @Role(BeanDefinition.ROLE_INFRASTRUCTURE) + @Bean + public MethodSecurityMetadataSourceAdvisor metaDataSourceAdvisor() { + MethodSecurityMetadataSourceAdvisor methodAdvisor = new MethodSecurityMetadataSourceAdvisor( + "methodSecurityInterceptor", methodSecurityMetadataSource(), + "methodSecurityMetadataSource"); + methodAdvisor.setOrder(order()); + return methodAdvisor; + } + + /** + * Obtains the attributes from {@link EnableGlobalMethodSecurity} if this class was imported using the {@link EnableGlobalMethodSecurity} annotation. + */ + @Override + public final void setImportMetadata(AnnotationMetadata importMetadata) { + Map annotationAttributes = importMetadata + .getAnnotationAttributes(EnableGlobalMethodSecurity.class + .getName()); + enableMethodSecurity = AnnotationAttributes + .fromMap(annotationAttributes); + } + + @SuppressWarnings("unchecked") + private T lazyBean(Class interfaceName) { + LazyInitTargetSource lazyTargetSource = new LazyInitTargetSource(); + String[] beanNamesForType = context.getBeanNamesForType(interfaceName); + Assert.isTrue(beanNamesForType.length == 1 , "Expecting to only find a single bean for type " + interfaceName + ", but found " + Arrays.asList(beanNamesForType)); + lazyTargetSource.setTargetBeanName(beanNamesForType[0]); + lazyTargetSource.setBeanFactory(context); + ProxyFactoryBean proxyFactory = new ProxyFactoryBean(); + proxyFactory.setTargetSource(lazyTargetSource); + proxyFactory.setInterfaces(new Class[] { interfaceName }); + return (T) proxyFactory.getObject(); + } + + private boolean prePostEnabled() { + return enableMethodSecurity().getBoolean("prePostEnabled"); + } + + private boolean securedEnabled() { + return enableMethodSecurity().getBoolean("securedEnabled"); + } + + private boolean jsr250Enabled() { + return enableMethodSecurity().getBoolean("jsr250Enabled"); + } + + private int order() { + return (Integer) enableMethodSecurity().get("order"); + } + + private AnnotationAttributes enableMethodSecurity() { + if (enableMethodSecurity == null) { + // if it is null look at this instance (i.e. a subclass was used) + EnableGlobalMethodSecurity methodSecurityAnnotation = AnnotationUtils + .findAnnotation(getClass(), + EnableGlobalMethodSecurity.class); + Assert.notNull(methodSecurityAnnotation, + EnableGlobalMethodSecurity.class.getName() + " is required"); + Map methodSecurityAttrs = AnnotationUtils + .getAnnotationAttributes(methodSecurityAnnotation); + this.enableMethodSecurity = AnnotationAttributes + .fromMap(methodSecurityAttrs); + } + return this.enableMethodSecurity; + } + +} diff --git a/config/src/main/java/org/springframework/security/config/annotation/method/configuration/GlobalMethodSecuritySelector.java b/config/src/main/java/org/springframework/security/config/annotation/method/configuration/GlobalMethodSecuritySelector.java new file mode 100644 index 0000000000..25de549ca6 --- /dev/null +++ b/config/src/main/java/org/springframework/security/config/annotation/method/configuration/GlobalMethodSecuritySelector.java @@ -0,0 +1,59 @@ +/* + * Copyright 2002-2013 the original author or authors. + * + * 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 org.springframework.security.config.annotation.method.configuration; + +import java.util.Map; + +import org.springframework.context.annotation.AdviceMode; +import org.springframework.context.annotation.AutoProxyRegistrar; +import org.springframework.context.annotation.ImportSelector; +import org.springframework.core.annotation.AnnotationAttributes; +import org.springframework.core.type.AnnotationMetadata; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; + +/** + * Dynamically determines which imports to include using the + * {@link EnableGlobalMethodSecurity} annotation. + * + * @author Rob Winch + * @since 3.2 + */ +final class GlobalMethodSecuritySelector implements ImportSelector { + + @Override + public final String[] selectImports(AnnotationMetadata importingClassMetadata) { + Class annoType = EnableGlobalMethodSecurity.class; + Map annotationAttributes = importingClassMetadata.getAnnotationAttributes(annoType.getName(), false); + AnnotationAttributes attributes = AnnotationAttributes.fromMap(annotationAttributes); + Assert.notNull(attributes, String.format( + "@%s is not present on importing class '%s' as expected", + annoType.getSimpleName(), importingClassMetadata.getClassName())); + + // TODO would be nice if could use BeanClassLoaderAware (does not work) + Class importingClass = ClassUtils.resolveClassName(importingClassMetadata.getClassName(), ClassUtils.getDefaultClassLoader()); + boolean skipMethodSecurityConfiguration = GlobalMethodSecurityConfiguration.class.isAssignableFrom(importingClass); + + AdviceMode mode = attributes.getEnum("mode"); + String autoProxyClassName = AdviceMode.PROXY == mode ? AutoProxyRegistrar.class.getName() + : GlobalMethodSecurityAspectJAutoProxyRegistrar.class.getName(); + if(skipMethodSecurityConfiguration) { + return new String[] { autoProxyClassName }; + } + return new String[] { autoProxyClassName, + GlobalMethodSecurityConfiguration.class.getName()}; + } +} diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/AbstractRequestMatcherConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/AbstractRequestMatcherConfigurer.java new file mode 100644 index 0000000000..09f6ab2ade --- /dev/null +++ b/config/src/main/java/org/springframework/security/config/annotation/web/AbstractRequestMatcherConfigurer.java @@ -0,0 +1,198 @@ +/* + * Copyright 2002-2013 the original author or authors. + * + * 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 org.springframework.security.config.annotation.web; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import org.springframework.http.HttpMethod; +import org.springframework.security.config.annotation.SecurityBuilder; +import org.springframework.security.config.annotation.SecurityConfigurerAdapter; +import org.springframework.security.config.annotation.web.configurers.AbstractRequestMatcherMappingConfigurer; +import org.springframework.security.web.util.AntPathRequestMatcher; +import org.springframework.security.web.util.AnyRequestMatcher; +import org.springframework.security.web.util.RegexRequestMatcher; +import org.springframework.security.web.util.RequestMatcher; + +/** + * A base class for registering {@link RequestMatcher}'s. For example, it might allow for specifying which + * {@link RequestMatcher} require a certain level of authorization. + * + * + * @param The Builder that is building Object O and is configured by this {@link AbstractRequestMatcherMappingConfigurer} + * @param The object that is returned or Chained after creating the RequestMatcher + * @param The Object being built by Builder B + * + * @author Rob Winch + * @since 3.2 + */ +public abstract class AbstractRequestMatcherConfigurer,C,O> extends SecurityConfigurerAdapter { + private static final RequestMatcher ANY_REQUEST = new AnyRequestMatcher(); + /** + * Maps any request. + * + * @param method the {@link HttpMethod} to use or {@code null} for any {@link HttpMethod}. + * @param antPatterns the ant patterns to create {@link org.springframework.security.web.util.AntPathRequestMatcher} + * from + * + * @return the object that is chained after creating the {@link RequestMatcher} + */ + public C anyRequest() { + return requestMatchers(ANY_REQUEST); + } + + /** + * Maps a {@link List} of {@link org.springframework.security.web.util.AntPathRequestMatcher} instances. + * + * @param method the {@link HttpMethod} to use or {@code null} for any {@link HttpMethod}. + * @param antPatterns the ant patterns to create {@link org.springframework.security.web.util.AntPathRequestMatcher} + * from + * + * @return the object that is chained after creating the {@link RequestMatcher} + */ + public C antMatchers(HttpMethod method, String... antPatterns) { + return chainRequestMatchers(RequestMatchers.antMatchers(method, antPatterns)); + } + + /** + * Maps a {@link List} of {@link org.springframework.security.web.util.AntPathRequestMatcher} instances that do + * not care which {@link HttpMethod} is used. + * + * @param antPatterns the ant patterns to create {@link org.springframework.security.web.util.AntPathRequestMatcher} + * from + * + * @return the object that is chained after creating the {@link RequestMatcher} + */ + public C antMatchers(String... antPatterns) { + return chainRequestMatchers(RequestMatchers.antMatchers(antPatterns)); + } + + /** + * Maps a {@link List} of {@link org.springframework.security.web.util.RegexRequestMatcher} instances. + * + * @param method the {@link HttpMethod} to use or {@code null} for any {@link HttpMethod}. + * @param regexPatterns the regular expressions to create + * {@link org.springframework.security.web.util.RegexRequestMatcher} from + * + * @return the object that is chained after creating the {@link RequestMatcher} + */ + public C regexMatchers(HttpMethod method, String... regexPatterns) { + return chainRequestMatchers(RequestMatchers.regexMatchers(method, + regexPatterns)); + } + + /** + * Create a {@link List} of {@link org.springframework.security.web.util.RegexRequestMatcher} instances that do not + * specify an {@link HttpMethod}. + * + * @param regexPatterns the regular expressions to create + * {@link org.springframework.security.web.util.RegexRequestMatcher} from + * + * @return the object that is chained after creating the {@link RequestMatcher} + */ + public C regexMatchers(String... regexPatterns) { + return chainRequestMatchers(RequestMatchers.regexMatchers(regexPatterns)); + } + + /** + * Associates a list of {@link RequestMatcher} instances with the {@link AbstractRequestMatcherMappingConfigurer} + * + * @param requestMatchers the {@link RequestMatcher} instances + * + * @return the object that is chained after creating the {@link RequestMatcher} + */ + public C requestMatchers(RequestMatcher... requestMatchers) { + return chainRequestMatchers(Arrays.asList(requestMatchers)); + } + + /** + * Subclasses should implement this method for returning the object that is chained to the creation of the + * {@link RequestMatcher} instances. + * + * @param requestMatchers the {@link RequestMatcher} instances that were created + * @return the chained Object for the subclass which allows association of something else to the + * {@link RequestMatcher} + */ + protected abstract C chainRequestMatchers(List requestMatchers); + + /** + * Utilities for creating {@link RequestMatcher} instances. + * + * @author Rob Winch + * @since 3.2 + */ + private static final class RequestMatchers { + + /** + * Create a {@link List} of {@link AntPathRequestMatcher} instances. + * + * @param httpMethod the {@link HttpMethod} to use or {@code null} for any {@link HttpMethod}. + * @param antPatterns the ant patterns to create {@link AntPathRequestMatcher} from + * + * @return a {@link List} of {@link AntPathRequestMatcher} instances + */ + public static List antMatchers(HttpMethod httpMethod, String...antPatterns) { + String method = httpMethod == null ? null : httpMethod.toString(); + List matchers = new ArrayList(); + for(String pattern : antPatterns) { + matchers.add(new AntPathRequestMatcher(pattern, method)); + } + return matchers; + } + + /** + * Create a {@link List} of {@link AntPathRequestMatcher} instances that do not specify an {@link HttpMethod}. + * + * @param antPatterns the ant patterns to create {@link AntPathRequestMatcher} from + * + * @return a {@link List} of {@link AntPathRequestMatcher} instances + */ + public static List antMatchers(String...antPatterns) { + return antMatchers(null, antPatterns); + } + + /** + * Create a {@link List} of {@link RegexRequestMatcher} instances. + * + * @param httpMethod the {@link HttpMethod} to use or {@code null} for any {@link HttpMethod}. + * @param regexPatterns the regular expressions to create {@link RegexRequestMatcher} from + * + * @return a {@link List} of {@link RegexRequestMatcher} instances + */ + public static List regexMatchers(HttpMethod httpMethod, String...regexPatterns) { + String method = httpMethod == null ? null : httpMethod.toString(); + List matchers = new ArrayList(); + for(String pattern : regexPatterns) { + matchers.add(new RegexRequestMatcher(pattern, method)); + } + return matchers; + } + + /** + * Create a {@link List} of {@link RegexRequestMatcher} instances that do not specify an {@link HttpMethod}. + * + * @param regexPatterns the regular expressions to create {@link RegexRequestMatcher} from + * + * @return a {@link List} of {@link RegexRequestMatcher} instances + */ + public static List regexMatchers(String...regexPatterns) { + return regexMatchers(null, regexPatterns); + } + + private RequestMatchers() {} + } +} diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/HttpSecurityBuilder.java b/config/src/main/java/org/springframework/security/config/annotation/web/HttpSecurityBuilder.java new file mode 100644 index 0000000000..842c3a73ae --- /dev/null +++ b/config/src/main/java/org/springframework/security/config/annotation/web/HttpSecurityBuilder.java @@ -0,0 +1,177 @@ +/* + * Copyright 2002-2013 the original author or authors. + * + * 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 org.springframework.security.config.annotation.web; + +import javax.servlet.Filter; + +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.AuthenticationProvider; +import org.springframework.security.config.annotation.SecurityBuilder; +import org.springframework.security.config.annotation.SecurityConfigurer; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.openid.OpenIDAuthenticationFilter; +import org.springframework.security.web.DefaultSecurityFilterChain; +import org.springframework.security.web.access.ExceptionTranslationFilter; +import org.springframework.security.web.access.channel.ChannelProcessingFilter; +import org.springframework.security.web.access.intercept.FilterSecurityInterceptor; +import org.springframework.security.web.authentication.AnonymousAuthenticationFilter; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; +import org.springframework.security.web.authentication.logout.LogoutFilter; +import org.springframework.security.web.authentication.preauth.AbstractPreAuthenticatedProcessingFilter; +import org.springframework.security.web.authentication.preauth.x509.X509AuthenticationFilter; +import org.springframework.security.web.authentication.rememberme.RememberMeAuthenticationFilter; +import org.springframework.security.web.authentication.switchuser.SwitchUserFilter; +import org.springframework.security.web.authentication.www.BasicAuthenticationFilter; +import org.springframework.security.web.authentication.www.DigestAuthenticationFilter; +import org.springframework.security.web.context.SecurityContextPersistenceFilter; +import org.springframework.security.web.jaasapi.JaasApiIntegrationFilter; +import org.springframework.security.web.savedrequest.RequestCacheAwareFilter; +import org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter; +import org.springframework.security.web.session.ConcurrentSessionFilter; +import org.springframework.security.web.session.SessionManagementFilter; + +/** + * + * @author Rob Winch + * + * @param + */ +public interface HttpSecurityBuilder> extends SecurityBuilder { + + /** + * Gets the {@link SecurityConfigurer} by its class name or + * null if not found. Note that object hierarchies are not + * considered. + * + * @param clazz the Class of the {@link SecurityConfigurer} to attempt to get. + */ + > C getConfigurer( + Class clazz); + + /** + * Removes the {@link SecurityConfigurer} by its class name or + * null if not found. Note that object hierarchies are not + * considered. + * + * @param clazz the Class of the {@link SecurityConfigurer} to attempt to remove. + * @return the {@link SecurityConfigurer} that was removed or null if not found + */ + > C removeConfigurer(Class clazz); + + /** + * Sets an object that is shared by multiple {@link SecurityConfigurer}. + * + * @param sharedType the Class to key the shared object by. + * @param object the Object to store + */ + void setSharedObject(Class sharedType, C object); + + /** + * Gets a shared Object. Note that object heirarchies are not considered. + * + * @param sharedType the type of the shared Object + * @return the shared Object or null if it is not found + */ + C getSharedObject(Class sharedType); + + /** + * Allows adding an additional {@link AuthenticationProvider} to be used + * + * @param authenticationProvider the {@link AuthenticationProvider} to be added + * @return the {@link HttpSecurity} for further customizations + */ + H authenticationProvider( + AuthenticationProvider authenticationProvider); + + /** + * Allows adding an additional {@link UserDetailsService} to be used + * + * @param userDetailsService the {@link UserDetailsService} to be added + * @return the {@link HttpSecurity} for further customizations + */ + H userDetailsService( + UserDetailsService userDetailsService) throws Exception; + + /** + * Allows adding a {@link Filter} after one of the known {@link Filter} + * classes. The known {@link Filter} instances are either a {@link Filter} + * listed in {@link #addFilter(Filter)} or a {@link Filter} that has already + * been added using {@link #addFilterAfter(Filter, Class)} or + * {@link #addFilterBefore(Filter, Class)}. + * + * @param filter the {@link Filter} to register before the type {@code afterFilter} + * @param afterFilter the Class of the known {@link Filter}. + * @return the {@link HttpSecurity} for further customizations + */ + H addFilterAfter(Filter filter, + Class afterFilter); + + /** + * Allows adding a {@link Filter} before one of the known {@link Filter} + * classes. The known {@link Filter} instances are either a {@link Filter} + * listed in {@link #addFilter(Filter)} or a {@link Filter} that has already + * been added using {@link #addFilterAfter(Filter, Class)} or + * {@link #addFilterBefore(Filter, Class)}. + * + * @param filter the {@link Filter} to register before the type {@code beforeFilter} + * @param beforeFilter the Class of the known {@link Filter}. + * @return the {@link HttpSecurity} for further customizations + */ + H addFilterBefore(Filter filter, + Class beforeFilter); + + /** + * Adds a {@link Filter} that must be an instance of or extend one of the + * Filters provided within the Security framework. The method ensures that + * the ordering of the Filters is automatically taken care of. + * + * The ordering of the Filters is: + * + *
    + *
  • {@link ChannelProcessingFilter}
  • + *
  • {@link ConcurrentSessionFilter}
  • + *
  • {@link SecurityContextPersistenceFilter}
  • + *
  • {@link LogoutFilter}
  • + *
  • {@link X509AuthenticationFilter}
  • + *
  • {@link AbstractPreAuthenticatedProcessingFilter}
  • + *
  • {@link org.springframework.security.cas.web.CasAuthenticationFilter}
  • + *
  • {@link UsernamePasswordAuthenticationFilter}
  • + *
  • {@link ConcurrentSessionFilter}
  • + *
  • {@link OpenIDAuthenticationFilter}
  • + *
  • {@link DefaultLoginPageViewFilter}
  • + *
  • {@link ConcurrentSessionFilter}
  • + *
  • {@link DigestAuthenticationFilter}
  • + *
  • {@link BasicAuthenticationFilter}
  • + *
  • {@link RequestCacheAwareFilter}
  • + *
  • {@link SecurityContextHolderAwareRequestFilter}
  • + *
  • {@link JaasApiIntegrationFilter}
  • + *
  • {@link RememberMeAuthenticationFilter}
  • + *
  • {@link AnonymousAuthenticationFilter}
  • + *
  • {@link SessionManagementFilter}
  • + *
  • {@link ExceptionTranslationFilter}
  • + *
  • {@link FilterSecurityInterceptor}
  • + *
  • {@link SwitchUserFilter}
  • + *
+ * + * @param filter the {@link Filter} to add + * @return the {@link HttpSecurity} for further customizations + */ + H addFilter(Filter filter); + + // FIXME shared object or explicit? + AuthenticationManager getAuthenticationManager(); +} \ No newline at end of file diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/WebSecurityConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/WebSecurityConfigurer.java new file mode 100644 index 0000000000..19ab476ea6 --- /dev/null +++ b/config/src/main/java/org/springframework/security/config/annotation/web/WebSecurityConfigurer.java @@ -0,0 +1,41 @@ +/* + * Copyright 2002-2013 the original author or authors. + * + * 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 org.springframework.security.config.annotation.web; + +import javax.servlet.Filter; + +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.SecurityBuilder; +import org.springframework.security.config.annotation.SecurityConfigurer; +import org.springframework.security.config.annotation.web.builders.WebSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; + +/** + * Allows customization to the {@link WebSecurity}. In most instances + * users will use {@link EnableWebSecurity} and a create {@link Configuration} + * that extends {@link WebSecurityConfigurerAdapter} which will automatically be + * applied to the {@link WebSecurity} by the {@link EnableWebSecurity} + * annotation. + * + * @see WebSecurityConfigurerAdapter + * + * @author Rob Winch + * @since 3.2 + */ +public interface WebSecurityConfigurer> extends SecurityConfigurer { + +} diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/builders/DebugFilter.java b/config/src/main/java/org/springframework/security/config/annotation/web/builders/DebugFilter.java new file mode 100644 index 0000000000..2370ae6854 --- /dev/null +++ b/config/src/main/java/org/springframework/security/config/annotation/web/builders/DebugFilter.java @@ -0,0 +1,191 @@ +/* + * Copyright 2002-2013 the original author or authors. + * + * 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 org.springframework.security.config.annotation.web.builders; + +import java.io.IOException; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.util.List; + +import javax.servlet.Filter; +import javax.servlet.FilterChain; +import javax.servlet.FilterConfig; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletRequestWrapper; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpSession; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.security.web.FilterChainProxy; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.util.UrlUtils; + +/** + * Spring Security debugging filter. + *

+ * Logs information (such as session creation) to help the user understand how requests are being handled + * by Spring Security and provide them with other relevant information (such as when sessions are being created). + * + * + * @author Luke Taylor + * @author Rob Winch + * @since 3.1 + */ +class DebugFilter implements Filter { + private static final String ALREADY_FILTERED_ATTR_NAME = DebugFilter.class.getName().concat(".FILTERED"); + + private final FilterChainProxy fcp; + private final Logger logger = new Logger(); + + public DebugFilter(FilterChainProxy fcp) { + this.fcp = fcp; + } + + public final void doFilter(ServletRequest srvltRequest, ServletResponse srvltResponse, FilterChain filterChain) + throws ServletException, IOException { + + if (!(srvltRequest instanceof HttpServletRequest) || !(srvltResponse instanceof HttpServletResponse)) { + throw new ServletException("DebugFilter just supports HTTP requests"); + } + HttpServletRequest request = (HttpServletRequest) srvltRequest; + HttpServletResponse response = (HttpServletResponse) srvltResponse; + + List filters = getFilters(request); + logger.log("Request received for '" + UrlUtils.buildRequestUrl(request) + "':\n\n" + + request + "\n\n" + + "servletPath:" + request.getServletPath() + "\n" + + "pathInfo:" + request.getPathInfo() + "\n\n" + + formatFilters(filters)); + + if (request.getAttribute(ALREADY_FILTERED_ATTR_NAME) == null) { + invokeWithWrappedRequest(request, response, filterChain); + } else { + fcp.doFilter(request, response, filterChain); + } + } + + private void invokeWithWrappedRequest(HttpServletRequest request, + HttpServletResponse response, FilterChain filterChain) throws IOException, ServletException { + request.setAttribute(ALREADY_FILTERED_ATTR_NAME, Boolean.TRUE); + request = new DebugRequestWrapper(request); + try { + fcp.doFilter(request, response, filterChain); + } + finally { + request.removeAttribute(ALREADY_FILTERED_ATTR_NAME); + } + } + + String formatFilters(List filters) { + StringBuilder sb = new StringBuilder(); + sb.append("Security filter chain: "); + if (filters == null) { + sb.append("no match"); + } else if (filters.isEmpty()) { + sb.append("[] empty (bypassed by security='none') "); + } else { + sb.append("[\n"); + for (Filter f : filters) { + sb.append(" ").append(f.getClass().getSimpleName()).append("\n"); + } + sb.append("]"); + } + + return sb.toString(); + } + + private List getFilters(HttpServletRequest request) { + for (SecurityFilterChain chain : fcp.getFilterChains()) { + if (chain.matches(request)) { + return chain.getFilters(); + } + } + + return null; + } + + public void init(FilterConfig filterConfig) throws ServletException { + } + + public void destroy() { + } +} + +class DebugRequestWrapper extends HttpServletRequestWrapper { + private static final Logger logger = new Logger(); + + public DebugRequestWrapper(HttpServletRequest request) { + super(request); + } + + @Override + public HttpSession getSession() { + boolean sessionExists = super.getSession(false) != null; + HttpSession session = super.getSession(); + + if (!sessionExists) { + logger.log("New HTTP session created: " + session.getId(), true); + } + + return session; + } + + @Override + public HttpSession getSession(boolean create) { + if (!create) { + return super.getSession(create); + } + return getSession(); + } +} + +/** + * Controls output for the Spring Security debug feature. + * + * @author Luke Taylor + * @since 3.1 + */ +final class Logger { + final static Log logger = LogFactory.getLog("Spring Security Debugger"); + + void log(String message) { + log(message, false); + } + + void log(String message, boolean dumpStack) { + StringBuilder output = new StringBuilder(256); + output.append("\n\n************************************************************\n\n"); + output.append(message).append("\n"); + + if (dumpStack) { + StringWriter os = new StringWriter(); + new Exception().printStackTrace(new PrintWriter(os)); + StringBuffer buffer = os.getBuffer(); + // Remove the exception in case it scares people. + int start = buffer.indexOf("java.lang.Exception"); + buffer.replace(start, start + 19, ""); + output.append("\nCall stack: \n").append(os.toString()); + } + + output.append("\n\n************************************************************\n\n"); + + logger.info(output.toString()); + } +} diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/builders/FilterComparator.java b/config/src/main/java/org/springframework/security/config/annotation/web/builders/FilterComparator.java new file mode 100644 index 0000000000..3a31312cda --- /dev/null +++ b/config/src/main/java/org/springframework/security/config/annotation/web/builders/FilterComparator.java @@ -0,0 +1,173 @@ +/* + * Copyright 2002-2013 the original author or authors. + * + * 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 org.springframework.security.config.annotation.web.builders; + +import java.io.Serializable; +import java.util.Comparator; +import java.util.HashMap; +import java.util.Map; + +import javax.servlet.Filter; + +import org.springframework.security.web.access.ExceptionTranslationFilter; +import org.springframework.security.web.access.channel.ChannelProcessingFilter; +import org.springframework.security.web.access.intercept.FilterSecurityInterceptor; +import org.springframework.security.web.authentication.AnonymousAuthenticationFilter; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; +import org.springframework.security.web.authentication.logout.LogoutFilter; +import org.springframework.security.web.authentication.preauth.AbstractPreAuthenticatedProcessingFilter; +import org.springframework.security.web.authentication.preauth.x509.X509AuthenticationFilter; +import org.springframework.security.web.authentication.rememberme.RememberMeAuthenticationFilter; +import org.springframework.security.web.authentication.switchuser.SwitchUserFilter; +import org.springframework.security.web.authentication.ui.DefaultLoginPageViewFilter; +import org.springframework.security.web.authentication.www.BasicAuthenticationFilter; +import org.springframework.security.web.authentication.www.DigestAuthenticationFilter; +import org.springframework.security.web.context.SecurityContextPersistenceFilter; +import org.springframework.security.web.jaasapi.JaasApiIntegrationFilter; +import org.springframework.security.web.savedrequest.RequestCacheAwareFilter; +import org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter; +import org.springframework.security.web.session.ConcurrentSessionFilter; +import org.springframework.security.web.session.SessionManagementFilter; + +/** + * An internal use only {@link Comparator} that sorts the Security {@link Filter} instances to ensure they are in the + * correct order. + * + * @author Rob Winch + * @since 3.2 + */ + +@SuppressWarnings("serial") +final class FilterComparator implements Comparator, Serializable { + private static final int STEP = 100; + private Map filterToOrder = new HashMap(); + + FilterComparator() { + int order = 100; + put(ChannelProcessingFilter.class, order); + order += STEP; + put(ConcurrentSessionFilter.class, order); + order += STEP; + put(SecurityContextPersistenceFilter.class, order); + order += STEP; + put(LogoutFilter.class, order); + order += STEP; + put(X509AuthenticationFilter.class, order); + order += STEP; + put(AbstractPreAuthenticatedProcessingFilter.class, order); + order += STEP; + filterToOrder.put("org.springframework.security.cas.web.CasAuthenticationFilter", order); + order += STEP; + put(UsernamePasswordAuthenticationFilter.class, order); + order += STEP; + put(ConcurrentSessionFilter.class, order); + order += STEP; + filterToOrder.put("org.springframework.security.openid.OpenIDAuthenticationFilter", order); + order += STEP; + put(DefaultLoginPageViewFilter.class, order); + order += STEP; + put(ConcurrentSessionFilter.class, order); + order += STEP; + put(DigestAuthenticationFilter.class, order); + order += STEP; + put(BasicAuthenticationFilter.class, order); + order += STEP; + put(RequestCacheAwareFilter.class, order); + order += STEP; + put(SecurityContextHolderAwareRequestFilter.class, order); + order += STEP; + put(JaasApiIntegrationFilter.class, order); + order += STEP; + put(RememberMeAuthenticationFilter.class, order); + order += STEP; + put(AnonymousAuthenticationFilter.class, order); + order += STEP; + put(SessionManagementFilter.class, order); + order += STEP; + put(ExceptionTranslationFilter.class, order); + order += STEP; + put(FilterSecurityInterceptor.class, order); + order += STEP; + put(SwitchUserFilter.class, order); + } + + @Override + public int compare(Filter lhs, Filter rhs) { + Integer left = getOrder(lhs.getClass()); + Integer right = getOrder(rhs.getClass()); + return left - right; + } + + /** + * Determines if a particular {@link Filter} is registered to be sorted + * + * @param filter + * @return + */ + public boolean isRegistered(Class filter) { + return getOrder(filter) != null; + } + + /** + * Registers a {@link Filter} to exist after a particular {@link Filter} that is already registered. + * @param filter the {@link Filter} to register + * @param afterFilter the {@link Filter} that is already registered and that {@code filter} should be placed after. + */ + public void registerAfter(Class filter, Class afterFilter) { + Integer position = getOrder(afterFilter); + if(position == null) { + throw new IllegalArgumentException("Cannot register after unregistered Filter "+afterFilter); + } + + put(filter, position + 1); + } + + /** + * Registers a {@link Filter} to exist before a particular {@link Filter} that is already registered. + * @param filter the {@link Filter} to register + * @param beforeFilter the {@link Filter} that is already registered and that {@code filter} should be placed before. + */ + public void registerBefore(Class filter, Class beforeFilter) { + Integer position = getOrder(beforeFilter); + if(position == null) { + throw new IllegalArgumentException("Cannot register after unregistered Filter "+beforeFilter); + } + + put(filter, position - 1); + } + + private void put(Class filter, int position) { + String className = filter.getName(); + filterToOrder.put(className, position); + } + + /** + * Gets the order of a particular {@link Filter} class taking into consideration superclasses. + * + * @param clazz the {@link Filter} class to determine the sort order + * @return the sort order or null if not defined + */ + private Integer getOrder(Class clazz) { + while(clazz != null) { + Integer result = filterToOrder.get(clazz.getName()); + if(result != null) { + return result; + } + clazz = clazz.getSuperclass(); + } + return null; + } +} diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/builders/HttpSecurity.java b/config/src/main/java/org/springframework/security/config/annotation/web/builders/HttpSecurity.java new file mode 100644 index 0000000000..4313d68e3a --- /dev/null +++ b/config/src/main/java/org/springframework/security/config/annotation/web/builders/HttpSecurity.java @@ -0,0 +1,1280 @@ +/* + * Copyright 2002-2013 the original author or authors. + * + * 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 org.springframework.security.config.annotation.web.builders; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import javax.servlet.Filter; +import javax.servlet.http.HttpServletRequest; + +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.AuthenticationProvider; +import org.springframework.security.config.annotation.AbstractConfiguredSecurityBuilder; +import org.springframework.security.config.annotation.ObjectPostProcessor; +import org.springframework.security.config.annotation.SecurityBuilder; +import org.springframework.security.config.annotation.SecurityConfigurer; +import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; +import org.springframework.security.config.annotation.web.AbstractRequestMatcherConfigurer; +import org.springframework.security.config.annotation.web.HttpSecurityBuilder; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfiguration; +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; +import org.springframework.security.config.annotation.web.configurers.AnonymousConfigurer; +import org.springframework.security.config.annotation.web.configurers.ChannelSecurityConfigurer; +import org.springframework.security.config.annotation.web.configurers.ExceptionHandlingConfigurer; +import org.springframework.security.config.annotation.web.configurers.ExpressionUrlAuthorizationConfigurer; +import org.springframework.security.config.annotation.web.configurers.FormLoginConfigurer; +import org.springframework.security.config.annotation.web.configurers.HttpBasicConfigurer; +import org.springframework.security.config.annotation.web.configurers.JeeConfigurer; +import org.springframework.security.config.annotation.web.configurers.LogoutConfigurer; +import org.springframework.security.config.annotation.web.configurers.PortMapperConfigurer; +import org.springframework.security.config.annotation.web.configurers.RememberMeConfigurer; +import org.springframework.security.config.annotation.web.configurers.RequestCacheConfigurer; +import org.springframework.security.config.annotation.web.configurers.SecurityContextConfigurer; +import org.springframework.security.config.annotation.web.configurers.ServletApiConfigurer; +import org.springframework.security.config.annotation.web.configurers.SessionManagementConfigurer; +import org.springframework.security.config.annotation.web.configurers.X509Configurer; +import org.springframework.security.config.annotation.web.configurers.openid.OpenIDLoginConfigurer; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.web.DefaultSecurityFilterChain; +import org.springframework.security.web.PortMapper; +import org.springframework.security.web.PortMapperImpl; +import org.springframework.security.web.context.AbstractSecurityWebApplicationInitializer; +import org.springframework.security.web.session.HttpSessionEventPublisher; +import org.springframework.security.web.util.AntPathRequestMatcher; +import org.springframework.security.web.util.AnyRequestMatcher; +import org.springframework.security.web.util.RegexRequestMatcher; +import org.springframework.security.web.util.RequestMatcher; +import org.springframework.util.Assert; + +/** + * A {@link HttpSecurity} is similar to Spring Security's XML element in the namespace + * configuration. It allows configuring web based security for specific http requests. By default + * it will be applied to all requests, but can be restricted using {@link #requestMatcher(RequestMatcher)} + * or other similar methods. + * + *

Example Usage

+ * + * The most basic form based configuration can be seen below. The configuration will require that any URL + * that is requested will require a User with the role "ROLE_USER". It also defines an in memory authentication + * scheme with a user that has the username "user", the password "password", and the role "ROLE_USER". For + * additional examples, refer to the Java Doc of individual methods on {@link HttpSecurity}. + * + *
+ * @Configuration
+ * @EnableWebSecurity
+ * public class FormLoginSecurityConfig extends WebSecurityConfigurerAdapter {
+ *
+ *     @Override
+ *     protected void configure(HttpSecurity http) throws Exception {
+ *         http
+ *             .authorizeUrls()
+ *                 .antMatchers("/**").hasRole("USER")
+ *                 .and()
+ *             .formLogin();
+ *     }
+ *
+ *     @Override
+ *     protected void registerAuthentication(AuthenticationManagerBuilder auth) throws Exception {
+ *         auth
+ *              .inMemoryAuthentication()
+ *                   .withUser("user")
+ *                        .password("password")
+ *                        .roles("USER");
+ *     }
+ * }
+ * 
+ * + * @author Rob Winch + * @since 3.2 + * @see EnableWebSecurity + */ +public final class HttpSecurity extends AbstractConfiguredSecurityBuilder implements SecurityBuilder, HttpSecurityBuilder { + private AuthenticationManager authenticationManager; + + private List filters = new ArrayList(); + private RequestMatcher requestMatcher = new AnyRequestMatcher(); + private FilterComparator comparitor = new FilterComparator(); + + /** + * Creates a new instance + * @param objectPostProcessor the {@link ObjectPostProcessor} that should be used + * @param authenticationBuilder the {@link AuthenticationManagerBuilder} to use for additional updates + * @param sharedObjects the shared Objects to initialize the {@link HttpSecurity} with + * @see WebSecurityConfiguration + */ + public HttpSecurity(ObjectPostProcessor objectPostProcessor, AuthenticationManagerBuilder authenticationBuilder, Map,Object> sharedObjects) { + super(objectPostProcessor); + Assert.notNull(authenticationBuilder, "authenticationBuilder cannot be null"); + setSharedObject(AuthenticationManagerBuilder.class, authenticationBuilder); + for(Map.Entry, Object> entry : sharedObjects.entrySet()) { + setSharedObject(entry.getKey(), entry.getValue()); + } + } + + /** + * Allows configuring OpenID based authentication. Multiple invocations of + * {@link #openidLogin()} will override previous invocations. + * + *

Example Configurations

+ * + * A basic example accepting the defaults and not using attribute exchange: + * + *
+     * @Configuration
+     * @EnableWebSecurity
+     * public class OpenIDLoginConfig extends WebSecurityConfigurerAdapter {
+     *
+     *     @Override
+     *     protected void configure(HttpSecurity http) {
+     *         http
+     *             .authorizeUrls()
+     *                 .antMatchers("/**").hasRole("USER")
+     *                 .and()
+     *             .openidLogin()
+     *                 .permitAll();
+     *     }
+     *
+     *     @Override
+     *     protected void registerAuthentication(AuthenticationManagerBuilder auth) throws Exception {
+     *         auth
+     *                 .inMemoryAuthentication()
+     *                     // the username must match the OpenID of the user you are
+     *                     // logging in with
+     *                     .withUser("https://www.google.com/accounts/o8/id?id=lmkCn9xzPdsxVwG7pjYMuDgNNdASFmobNkcRPaWU")
+     *                         .password("password")
+     *                         .roles("USER");
+     *     }
+     * }
+     * 
+ * + * A more advanced example demonstrating using attribute exchange and + * providing a custom AuthenticationUserDetailsService that will make any + * user that authenticates a valid user. + * + *
+     * @Configuration
+     * @EnableWebSecurity
+     * public class OpenIDLoginConfig extends WebSecurityConfigurerAdapter {
+     *
+     *     @Override
+     *     protected void configure(HttpSecurity http) {
+     *         http
+     *             .authorizeUrls()
+     *                 .antMatchers("/**").hasRole("USER")
+     *                 .and()
+     *             .openidLogin()
+     *                 .loginPage("/login")
+     *                 .permitAll()
+     *                 .authenticationUserDetailsService(new AutoProvisioningUserDetailsService())
+     *                     .attributeExchange("https://www.google.com/.*")
+     *                         .attribute("email")
+     *                             .type("http://axschema.org/contact/email")
+     *                             .required(true)
+     *                             .and()
+     *                         .attribute("firstname")
+     *                             .type("http://axschema.org/namePerson/first")
+     *                             .required(true)
+     *                             .and()
+     *                         .attribute("lastname")
+     *                             .type("http://axschema.org/namePerson/last")
+     *                             .required(true)
+     *                             .and()
+     *                         .and()
+     *                     .attributeExchange(".*yahoo.com.*")
+     *                         .attribute("email")
+     *                             .type("http://schema.openid.net/contact/email")
+     *                             .required(true)
+     *                             .and()
+     *                         .attribute("fullname")
+     *                             .type("http://axschema.org/namePerson")
+     *                             .required(true)
+     *                             .and()
+     *                         .and()
+     *                     .attributeExchange(".*myopenid.com.*")
+     *                         .attribute("email")
+     *                             .type("http://schema.openid.net/contact/email")
+     *                             .required(true)
+     *                             .and()
+     *                         .attribute("fullname")
+     *                             .type("http://schema.openid.net/namePerson")
+     *                             .required(true);
+     *     }
+     * }
+     *
+     * public class AutoProvisioningUserDetailsService implements
+     *         AuthenticationUserDetailsService<OpenIDAuthenticationToken> {
+     *     public UserDetails loadUserDetails(OpenIDAuthenticationToken token) throws UsernameNotFoundException {
+     *         return new User(token.getName(), "NOTUSED", AuthorityUtils.createAuthorityList("ROLE_USER"));
+     *     }
+     * }
+     * 
+ * + * @return the {@link OpenIDLoginConfigurer} for further customizations. + * + * @throws Exception + * @see OpenIDLoginConfigurer + */ + public OpenIDLoginConfigurer openidLogin() throws Exception { + return apply(new OpenIDLoginConfigurer()); + } + + /** + * Allows configuring of Session Management. Multiple invocations of + * {@link #sessionManagement()} will override previous invocations. + * + *

Example Configuration

+ * + * The following configuration demonstrates how to enforce that only a + * single instance of a user is authenticated at a time. If a user + * authenticates with the username "user" without logging out and an attempt + * to authenticate with "user" is made the first session will be forcibly + * terminated and sent to the "/login?expired" URL. + * + *
+     * @Configuration
+     * @EnableWebSecurity
+     * public class SessionManagementSecurityConfig extends
+     *         WebSecurityConfigurerAdapter {
+     *
+     *     @Override
+     *     protected void configure(HttpSecurity http) throws Exception {
+     *         http
+     *             .authorizeUrls()
+     *                 .anyRequest().hasRole("USER")
+     *                 .and()
+     *            .formLogin()
+     *                 .permitAll()
+     *                 .and()
+     *            .sessionManagement()
+     *                 .maximumSessions(1)
+     *                 .expiredUrl("/login?expired");
+     *     }
+     *
+     *     @Override
+     *     protected void registerAuthentication(AuthenticationManagerBuilder auth)
+     *             throws Exception {
+     *         auth.
+     *             inMemoryAuthentication()
+     *                 .withUser("user")
+     *                     .password("password")
+     *                     .roles("USER");
+     *     }
+     * }
+     * 
+ * + * When using {@link SessionManagementConfigurer#maximumSessions(int)}, do + * not forget to configure {@link HttpSessionEventPublisher} for the + * application to ensure that expired sessions are cleaned up. + * + * In a web.xml this can be configured using the following: + * + *
+     * <listener>
+     *      <listener-class>org.springframework.security.web.session.HttpSessionEventPublisher</listener-class>
+     * </listener>
+     * 
+ * + * Alternatively, + * {@link AbstractSecurityWebApplicationInitializer#enableHttpSessionEventPublisher()} + * could return true. + * + * @return the {@link SessionManagementConfigurer} for further + * customizations + * @throws Exception + */ + public SessionManagementConfigurer sessionManagement() throws Exception { + return apply(new SessionManagementConfigurer()); + } + + /** + * Allows configuring a {@link PortMapper} that is available from + * {@link HttpSecurity#getSharedObject(Class)}. Other provided + * {@link SecurityConfigurer} objects use this configured + * {@link PortMapper} as a default {@link PortMapper} when redirecting from + * HTTP to HTTPS or from HTTPS to HTTP (for example when used in combination + * with {@link #requiresChannel()}. By default Spring Security uses a + * {@link PortMapperImpl} which maps the HTTP port 8080 to the HTTPS port + * 8443 and the HTTP port of 80 to the HTTPS port of 443. + * + *

Example Configuration

+ * + * The following configuration will ensure that redirects within Spring + * Security from HTTP of a port of 9090 will redirect to HTTPS port of 9443 + * and the HTTP port of 80 to the HTTPS port of 443. + * + *
+     * @Configuration
+     * @EnableWebSecurity
+     * public class PortMapperSecurityConfig extends WebSecurityConfigurerAdapter {
+     *
+     *     @Override
+     *     protected void configure(HttpSecurity http) throws Exception {
+     *         http
+     *             .authorizeUrls()
+     *                 .antMatchers("/**").hasRole("USER")
+     *                 .and()
+     *             .formLogin()
+     *                 .permitAll()
+     *                 .and()
+     *                 // Example portMapper() configuration
+     *                 .portMapper()
+     *                     .http(9090).mapsTo(9443)
+     *                     .http(80).mapsTo(443);
+     *     }
+     *
+     *     @Override
+     *     protected void registerAuthentication(AuthenticationManagerBuilder auth) throws Exception {
+     *         auth
+     *             .inMemoryAuthentication()
+     *                 .withUser("user")
+     *                     .password("password")
+     *                     .roles("USER");
+     *     }
+     * }
+     * 
+ * + * @return the {@link PortMapperConfigurer} for further customizations + * @throws Exception + * @see {@link #requiresChannel()} + */ + public PortMapperConfigurer portMapper() throws Exception { + return apply(new PortMapperConfigurer()); + } + + /** + * Configures container based based pre authentication. In this case, + * authentication is managed by the Servlet Container. + * + *

Example Configuration

+ * + * The following configuration will use the principal found on the + * {@link HttpServletRequest} and if the user is in the role "ROLE_USER" or + * "ROLE_ADMIN" will add that to the resulting {@link Authentication}. + * + *
+     * @Configuration
+     * @EnableWebSecurity
+     * public class JeeSecurityConfig extends WebSecurityConfigurerAdapter {
+     *
+     *     @Override
+     *     protected void configure(HttpSecurity http) throws Exception {
+     *         http
+     *             .authorizeUrls()
+     *                 .antMatchers("/**").hasRole("USER")
+     *                 .and()
+     *             // Example jee() configuration
+     *             .jee()
+     *                 .mappableRoles("ROLE_USER", "ROLE_ADMIN");
+     *     }
+     * }
+     * 
+ * + * Developers wishing to use pre authentication with the container will need + * to ensure their web.xml configures the security constraints. For example, + * the web.xml (there is no equivalent Java based configuration supported by + * the Servlet specification) might look like: + * + *
+     * <login-config>
+     *     <auth-method>FORM</auth-method>
+     *     <form-login-config>
+     *         <form-login-page>/login</form-login-page>
+     *         <form-error-page>/login?error</form-error-page>
+     *     </form-login-config>
+     * </login-config>
+     *
+     * <security-role>
+     *     <role-name>ROLE_USER</role-name>
+     * </security-role>
+     * <security-constraint>
+     *     <web-resource-collection>
+     *     <web-resource-name>Public</web-resource-name>
+     *         <description>Matches unconstrained pages</description>
+     *         <url-pattern>/login</url-pattern>
+     *         <url-pattern>/logout</url-pattern>
+     *         <url-pattern>/resources/*</url-pattern>
+     *     </web-resource-collection>
+     * </security-constraint>
+     * <security-constraint>
+     *     <web-resource-collection>
+     *         <web-resource-name>Secured Areas</web-resource-name>
+     *         <url-pattern>/*</url-pattern>
+     *     </web-resource-collection>
+     *     <auth-constraint>
+     *         <role-name>ROLE_USER</role-name>
+     *     </auth-constraint>
+     * </security-constraint>
+     * 
+ * + * Last you will need to configure your container to contain the user with the + * correct roles. This configuration is specific to the Servlet Container, so consult + * your Servlet Container's documentation. + * + * @return the {@link JeeConfigurer} for further customizations + * @throws Exception + */ + public JeeConfigurer jee() throws Exception { + return apply(new JeeConfigurer()); + } + + /** + * Configures X509 based pre authentication. + * + *

Example Configuration

+ * + * The following configuration will attempt to extract the username from + * the X509 certificate. Remember that the Servlet Container will need to be + * configured to request client certificates in order for this to work. + * + *
+     * @Configuration
+     * @EnableWebSecurity
+     * public class X509SecurityConfig extends WebSecurityConfigurerAdapter {
+     *
+     *     @Override
+     *     protected void configure(HttpSecurity http) throws Exception {
+     *         http
+     *             .authorizeUrls()
+     *                 .antMatchers("/**").hasRole("USER")
+     *                 .and()
+     *             // Example x509() configuration
+     *             .x509();
+     *     }
+     * }
+     * 
+ * + * @return the {@link X509Configurer} for further customizations + * @throws Exception + */ + public X509Configurer x509() throws Exception { + return apply(new X509Configurer()); + } + + /** + * Allows configuring of Remember Me authentication. Multiple invocations of + * {@link #rememberMe()} will override previous invocations. + * + *

Example Configuration

+ * + * The following configuration demonstrates how to allow token based remember me + * authentication. Upon authenticating if the HTTP parameter named "remember-me" exists, + * then the user will be remembered even after their {@link javax.servlet.http.HttpSession} expires. + * + *
+     * @Configuration
+     * @EnableWebSecurity
+     * public class RememberMeSecurityConfig extends WebSecurityConfigurerAdapter {
+     *
+     *     @Override
+     *     protected void registerAuthentication(AuthenticationManagerBuilder auth)
+     *             throws Exception {
+     *         auth
+     *              .inMemoryAuthentication()
+     *                   .withUser("user")
+     *                        .password("password")
+     *                        .roles("USER");
+     *     }
+     *
+     *     @Override
+     *     protected void configure(HttpSecurity http) throws Exception {
+     *         http
+     *             .authorizeUrls()
+     *                 .antMatchers("/**").hasRole("USER")
+     *                 .and()
+     *             .formLogin()
+     *                 .permitAll()
+     *                 .and()
+     *              // Example Remember Me Configuration
+     *             .rememberMe();
+     *     }
+     * }
+     * 
+ * + * @return the {@link RememberMeConfigurer} for further customizations + * @throws Exception + */ + public RememberMeConfigurer rememberMe() throws Exception { + return apply(new RememberMeConfigurer()); + } + + + /** + * Allows restricting access based upon the {@link HttpServletRequest} using + * {@link RequestMatcher} implementations (i.e. via URL patterns). Invoking + * {@link #authorizeUrls()} twice will override previous invocations of + * {@link #authorizeUrls()}. + * + *

Example Configurations

+ * + * The most basic example is to configure all URLs to require the role "ROLE_USER". The + * configuration below requires authentication to every URL and will grant access to + * both the user "admin" and "user". + * + *
+     * @Configuration
+     * @EnableWebSecurity
+     * public class AuthorizeUrlsSecurityConfig extends WebSecurityConfigurerAdapter {
+     *
+     *     @Override
+     *     protected void configure(HttpSecurity http) throws Exception {
+     *         http
+     *             .authorizeUrls()
+     *                 .antMatchers("/**").hasRole("USER")
+     *                 .and()
+     *             .formLogin();
+     *     }
+     *
+     *     @Override
+     *     protected void registerAuthentication(AuthenticationManagerBuilder auth)
+     *             throws Exception {
+     *         auth
+     *              .inMemoryAuthentication()
+     *                   .withUser("user")
+     *                        .password("password")
+     *                        .roles("USER")
+     *                        .and()
+     *                   .withUser("adminr")
+     *                        .password("password")
+     *                        .roles("ADMIN","USER");
+     *     }
+     * }
+     * 
+ * + * We can also configure multiple URLs. The configuration below requires authentication to every URL + * and will grant access to URLs starting with /admin/ to only the "admin" user. All other URLs either + * user can access. + * + *
+     * @Configuration
+     * @EnableWebSecurity
+     * public class AuthorizeUrlsSecurityConfig extends WebSecurityConfigurerAdapter {
+     *
+     *     @Override
+     *     protected void configure(HttpSecurity http) throws Exception {
+     *         http
+     *             .authorizeUrls()
+     *                 .antMatchers("/admin/**").hasRole("ADMIN")
+     *                 .antMatchers("/**").hasRole("USER")
+     *                 .and()
+     *             .formLogin();
+     *     }
+     *
+     *     @Override
+     *     protected void registerAuthentication(AuthenticationManagerBuilder auth)
+     *             throws Exception {
+     *         auth
+     *              .inMemoryAuthentication()
+     *                   .withUser("user")
+     *                        .password("password")
+     *                        .roles("USER")
+     *                        .and()
+     *                   .withUser("adminr")
+     *                        .password("password")
+     *                        .roles("ADMIN","USER");
+     *     }
+     * }
+     * 
+ * + * Note that the matchers are considered in order. Therefore, the following is invalid because the first + * matcher matches every request and will never get to the second mapping: + * + *
+     * http
+     *     .authorizeUrls()
+     *         .antMatchers("/**").hasRole("USER")
+     *         .antMatchers("/admin/**").hasRole("ADMIN")
+     * 
+ * + * @see #requestMatcher(RequestMatcher) + * + * @return + * @throws Exception + */ + public ExpressionUrlAuthorizationConfigurer authorizeUrls() throws Exception { + return apply(new ExpressionUrlAuthorizationConfigurer()); + } + + /** + * Allows configuring the Request Cache. For example, a protected page (/protected) may be requested prior + * to authentication. The application will redirect the user to a login page. After authentication, Spring + * Security will redirect the user to the originally requested protected page (/protected). This is + * automatically applied when using {@link WebSecurityConfigurerAdapter}. + * + * @return the {@link RequestCacheConfigurer} for further customizations + * @throws Exception + */ + public RequestCacheConfigurer requestCache() throws Exception { + return apply(new RequestCacheConfigurer()); + } + + /** + * Allows configuring exception handling. This is automatically applied when using + * {@link WebSecurityConfigurerAdapter}. + * + * @return the {@link ExceptionHandlingConfigurer} for further customizations + * @throws Exception + */ + public ExceptionHandlingConfigurer exceptionHandling() throws Exception { + return apply(new ExceptionHandlingConfigurer()); + } + + /** + * Sets up management of the {@link SecurityContext} on the + * {@link SecurityContextHolder} between {@link HttpServletRequest}'s. This is automatically + * applied when using {@link WebSecurityConfigurerAdapter}. + * + * @return the {@link SecurityContextConfigurer} for further customizations + * @throws Exception + */ + public SecurityContextConfigurer securityContext() throws Exception { + return apply(new SecurityContextConfigurer()); + } + + /** + * Integrates the {@link HttpServletRequest} methods with the values found + * on the {@link SecurityContext}. This is automatically applied when using + * {@link WebSecurityConfigurerAdapter}. + * + * @return the {@link ServletApiConfigurer} for further customizations + * @throws Exception + */ + public ServletApiConfigurer servletApi() throws Exception { + return apply(new ServletApiConfigurer()); + } + + /** + * Provides logout support. This is automatically applied when using + * {@link WebSecurityConfigurerAdapter}. The default is that accessing + * the URL "/logout" will log the user out by invalidating the HTTP Session, + * cleaning up any {@link #rememberMe()} authentication that was configured, + * clearing the {@link SecurityContextHolder}, and then redirect to + * "/login?success". + * + *

Example Custom Configuration

+ * + * The following customization to log out when the URL "/custom-logout" is + * invoked. Log out will remove the cookie named "remove", not invalidate the + * HttpSession, clear the SecurityContextHolder, and upon completion redirect + * to "/logout-success". + * + *
+     * @Configuration
+     * @EnableWebSecurity
+     * public class LogoutSecurityConfig extends WebSecurityConfigurerAdapter {
+     *
+     *     @Override
+     *     protected void configure(HttpSecurity http) throws Exception {
+     *         http
+     *             .authorizeUrls()
+     *                 .antMatchers("/**").hasRole("USER")
+     *                 .and()
+     *             .formLogin()
+     *                 .and()
+     *             // sample logout customization
+     *             .logout()
+     *                 .logout()
+     *                    .deleteCookies("remove")
+     *                    .invalidateHttpSession(false)
+     *                    .logoutUrl("/custom-logout")
+     *                    .logoutSuccessUrl("/logout-success");
+     *     }
+     *
+     *     @Override
+     *     protected void registerAuthentication(AuthenticationManagerBuilder auth)
+     *             throws Exception {
+     *         auth
+     *              .inMemoryAuthentication()
+     *                   .withUser("user")
+     *                        .password("password")
+     *                        .roles("USER");
+     *     }
+     * }
+     * 
+ * + * @return + * @throws Exception + */ + public LogoutConfigurer logout() throws Exception { + return apply(new LogoutConfigurer()); + } + + /** + * Allows configuring how an anonymous user is represented. This is automatically applied + * when used in conjunction with {@link WebSecurityConfigurerAdapter}. By default anonymous + * users will be represented with an {@link org.springframework.security.authentication.AnonymousAuthenticationToken} and contain the role + * "ROLE_ANONYMOUS". + * + *

Example Configuration

+ * @Configuration + * @EnableWebSecurity + * public class AnononymousSecurityConfig extends WebSecurityConfigurerAdapter { + * + * @Override + * protected void configure(HttpSecurity http) throws Exception { + * http + * .authorizeUrls() + * .antMatchers("/**").hasRole("USER") + * .and() + * .formLogin() + * .and() + * // sample anonymous customization + * .anonymous() + * .authorities("ROLE_ANON"); + * } + * + * @Override + * protected void registerAuthentication(AuthenticationManagerBuilder auth) + * throws Exception { + * auth + * .inMemoryAuthentication() + * .withUser("user") + * .password("password") + * .roles("USER"); + * } + * } + * + * + * The following demonstrates how to represent anonymous users as null. Note that this can cause + * {@link NullPointerException} in code that assumes anonymous authentication is enabled. + * + *
+     * @Configuration
+     * @EnableWebSecurity
+     * public class AnononymousSecurityConfig extends WebSecurityConfigurerAdapter {
+     *
+     *     @Override
+     *     protected void configure(HttpSecurity http) throws Exception {
+     *         http
+     *             .authorizeUrls()
+     *                 .antMatchers("/**").hasRole("USER")
+     *                 .and()
+     *             .formLogin()
+     *                 .and()
+     *             // sample anonymous customization
+     *             .anonymous()
+     *                 .disabled();
+     *     }
+     *
+     *     @Override
+     *     protected void registerAuthentication(AuthenticationManagerBuilder auth)
+     *             throws Exception {
+     *         auth
+     *              .inMemoryAuthentication()
+     *                   .withUser("user")
+     *                        .password("password")
+     *                        .roles("USER");
+     *     }
+     * }
+     * 
+ * + * @return + * @throws Exception + */ + public AnonymousConfigurer anonymous() throws Exception { + return apply(new AnonymousConfigurer()); + } + + /** + * Specifies to support form based authentication. If + * {@link FormLoginConfigurer#loginPage(String)} is not specified a + * default login page will be generated. + * + *

Example Configurations

+ * + * The most basic configuration defaults to automatically generating a login + * page at the URL "/login", redirecting to "/login?error" for + * authentication failure. The details of the login page can be found on + * {@link FormLoginConfigurer#loginPage(String)} + * + *
+     * @Configuration
+     * @EnableWebSecurity
+     * public class FormLoginSecurityConfig extends WebSecurityConfigurerAdapter {
+     *
+     *     @Override
+     *     protected void configure(HttpSecurity http) throws Exception {
+     *         http
+     *             .authorizeUrls()
+     *                 .antMatchers("/**").hasRole("USER")
+     *                 .and()
+     *             .formLogin();
+     *     }
+     *
+     *     @Override
+     *     protected void registerAuthentication(AuthenticationManagerBuilder auth)
+     *             throws Exception {
+     *         auth
+     *              .inMemoryAuthentication()
+     *                   .withUser("user")
+     *                        .password("password")
+     *                        .roles("USER");
+     *     }
+     * }
+     * 
+ * + * The configuration below demonstrates customizing the defaults. + * + *
+     * @Configuration
+     * @EnableWebSecurity
+     * public class FormLoginSecurityConfig extends WebSecurityConfigurerAdapter {
+     *
+     *     @Override
+     *     protected void configure(HttpSecurity http) throws Exception {
+     *         http
+     *             .authorizeUrls()
+     *                 .antMatchers("/**").hasRole("USER")
+     *                 .and()
+     *             .formLogin()
+     *                    .usernameParameter("j_username") // default is username
+     *                    .passwordParameter("j_password") // default is password
+     *                    .loginPage("/authentication/login") // default is /login with an HTTP get
+     *                    .failureUrl("/authentication/login?failed") // default is /login?error
+     *                    .loginProcessingUrl("/authentication/login/process"); // default is /login with an HTTP post
+     *     }
+     *
+     *     @Override
+     *     protected void registerAuthentication(AuthenticationManagerBuilder auth)
+     *             throws Exception {
+     *         auth
+     *              .inMemoryAuthentication()
+     *                   .withUser("user")
+     *                        .password("password")
+     *                        .roles("USER");
+     *     }
+     * }
+     * 
+ * + * @see FormLoginConfigurer#loginPage(String) + * + * @return + * @throws Exception + */ + public FormLoginConfigurer formLogin() throws Exception { + return apply(new FormLoginConfigurer()); + } + + /** + * Configures channel security. In order for this configuration to be useful at least + * one mapping to a required channel must be provided. Invoking this method multiple times + * will reset previous invocations of the method. + * + *

Example Configuration

+ * + * The example below demonstrates how to require HTTPs for every request. Only requiring HTTPS + * for some requests is supported, but not recommended since an application that allows for HTTP + * introduces many security vulnerabilities. For one such example, read about + * Firesheep. + * + *
+     * @Configuration
+     * @EnableWebSecurity
+     * public class ChannelSecurityConfig extends WebSecurityConfigurerAdapter {
+     *
+     *     @Override
+     *     protected void configure(HttpSecurity http) throws Exception {
+     *         http
+     *             .authorizeUrls()
+     *                 .antMatchers("/**").hasRole("USER")
+     *                 .and()
+     *             .formLogin()
+     *                 .and()
+     *             .channelSecurity()
+     *                 .anyRequest().requiresSecure();
+     *     }
+     *
+     *     @Override
+     *     protected void registerAuthentication(AuthenticationManagerBuilder auth)
+     *             throws Exception {
+     *         auth
+     *              .inMemoryAuthentication()
+     *                   .withUser("user")
+     *                        .password("password")
+     *                        .roles("USER");
+     *     }
+     * }
+     * 
+ * + * + * @return the {@link ChannelSecurityConfigurer} for further customizations + * @throws Exception + */ + public ChannelSecurityConfigurer requiresChannel() throws Exception { + return apply(new ChannelSecurityConfigurer()); + } + + /** + * Configures HTTP Basic authentication. Multiple infocations of + * {@link #httpBasic()} will override previous invocations. + * + *

Example Configuration

+ * + * The example below demonstrates how to configure HTTP Basic authentication + * for an application. The default realm is "Spring Security Application", + * but can be customized using + * {@link HttpBasicConfigurer#realmName(String)}. + * + *
+     * @Configuration
+     * @EnableWebSecurity
+     * public class HttpBasicSecurityConfig extends WebSecurityConfigurerAdapter {
+     *
+     *     @Override
+     *     protected void configure(HttpSecurity http) throws Exception {
+     *         http
+     *             .authorizeUrls()
+     *                 .antMatchers("/**").hasRole("USER").and()
+     *                 .httpBasic();
+     *     }
+     *
+     *     @Override
+     *     protected void registerAuthentication(AuthenticationManagerBuilder auth)
+     *             throws Exception {
+     *         auth
+     *             .inMemoryAuthentication()
+     *                 .withUser("user")
+     *                     .password("password")
+     *                     .roles("USER");
+     *     }
+     * }
+     * 
+ * + * @return the {@link HttpBasicConfigurer} for further customizations + * @throws Exception + */ + public HttpBasicConfigurer httpBasic() throws Exception { + return apply(new HttpBasicConfigurer()); + } + + @Override + protected void beforeConfigure() throws Exception { + this.authenticationManager = getAuthenticationRegistry().build(); + } + + @Override + protected DefaultSecurityFilterChain performBuild() throws Exception { + Collections.sort(filters,comparitor); + return new DefaultSecurityFilterChain(requestMatcher, filters); + } + + /* (non-Javadoc) + * @see org.springframework.security.config.annotation.web.HttpBuilder#authenticationProvider(org.springframework.security.authentication.AuthenticationProvider) + */ + @Override + public HttpSecurity authenticationProvider(AuthenticationProvider authenticationProvider) { + getAuthenticationRegistry().authenticationProvider(authenticationProvider); + return this; + } + + /* (non-Javadoc) + * @see org.springframework.security.config.annotation.web.HttpBuilder#userDetailsService(org.springframework.security.core.userdetails.UserDetailsService) + */ + @Override + public HttpSecurity userDetailsService(UserDetailsService userDetailsService) throws Exception { + getAuthenticationRegistry().userDetailsService(userDetailsService); + return this; + } + + private AuthenticationManagerBuilder getAuthenticationRegistry() { + return getSharedObject(AuthenticationManagerBuilder.class); + } + + /* (non-Javadoc) + * @see org.springframework.security.config.annotation.web.HttpBuilder#addFilterAfter(javax.servlet.Filter, java.lang.Class) + */ + @Override + public HttpSecurity addFilterAfter(Filter filter, Class afterFilter) { + comparitor.registerAfter(filter.getClass(), afterFilter); + return addFilter(filter); + } + + /* (non-Javadoc) + * @see org.springframework.security.config.annotation.web.HttpBuilder#addFilterBefore(javax.servlet.Filter, java.lang.Class) + */ + @Override + public HttpSecurity addFilterBefore(Filter filter, Class beforeFilter) { + comparitor.registerBefore(filter.getClass(), beforeFilter); + return addFilter(filter); + } + + /* (non-Javadoc) + * @see org.springframework.security.config.annotation.web.HttpBuilder#addFilter(javax.servlet.Filter) + */ + @Override + public HttpSecurity addFilter(Filter filter) { + Class filterClass = filter.getClass(); + if(!comparitor.isRegistered(filterClass)) { + throw new IllegalArgumentException( + "The Filter class " + filterClass.getName() + + " does not have a registered order and cannot be added without a specified order. Consider using addFilterBefore or addFilterAfter instead."); + } + this.filters.add(filter); + return this; + } + + /** + * Allows specifying which {@link HttpServletRequest} instances this + * {@link HttpSecurity} will be invoked on. This method allows for + * easily invoking the {@link HttpSecurity} for multiple + * different {@link RequestMatcher} instances. If only a single {@link RequestMatcher} + * is necessary consider using {@link #antMatcher(String)}, + * {@link #regexMatcher(String)}, or {@link #requestMatcher(RequestMatcher)}. + * + *

+ * Invoking {@link #requestMatchers()} will override previous invocations of + * {@link #requestMatchers()}, {@link #antMatcher(String)}, {@link #regexMatcher(String)}, + * and {@link #requestMatcher(RequestMatcher)}. + *

+ * + *

Example Configurations

+ * + * The following configuration enables the {@link HttpSecurity} for URLs that + * begin with "/api/" or "/oauth/". + * + *
+     * @Configuration
+     * @EnableWebSecurity
+     * public class RequestMatchersSecurityConfig extends WebSecurityConfigurerAdapter {
+     *
+     *     @Override
+     *     protected void configure(HttpSecurity http) throws Exception {
+     *         http
+     *             .requestMatchers()
+     *                 .antMatchers("/api/**","/oauth/**")
+     *                 .and()
+     *             .authorizeUrls()
+     *                 .antMatchers("/**").hasRole("USER").and()
+     *                 .httpBasic();
+     *     }
+     *
+     *     @Override
+     *     protected void registerAuthentication(AuthenticationManagerBuilder auth)
+     *             throws Exception {
+     *         auth
+     *             .inMemoryAuthentication()
+     *                 .withUser("user")
+     *                     .password("password")
+     *                     .roles("USER");
+     *     }
+     * }
+     * 
+ * + * The configuration below is the same as the previous configuration. + * + *
+     * @Configuration
+     * @EnableWebSecurity
+     * public class RequestMatchersSecurityConfig extends WebSecurityConfigurerAdapter {
+     *
+     *     @Override
+     *     protected void configure(HttpSecurity http) throws Exception {
+     *         http
+     *             .requestMatchers()
+     *                 .antMatchers("/api/**")
+     *                 .antMatchers("/oauth/**")
+     *                 .and()
+     *             .authorizeUrls()
+     *                 .antMatchers("/**").hasRole("USER").and()
+     *                 .httpBasic();
+     *     }
+     *
+     *     @Override
+     *     protected void registerAuthentication(AuthenticationManagerBuilder auth)
+     *             throws Exception {
+     *         auth
+     *             .inMemoryAuthentication()
+     *                 .withUser("user")
+     *                     .password("password")
+     *                     .roles("USER");
+     *     }
+     * }
+     * 
+ * + * The configuration differs from the previous configurations because it invokes + * {@link #requestMatchers()} twice which resets the {@link RequestMatcherConfigurer}. + * Therefore the configuration below only matches on URLs that start with "/oauth/**". + * + *
+     * @Configuration
+     * @EnableWebSecurity
+     * public class RequestMatchersSecurityConfig extends WebSecurityConfigurerAdapter {
+     *
+     *     @Override
+     *     protected void configure(HttpSecurity http) throws Exception {
+     *         http
+     *             .requestMatchers()
+     *                 .antMatchers("/api/**")
+     *                 .and()
+     *             .requestMatchers()
+     *                 .antMatchers("/oauth/**")
+     *                 .and()
+     *             .authorizeUrls()
+     *                 .antMatchers("/**").hasRole("USER").and()
+     *                 .httpBasic();
+     *     }
+     *
+     *     @Override
+     *     protected void registerAuthentication(AuthenticationManagerBuilder auth)
+     *             throws Exception {
+     *         auth
+     *             .inMemoryAuthentication()
+     *                 .withUser("user")
+     *                     .password("password")
+     *                     .roles("USER");
+     *     }
+     * }
+     * 
+ * + * @return the {@link RequestMatcherConfigurer} for further customizations + */ + public RequestMatcherConfigurer requestMatchers() { + return new RequestMatcherConfigurer(); + } + + /** + * Allows configuring the {@link HttpSecurity} to only be invoked when + * matching the provided {@link RequestMatcher}. If more advanced configuration is + * necessary, consider using {@link #requestMatchers()}. + * + *

+ * Invoking {@link #requestMatcher(RequestMatcher)} will override previous invocations of + * {@link #requestMatchers()}, {@link #antMatcher(String)}, {@link #regexMatcher(String)}, + * and {@link #requestMatcher(RequestMatcher)}. + *

+ * + * @param requestMatcher the {@link RequestMatcher} to use (i.e. new AntPathRequestMatcher("/admin/**","GET") ) + * @return the {@link HttpSecurity} for further customizations + * @see #requestMatchers() + * @see #antMatcher(String) + * @see #regexMatcher(String) + */ + public HttpSecurity requestMatcher(RequestMatcher requestMatcher) { + this.requestMatcher = requestMatcher; + return this; + } + + /** + * Allows configuring the {@link HttpSecurity} to only be invoked when + * matching the provided ant pattern. If more advanced configuration is + * necessary, consider using {@link #requestMatchers()} or + * {@link #requestMatcher(RequestMatcher)}. + * + *

+ * Invoking {@link #antMatcher(String)} will override previous invocations of + * {@link #requestMatchers()}, {@link #antMatcher(String)}, {@link #regexMatcher(String)}, + * and {@link #requestMatcher(RequestMatcher)}. + *

+ * + * @param antPattern the Ant Pattern to match on (i.e. "/admin/**") + * @return the {@link HttpSecurity} for further customizations + * @see AntPathRequestMatcher + */ + public HttpSecurity antMatcher(String antPattern) { + return requestMatcher(new AntPathRequestMatcher(antPattern)); + } + + /** + * Allows configuring the {@link HttpSecurity} to only be invoked when + * matching the provided regex pattern. If more advanced configuration is + * necessary, consider using {@link #requestMatchers()} or + * {@link #requestMatcher(RequestMatcher)}. + * + *

+ * Invoking {@link #regexMatcher(String)} will override previous invocations of + * {@link #requestMatchers()}, {@link #antMatcher(String)}, {@link #regexMatcher(String)}, + * and {@link #requestMatcher(RequestMatcher)}. + *

+ * + * @param pattern the Regular Expression to match on (i.e. "/admin/.+") + * @return the {@link HttpSecurity} for further customizations + * @see RegexRequestMatcher + */ + public HttpSecurity regexMatcher(String pattern) { + return requestMatcher(new RegexRequestMatcher(pattern, null)); + } + + /* + * (non-Javadoc) + * @see org.springframework.security.config.annotation.web.HttpBuilder#getAuthenticationManager() + */ + @Override + public AuthenticationManager getAuthenticationManager() { + return authenticationManager; + } + + /** + * Allows mapping HTTP requests that this {@link HttpSecurity} will be used for + * + * @author Rob Winch + * @since 3.2 + */ + public final class RequestMatcherConfigurer extends AbstractRequestMatcherConfigurer { + + protected RequestMatcherConfigurer chainRequestMatchers(List requestMatchers) { + requestMatcher(new OrRequestMatcher(requestMatchers)); + return this; + } + + /** + * Return the {@link HttpSecurity} for further customizations + * + * @return the {@link HttpSecurity} for further customizations + */ + public HttpSecurity and() { + return HttpSecurity.this; + } + + private RequestMatcherConfigurer(){} + } + + /** + * Internal {@link RequestMatcher} instance used by {@link RequestMatcher} + * that will match if any of the passed in {@link RequestMatcher} instances + * match. + * + * @author Rob Winch + * @since 3.2 + */ + private static final class OrRequestMatcher implements RequestMatcher { + private final List requestMatchers; + + private OrRequestMatcher(List requestMatchers) { + this.requestMatchers = requestMatchers; + } + + @Override + public boolean matches(HttpServletRequest request) { + for(RequestMatcher matcher : requestMatchers) { + if(matcher.matches(request)) { + return true; + } + } + return false; + } + } +} \ No newline at end of file diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/builders/WebSecurity.java b/config/src/main/java/org/springframework/security/config/annotation/web/builders/WebSecurity.java new file mode 100644 index 0000000000..e96fc9ce8a --- /dev/null +++ b/config/src/main/java/org/springframework/security/config/annotation/web/builders/WebSecurity.java @@ -0,0 +1,309 @@ +/* + * Copyright 2002-2013 the original author or authors. + * + * 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 org.springframework.security.config.annotation.web.builders; + +import java.util.ArrayList; +import java.util.List; + +import javax.servlet.Filter; +import javax.servlet.http.HttpServletRequest; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.security.access.expression.SecurityExpressionHandler; +import org.springframework.security.config.annotation.AbstractConfiguredSecurityBuilder; +import org.springframework.security.config.annotation.SecurityBuilder; +import org.springframework.security.config.annotation.web.AbstractRequestMatcherConfigurer; +import org.springframework.security.config.annotation.web.WebSecurityConfigurer; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfiguration; +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.web.DefaultSecurityFilterChain; +import org.springframework.security.web.FilterChainProxy; +import org.springframework.security.web.FilterInvocation; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.access.DefaultWebInvocationPrivilegeEvaluator; +import org.springframework.security.web.access.WebInvocationPrivilegeEvaluator; +import org.springframework.security.web.access.expression.DefaultWebSecurityExpressionHandler; +import org.springframework.security.web.access.intercept.FilterSecurityInterceptor; +import org.springframework.security.web.firewall.DefaultHttpFirewall; +import org.springframework.security.web.firewall.HttpFirewall; +import org.springframework.security.web.util.RequestMatcher; +import org.springframework.util.Assert; +import org.springframework.web.filter.DelegatingFilterProxy; + +/** + *

+ * The {@link WebSecurity} is created by {@link WebSecurityConfiguration} + * to create the {@link FilterChainProxy} known as the Spring Security Filter + * Chain (springSecurityFilterChain). The springSecurityFilterChain is the + * {@link Filter} that the {@link DelegatingFilterProxy} delegates to. + *

+ * + *

+ * Customizations to the {@link WebSecurity} can be made by creating a + * {@link WebSecurityConfigurer} or more likely by overriding + * {@link WebSecurityConfigurerAdapter}. + *

+ * + * @see EnableWebSecurity + * @see WebSecurityConfiguration + * + * @author Rob Winch + * @since 3.2 + */ +public final class WebSecurity extends + AbstractConfiguredSecurityBuilder implements SecurityBuilder { + private final Log logger = LogFactory.getLog(getClass()); + + private final List ignoredRequests = new ArrayList(); + + private final List> securityFilterChainBuilders = + new ArrayList>(); + + private final IgnoredRequestConfigurer ignoredRequestRegistry = + new IgnoredRequestConfigurer(); + + private FilterSecurityInterceptor filterSecurityInterceptor; + + private HttpFirewall httpFirewall; + + private boolean debugEnabled; + + private WebInvocationPrivilegeEvaluator privilegeEvaluator; + + private SecurityExpressionHandler expressionHandler = new DefaultWebSecurityExpressionHandler(); + + /** + * Creates a new instance + * @see WebSecurityConfiguration + */ + public WebSecurity() { + } + + /** + *

+ * Allows adding {@link RequestMatcher} instances that should that Spring + * Security should ignore. Web Security provided by Spring Security + * (including the {@link SecurityContext}) will not be available on + * {@link HttpServletRequest} that match. Typically the requests that are + * registered should be that of only static resources. For requests that are + * dynamic, consider mapping the request to allow all users instead. + *

+ * + * Example Usage: + * + *
+     * webSecurityBuilder
+     *     .ignoring()
+     *         // ignore all URLs that start with /resources/ or /static/
+     *         .antMatchers("/resources/**", "/static/**");
+     * 
+ * + * Alternatively this will accomplish the same result: + * + *
+     * webSecurityBuilder
+     *     .ignoring()
+     *         // ignore all URLs that start with /resources/ or /static/
+     *         .antMatchers("/resources/**")
+     *         .antMatchers("/static/**");
+     * 
+ * + * Multiple invocations of ignoring() are also additive, so the following is + * also equivalent to the previous two examples: + * + * Alternatively this will accomplish the same result: + * + *
+     * webSecurityBuilder
+     *     .ignoring()
+     *         // ignore all URLs that start with /resources/
+     *         .antMatchers("/resources/**");
+     * webSecurityBuilder
+     *     .ignoring()
+     *         // ignore all URLs that start with /static/
+     *         .antMatchers("/static/**");
+     * // now both URLs that start with /resources/ and /static/ will be ignored
+     * 
+ * + * @return the {@link IgnoredRequestConfigurer} to use for registering request + * that should be ignored + */ + public IgnoredRequestConfigurer ignoring() { + return ignoredRequestRegistry; + } + + /** + * Allows customizing the {@link HttpFirewall}. The default is + * {@link DefaultHttpFirewall}. + * + * @param httpFirewall the custom {@link HttpFirewall} + * @return the {@link WebSecurity} for further customizations + */ + public WebSecurity httpFirewall(HttpFirewall httpFirewall) { + this.httpFirewall = httpFirewall; + return this; + } + + /** + * Controls debugging support for Spring Security. + * + * @param debugEnabled + * if true, enables debug support with Spring Security. Default + * is false. + * + * @return the {@link WebSecurity} for further customization. + * @see EnableWebSecurity#debug() + */ + public WebSecurity debug(boolean debugEnabled) { + this.debugEnabled = debugEnabled; + return this; + } + + /** + *

+ * Adds builders to create {@link SecurityFilterChain} instances. + *

+ * + *

+ * Typically this method is invoked automatically within the framework from + * {@link WebSecurityConfigurerAdapter#init(WebSecurity)} + *

+ * + * @param securityFilterChainBuilder + * the builder to use to create the {@link SecurityFilterChain} + * instances + * @return the {@link WebSecurity} for further customizations + */ + public WebSecurity addSecurityFilterChainBuilder(SecurityBuilder securityFilterChainBuilder) { + this.securityFilterChainBuilders.add(securityFilterChainBuilder); + return this; + } + + /** + * Set the {@link WebInvocationPrivilegeEvaluator} to be used. If this is + * null, then a {@link DefaultWebInvocationPrivilegeEvaluator} will be + * created when {@link #setSecurityInterceptor(FilterSecurityInterceptor)} + * is non null. + * + * @param privilegeEvaluator + * the {@link WebInvocationPrivilegeEvaluator} to use + * @return the {@link WebSecurity} for further customizations + */ + public WebSecurity privilegeEvaluator(WebInvocationPrivilegeEvaluator privilegeEvaluator) { + this.privilegeEvaluator = privilegeEvaluator; + return this; + } + + /** + * Set the {@link SecurityExpressionHandler} to be used. If this is null, + * then a {@link DefaultWebSecurityExpressionHandler} will be used. + * + * @param expressionHandler + * the {@link SecurityExpressionHandler} to use + * @return the {@link WebSecurity} for further customizations + */ + public WebSecurity expressionHandler(SecurityExpressionHandler expressionHandler) { + Assert.notNull(expressionHandler, "expressionHandler cannot be null"); + this.expressionHandler = expressionHandler; + return this; + } + + /** + * Gets the {@link SecurityExpressionHandler} to be used. + * @return + */ + public SecurityExpressionHandler getExpressionHandler() { + return expressionHandler; + } + + /** + * Gets the {@link WebInvocationPrivilegeEvaluator} to be used. + * @return + */ + public WebInvocationPrivilegeEvaluator getPrivilegeEvaluator() { + if(privilegeEvaluator != null) { + return privilegeEvaluator; + } + return filterSecurityInterceptor == null ? null : new DefaultWebInvocationPrivilegeEvaluator(filterSecurityInterceptor); + } + + /** + * Sets the {@link FilterSecurityInterceptor}. This is typically invoked by {@link WebSecurityConfigurerAdapter}. + * @param securityInterceptor the {@link FilterSecurityInterceptor} to use + */ + public void setSecurityInterceptor(FilterSecurityInterceptor securityInterceptor) { + this.filterSecurityInterceptor = securityInterceptor; + } + + @Override + protected Filter performBuild() throws Exception { + Assert.state(!securityFilterChainBuilders.isEmpty(), "At least one SecurityFilterBuilder needs to be specified. Invoke FilterChainProxyBuilder.securityFilterChains"); + int chainSize = ignoredRequests.size() + securityFilterChainBuilders.size(); + List securityFilterChains = new ArrayList(chainSize); + for(RequestMatcher ignoredRequest : ignoredRequests) { + securityFilterChains.add(new DefaultSecurityFilterChain(ignoredRequest)); + } + for(SecurityBuilder securityFilterChainBuilder : securityFilterChainBuilders) { + securityFilterChains.add(securityFilterChainBuilder.build()); + } + FilterChainProxy filterChainProxy = new FilterChainProxy(securityFilterChains); + if(httpFirewall != null) { + filterChainProxy.setFirewall(httpFirewall); + } + filterChainProxy.afterPropertiesSet(); + + Filter result = filterChainProxy; + if(debugEnabled) { + logger.warn("\n\n" + + "********************************************************************\n" + + "********** Security debugging is enabled. *************\n" + + "********** This may include sensitive information. *************\n" + + "********** Do not use in a production system! *************\n" + + "********************************************************************\n\n"); + result = new DebugFilter(filterChainProxy); + } + return result; + } + + /** + * Allows registering {@link RequestMatcher} instances that should be + * ignored by Spring Security. + * + * @author Rob Winch + * @since 3.2 + */ + public final class IgnoredRequestConfigurer extends AbstractRequestMatcherConfigurer { + + @Override + protected IgnoredRequestConfigurer chainRequestMatchers(List requestMatchers) { + ignoredRequests.addAll(requestMatchers); + return this; + } + + /** + * Returns the {@link WebSecurity} to be returned for chaining. + */ + @Override + public WebSecurity and() { + return WebSecurity.this; + } + + private IgnoredRequestConfigurer(){} + } +} diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configuration/EnableWebSecurity.java b/config/src/main/java/org/springframework/security/config/annotation/web/configuration/EnableWebSecurity.java new file mode 100644 index 0000000000..a50d2e73bf --- /dev/null +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configuration/EnableWebSecurity.java @@ -0,0 +1,87 @@ +/* + * Copyright 2002-2013 the original author or authors. + * + * 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 org.springframework.security.config.annotation.web.configuration; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import org.springframework.context.annotation.Import; +import org.springframework.security.config.annotation.configuration.ObjectPostProcessorConfiguration; +import org.springframework.security.config.annotation.web.WebSecurityConfigurer; + +/** + * Add this annotation to an {@code @Configuration} class to have the Spring Security + * configuration defined in any {@link WebSecurityConfigurer} or more likely by extending the + * {@link WebSecurityConfigurerAdapter} base class and overriding individual methods: + * + *
+ * @Configuration
+ * @EnableWebSecurity
+ * public class MyWebSecurityConfiguration extends WebSecurityConfigurerAdapter {
+ *
+ *    @Override
+ *    public void configure(WebSecurity web) throws Exception {
+ *        web
+ *            .ignoring()
+ *                // Spring Security should completely ignore URLs starting with /resources/
+ *                .antMatchers("/resources/**");
+ *    }
+ *
+ *    @Override
+ *    protected void configure(HttpSecurity http) throws Exception {
+ *        http
+ *            .authorizeUrls()
+ *                .antMatchers("/public/**").permitAll()
+ *                .anyRequest().hasRole("USER")
+ *                .and()
+ *            // Possibly more configuration ...
+ *            .formLogin() // enable form based log in
+ *                // set permitAll for all URLs associated with Form Login
+ *               .permitAll();
+ *    }
+ *
+ *    @Override
+ *    protected void registerAuthentication(AuthenticationManagerBuilder auth) {
+ *        registry
+ *            // enable in memory based authentication with a user named "user" and "admin"
+ *            .inMemoryAuthentication()
+ *                .withUser("user").password("password").roles("USER").and()
+ *                .withUser("admin").password("password").roles("USER", "ADMIN");
+ *    }
+ *
+ *    // Possibly more overridden methods ...
+ * }
+ * 
+ * + * @see WebSecurityConfigurer + * @see WebSecurityConfigurerAdapter + * + * @author Rob Winch + * @since 3.2 + */ +@Retention(value=java.lang.annotation.RetentionPolicy.RUNTIME) +@Target(value={java.lang.annotation.ElementType.TYPE}) +@Documented +@Import({WebSecurityConfiguration.class,ObjectPostProcessorConfiguration.class}) +public @interface EnableWebSecurity { + + /** + * Controls debugging support for Spring Security. Default is false. + * @return if true, enables debug support with Spring Security + */ + boolean debug() default false; +} \ No newline at end of file diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configuration/WebSecurityConfiguration.java b/config/src/main/java/org/springframework/security/config/annotation/web/configuration/WebSecurityConfiguration.java new file mode 100644 index 0000000000..37ca584f41 --- /dev/null +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configuration/WebSecurityConfiguration.java @@ -0,0 +1,187 @@ +/* + * Copyright 2002-2013 the original author or authors. + * + * 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 org.springframework.security.config.annotation.web.configuration; + +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import javax.servlet.Filter; + +import org.springframework.beans.factory.BeanClassLoaderAware; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.DependsOn; +import org.springframework.context.annotation.ImportAware; +import org.springframework.core.OrderComparator; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.AnnotationAttributes; +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.core.annotation.Order; +import org.springframework.core.type.AnnotationMetadata; +import org.springframework.security.access.expression.SecurityExpressionHandler; +import org.springframework.security.config.annotation.SecurityConfigurer; +import org.springframework.security.config.annotation.web.WebSecurityConfigurer; +import org.springframework.security.config.annotation.web.builders.WebSecurity; +import org.springframework.security.web.FilterChainProxy; +import org.springframework.security.web.FilterInvocation; +import org.springframework.security.web.access.WebInvocationPrivilegeEvaluator; +import org.springframework.util.ClassUtils; + +/** + * Uses a {@link WebSecurity} to create the {@link FilterChainProxy} that + * performs the web based security for Spring Security. It then exports the + * necessary beans. Customizations can be made to {@link WebSecurity} by + * extending {@link WebSecurityConfigurerAdapter} and exposing it as a + * {@link Configuration} or implementing {@link WebSecurityConfigurer} and + * exposing it as a {@link Configuration}. This configuration is imported when + * using {@link EnableWebSecurity}. + * + * @see EnableWebSecurity + * @see WebSecurity + * + * @author Rob Winch + * @since 3.2 + */ +@Configuration +public class WebSecurityConfiguration implements ImportAware, BeanClassLoaderAware { + private final WebSecurity webSecurity = new WebSecurity(); + + private List> webSecurityConfigurers; + + private ClassLoader beanClassLoader; + + @Bean + @DependsOn("springSecurityFilterChain") + public SecurityExpressionHandler webSecurityExpressionHandler() { + return webSecurity.getExpressionHandler(); + } + + /** + * Creates the Spring Security Filter Chain + * @return + * @throws Exception + */ + @Bean(name="springSecurityFilterChain") + public Filter springSecurityFilterChain() throws Exception { + boolean hasConfigurers = webSecurityConfigurers != null && !webSecurityConfigurers.isEmpty(); + if(!hasConfigurers) { + throw new IllegalStateException("At least one non-null instance of "+ WebSecurityConfigurer.class.getSimpleName()+" must be exposed as a @Bean when using @EnableWebSecurity. Hint try extending "+ WebSecurityConfigurerAdapter.class.getSimpleName()); + } + return webSecurity.build(); + } + + /** + * Creates the {@link WebInvocationPrivilegeEvaluator} that is necessary for the JSP tag support. + * @return the {@link WebInvocationPrivilegeEvaluator} + * @throws Exception + */ + @Bean + @DependsOn("springSecurityFilterChain") + public WebInvocationPrivilegeEvaluator privilegeEvaluator() throws Exception { + return webSecurity.getPrivilegeEvaluator(); + } + + /** + * Sets the {@code } instances used to create the web configuration. + * + * @param webSecurityConfigurers the {@code } instances used to create the web configuration + * @throws Exception + */ + @Autowired(required = false) + public void setFilterChainProxySecurityConfigurer( + List> webSecurityConfigurers) throws Exception { + Collections.sort(webSecurityConfigurers, AnnotationAwareOrderComparator.INSTANCE); + + Integer previousOrder = null; + for(SecurityConfigurer config : webSecurityConfigurers) { + Integer order = AnnotationAwareOrderComparator.lookupOrder(config); + if(previousOrder != null && previousOrder.equals(order)) { + throw new IllegalStateException("@Order on WebSecurityConfigurers must be unique. Order of " + order + " was already used, so it cannot be used on " + config + " too."); + } + previousOrder = order; + } + for(SecurityConfigurer webSecurityConfigurer : webSecurityConfigurers) { + webSecurity.apply(webSecurityConfigurer); + } + this.webSecurityConfigurers = webSecurityConfigurers; + } + + + /** + * A custom verision of the Spring provided AnnotationAwareOrderComparator + * that uses {@link AnnotationUtils#findAnnotation(Class, Class)} to look on + * super class instances for the {@link Order} annotation. + * + * @author Rob Winch + * @since 3.2 + */ + private static class AnnotationAwareOrderComparator extends OrderComparator { + private static final AnnotationAwareOrderComparator INSTANCE = new AnnotationAwareOrderComparator(); + + @Override + protected int getOrder(Object obj) { + return lookupOrder(obj); + } + + private static int lookupOrder(Object obj) { + if (obj instanceof Ordered) { + return ((Ordered) obj).getOrder(); + } + if (obj != null) { + Class clazz = (obj instanceof Class ? (Class) obj : obj.getClass()); + Order order = AnnotationUtils.findAnnotation(clazz,Order.class); + if (order != null) { + return order.value(); + } + } + return Ordered.LOWEST_PRECEDENCE; + } + } + + /* (non-Javadoc) + * @see org.springframework.context.annotation.ImportAware#setImportMetadata(org.springframework.core.type.AnnotationMetadata) + */ + @Override + public void setImportMetadata(AnnotationMetadata importMetadata) { + Map enableWebSecurityAttrMap = importMetadata.getAnnotationAttributes(EnableWebSecurity.class.getName()); + AnnotationAttributes enableWebSecurityAttrs = AnnotationAttributes.fromMap(enableWebSecurityAttrMap); + if(enableWebSecurityAttrs == null) { + // search parent classes + Class currentClass = ClassUtils.resolveClassName(importMetadata.getClassName(), beanClassLoader); + for(Class classToInspect = currentClass ;classToInspect != null; classToInspect = classToInspect.getSuperclass()) { + EnableWebSecurity enableWebSecurityAnnotation = AnnotationUtils.findAnnotation(classToInspect, EnableWebSecurity.class); + if(enableWebSecurityAnnotation == null) { + continue; + } + enableWebSecurityAttrMap = AnnotationUtils + .getAnnotationAttributes(enableWebSecurityAnnotation); + enableWebSecurityAttrs = AnnotationAttributes.fromMap(enableWebSecurityAttrMap); + } + } + boolean debugEnabled = enableWebSecurityAttrs.getBoolean("debug"); + this.webSecurity.debug(debugEnabled); + } + + /* (non-Javadoc) + * @see org.springframework.beans.factory.BeanClassLoaderAware#setBeanClassLoader(java.lang.ClassLoader) + */ + @Override + public void setBeanClassLoader(ClassLoader classLoader) { + this.beanClassLoader = classLoader; + } +} diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configuration/WebSecurityConfigurerAdapter.java b/config/src/main/java/org/springframework/security/config/annotation/web/configuration/WebSecurityConfigurerAdapter.java new file mode 100644 index 0000000000..ab4975bf86 --- /dev/null +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configuration/WebSecurityConfigurerAdapter.java @@ -0,0 +1,326 @@ +/* + * Copyright 2002-2013 the original author or authors. + * + * 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 org.springframework.security.config.annotation.web.configuration; + + +import javax.servlet.Filter; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.DefaultAuthenticationEventPublisher; +import org.springframework.security.config.annotation.ObjectPostProcessor; +import org.springframework.security.config.annotation.SecurityConfigurer; +import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; +import org.springframework.security.config.annotation.web.WebSecurityConfigurer; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.builders.WebSecurity; +import org.springframework.security.config.annotation.web.configurers.DefaultLoginPageConfigurer; +import org.springframework.security.config.annotation.web.configurers.SecurityContextConfigurer; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.web.access.intercept.FilterSecurityInterceptor; + +/** + * Provides a convenient base class for creating a {@link WebSecurityConfigurer} + * instance. The implementation allows customization by overriding methods. + * + * @see EnableWebSecurity + * + * @author Rob Winch + */ +public abstract class WebSecurityConfigurerAdapter implements SecurityConfigurer { + private final Log logger = LogFactory.getLog(getClass()); + + @Autowired + private ApplicationContext context; + + @Autowired(required=false) + private ObjectPostProcessor objectPostProcessor = new ObjectPostProcessor() { + @Override + public T postProcess(T object) { + throw new IllegalStateException(ObjectPostProcessor.class.getName()+ " is a required bean. Ensure you have used @EnableWebSecurity and @Configuration"); + } + }; + + private final AuthenticationManagerBuilder authenticationBuilder = new AuthenticationManagerBuilder(); + private final AuthenticationManagerBuilder parentAuthenticationBuilder = new AuthenticationManagerBuilder() { + @Override + public AuthenticationManagerBuilder eraseCredentials(boolean eraseCredentials) { + authenticationBuilder.eraseCredentials(eraseCredentials); + return super.eraseCredentials(eraseCredentials); + } + + }; + private boolean disableAuthenticationRegistration; + private boolean authenticationManagerInitialized; + private AuthenticationManager authenticationManager; + private HttpSecurity http; + private boolean disableDefaults; + + /** + * Creates an instance with the default configuration enabled. + */ + protected WebSecurityConfigurerAdapter() { + this(false); + } + + /** + * Creates an instance which allows specifying if the default configuration + * should be enabled. Disabling the default configuration should be + * considered more advanced usage as it requires more understanding of how + * the framework is implemented. + * + * @param disableDefaults + * true if the default configuration should be enabled, else + * false + */ + protected WebSecurityConfigurerAdapter(boolean disableDefaults) { + this.disableDefaults = disableDefaults; + } + + /** + * Used by the default implementation of {@link #authenticationManager()} to attempt to obtain an + * {@link AuthenticationManager}. If overridden, the {@link AuthenticationManagerBuilder} should be used to specify + * the {@link AuthenticationManager}. The resulting {@link AuthenticationManager} + * will be exposed as a Bean as will the last populated {@link UserDetailsService} that is created with the + * {@link AuthenticationManagerBuilder}. The {@link UserDetailsService} will also automatically be populated on + * {@link HttpSecurity#getSharedObject(Class)} for use with other {@link SecurityContextConfigurer} + * (i.e. RememberMeConfigurer ) + * + *

For example, the following configuration could be used to register + * in memory authentication that exposes an in memory {@link UserDetailsService}:

+ * + *
+     * @Override
+     * protected void registerAuthentication(AuthenticationManagerBuilder auth) {
+     *     registry
+     *         // enable in memory based authentication with a user named "user" and "admin"
+     *         .inMemoryAuthentication()
+     *             .withUser("user").password("password").roles("USER").and()
+     *             .withUser("admin").password("password").roles("USER", "ADMIN");
+     * }
+     * 
+ * + * @param auth the {@link AuthenticationManagerBuilder} to use + * @throws Exception + */ + protected void registerAuthentication(AuthenticationManagerBuilder auth) throws Exception { + this.disableAuthenticationRegistration = true; + } + + /** + * Creates the {@link HttpSecurity} or returns the current instance + * + * @return the {@link HttpSecurity} + * @throws Exception + */ + protected final HttpSecurity getHttp() throws Exception { + if(http != null) { + return http; + } + + authenticationBuilder.objectPostProcessor(objectPostProcessor); + parentAuthenticationBuilder.objectPostProcessor(objectPostProcessor); + + DefaultAuthenticationEventPublisher eventPublisher = objectPostProcessor.postProcess(new DefaultAuthenticationEventPublisher()); + parentAuthenticationBuilder.authenticationEventPublisher(eventPublisher); + + AuthenticationManager authenticationManager = authenticationManager(); + authenticationBuilder.parentAuthenticationManager(authenticationManager); + http = new HttpSecurity(objectPostProcessor,authenticationBuilder, parentAuthenticationBuilder.getSharedObjects()); + http.setSharedObject(UserDetailsService.class, userDetailsService()); + if(!disableDefaults) { + http + .exceptionHandling().and() + .sessionManagement().and() + .securityContext().and() + .requestCache().and() + .anonymous().and() + .servletApi().and() + .apply(new DefaultLoginPageConfigurer()).and() + .logout(); + } + configure(http); + return http; + } + + /** + * Override this method to expose the {@link AuthenticationManager} from + * {@link #registerAuthentication(AuthenticationManagerBuilder)} to be exposed as + * a Bean. For example: + * + *
+     * @Bean(name name="myAuthenticationManager")
+     * @Override
+     * public AuthenticationManager authenticationManagerBean() throws Exception {
+     *     return super.authenticationManagerBean();
+     * }
+     * 
+ * + * @return the {@link AuthenticationManager} + * @throws Exception + */ + public AuthenticationManager authenticationManagerBean() throws Exception { + return new AuthenticationManagerDelegator(authenticationBuilder); + } + + /** + * Gets the {@link AuthenticationManager} to use. The default strategy is if + * {@link #registerAuthentication(AuthenticationManagerBuilder)} method is + * overridden to use the {@link AuthenticationManagerBuilder} that was passed in. + * Otherwise, autowire the {@link AuthenticationManager} by type. + * + * @return + * @throws Exception + */ + protected AuthenticationManager authenticationManager() throws Exception { + if(!authenticationManagerInitialized) { + registerAuthentication(parentAuthenticationBuilder); + if(disableAuthenticationRegistration) { + try { + authenticationManager = context.getBean(AuthenticationManager.class); + } catch(NoSuchBeanDefinitionException e) { + logger.debug("The AuthenticationManager was not found. This is ok for now as it may not be required.",e); + } + } else { + authenticationManagerInitialized = true; + authenticationManager = parentAuthenticationBuilder.build(); + } + authenticationManagerInitialized = true; + } + return authenticationManager; + } + + /** + * Override this method to expose a {@link UserDetailsService} created from + * {@link #registerAuthentication(AuthenticationManagerBuilder)} as a bean. In + * general only the following override should be done of this method: + * + *
+     * @Bean(name = "myUserDetailsService") // any or no name specified is allowed
+     * @Override
+     * public UserDetailsService userDetailsServiceBean() throws Exception {
+     *     return super.userDetailsServiceBean();
+     * }
+     * 
+ * + * To change the instance returned, developers should change + * {@link #userDetailsService()} instead + * @return + * @throws Exception + * @see {@link #userDetailsService()} + */ + public UserDetailsService userDetailsServiceBean() throws Exception { + return userDetailsService(); + } + + /** + * Allows modifying and accessing the {@link UserDetailsService} from + * {@link #userDetailsServiceBean()()} without interacting with the + * {@link ApplicationContext}. Developers should override this method when + * changing the instance of {@link #userDetailsServiceBean()}. + * + * @return + */ + protected UserDetailsService userDetailsService() { + return parentAuthenticationBuilder.getDefaultUserDetailsService(); + } + + @Override + public void init(WebSecurity web) throws Exception { + HttpSecurity http = getHttp(); + FilterSecurityInterceptor securityInterceptor = http.getSharedObject(FilterSecurityInterceptor.class); + web + .addSecurityFilterChainBuilder(http) + .setSecurityInterceptor(securityInterceptor); + } + + /** + * Override this method to configure {@link WebSecurity}. For + * example, if you wish to ignore certain requests. + */ + @Override + public void configure(WebSecurity web) throws Exception { + } + + /** + * Override this method to configure the {@link HttpSecurity}. + * Typically subclasses should not invoke this method by calling super + * as it may override their configuration. The default configuration is: + * + *
+     * http
+     *     .authorizeUrls()
+     *         .anyRequest().authenticated().and()
+     *     .formLogin().and()
+     *     .httpBasic();
+     * 
+ * + * @param http + * the {@link HttpSecurity} to modify + * @throws Exception + * if an error occurs + */ + protected void configure(HttpSecurity http) throws Exception { + logger.debug("Using default configure(HttpSecurity). If subclassed this will potentially override subclass configure(HttpSecurity)."); + + http + .authorizeUrls() + .anyRequest().authenticated() + .and() + .formLogin().and() + .httpBasic(); + } + + /** + * Delays the use of the {@link AuthenticationManager} build from the + * {@link AuthenticationManagerBuilder} to ensure that it has been fully + * configured. + * + * @author Rob Winch + * @since 3.2 + */ + static final class AuthenticationManagerDelegator implements AuthenticationManager { + private AuthenticationManagerBuilder delegateBuilder; + private AuthenticationManager delegate; + private final Object delegateMonitor = new Object(); + + AuthenticationManagerDelegator(AuthenticationManagerBuilder authentication) { + this.delegateBuilder = authentication; + } + + @Override + public Authentication authenticate(Authentication authentication) throws AuthenticationException { + if(delegate != null) { + return delegate.authenticate(authentication); + } + + synchronized(delegateMonitor) { + if (delegate == null) { + delegate = this.delegateBuilder.getObject(); + this.delegateBuilder = null; + } + } + + return delegate.authenticate(authentication); + } + } +} \ No newline at end of file diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/AbstractAuthenticationFilterConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/AbstractAuthenticationFilterConfigurer.java new file mode 100644 index 0000000000..92d6881cf1 --- /dev/null +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/AbstractAuthenticationFilterConfigurer.java @@ -0,0 +1,319 @@ +/* + * Copyright 2002-2013 the original author or authors. + * + * 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 org.springframework.security.config.annotation.web.configurers; + +import javax.servlet.http.HttpServletRequest; + +import org.springframework.security.authentication.AuthenticationDetailsSource; +import org.springframework.security.config.annotation.web.HttpSecurityBuilder; +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; +import org.springframework.security.config.annotation.web.configurers.openid.OpenIDLoginConfigurer; +import org.springframework.security.web.AuthenticationEntryPoint; +import org.springframework.security.web.PortMapper; +import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter; +import org.springframework.security.web.authentication.AuthenticationFailureHandler; +import org.springframework.security.web.authentication.AuthenticationSuccessHandler; +import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint; +import org.springframework.security.web.authentication.RememberMeServices; +import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler; +import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; +import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; +import org.springframework.security.web.authentication.session.SessionAuthenticationStrategy; + +/** + * Base class for confuring {@link AbstractAuthenticationFilterConfigurer}. This is intended for internal use only. + * + * @see FormLoginConfigurer + * @see OpenIDLoginConfigurer + * + * @param T refers to "this" for returning the current configurer + * @param F refers to the {@link AbstractAuthenticationProcessingFilter} that is being built + * + * @author Rob Winch + * @since 3.2 + */ +public abstract class AbstractAuthenticationFilterConfigurer,T extends AbstractAuthenticationFilterConfigurer, F extends AbstractAuthenticationProcessingFilter> extends AbstractHttpConfigurer { + + private final F authFilter; + + private AuthenticationDetailsSource authenticationDetailsSource; + + private AuthenticationSuccessHandler successHandler = new SavedRequestAwareAuthenticationSuccessHandler(); + + private LoginUrlAuthenticationEntryPoint authenticationEntryPoint; + + private boolean customLoginPage; + private String loginPage; + private String loginProcessingUrl; + + private AuthenticationFailureHandler failureHandler; + + private boolean permitAll; + + private String failureUrl; + + /** + * Creates a new instance + * @param authenticationFilter the {@link AbstractAuthenticationProcessingFilter} to use + * @param defaultLoginProcessingUrl the default URL to use for {@link #loginProcessingUrl(String)} + */ + protected AbstractAuthenticationFilterConfigurer(F authenticationFilter, String defaultLoginProcessingUrl) { + this.authFilter = authenticationFilter; + loginUrl("/login"); + failureUrl("/login?error"); + loginProcessingUrl(defaultLoginProcessingUrl); + this.customLoginPage = false; + } + + /** + * Specifies where users will go after authenticating successfully if they + * have not visited a secured page prior to authenticating. This is a + * shortcut for calling {@link #defaultSuccessUrl(String)}. + * + * @param defaultSuccessUrl + * the default success url + * @return the {@link FormLoginConfigurer} for additional customization + */ + public final T defaultSuccessUrl(String defaultSuccessUrl) { + return defaultSuccessUrl(defaultSuccessUrl, false); + } + + /** + * Specifies where users will go after authenticating successfully if they + * have not visited a secured page prior to authenticating or + * {@code alwaysUse} is true. This is a shortcut for calling + * {@link #successHandler(AuthenticationSuccessHandler)}. + * + * @param defaultSuccessUrl + * the default success url + * @param alwaysUse + * true if the {@code defaultSuccesUrl} should be used after + * authentication despite if a protected page had been previously + * visited + * @return the {@link FormLoginConfigurer} for additional customization + */ + public final T defaultSuccessUrl(String defaultSuccessUrl, boolean alwaysUse) { + SavedRequestAwareAuthenticationSuccessHandler handler = new SavedRequestAwareAuthenticationSuccessHandler(); + handler.setDefaultTargetUrl(defaultSuccessUrl); + handler.setAlwaysUseDefaultTargetUrl(alwaysUse); + return successHandler(handler); + } + + /** + * Specifies the URL used to log in. If the request matches the URL and is an HTTP POST, the + * {@link UsernamePasswordAuthenticationFilter} will attempt to authenticate + * the request. Otherwise, if the request matches the URL the user will be sent to the login form. + * + * @param loginUrl the URL used to perform authentication + * @return the {@link FormLoginConfigurer} for additional customization + */ + public final T loginUrl(String loginUrl) { + loginProcessingUrl(loginUrl); + return loginPage(loginUrl); + } + + /** + * Specifies the URL to validate the credentials. + * + * @param loginProcessingUrl + * the URL to validate username and password + * @return the {@link FormLoginConfigurer} for additional customization + */ + public T loginProcessingUrl(String loginProcessingUrl) { + this.loginProcessingUrl = loginProcessingUrl; + authFilter.setFilterProcessesUrl(loginProcessingUrl); + return getSelf(); + } + + /** + * Specifies a custom {@link AuthenticationDetailsSource}. The default is {@link WebAuthenticationDetailsSource}. + * + * @param authenticationDetailsSource the custom {@link AuthenticationDetailsSource} + * @return the {@link FormLoginConfigurer} for additional customization + */ + public final T authenticationDetailsSource(AuthenticationDetailsSource authenticationDetailsSource) { + this.authenticationDetailsSource = authenticationDetailsSource; + return getSelf(); + } + + /** + * Specifies the {@link AuthenticationSuccessHandler} to be used. The + * default is {@link SavedRequestAwareAuthenticationSuccessHandler} with no + * additional properites set. + * + * @param successHandler + * the {@link AuthenticationSuccessHandler}. + * @return the {@link FormLoginConfigurer} for additional customization + */ + public final T successHandler(AuthenticationSuccessHandler successHandler) { + this.successHandler = successHandler; + return getSelf(); + } + + /** + * Equivalent of invoking permitAll(true) + * @return + */ + public final T permitAll() { + return permitAll(true); + } + + /** + * Ensures the urls for {@link #failureUrl(String)} and + * {@link #loginUrl(String)} are granted access to any user. + * + * @param permitAll true to grant access to the URLs false to skip this step + * @return the {@link FormLoginConfigurer} for additional customization + */ + public final T permitAll(boolean permitAll) { + this.permitAll = permitAll; + return getSelf(); + } + + /** + * The URL to send users if authentication fails. This is a shortcut for + * invoking {@link #failureHandler(AuthenticationFailureHandler)}. The + * default is "/login?error". + * + * @param authenticationFailureUrl + * the URL to send users if authentication fails (i.e. + * "/login?error"). + * @return the {@link FormLoginConfigurer} for additional customization + */ + public final T failureUrl(String authenticationFailureUrl) { + T result = failureHandler(new SimpleUrlAuthenticationFailureHandler(authenticationFailureUrl)); + this.failureUrl = authenticationFailureUrl; + return result; + } + + /** + * Specifies the {@link AuthenticationFailureHandler} to use when + * authentication fails. The default is redirecting to "/login?error" using + * {@link SimpleUrlAuthenticationFailureHandler} + * + * @param authenticationFailureHandler + * the {@link AuthenticationFailureHandler} to use when + * authentication fails. + * @return the {@link FormLoginConfigurer} for additional customization + */ + public final T failureHandler(AuthenticationFailureHandler authenticationFailureHandler) { + this.failureUrl = null; + this.failureHandler = authenticationFailureHandler; + return getSelf(); + } + + @Override + public void init(B http) throws Exception { + if(permitAll) { + PermitAllSupport.permitAll(http, loginPage, loginProcessingUrl, failureUrl); + } + http.setSharedObject(AuthenticationEntryPoint.class, postProcess(authenticationEntryPoint)); + } + + @Override + public void configure(B http) throws Exception { + PortMapper portMapper = http.getSharedObject(PortMapper.class); + if(portMapper != null) { + authenticationEntryPoint.setPortMapper(portMapper); + } + + authFilter.setAuthenticationManager(http.getAuthenticationManager()); + authFilter.setAuthenticationSuccessHandler(successHandler); + authFilter.setAuthenticationFailureHandler(failureHandler); + if(authenticationDetailsSource != null) { + authFilter.setAuthenticationDetailsSource(authenticationDetailsSource); + } + SessionAuthenticationStrategy sessionAuthenticationStrategy = http.getSharedObject(SessionAuthenticationStrategy.class); + if(sessionAuthenticationStrategy != null) { + authFilter.setSessionAuthenticationStrategy(sessionAuthenticationStrategy); + } + RememberMeServices rememberMeServices = http.getSharedObject(RememberMeServices.class); + if(rememberMeServices != null) { + authFilter.setRememberMeServices(rememberMeServices); + } + F filter = postProcess(authFilter); + http.addFilter(filter); + } + + /** + *

+ * Specifies the URL to send users to if login is required. If used with + * {@link WebSecurityConfigurerAdapter} a default login page will be + * generated when this attribute is not specified. + *

+ * + *

+ * If a URL is specified or this is not being used in conjuction with + * {@link WebSecurityConfigurerAdapter}, users are required to process the + * specified URL to generate a login page. + *

+ */ + protected T loginPage(String loginPage) { + this.loginPage = loginPage; + this.authenticationEntryPoint = new LoginUrlAuthenticationEntryPoint(loginPage); + this.customLoginPage = true; + return getSelf(); + } + + /** + * + * @return true if a custom login page has been specified, else false + */ + public final boolean isCustomLoginPage() { + return customLoginPage; + } + + /** + * Gets the Authentication Filter + * @return + */ + protected final F getAuthenticationFilter() { + return authFilter; + } + + /** + * Gets the login page + * @return the login page + */ + protected final String getLoginPage() { + return loginPage; + } + + /** + * Gets the URL to submit an authentication request to (i.e. where + * username/password must be submitted) + * + * @return the URL to submit an authentication request to + */ + protected final String getLoginProcessingUrl() { + return loginProcessingUrl; + } + + /** + * Gets the URL to send users to if authentication fails + * @return + */ + protected final String getFailureUrl() { + return failureUrl; + } + + + @SuppressWarnings("unchecked") + private T getSelf() { + return (T) this; + } +} diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/AbstractHttpConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/AbstractHttpConfigurer.java new file mode 100644 index 0000000000..5c6dd82f0b --- /dev/null +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/AbstractHttpConfigurer.java @@ -0,0 +1,33 @@ +/* + * Copyright 2002-2013 the original author or authors. + * + * 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 org.springframework.security.config.annotation.web.configurers; + +import org.springframework.security.config.annotation.SecurityConfigurer; +import org.springframework.security.config.annotation.SecurityConfigurerAdapter; +import org.springframework.security.config.annotation.web.HttpSecurityBuilder; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.web.DefaultSecurityFilterChain; + +/** + * Adds a convenient base class for {@link SecurityConfigurer} instances that + * operate on {@link HttpSecurity}. + * + * @author Rob Winch + * + */ +abstract class AbstractHttpConfigurer> extends SecurityConfigurerAdapter { + +} diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/AbstractInterceptUrlConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/AbstractInterceptUrlConfigurer.java new file mode 100644 index 0000000000..c1a5cd424f --- /dev/null +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/AbstractInterceptUrlConfigurer.java @@ -0,0 +1,181 @@ +/* + * Copyright 2002-2013 the original author or authors. + * + * 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 org.springframework.security.config.annotation.web.configurers; + +import java.util.List; + +import org.springframework.security.access.AccessDecisionManager; +import org.springframework.security.access.AccessDecisionVoter; +import org.springframework.security.access.vote.AffirmativeBased; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.config.annotation.SecurityConfigurer; +import org.springframework.security.config.annotation.web.HttpSecurityBuilder; +import org.springframework.security.web.DefaultSecurityFilterChain; +import org.springframework.security.web.access.intercept.FilterInvocationSecurityMetadataSource; +import org.springframework.security.web.access.intercept.FilterSecurityInterceptor; + +/** + * A base class for configuring the {@link FilterSecurityInterceptor}. + * + *

Security Filters

+ * + * The following Filters are populated + * + *
    + *
  • {@link FilterSecurityInterceptor}
  • + *
+ * + *

Shared Objects Created

+ * + * The following shared objects are populated to allow other {@link SecurityConfigurer}'s to customize: + *
    + *
  • {@link FilterSecurityInterceptor}
  • + *
+ * + *

Shared Objects Used

+ * + * The following shared objects are used: + * + *
    + *
  • {@link org.springframework.security.config.annotation.web.builders.HttpSecurity#getAuthenticationManager()}
  • + *
+ * + * @param the type of {@link HttpSecurityBuilder} that is being configured + * @param the type of object that is changed + * @param the type of object that is changed for the {@link AbstractRequestMatcherMappingConfigurer} + * + * @author Rob Winch + * @since 3.2 + * @see ExpressionUrlAuthorizationConfigurer + * @see UrlAuthorizationConfigurer + */ +abstract class AbstractInterceptUrlConfigurer,C,R> extends + AbstractRequestMatcherMappingConfigurer implements + SecurityConfigurer { + private Boolean filterSecurityInterceptorOncePerRequest; + + private AccessDecisionManager accessDecisionManager; + + /** + * Allows setting the {@link AccessDecisionManager}. If none is provided, a default {@l AccessDecisionManager} is + * created. + * + * @param accessDecisionManager the {@link AccessDecisionManager} to use + * @return the {@link AbstractInterceptUrlConfigurer} for further customization + */ + public C accessDecisionManager( + AccessDecisionManager accessDecisionManager) { + this.accessDecisionManager = accessDecisionManager; + return getSelf(); + } + + /** + * Allows setting if the {@link FilterSecurityInterceptor} should be only applied once per request (i.e. if the + * filter intercepts on a forward, should it be applied again). + * + * @param filterSecurityInterceptorOncePerRequest if the {@link FilterSecurityInterceptor} should be only applied + * once per request + * @return the {@link AbstractInterceptUrlConfigurer} for further customization + */ + public C filterSecurityInterceptorOncePerRequest( + boolean filterSecurityInterceptorOncePerRequest) { + this.filterSecurityInterceptorOncePerRequest = filterSecurityInterceptorOncePerRequest; + return getSelf(); + } + + @Override + public void configure(H http) throws Exception { + FilterInvocationSecurityMetadataSource metadataSource = createMetadataSource(); + if(metadataSource == null) { + return; + } + FilterSecurityInterceptor securityInterceptor = createFilterSecurityInterceptor(metadataSource, http.getAuthenticationManager()); + if(filterSecurityInterceptorOncePerRequest != null) { + securityInterceptor.setObserveOncePerRequest(filterSecurityInterceptorOncePerRequest); + } + http.addFilter(securityInterceptor); + http.setSharedObject(FilterSecurityInterceptor.class, securityInterceptor); + } + + /** + * Subclasses should implement this method to provide a {@link FilterInvocationSecurityMetadataSource} for the + * {@link FilterSecurityInterceptor}. + * + * @return the {@link FilterInvocationSecurityMetadataSource} to set on the {@link FilterSecurityInterceptor}. + * Cannot be null. + */ + abstract FilterInvocationSecurityMetadataSource createMetadataSource(); + + /** + * Subclasses should implement this method to provide the {@link AccessDecisionVoter} instances used to create the + * default {@link AccessDecisionManager} + * + * @return the {@link AccessDecisionVoter} instances used to create the + * default {@link AccessDecisionManager} + */ + @SuppressWarnings("rawtypes") + abstract List getDecisionVoters(); + + /** + * Creates the default {@code AccessDecisionManager} + * @return the default {@code AccessDecisionManager} + */ + private AccessDecisionManager createDefaultAccessDecisionManager() { + return new AffirmativeBased(getDecisionVoters()); + } + + /** + * If currently null, creates a default {@link AccessDecisionManager} using + * {@link #createDefaultAccessDecisionManager()}. Otherwise returns the {@link AccessDecisionManager}. + * + * @return the {@link AccessDecisionManager} to use + */ + private AccessDecisionManager getAccessDecisionManager() { + if (accessDecisionManager == null) { + accessDecisionManager = createDefaultAccessDecisionManager(); + } + return accessDecisionManager; + } + + /** + * Creates the {@link FilterSecurityInterceptor} + * + * @param metadataSource the {@link FilterInvocationSecurityMetadataSource} to use + * @param authenticationManager the {@link AuthenticationManager} to use + * @return the {@link FilterSecurityInterceptor} + * @throws Exception + */ + private FilterSecurityInterceptor createFilterSecurityInterceptor(FilterInvocationSecurityMetadataSource metadataSource, + AuthenticationManager authenticationManager) throws Exception { + FilterSecurityInterceptor securityInterceptor = new FilterSecurityInterceptor(); + securityInterceptor.setSecurityMetadataSource(metadataSource); + securityInterceptor.setAccessDecisionManager(getAccessDecisionManager()); + securityInterceptor.setAuthenticationManager(authenticationManager); + securityInterceptor.afterPropertiesSet(); + return securityInterceptor; + } + + /** + * Returns a reference to the current object with a single suppression of + * the type + * + * @return a reference to the current object + */ + @SuppressWarnings("unchecked") + private C getSelf() { + return (C) this; + } +} \ No newline at end of file diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/AbstractRequestMatcherMappingConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/AbstractRequestMatcherMappingConfigurer.java new file mode 100644 index 0000000000..228a59fa81 --- /dev/null +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/AbstractRequestMatcherMappingConfigurer.java @@ -0,0 +1,144 @@ +/* + * Copyright 2002-2013 the original author or authors. + * + * 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 org.springframework.security.config.annotation.web.configurers; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.LinkedHashMap; +import java.util.List; + +import org.springframework.security.access.ConfigAttribute; +import org.springframework.security.config.annotation.SecurityBuilder; +import org.springframework.security.config.annotation.web.AbstractRequestMatcherConfigurer; +import org.springframework.security.web.util.RequestMatcher; + +/** + * A base class for registering {@link RequestMatcher}'s. For example, it might allow for specifying which + * {@link RequestMatcher} require a certain level of authorization. + * + * @author Rob Winch + * @since 3.2 + * + * @param The Builder that is building Object O and is configured by this {@link AbstractRequestMatcherMappingConfigurer} + * @param The object that is returned or Chained after creating the RequestMatcher + * @param The Object being built by Builder B + * + * @see ChannelSecurityConfigurer + * @see UrlAuthorizationConfigurer + * @see ExpressionUrlAuthorizationConfigurer + */ +public abstract class AbstractRequestMatcherMappingConfigurer,C,O> extends AbstractRequestMatcherConfigurer { + private List urlMappings = new ArrayList(); + private List unmappedMatchers; + + /** + * Gets the {@link UrlMapping} added by subclasses in {@link #chainRequestMatchers(java.util.List)}. May be empty. + * + * @return the {@link UrlMapping} added by subclasses in {@link #chainRequestMatchers(java.util.List)} + */ + final List getUrlMappings() { + return urlMappings; + } + + /** + * Adds a {@link UrlMapping} added by subclasses in + * {@link #chainRequestMatchers(java.util.List)} and resets the unmapped + * {@link RequestMatcher}'s. + * + * @param urlMapping + * {@link UrlMapping} the mapping to add + */ + final void addMapping(UrlMapping urlMapping) { + this.unmappedMatchers = null; + this.urlMappings.add(urlMapping); + } + + /** + * Marks the {@link RequestMatcher}'s as unmapped and then calls {@link #chainRequestMatchersInternal(List)}. + * + * @param requestMatchers the {@link RequestMatcher} instances that were created + * @return the chained Object for the subclass which allows association of something else to the + * {@link RequestMatcher} + */ + protected final C chainRequestMatchers(List requestMatchers) { + this.unmappedMatchers = requestMatchers; + return chainRequestMatchersInternal(requestMatchers); + } + + /** + * Subclasses should implement this method for returning the object that is chained to the creation of the + * {@link RequestMatcher} instances. + * + * @param requestMatchers the {@link RequestMatcher} instances that were created + * @return the chained Object for the subclass which allows association of something else to the + * {@link RequestMatcher} + */ + protected abstract C chainRequestMatchersInternal(List requestMatchers); + + /** + * Adds a {@link UrlMapping} added by subclasses in {@link #chainRequestMatchers(java.util.List)} at a particular + * index. + * + * @param index the index to add a {@link UrlMapping} + * @param urlMapping {@link UrlMapping} the mapping to add + */ + final void addMapping(int index, UrlMapping urlMapping) { + this.urlMappings.add(index, urlMapping); + } + + /** + * Creates the mapping of {@link RequestMatcher} to {@link Collection} of {@link ConfigAttribute} instances + * + * @return the mapping of {@link RequestMatcher} to {@link Collection} of {@link ConfigAttribute} instances. Cannot + * be null. + */ + final LinkedHashMap> createRequestMap() { + if(unmappedMatchers != null) { + throw new IllegalStateException("An incomplete mapping was found for " + unmappedMatchers +". Try completing it with something like requestUrls()..hasRole('USER')"); + } + + LinkedHashMap> requestMap = new LinkedHashMap>(); + for (UrlMapping mapping : getUrlMappings()) { + RequestMatcher matcher = mapping.getRequestMatcher(); + Collection configAttrs = mapping.getConfigAttrs(); + requestMap.put(matcher,configAttrs); + } + return requestMap; + } + + /** + * A mapping of {@link RequestMatcher} to {@link Collection} of {@link ConfigAttribute} instances + */ + static final class UrlMapping { + private RequestMatcher requestMatcher; + private Collection configAttrs; + + UrlMapping(RequestMatcher requestMatcher, + Collection configAttrs) { + this.requestMatcher = requestMatcher; + this.configAttrs = configAttrs; + } + + public RequestMatcher getRequestMatcher() { + return requestMatcher; + } + + public Collection getConfigAttrs() { + return configAttrs; + } + } +} + diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/AnonymousConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/AnonymousConfigurer.java new file mode 100644 index 0000000000..c227c1b558 --- /dev/null +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/AnonymousConfigurer.java @@ -0,0 +1,165 @@ +/* + * Copyright 2002-2013 the original author or authors. + * + * 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 org.springframework.security.config.annotation.web.configurers; + +import java.util.List; +import java.util.UUID; + +import org.springframework.security.authentication.AnonymousAuthenticationProvider; +import org.springframework.security.authentication.AuthenticationProvider; +import org.springframework.security.config.annotation.SecurityConfigurer; +import org.springframework.security.config.annotation.SecurityConfigurerAdapter; +import org.springframework.security.config.annotation.web.HttpSecurityBuilder; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.AuthorityUtils; +import org.springframework.security.web.DefaultSecurityFilterChain; +import org.springframework.security.web.authentication.AnonymousAuthenticationFilter; + +/** + * Configures Anonymous authentication (i.e. populate an {@link Authentication} that represents an anonymous user + * instead of having a null value) for an {@link HttpSecurity}. Specifically this will configure an + * {@link AnonymousAuthenticationFilter} and an {@link AnonymousAuthenticationProvider}. All properties have reasonable + * defaults, so no additional configuration is required other than applying this {@link SecurityConfigurer}. + * + * @author Rob Winch + * @since 3.2 + */ +public final class AnonymousConfigurer> extends SecurityConfigurerAdapter { + private String key; + private AuthenticationProvider authenticationProvider; + private AnonymousAuthenticationFilter authenticationFilter; + private Object principal = "anonymousUser"; + private List authorities = AuthorityUtils.createAuthorityList("ROLE_ANONYMOUS"); + + /** + * Creates a new instance + * @see HttpSecurity#anonymous() + */ + public AnonymousConfigurer() { + } + + /** + * Disables anonymous authentication. + * + * @return the {@link HttpSecurity} since no further customization of anonymous authentication would be + * meaningful. + */ + @SuppressWarnings("unchecked") + public H disable() { + getBuilder().removeConfigurer(getClass()); + return getBuilder(); + } + + /** + * Sets the key to identify tokens created for anonymous authentication. Default is a secure randomly generated + * key. + * + * @param key the key to identify tokens created for anonymous authentication. Default is a secure randomly generated + * key. + * @return the {@link AnonymousConfigurer} for further customization of anonymous authentication + */ + public AnonymousConfigurer key(String key) { + this.key = key; + return this; + } + + /** + * Sets the principal for {@link Authentication} objects of anonymous users + * + * @param principal used for the {@link Authentication} object of anonymous users + * @return the {@link AnonymousConfigurer} for further customization of anonymous authentication + */ + public AnonymousConfigurer principal(Object principal) { + this.principal = principal; + return this; + } + + /** + * Sets the {@link org.springframework.security.core.Authentication#getAuthorities()} for anonymous users + * + * @param authorities Sets the {@link org.springframework.security.core.Authentication#getAuthorities()} for anonymous users + * @return the {@link AnonymousConfigurer} for further customization of anonymous authentication + */ + public AnonymousConfigurer authorities(List authorities) { + this.authorities = authorities; + return this; + } + + /** + * Sets the {@link org.springframework.security.core.Authentication#getAuthorities()} for anonymous users + * + * @param authorities Sets the {@link org.springframework.security.core.Authentication#getAuthorities()} for + * anonymous users (i.e. "ROLE_ANONYMOUS") + * @return the {@link AnonymousConfigurer} for further customization of anonymous authentication + */ + public AnonymousConfigurer authorities(String... authorities) { + return authorities(AuthorityUtils.createAuthorityList(authorities)); + } + + /** + * Sets the {@link AuthenticationProvider} used to validate an anonymous user. If this is set, no attributes + * on the {@link AnonymousConfigurer} will be set on the {@link AuthenticationProvider}. + * + * @param authenticationProvider the {@link AuthenticationProvider} used to validate an anonymous user. Default is + * {@link AnonymousAuthenticationProvider} + * + * @return the {@link AnonymousConfigurer} for further customization of anonymous authentication + */ + public AnonymousConfigurer authenticationProvider(AuthenticationProvider authenticationProvider) { + this.authenticationProvider = authenticationProvider; + return this; + } + + /** + * Sets the {@link AnonymousAuthenticationFilter} used to populate an anonymous user. If this is set, no attributes + * on the {@link AnonymousConfigurer} will be set on the {@link AnonymousAuthenticationFilter}. + * + * @param authenticationFilter the {@link AnonymousAuthenticationFilter} used to populate an anonymous user. + * + * @return the {@link AnonymousConfigurer} for further customization of anonymous authentication + */ + public AnonymousConfigurer authenticationFilter(AnonymousAuthenticationFilter authenticationFilter) { + this.authenticationFilter = authenticationFilter; + return this; + } + + @Override + public void init(H http) throws Exception { + if(authenticationProvider == null) { + authenticationProvider = new AnonymousAuthenticationProvider(getKey()); + } + if(authenticationFilter == null) { + authenticationFilter = new AnonymousAuthenticationFilter(getKey(), principal, authorities); + } + authenticationProvider = postProcess(authenticationProvider); + http.authenticationProvider(authenticationProvider); + } + + @Override + public void configure(H http) throws Exception { + authenticationFilter.afterPropertiesSet(); + http.addFilter(authenticationFilter); + } + + private String getKey() { + if(key == null) { + key = UUID.randomUUID().toString(); + } + return key; + } +} diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/ChannelSecurityConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/ChannelSecurityConfigurer.java new file mode 100644 index 0000000000..1a2547cf00 --- /dev/null +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/ChannelSecurityConfigurer.java @@ -0,0 +1,168 @@ +/* + * Copyright 2002-2013 the original author or authors. + * + * 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 org.springframework.security.config.annotation.web.configurers; + +import java.util.Arrays; +import java.util.Collection; +import java.util.LinkedHashMap; +import java.util.List; + +import org.springframework.security.access.ConfigAttribute; +import org.springframework.security.access.SecurityConfig; +import org.springframework.security.config.annotation.web.HttpSecurityBuilder; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.web.DefaultSecurityFilterChain; +import org.springframework.security.web.PortMapper; +import org.springframework.security.web.access.channel.ChannelDecisionManagerImpl; +import org.springframework.security.web.access.channel.ChannelProcessingFilter; +import org.springframework.security.web.access.channel.ChannelProcessor; +import org.springframework.security.web.access.channel.InsecureChannelProcessor; +import org.springframework.security.web.access.channel.RetryWithHttpEntryPoint; +import org.springframework.security.web.access.channel.RetryWithHttpsEntryPoint; +import org.springframework.security.web.access.channel.SecureChannelProcessor; +import org.springframework.security.web.access.intercept.DefaultFilterInvocationSecurityMetadataSource; +import org.springframework.security.web.util.RequestMatcher; + +/** + * Adds channel security (i.e. requires HTTPS or HTTP) to an application. In order for + * {@link ChannelSecurityConfigurer} to be useful, at least one {@link RequestMatcher} should be mapped to HTTP + * or HTTPS. + * + *

+ * By default an {@link InsecureChannelProcessor} and a {@link SecureChannelProcessor} will be registered. + *

+ * + *

Security Filters

+ * + * The following Filters are populated + * + *
    + *
  • {@link ChannelProcessingFilter}
  • + *
+ * + *

Shared Objects Created

+ * + * No shared objects are created. + * + *

Shared Objects Used

+ * + * The following shared objects are used: + * + *
    + *
  • {@link PortMapper} is used to create the default {@link ChannelProcessor} instances
  • + *
+ * + * @param the type of {@link HttpSecurityBuilder} that is being configured + * + * @author Rob Winch + * @since 3.2 + */ +public final class ChannelSecurityConfigurer> extends + AbstractRequestMatcherMappingConfigurer.RequiresChannelUrl,DefaultSecurityFilterChain> { + private ChannelProcessingFilter channelFilter = new ChannelProcessingFilter(); + private LinkedHashMap> requestMap = new LinkedHashMap>(); + private List channelProcessors; + + /** + * Creates a new instance + * @see HttpSecurity#requiresChannel() + */ + public ChannelSecurityConfigurer() { + } + + @Override + public void configure(H http) throws Exception { + ChannelDecisionManagerImpl channelDecisionManager = new ChannelDecisionManagerImpl(); + channelDecisionManager.setChannelProcessors(getChannelProcessors(http)); + channelDecisionManager = postProcess(channelDecisionManager); + + channelFilter.setChannelDecisionManager(channelDecisionManager); + + DefaultFilterInvocationSecurityMetadataSource filterInvocationSecurityMetadataSource = + new DefaultFilterInvocationSecurityMetadataSource(requestMap); + channelFilter.setSecurityMetadataSource(filterInvocationSecurityMetadataSource); + + channelFilter = postProcess(channelFilter); + http.addFilter(channelFilter); + } + + /** + * Sets the {@link ChannelProcessor} instances to use in {@link ChannelDecisionManagerImpl} + * @param channelProcessors + * @return + */ + public ChannelSecurityConfigurer channelProcessors(List channelProcessors) { + this.channelProcessors = channelProcessors; + return this; + } + + private List getChannelProcessors(H http) { + if(channelProcessors != null) { + return channelProcessors; + } + + InsecureChannelProcessor insecureChannelProcessor = new InsecureChannelProcessor(); + SecureChannelProcessor secureChannelProcessor = new SecureChannelProcessor(); + + PortMapper portMapper = http.getSharedObject(PortMapper.class); + if(portMapper != null) { + RetryWithHttpEntryPoint httpEntryPoint = new RetryWithHttpEntryPoint(); + httpEntryPoint.setPortMapper(portMapper); + insecureChannelProcessor.setEntryPoint(httpEntryPoint); + + RetryWithHttpsEntryPoint httpsEntryPoint = new RetryWithHttpsEntryPoint(); + httpsEntryPoint.setPortMapper(portMapper); + secureChannelProcessor.setEntryPoint(httpsEntryPoint); + } + insecureChannelProcessor = postProcess(insecureChannelProcessor); + secureChannelProcessor = postProcess(secureChannelProcessor); + return Arrays.asList(insecureChannelProcessor, secureChannelProcessor); + } + + + private ChannelSecurityConfigurer addAttribute(String attribute, List matchers) { + for(RequestMatcher matcher : matchers) { + Collection attrs = Arrays.asList(new SecurityConfig(attribute)); + requestMap.put(matcher, attrs); + } + return this; + } + + @Override + protected RequiresChannelUrl chainRequestMatchersInternal(List requestMatchers) { + return new RequiresChannelUrl(requestMatchers); + } + + public final class RequiresChannelUrl { + private List requestMatchers; + + private RequiresChannelUrl(List requestMatchers) { + this.requestMatchers = requestMatchers; + } + + public ChannelSecurityConfigurer requiresSecure() { + return requires("REQUIRES_SECURE_CHANNEL"); + } + + public ChannelSecurityConfigurer requiresInsecure() { + return requires("REQUIRES_INSECURE_CHANNEL"); + } + + public ChannelSecurityConfigurer requires(String attribute) { + return addAttribute(attribute, requestMatchers); + } + } +} \ No newline at end of file diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/DefaultLoginPageConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/DefaultLoginPageConfigurer.java new file mode 100644 index 0000000000..19b7c3c026 --- /dev/null +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/DefaultLoginPageConfigurer.java @@ -0,0 +1,82 @@ +/* + * Copyright 2002-2013 the original author or authors. + * + * 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 org.springframework.security.config.annotation.web.configurers; + +import org.springframework.security.config.annotation.web.HttpSecurityBuilder; +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; +import org.springframework.security.web.AuthenticationEntryPoint; +import org.springframework.security.web.authentication.ui.DefaultLoginPageViewFilter; + +/** + * Adds a Filter that will generate a login page if one is not specified otherwise when using {@link WebSecurityConfigurerAdapter}. + * + *

+ * By default an {@link org.springframework.security.web.access.channel.InsecureChannelProcessor} and a {@link org.springframework.security.web.access.channel.SecureChannelProcessor} will be registered. + *

+ * + *

Security Filters

+ * + * The following Filters are conditionally populated + * + *
    + *
  • {@link DefaultLoginPageViewFilter} if the {@link FormLoginConfigurer} did not have a login page specified
  • + *
+ * + *

Shared Objects Created

+ * + * No shared objects are created. + *isLogoutRequest + *

Shared Objects Used

+ * + * The following shared objects are used: + * + *
    + *
  • {@link org.springframework.security.web.PortMapper} is used to create the default {@link org.springframework.security.web.access.channel.ChannelProcessor} instances
  • + *
  • {@link FormLoginConfigurer} is used to determine if the {@link DefaultLoginPageConfigurer} should be added and how to configure it.
  • + *
+ * + * @see WebSecurityConfigurerAdapter + * + * @author Rob Winch + * @since 3.2 + */ +public final class DefaultLoginPageConfigurer> extends + AbstractHttpConfigurer { + + private DefaultLoginPageViewFilter loginPageGeneratingFilter = new DefaultLoginPageViewFilter(); + + @Override + public void init(H http) throws Exception { + http.setSharedObject(DefaultLoginPageViewFilter.class, loginPageGeneratingFilter); + } + + @Override + @SuppressWarnings("unchecked") + public void configure(H http) throws Exception { + AuthenticationEntryPoint authenticationEntryPoint = null; + ExceptionHandlingConfigurer exceptionConf = http.getConfigurer(ExceptionHandlingConfigurer.class); + if(exceptionConf != null) { + authenticationEntryPoint = exceptionConf.getAuthenticationEntryPoint(); + } + + if(loginPageGeneratingFilter.isEnabled() && authenticationEntryPoint == null) { + loginPageGeneratingFilter = postProcess(loginPageGeneratingFilter); + http.addFilter(loginPageGeneratingFilter); + } + } + + +} \ No newline at end of file diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/ExceptionHandlingConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/ExceptionHandlingConfigurer.java new file mode 100644 index 0000000000..128327f0d4 --- /dev/null +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/ExceptionHandlingConfigurer.java @@ -0,0 +1,164 @@ +/* + * Copyright 2002-2013 the original author or authors. + * + * 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 org.springframework.security.config.annotation.web.configurers; + +import org.springframework.security.config.annotation.web.HttpSecurityBuilder; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.web.AuthenticationEntryPoint; +import org.springframework.security.web.access.AccessDeniedHandler; +import org.springframework.security.web.access.AccessDeniedHandlerImpl; +import org.springframework.security.web.access.ExceptionTranslationFilter; +import org.springframework.security.web.authentication.Http403ForbiddenEntryPoint; +import org.springframework.security.web.savedrequest.HttpSessionRequestCache; +import org.springframework.security.web.savedrequest.RequestCache; + +/** + * Adds exception handling for Spring Security related exceptions to an application. All properties have reasonable + * defaults, so no additional configuration is required other than applying this + * {@link org.springframework.security.config.annotation.SecurityConfigurer}. + * + *

Security Filters

+ * + * The following Filters are populated + * + *
    + *
  • {@link ExceptionTranslationFilter}
  • + *
+ * + *

Shared Objects Created

+ * + * No shared objects are created. + * + *

Shared Objects Used

+ * + * The following shared objects are used: + * + *
    + *
  • {@link HttpSecurity#authenticationEntryPoint()} is used to process requests that require + * authentication
  • + *
  • If no explicit {@link RequestCache}, is provided a {@link RequestCache} shared object is used to replay + * the request after authentication is successful
  • + *
  • {@link AuthenticationEntryPoint} - see {@link #authenticationEntryPoint(AuthenticationEntryPoint)}
  • + *
+ * + * @author Rob Winch + * @since 3.2 + */ +public final class ExceptionHandlingConfigurer> extends AbstractHttpConfigurer { + + private AuthenticationEntryPoint authenticationEntryPoint; + + private AccessDeniedHandler accessDeniedHandler; + + /** + * Creates a new instance + * @see HttpSecurity#exceptionHandling() + */ + public ExceptionHandlingConfigurer() { + } + + /** + * Shortcut to specify the {@link AccessDeniedHandler} to be used is a specific error page + * + * @param accessDeniedUrl the URL to the access denied page (i.e. /errors/401) + * @return the {@link ExceptionHandlingConfigurer} for further customization + * @see AccessDeniedHandlerImpl + * @see {@link #accessDeniedHandler(org.springframework.security.web.access.AccessDeniedHandler)} + */ + public ExceptionHandlingConfigurer accessDeniedPage(String accessDeniedUrl) { + AccessDeniedHandlerImpl accessDeniedHandler = new AccessDeniedHandlerImpl(); + accessDeniedHandler.setErrorPage(accessDeniedUrl); + return accessDeniedHandler(accessDeniedHandler); + } + + /** + * Specifies the {@link AccessDeniedHandler} to be used + * + * @param accessDeniedHandler the {@link AccessDeniedHandler} to be used + * @return the {@link ExceptionHandlingConfigurer} for further customization + */ + public ExceptionHandlingConfigurer accessDeniedHandler(AccessDeniedHandler accessDeniedHandler) { + this.accessDeniedHandler = accessDeniedHandler; + return this; + } + + /** + * Sets the {@link AuthenticationEntryPoint} to be used. Defaults to the + * {@link HttpSecurity#getSharedObject(Class)} value. If that is not + * provided defaults to {@link Http403ForbiddenEntryPoint}. + * + * @param authenticationEntryPoint the {@link AuthenticationEntryPoint} to use + * @return the {@link ExceptionHandlingConfigurer} for further customizations + */ + public ExceptionHandlingConfigurer authenticationEntryPoint(AuthenticationEntryPoint authenticationEntryPoint) { + this.authenticationEntryPoint = authenticationEntryPoint; + return this; + } + + /** + * Gets any explicitly configured {@link AuthenticationEntryPoint} + * @return + */ + AuthenticationEntryPoint getAuthenticationEntryPoint() { + return this.authenticationEntryPoint; + } + + @Override + public void configure(H http) throws Exception { + AuthenticationEntryPoint entryPoint = getEntryPoint(http); + ExceptionTranslationFilter exceptionTranslationFilter = new ExceptionTranslationFilter(entryPoint, getRequestCache(http)); + if(accessDeniedHandler != null) { + exceptionTranslationFilter.setAccessDeniedHandler(accessDeniedHandler); + } + exceptionTranslationFilter = postProcess(exceptionTranslationFilter); + http.addFilter(exceptionTranslationFilter); + } + + /** + * Gets the {@link AuthenticationEntryPoint} according to the rules specified by {@link #authenticationEntryPoint(AuthenticationEntryPoint)} + * @param http the {@link HttpSecurity} used to look up shared {@link AuthenticationEntryPoint} + * @return the {@link AuthenticationEntryPoint} to use + */ + private AuthenticationEntryPoint getEntryPoint(H http) { + AuthenticationEntryPoint entryPoint = this.authenticationEntryPoint; + if(entryPoint == null) { + AuthenticationEntryPoint sharedEntryPoint = http.getSharedObject(AuthenticationEntryPoint.class); + if(sharedEntryPoint != null) { + entryPoint = sharedEntryPoint; + } else { + entryPoint = new Http403ForbiddenEntryPoint(); + } + } + return entryPoint; + } + + /** + * Gets the {@link RequestCache} to use. If one is defined using + * {@link #requestCache(org.springframework.security.web.savedrequest.RequestCache)}, then it is used. Otherwise, an + * attempt to find a {@link RequestCache} shared object is made. If that fails, an {@link HttpSessionRequestCache} + * is used + * + * @param http the {@link HttpSecurity} to attempt to fined the shared object + * @return the {@link RequestCache} to use + */ + private RequestCache getRequestCache(H http) { + RequestCache result = http.getSharedObject(RequestCache.class); + if(result != null) { + return result; + } + return new HttpSessionRequestCache(); + } +} diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/ExpressionUrlAuthorizationConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/ExpressionUrlAuthorizationConfigurer.java new file mode 100644 index 0000000000..7ae103eb98 --- /dev/null +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/ExpressionUrlAuthorizationConfigurer.java @@ -0,0 +1,296 @@ +/* + * Copyright 2002-2013 the original author or authors. + * + * 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 org.springframework.security.config.annotation.web.configurers; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.LinkedHashMap; +import java.util.List; + +import org.springframework.security.access.AccessDecisionVoter; +import org.springframework.security.access.ConfigAttribute; +import org.springframework.security.access.SecurityConfig; +import org.springframework.security.access.expression.SecurityExpressionHandler; +import org.springframework.security.config.annotation.web.HttpSecurityBuilder; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.web.FilterInvocation; +import org.springframework.security.web.access.expression.DefaultWebSecurityExpressionHandler; +import org.springframework.security.web.access.expression.ExpressionBasedFilterInvocationSecurityMetadataSource; +import org.springframework.security.web.access.expression.WebExpressionVoter; +import org.springframework.security.web.util.RequestMatcher; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +/** + * Adds URL based authorization based upon SpEL expressions to an application. At least one + * {@link org.springframework.web.bind.annotation.RequestMapping} needs to be mapped to {@link ConfigAttribute}'s for + * this {@link SecurityContextConfigurer} to have meaning. + *

Security Filters

+ * + * The following Filters are populated + * + *
    + *
  • {@link org.springframework.security.web.access.intercept.FilterSecurityInterceptor}
  • + *
+ * + *

Shared Objects Created

+ * + * The following shared objects are populated to allow other {@link org.springframework.security.config.annotation.SecurityConfigurer}'s to customize: + *
    + *
  • {@link org.springframework.security.web.access.intercept.FilterSecurityInterceptor}
  • + *
+ * + *

Shared Objects Used

+ * + * The following shared objects are used: + * + *
    + *
  • {@link org.springframework.security.config.annotation.web.builders.HttpSecurity#getAuthenticationManager()}
  • + *
+ * + * @param the type of {@link HttpSecurityBuilder} that is being configured + * + * @author Rob Winch + * @since 3.2 + * @see {@link org.springframework.security.config.annotation.web.builders.HttpSecurity#authorizeUrls()} + */ +public final class ExpressionUrlAuthorizationConfigurer> extends AbstractInterceptUrlConfigurer,ExpressionUrlAuthorizationConfigurer.AuthorizedUrl> { + static final String permitAll = "permitAll"; + private static final String denyAll = "denyAll"; + private static final String anonymous = "anonymous"; + private static final String authenticated = "authenticated"; + private static final String fullyAuthenticated = "fullyAuthenticated"; + private static final String rememberMe = "rememberMe"; + + private SecurityExpressionHandler expressionHandler = new DefaultWebSecurityExpressionHandler(); + + /** + * Creates a new instance + * @see HttpSecurity#authorizeUrls() + */ + public ExpressionUrlAuthorizationConfigurer() { + } + + /** + * Allows customization of the {@link SecurityExpressionHandler} to be used. The default is {@link DefaultWebSecurityExpressionHandler} + * + * @param expressionHandler the {@link SecurityExpressionHandler} to be used + * @return the {@link ExpressionUrlAuthorizationConfigurer} for further customization. + */ + public ExpressionUrlAuthorizationConfigurer expressionHandler(SecurityExpressionHandler expressionHandler) { + this.expressionHandler = expressionHandler; + return this; + } + + @Override + protected final AuthorizedUrl chainRequestMatchersInternal(List requestMatchers) { + return new AuthorizedUrl(requestMatchers); + } + + @Override + @SuppressWarnings("rawtypes") + final List getDecisionVoters() { + List decisionVoters = new ArrayList(); + WebExpressionVoter expressionVoter = new WebExpressionVoter(); + expressionVoter.setExpressionHandler(expressionHandler); + decisionVoters.add(expressionVoter); + return decisionVoters; + } + + @Override + final ExpressionBasedFilterInvocationSecurityMetadataSource createMetadataSource() { + LinkedHashMap> requestMap = createRequestMap(); + if(requestMap.isEmpty()) { + throw new IllegalStateException("At least one mapping is required (i.e. authorizeUrls().anyRequest.authenticated())"); + } + return new ExpressionBasedFilterInvocationSecurityMetadataSource(requestMap, expressionHandler); + } + + /** + * Allows registering multiple {@link RequestMatcher} instances to a collection of {@link ConfigAttribute} instances + * + * @param requestMatchers the {@link RequestMatcher} instances to register to the {@link ConfigAttribute} instances + * @param configAttributes the {@link ConfigAttribute} to be mapped by the {@link RequestMatcher} instances + * @return the {@link ExpressionUrlAuthorizationConfigurer} for further customization. + */ + private ExpressionUrlAuthorizationConfigurer interceptUrl(Iterable requestMatchers, Collection configAttributes) { + for(RequestMatcher requestMatcher : requestMatchers) { + addMapping(new UrlMapping(requestMatcher, configAttributes)); + } + return this; + } + + private static String hasRole(String role) { + Assert.notNull(role, "role cannot be null"); + if (role.startsWith("ROLE_")) { + throw new IllegalArgumentException("role should not start with 'ROLE_' since it is automatically inserted. Got '" + role + "'"); + } + return "hasRole('ROLE_" + role + "')"; + } + + private static String hasAuthority(String authority) { + return "hasAuthority('" + authority + "')"; + } + + private static String hasAnyAuthority(String... authorities) { + String anyAuthorities = StringUtils.arrayToDelimitedString(authorities, "','"); + return "hasAnyAuthority('" + anyAuthorities + "')"; + } + + private static String hasIpAddress(String ipAddressExpression) { + return "hasIpAddress('" + ipAddressExpression + "')"; + } + + public final class AuthorizedUrl { + private List requestMatchers; + private boolean not; + + /** + * Creates a new instance + * + * @param requestMatchers the {@link RequestMatcher} instances to map + */ + private AuthorizedUrl(List requestMatchers) { + this.requestMatchers = requestMatchers; + } + + /** + * Negates the following expression. + * + * @param role the role to require (i.e. USER, ADMIN, etc). Note, it should not start with "ROLE_" as + * this is automatically inserted. + * @return the {@link ExpressionUrlAuthorizationConfigurer} for further customization + */ + public AuthorizedUrl not() { + this.not = true; + return this; + } + + /** + * Shortcut for specifying URLs require a particular role. If you do not want to have "ROLE_" automatically + * inserted see {@link #hasAuthority(String)}. + * + * @param role the role to require (i.e. USER, ADMIN, etc). Note, it should not start with "ROLE_" as + * this is automatically inserted. + * @return the {@link ExpressionUrlAuthorizationConfigurer} for further customization + */ + public ExpressionUrlAuthorizationConfigurer hasRole(String role) { + return access(ExpressionUrlAuthorizationConfigurer.hasRole(role)); + } + + /** + * Specify that URLs require a particular authority. + * + * @param authority the authority to require (i.e. ROLE_USER, ROLE_ADMIN, etc). + * @return the {@link ExpressionUrlAuthorizationConfigurer} for further customization + */ + public ExpressionUrlAuthorizationConfigurer hasAuthority(String authority) { + return access(ExpressionUrlAuthorizationConfigurer.hasAuthority(authority)); + } + + /** + * Specify that URLs requires any of a number authorities. + * + * @param authorities the requests require at least one of the authorities (i.e. "ROLE_USER","ROLE_ADMIN" would + * mean either "ROLE_USER" or "ROLE_ADMIN" is required). + * @return the {@link ExpressionUrlAuthorizationConfigurer} for further customization + */ + public ExpressionUrlAuthorizationConfigurer hasAnyAuthority(String... authorities) { + return access(ExpressionUrlAuthorizationConfigurer.hasAnyAuthority(authorities)); + } + + /** + * Specify that URLs requires a specific IP Address or + * subnet. + * + * @param ipaddressExpression the ipaddress (i.e. 192.168.1.79) or local subnet (i.e. 192.168.0/24) + * @return the {@link ExpressionUrlAuthorizationConfigurer} for further customization + */ + public ExpressionUrlAuthorizationConfigurer hasIpAddress(String ipaddressExpression) { + return access(ExpressionUrlAuthorizationConfigurer.hasIpAddress(ipaddressExpression)); + } + + /** + * Specify that URLs are allowed by anyone. + * + * @return the {@link ExpressionUrlAuthorizationConfigurer} for further customization + */ + public ExpressionUrlAuthorizationConfigurer permitAll() { + return access(permitAll); + } + + /** + * Specify that URLs are allowed by anonymous users. + * + * @return the {@link ExpressionUrlAuthorizationConfigurer} for further customization + */ + public ExpressionUrlAuthorizationConfigurer anonymous() { + return access(anonymous); + } + + /** + * Specify that URLs are allowed by users that have been remembered. + * + * @return the {@link ExpressionUrlAuthorizationConfigurer} for further customization + * @see {@link RememberMeConfigurer} + */ + public ExpressionUrlAuthorizationConfigurer rememberMe() { + return access(rememberMe); + } + + /** + * Specify that URLs are not allowed by anyone. + * + * @return the {@link ExpressionUrlAuthorizationConfigurer} for further customization + */ + public ExpressionUrlAuthorizationConfigurer denyAll() { + return access(denyAll); + } + + /** + * Specify that URLs are allowed by any authenticated user. + * + * @return the {@link ExpressionUrlAuthorizationConfigurer} for further customization + */ + public ExpressionUrlAuthorizationConfigurer authenticated() { + return access(authenticated); + } + + /** + * Specify that URLs are allowed by users who have authenticated and were not "remembered". + * + * @return the {@link ExpressionUrlAuthorizationConfigurer} for further customization + * @see {@link RememberMeConfigurer} + */ + public ExpressionUrlAuthorizationConfigurer fullyAuthenticated() { + return access(fullyAuthenticated); + } + + /** + * Allows specifying that URLs are secured by an arbitrary expression + * + * @param attribute the expression to secure the URLs (i.e. "hasRole('ROLE_USER') and hasRole('ROLE_SUPER')") + * @return the {@link ExpressionUrlAuthorizationConfigurer} for further customization + */ + public ExpressionUrlAuthorizationConfigurer access(String attribute) { + if(not) { + attribute = "!" + attribute; + } + interceptUrl(requestMatchers, SecurityConfig.createList(attribute)); + return ExpressionUrlAuthorizationConfigurer.this; + } + } +} \ No newline at end of file diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/FormLoginConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/FormLoginConfigurer.java new file mode 100644 index 0000000000..4b5ac73d3d --- /dev/null +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/FormLoginConfigurer.java @@ -0,0 +1,239 @@ +/* + * Copyright 2002-2013 the original author or authors. + * + * 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 org.springframework.security.config.annotation.web.configurers; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.springframework.security.config.annotation.web.HttpSecurityBuilder; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; +import org.springframework.security.web.AuthenticationEntryPoint; +import org.springframework.security.web.authentication.RememberMeServices; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; +import org.springframework.security.web.authentication.session.SessionAuthenticationStrategy; +import org.springframework.security.web.authentication.ui.DefaultLoginPageViewFilter; + +/** + * Adds form based authentication. All attributes have reasonable defaults + * making all parameters are optional. If no {@link #loginPage(String)} is + * specified, a default login page will be generated by the framework. + * + *

Security Filters

+ * + * The following Filters are populated + * + *
    + *
  • + * {@link UsernamePasswordAuthenticationFilter} + *
  • + *
+ * + *

Shared Objects Created

+ * + * The following shared objects are populated + * + *
    + *
  • {@link AuthenticationEntryPoint}
  • + *
+ * + *

Shared Objects Used

+ * + * The following shared objects are used: + * + *
    + *
  • {@link HttpSecurity#getAuthenticationManager()}
  • + *
  • {@link RememberMeServices} - is optionally used. See {@link RememberMeConfigurer}
  • + *
  • {@link SessionAuthenticationStrategy} - is optionally used. See {@link SessionManagementConfigurer}
  • + *
  • {@link DefaultLoginPageViewFilter} - if present will be populated with information from the configuration
  • + *
+ * + * @author Rob Winch + * @since 3.2 + */ +public final class FormLoginConfigurer> extends AbstractAuthenticationFilterConfigurer,UsernamePasswordAuthenticationFilter> { + + /** + * Creates a new instance + * @see HttpSecurity#formLogin() + */ + public FormLoginConfigurer() { + super(createUsernamePasswordAuthenticationFilter(),"/login"); + usernameParameter("username"); + passwordParameter("password"); + } + + /** + *

+ * Specifies the URL to send users to if login is required. If used with + * {@link WebSecurityConfigurerAdapter} a default login page will be + * generated when this attribute is not specified. + *

+ * + *

+ * If a URL is specified or this is not being used in conjuction with + * {@link WebSecurityConfigurerAdapter}, users are required to process the + * specified URL to generate a login page. In general, the login page should + * create a form that submits a request with the following requirements to + * work with {@link UsernamePasswordAuthenticationFilter}: + *

+ * + *
    + *
  • It must be an HTTP POST
  • + *
  • It must be submitted to {@link #loginProcessingUrl(String)}
  • + *
  • It should include the username as an HTTP parameter by the name of + * {@link #usernameParameter(String)}
  • + *
  • It should include the password as an HTTP parameter by the name of + * {@link #passwordParameter(String)}
  • + *
+ * + *

Example login.jsp

+ * + * Login pages can be rendered with any technology you choose so long as the + * rules above are followed. Below is an example login.jsp that can be used as + * a quick start when using JSP's or as a baseline to translate into another view + * technology. + * + *
+     * 
+     * <c:url value="/login" var="loginProcessingUrl"/>
+     * <form action="${loginProcessingUrl}" method="post">
+     *    <fieldset>
+     *        <legend>Please Login</legend>
+     *        <!-- use param.error assuming FormLoginConfigurer#failureUrl contains the query parameter error -->
+     *        <c:if test="${param.error != null}">
+     *            <div>
+     *                Failed to login.
+     *                <c:if test="${SPRING_SECURITY_LAST_EXCEPTION != null}">
+     *                  Reason: <c:out value="${SPRING_SECURITY_LAST_EXCEPTION.message}" />
+     *                </c:if>
+     *            </div>
+     *        </c:if>
+     *        <!-- the configured LogoutConfigurer#logoutSuccessUrl is /login?logout and contains the query param logout -->
+     *        <c:if test="${param.logout != null}">
+     *            <div>
+     *                You have been logged out.
+     *            </div>
+     *        </c:if>
+     *        <p>
+     *        <label for="username">Username</label>
+     *        <input type="text" id="username" name="username"/>
+     *        </p>
+     *        <p>
+     *        <label for="password">Password</label>
+     *        <input type="password" id="password" name="password"/>
+     *        </p>
+     *        <!-- if using RememberMeConfigurer make sure remember-me matches RememberMeConfigurer#rememberMeParameter -->
+     *        <p>
+     *        <label for="remember-me">Remember Me?</label>
+     *        <input type="checkbox" id="remember-me" name="remember-me"/>
+     *        </p>
+     *        <div>
+     *            <button type="submit" class="btn">Log in</button>
+     *        </div>
+     *    </fieldset>
+     * </form>
+     * 
+ * + * @param loginPage + * the login page to redirect to if authentication is required + * (i.e. "/login") + * @return the {@link FormLoginConfigurer} for additional customization + */ + public FormLoginConfigurer loginPage(String loginPage) { + return super.loginPage(loginPage); + } + + + + /** + * The HTTP parameter to look for the username when performing + * authentication. Default is "username". + * + * @param usernameParameter + * the HTTP parameter to look for the username when performing + * authentication + * @return the {@link FormLoginConfigurer} for additional customization + */ + public FormLoginConfigurer usernameParameter(String usernameParameter) { + getAuthenticationFilter().setUsernameParameter(usernameParameter); + return this; + } + + /** + * The HTTP parameter to look for the password when performing + * authentication. Default is "password". + * + * @param passwordParameter + * the HTTP parameter to look for the password when performing + * authentication + * @return the {@link FormLoginConfigurer} for additional customization + */ + public FormLoginConfigurer passwordParameter(String passwordParameter) { + getAuthenticationFilter().setPasswordParameter(passwordParameter); + return this; + } + + @Override + public void init(H http) throws Exception { + super.init(http); + initDefaultLoginFilter(http); + } + + /** + * Gets the HTTP parameter that is used to submit the username. + * + * @return the HTTP parameter that is used to submit the username + */ + private String getUsernameParameter() { + return getAuthenticationFilter().getUsernameParameter(); + } + + /** + * Gets the HTTP parameter that is used to submit the password. + * + * @return the HTTP parameter that is used to submit the password + */ + private String getPasswordParameter() { + return getAuthenticationFilter().getPasswordParameter(); + } + + /** + * If available, initializes the {@link DefaultLoginPageViewFilter} shared object. + * + * @param http the {@link HttpSecurityBuilder} to use + */ + private void initDefaultLoginFilter(H http) { + DefaultLoginPageViewFilter loginPageGeneratingFilter = http.getSharedObject(DefaultLoginPageViewFilter.class); + if(loginPageGeneratingFilter != null && !isCustomLoginPage()) { + loginPageGeneratingFilter.setFormLoginEnabled(true); + loginPageGeneratingFilter.setUsernameParameter(getUsernameParameter()); + loginPageGeneratingFilter.setPasswordParameter(getPasswordParameter()); + loginPageGeneratingFilter.setLoginPageUrl(getLoginPage()); + loginPageGeneratingFilter.setFailureUrl(getFailureUrl()); + loginPageGeneratingFilter.setAuthenticationUrl(getLoginProcessingUrl()); + } + } + + private static UsernamePasswordAuthenticationFilter createUsernamePasswordAuthenticationFilter() { + return new UsernamePasswordAuthenticationFilter() { + @Override + protected boolean requiresAuthentication(HttpServletRequest request, HttpServletResponse response) { + return "POST".equals(request.getMethod()) && super.requiresAuthentication(request, response); + } + }; + } +} diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/HttpBasicConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/HttpBasicConfigurer.java new file mode 100644 index 0000000000..174c080c45 --- /dev/null +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/HttpBasicConfigurer.java @@ -0,0 +1,127 @@ +/* + * Copyright 2002-2013 the original author or authors. + * + * 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 org.springframework.security.config.annotation.web.configurers; + +import javax.servlet.http.HttpServletRequest; + +import org.springframework.security.authentication.AuthenticationDetailsSource; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.config.annotation.web.HttpSecurityBuilder; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.web.AuthenticationEntryPoint; +import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; +import org.springframework.security.web.authentication.www.BasicAuthenticationEntryPoint; +import org.springframework.security.web.authentication.www.BasicAuthenticationFilter; + +/** + * Adds HTTP basic based authentication. All attributes have reasonable defaults + * making all parameters are optional. + * + *

Security Filters

+ * + * The following Filters are populated + * + *
    + *
  • + * {@link BasicAuthenticationFilter} + *
  • + *
+ * + *

Shared Objects Created

+ * + * No shared objects are populated + * + *

Shared Objects Used

+ * + * The following shared objects are used: + * + *
    + *
  • {@link HttpSecurity#getAuthenticationManager()}
  • + *
+ * + * @author Rob Winch + * @since 3.2 + */ +public final class HttpBasicConfigurer> extends AbstractHttpConfigurer { + private static final String DEFAULT_REALM = "Spring Security Application"; + + private AuthenticationEntryPoint authenticationEntryPoint; + private AuthenticationDetailsSource authenticationDetailsSource; + + /** + * Creates a new instance + * @throws Exception + * @see {@link HttpSecurity#httpBasic()} + */ + public HttpBasicConfigurer() throws Exception { + realmName(DEFAULT_REALM); + } + + /** + * Shortcut for {@link #authenticationEntryPoint(AuthenticationEntryPoint)} + * specifying a {@link BasicAuthenticationEntryPoint} with the specified + * realm name. + * + * @param realmName + * the HTTP Basic realm to use + * @return {@link HttpBasicConfigurer} for additional customization + * @throws Exception + */ + public HttpBasicConfigurer realmName(String realmName) throws Exception { + BasicAuthenticationEntryPoint basicAuthEntryPoint = new BasicAuthenticationEntryPoint(); + basicAuthEntryPoint.setRealmName(realmName); + basicAuthEntryPoint.afterPropertiesSet(); + return authenticationEntryPoint(basicAuthEntryPoint); + } + + /** + * The {@link AuthenticationEntryPoint} to be po pulated on + * {@link BasicAuthenticationFilter} in the event that authentication fails. + * The default to use {@link BasicAuthenticationEntryPoint} with the realm + * "Spring Security Application". + * + * @param authenticationEntryPoint the {@link AuthenticationEntryPoint} to use + * @return {@link HttpBasicConfigurer} for additional customization + */ + public HttpBasicConfigurer authenticationEntryPoint(AuthenticationEntryPoint authenticationEntryPoint) { + this.authenticationEntryPoint = authenticationEntryPoint; + return this; + } + + /** + * Specifies a custom {@link AuthenticationDetailsSource} to use for basic + * authentication. The default is {@link WebAuthenticationDetailsSource}. + * + * @param authenticationDetailsSource + * the custom {@link AuthenticationDetailsSource} to use + * @return {@link HttpBasicConfigurer} for additional customization + */ + public HttpBasicConfigurer authenticationDetailsSource(AuthenticationDetailsSource authenticationDetailsSource) { + this.authenticationDetailsSource = authenticationDetailsSource; + return this; + } + + @Override + public void configure(B http) throws Exception { + AuthenticationManager authenticationManager = http.getAuthenticationManager(); + BasicAuthenticationFilter basicAuthenticationFilter = new BasicAuthenticationFilter(authenticationManager, authenticationEntryPoint); + if(authenticationDetailsSource != null) { + basicAuthenticationFilter.setAuthenticationDetailsSource(authenticationDetailsSource); + } + basicAuthenticationFilter = postProcess(basicAuthenticationFilter); + http.addFilter(basicAuthenticationFilter); + } +} diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/JeeConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/JeeConfigurer.java new file mode 100644 index 0000000000..ba9c957d3c --- /dev/null +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/JeeConfigurer.java @@ -0,0 +1,261 @@ +/* + * Copyright 2002-2013 the original author or authors. + * + * 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 org.springframework.security.config.annotation.web.configurers; + +import java.util.HashSet; +import java.util.Set; + +import javax.servlet.http.HttpServletRequest; + +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.config.annotation.web.HttpSecurityBuilder; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.core.authority.mapping.SimpleMappableAttributesRetriever; +import org.springframework.security.core.userdetails.AuthenticationUserDetailsService; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.web.AuthenticationEntryPoint; +import org.springframework.security.web.authentication.Http403ForbiddenEntryPoint; +import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationProvider; +import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken; +import org.springframework.security.web.authentication.preauth.PreAuthenticatedGrantedAuthoritiesUserDetailsService; +import org.springframework.security.web.authentication.preauth.j2ee.J2eeBasedPreAuthenticatedWebAuthenticationDetailsSource; +import org.springframework.security.web.authentication.preauth.j2ee.J2eePreAuthenticatedProcessingFilter; + +/** + * Adds support for J2EE pre authentication. + * + *

Security Filters

+ * + * The following Filters are populated + * + *
    + *
  • + * {@link J2eePreAuthenticatedProcessingFilter}
  • + *
+ * + *

Shared Objects Created

+ * + *
    + *
  • + * {@link AuthenticationEntryPoint} + * is populated with an {@link Http403ForbiddenEntryPoint}
  • + *
  • A {@link PreAuthenticatedAuthenticationProvider} is populated into + * {@link HttpSecurity#authenticationProvider(org.springframework.security.authentication.AuthenticationProvider)} + *
  • + *
+ * + *

Shared Objects Used

+ * + * The following shared objects are used: + * + *
    + *
  • {@link HttpSecurity#getAuthenticationManager()}
  • + *
+ * + * @author Rob Winch + * @since 3.2 + */ +public final class JeeConfigurer> extends AbstractHttpConfigurer { + private J2eePreAuthenticatedProcessingFilter j2eePreAuthenticatedProcessingFilter; + private AuthenticationUserDetailsService authenticationUserDetailsService; + private Set mappableRoles = new HashSet(); + + /** + * Creates a new instance + * @see HttpSecurity#jee() + */ + public JeeConfigurer() { + } + + /** + * Specifies roles to use map from the {@link HttpServletRequest} to the + * {@link UserDetails}. If {@link HttpServletRequest#isUserInRole(String)} + * returns true, the role is added to the {@link UserDetails}. This method + * is the equivalent of invoking {@link #mappableAuthorities(Set)}. Multiple + * invocations of {@link #mappableAuthorities(String...)} will override previous + * invocations. + * + *

+ * There are no default roles that are mapped. + *

+ * + * @param mappableRoles + * the roles to attempt to map to the {@link UserDetails} (i.e. + * "ROLE_USER", "ROLE_ADMIN", etc). + * @return the {@link JeeConfigurer} for further customizations + * @see SimpleMappableAttributesRetriever + * @see #mappableRoles(String...) + */ + public JeeConfigurer mappableAuthorities(String... mappableRoles) { + this.mappableRoles.clear(); + for(String role : mappableRoles) { + this.mappableRoles.add(role); + } + return this; + } + + /** + * Specifies roles to use map from the {@link HttpServletRequest} to the + * {@link UserDetails} and automatically prefixes it with "ROLE_". If + * {@link HttpServletRequest#isUserInRole(String)} returns true, the role is + * added to the {@link UserDetails}. This method is the equivalent of + * invoking {@link #mappableAuthorities(Set)}. Multiple invocations of + * {@link #mappableRoles(String...)} will override previous invocations. + * + *

+ * There are no default roles that are mapped. + *

+ * + * @param mappableRoles + * the roles to attempt to map to the {@link UserDetails} (i.e. + * "USER", "ADMIN", etc). + * @return the {@link JeeConfigurer} for further customizations + * @see SimpleMappableAttributesRetriever + * @see #mappableAuthorities(String...) + */ + public JeeConfigurer mappableRoles(String... mappableRoles) { + this.mappableRoles.clear(); + for(String role : mappableRoles) { + this.mappableRoles.add("ROLE_" + role); + } + return this; + } + + /** + * Specifies roles to use map from the {@link HttpServletRequest} to the + * {@link UserDetails}. If {@link HttpServletRequest#isUserInRole(String)} + * returns true, the role is added to the {@link UserDetails}. This is the + * equivalent of {@link #mappableRoles(String...)}. Multiple invocations of + * {@link #mappableAuthorities(Set)} will override previous invocations. + * + *

+ * There are no default roles that are mapped. + *

+ * + * @param mappableRoles + * the roles to attempt to map to the {@link UserDetails}. + * @return the {@link JeeConfigurer} for further customizations + * @see SimpleMappableAttributesRetriever + */ + public JeeConfigurer mappableAuthorities(Set mappableRoles) { + this.mappableRoles = mappableRoles; + return this; + } + + /** + * Specifies the {@link AuthenticationUserDetailsService} that is used with + * the {@link PreAuthenticatedAuthenticationProvider}. The default is a + * {@link PreAuthenticatedGrantedAuthoritiesUserDetailsService}. + * + * @param authenticatedUserDetailsService the {@link AuthenticationUserDetailsService} to use. + * @return the {@link JeeConfigurer} for further configuration + */ + public JeeConfigurer authenticatedUserDetailsService( + AuthenticationUserDetailsService authenticatedUserDetailsService) { + this.authenticationUserDetailsService = authenticatedUserDetailsService; + return this; + } + + /** + * Allows specifying the {@link J2eePreAuthenticatedProcessingFilter} to + * use. If {@link J2eePreAuthenticatedProcessingFilter} is provided, all of its attributes must also be + * configured manually (i.e. all attributes populated in the {@link JeeConfigurer} are not used). + * + * @param j2eePreAuthenticatedProcessingFilter the {@link J2eePreAuthenticatedProcessingFilter} to use. + * @return the {@link JeeConfigurer} for further configuration + */ + public JeeConfigurer j2eePreAuthenticatedProcessingFilter( + J2eePreAuthenticatedProcessingFilter j2eePreAuthenticatedProcessingFilter) { + this.j2eePreAuthenticatedProcessingFilter = j2eePreAuthenticatedProcessingFilter; + return this; + } + + /** + * Populates a {@link PreAuthenticatedAuthenticationProvider} into + * {@link HttpSecurity#authenticationProvider(org.springframework.security.authentication.AuthenticationProvider)} + * and a {@link Http403ForbiddenEntryPoint} into + * {@link HttpSecurity#authenticationEntryPoint(org.springframework.security.web.AuthenticationEntryPoint)} + * + * @see org.springframework.security.config.annotation.SecurityConfigurerAdapter#init(org.springframework.security.config.annotation.SecurityBuilder) + */ + @Override + public void init(H http) throws Exception { + PreAuthenticatedAuthenticationProvider authenticationProvider = new PreAuthenticatedAuthenticationProvider(); + authenticationProvider.setPreAuthenticatedUserDetailsService(getUserDetailsService()); + authenticationProvider = postProcess(authenticationProvider); + + http + .authenticationProvider(authenticationProvider) + .setSharedObject(AuthenticationEntryPoint.class,new Http403ForbiddenEntryPoint()); + } + + @Override + public void configure(H http) throws Exception { + J2eePreAuthenticatedProcessingFilter filter = getFilter(http + .getAuthenticationManager()); + http.addFilter(filter); + } + + /** + * Gets the {@link J2eePreAuthenticatedProcessingFilter} or creates a default instance using the properties provided. + * @param authenticationManager the {@link AuthenticationManager} to use. + * @return the {@link J2eePreAuthenticatedProcessingFilter} to use. + */ + private J2eePreAuthenticatedProcessingFilter getFilter( + AuthenticationManager authenticationManager) { + if (j2eePreAuthenticatedProcessingFilter == null) { + j2eePreAuthenticatedProcessingFilter = new J2eePreAuthenticatedProcessingFilter(); + j2eePreAuthenticatedProcessingFilter + .setAuthenticationManager(authenticationManager); + j2eePreAuthenticatedProcessingFilter + .setAuthenticationDetailsSource(createWebAuthenticationDetailsSource()); + j2eePreAuthenticatedProcessingFilter = postProcess(j2eePreAuthenticatedProcessingFilter); + } + + return j2eePreAuthenticatedProcessingFilter; + } + + /** + * Gets the {@link AuthenticationUserDetailsService} that was specified or + * defaults to {@link PreAuthenticatedGrantedAuthoritiesUserDetailsService}. + * + * @return the {@link AuthenticationUserDetailsService} to use + */ + private AuthenticationUserDetailsService getUserDetailsService() { + return authenticationUserDetailsService == null ? new PreAuthenticatedGrantedAuthoritiesUserDetailsService() + : authenticationUserDetailsService; + } + + /** + * Creates the + * {@link J2eeBasedPreAuthenticatedWebAuthenticationDetailsSource} to set on + * the {@link J2eePreAuthenticatedProcessingFilter}. It is populated with a + * {@link SimpleMappableAttributesRetriever}. + * + * @return the + * {@link J2eeBasedPreAuthenticatedWebAuthenticationDetailsSource} + * to use. + */ + private J2eeBasedPreAuthenticatedWebAuthenticationDetailsSource createWebAuthenticationDetailsSource() { + J2eeBasedPreAuthenticatedWebAuthenticationDetailsSource detailsSource = new J2eeBasedPreAuthenticatedWebAuthenticationDetailsSource(); + SimpleMappableAttributesRetriever rolesRetriever = new SimpleMappableAttributesRetriever(); + rolesRetriever.setMappableAttributes(mappableRoles); + detailsSource.setMappableRolesRetriever(rolesRetriever); + + detailsSource = postProcess(detailsSource); + return detailsSource; + } +} \ No newline at end of file diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/LogoutConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/LogoutConfigurer.java new file mode 100644 index 0000000000..238b8a7997 --- /dev/null +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/LogoutConfigurer.java @@ -0,0 +1,243 @@ +/* + * Copyright 2002-2013 the original author or authors. + * + * 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 org.springframework.security.config.annotation.web.configurers; + +import java.util.ArrayList; +import java.util.List; + +import javax.servlet.http.HttpSession; + +import org.springframework.security.config.annotation.SecurityConfigurer; +import org.springframework.security.config.annotation.web.HttpSecurityBuilder; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.web.authentication.logout.CookieClearingLogoutHandler; +import org.springframework.security.web.authentication.logout.LogoutFilter; +import org.springframework.security.web.authentication.logout.LogoutHandler; +import org.springframework.security.web.authentication.logout.LogoutSuccessHandler; +import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler; +import org.springframework.security.web.authentication.logout.SimpleUrlLogoutSuccessHandler; +import org.springframework.security.web.authentication.ui.DefaultLoginPageViewFilter; + +/** + * Adds logout support. Other {@link SecurityConfigurer} instances may invoke + * {@link #addLogoutHandler(LogoutHandler)} in the + * {@link #init(HttpSecurity)} phase. + * + *

Security Filters

+ * + * The following Filters are populated + * + *
    + *
  • + * {@link LogoutFilter}
  • + *
+ * + *

Shared Objects Created

+ * + * No shared Objects are created + * + *

Shared Objects Used

+ * + * No shared objects are used. + * + * @author Rob Winch + * @since 3.2 + * @see RememberMeConfigurer + */ +public final class LogoutConfigurer> extends AbstractHttpConfigurer { + private List logoutHandlers = new ArrayList(); + private SecurityContextLogoutHandler contextLogoutHandler = new SecurityContextLogoutHandler(); + private String logoutSuccessUrl = "/login?logout"; + private LogoutSuccessHandler logoutSuccessHandler; + private String logoutUrl = "/logout"; + private boolean permitAll; + private boolean customLogoutSuccess; + + /** + * Creates a new instance + * @see HttpSecurity#logout() + */ + public LogoutConfigurer() { + } + + /** + * Adds a {@link LogoutHandler}. The {@link SecurityContextLogoutHandler} is + * added as the last {@link LogoutHandler} by default. + * + * @param logoutHandler the {@link LogoutHandler} to add + * @return the {@link LogoutConfigurer} for further customization + */ + public LogoutConfigurer addLogoutHandler(LogoutHandler logoutHandler) { + this.logoutHandlers.add(logoutHandler); + return this; + } + + /** + * Configures {@link SecurityContextLogoutHandler} to invalidate the {@link HttpSession} at the time of logout. + * @param invalidateHttpSession true if the {@link HttpSession} should be invalidated (default), or false otherwise. + * @return the {@link LogoutConfigurer} for further customization + */ + public LogoutConfigurer invalidateHttpSession(boolean invalidateHttpSession) { + contextLogoutHandler.setInvalidateHttpSession(invalidateHttpSession); + return this; + } + + /** + * The URL that triggers logout to occur. The default is "/logout" + * @param logoutUrl the URL that will invoke logout. + * @return the {@link LogoutConfigurer} for further customization + */ + public LogoutConfigurer logoutUrl(String logoutUrl) { + this.logoutUrl = logoutUrl; + return this; + } + + /** + * The URL to redirect to after logout has occurred. The default is + * "/login?logout". This is a shortcut for invoking + * {@link #logoutSuccessHandler(LogoutSuccessHandler)} with a + * {@link SimpleUrlLogoutSuccessHandler}. + * + * @param logoutSuccessUrl + * the URL to redirect to after logout occurred + * @return the {@link LogoutConfigurer} for further customization + */ + public LogoutConfigurer logoutSuccessUrl(String logoutSuccessUrl) { + this.customLogoutSuccess = true; + this.logoutSuccessUrl = logoutSuccessUrl; + return this; + } + + /** + * A shortcut for {@link #permitAll(boolean)} with true as an argument. + * @return the {@link LogoutConfigurer} for further customizations + */ + public LogoutConfigurer permitAll() { + return permitAll(true); + } + + /** + * Allows specifying the names of cookies to be removed on logout success. + * This is a shortcut to easily invoke + * {@link #addLogoutHandler(LogoutHandler)} with a + * {@link CookieClearingLogoutHandler}. + * + * @param cookieNamesToClear the names of cookies to be removed on logout success. + * @return the {@link LogoutConfigurer} for further customization + */ + public LogoutConfigurer deleteCookies(String... cookieNamesToClear) { + return addLogoutHandler(new CookieClearingLogoutHandler(cookieNamesToClear)); + } + + /** + * Sets the {@link LogoutSuccessHandler} to use. If this is specified, + * {@link #logoutSuccessUrl(String)} is ignored. + * + * @param logoutSuccessHandler + * the {@link LogoutSuccessHandler} to use after a user has been + * logged out. + * @return the {@link LogoutConfigurer} for further customizations + */ + public LogoutConfigurer logoutSuccessHandler(LogoutSuccessHandler logoutSuccessHandler) { + this.logoutSuccessUrl = null; + this.customLogoutSuccess = true; + this.logoutSuccessHandler = logoutSuccessHandler; + return this; + } + + /** + * Grants access to the {@link #logoutSuccessUrl(String)} and the {@link #logoutUrl(String)} for every user. + * + * @param permitAll if true grants access, else nothing is done + * @return the {@link LogoutConfigurer} for further customization. + */ + public LogoutConfigurer permitAll(boolean permitAll) { + this.permitAll = permitAll; + return this; + } + + /** + * Gets the {@link LogoutSuccessHandler} if not null, otherwise creates a + * new {@link SimpleUrlLogoutSuccessHandler} using the + * {@link #logoutSuccessUrl(String)}. + * + * @return the {@link LogoutSuccessHandler} to use + */ + private LogoutSuccessHandler getLogoutSuccessHandler() { + if(logoutSuccessHandler != null) { + return logoutSuccessHandler; + } + SimpleUrlLogoutSuccessHandler logoutSuccessHandler = new SimpleUrlLogoutSuccessHandler(); + logoutSuccessHandler.setDefaultTargetUrl(logoutSuccessUrl); + return logoutSuccessHandler; + } + + @Override + public void init(H http) throws Exception { + if(permitAll) { + PermitAllSupport.permitAll(http, this.logoutUrl, this.logoutSuccessUrl); + } + + DefaultLoginPageViewFilter loginPageGeneratingFilter = http.getSharedObject(DefaultLoginPageViewFilter.class); + if(loginPageGeneratingFilter != null && !isCustomLogoutSuccess()) { + loginPageGeneratingFilter.setLogoutSuccessUrl(getLogoutSuccessUrl()); + } + } + + @Override + public void configure(H http) throws Exception { + LogoutFilter logoutFilter = createLogoutFilter(); + http.addFilter(logoutFilter); + } + + /** + * Returns true if the logout success has been customized via + * {@link #logoutSuccessUrl(String)} or + * {@link #logoutSuccessHandler(LogoutSuccessHandler)}. + * + * @return true if logout success handling has been customized, else false + */ + private boolean isCustomLogoutSuccess() { + return customLogoutSuccess; + } + + /** + * Gets the logoutSuccesUrl or null if a + * {@link #logoutSuccessHandler(LogoutSuccessHandler)} was configured. + * + * @return the logoutSuccessUrl + */ + private String getLogoutSuccessUrl() { + return logoutSuccessUrl; + } + + /** + * Creates the {@link LogoutFilter} using the {@link LogoutHandler} + * instances, the {@link #logoutSuccessHandler(LogoutSuccessHandler)} and + * the {@link #logoutUrl(String)}. + * + * @return the {@link LogoutFilter} to use. + * @throws Exception + */ + private LogoutFilter createLogoutFilter() throws Exception { + logoutHandlers.add(contextLogoutHandler); + LogoutHandler[] handlers = logoutHandlers.toArray(new LogoutHandler[logoutHandlers.size()]); + LogoutFilter result = new LogoutFilter(getLogoutSuccessHandler(), handlers); + result.setFilterProcessesUrl(logoutUrl); + result = postProcess(result); + return result; + } +} \ No newline at end of file diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/PermitAllSupport.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/PermitAllSupport.java new file mode 100644 index 0000000000..606679bbd0 --- /dev/null +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/PermitAllSupport.java @@ -0,0 +1,73 @@ +/* + * Copyright 2002-2013 the original author or authors. + * + * 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 org.springframework.security.config.annotation.web.configurers; + + +import javax.servlet.http.HttpServletRequest; + +import org.springframework.security.access.SecurityConfig; +import org.springframework.security.config.annotation.web.HttpSecurityBuilder; +import org.springframework.security.config.annotation.web.configurers.AbstractRequestMatcherMappingConfigurer.UrlMapping; +import org.springframework.security.web.util.RequestMatcher; + + +/** + * Configures non-null URL's to grant access to every URL + * @author Rob Winch + * @since 3.2 + */ +final class PermitAllSupport { + + @SuppressWarnings("unchecked") + public static void permitAll(HttpSecurityBuilder> http, String... urls) { + ExpressionUrlAuthorizationConfigurer configurer = http.getConfigurer(ExpressionUrlAuthorizationConfigurer.class); + + if(configurer == null) { + throw new IllegalStateException("permitAll only works with HttpSecurity.authorizeUrls()"); + } + + for(String url : urls) { + if(url != null) { + configurer.addMapping(0, new UrlMapping(new ExactUrlRequestMatcher(url), SecurityConfig.createList(ExpressionUrlAuthorizationConfigurer.permitAll))); + } + } + } + + private final static class ExactUrlRequestMatcher implements RequestMatcher { + private String processUrl; + + private ExactUrlRequestMatcher(String processUrl) { + this.processUrl = processUrl; + } + + public boolean matches(HttpServletRequest request) { + String uri = request.getRequestURI(); + String query = request.getQueryString(); + + if(query != null) { + uri += "?" + query; + } + + if ("".equals(request.getContextPath())) { + return uri.equals(processUrl); + } + + return uri.equals(request.getContextPath() + processUrl); + } + } + + private PermitAllSupport() {} +} \ No newline at end of file diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/PortMapperConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/PortMapperConfigurer.java new file mode 100644 index 0000000000..33c5f65587 --- /dev/null +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/PortMapperConfigurer.java @@ -0,0 +1,115 @@ +/* + * Copyright 2002-2013 the original author or authors. + * + * 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 org.springframework.security.config.annotation.web.configurers; + +import java.util.HashMap; +import java.util.Map; + +import org.springframework.security.config.annotation.SecurityConfigurerAdapter; +import org.springframework.security.config.annotation.web.HttpSecurityBuilder; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.web.DefaultSecurityFilterChain; +import org.springframework.security.web.PortMapper; +import org.springframework.security.web.PortMapperImpl; + +/** + * Allows configuring a shared {@link PortMapper} instance used to determine the + * ports when redirecting between HTTP and HTTPS. The {@link PortMapper} can be + * obtained from {@link HttpSecurity#getSharedObject(Class)}. + * + * @author Rob Winch + * @since 3.2 + */ +public final class PortMapperConfigurer> extends SecurityConfigurerAdapter { + private PortMapper portMapper; + private Map httpsPortMappings = new HashMap(); + + /** + * Creates a new instance + */ + public PortMapperConfigurer() { + } + + /** + * Allows specifying the {@link PortMapper} instance. + * @param portMapper + * @return + */ + public PortMapperConfigurer portMapper(PortMapper portMapper) { + this.portMapper = portMapper; + return this; + } + + /** + * Adds a port mapping + * @param httpPort the HTTP port that maps to a specific HTTPS port. + * @return {@link HttpPortMapping} to define the HTTPS port + */ + public HttpPortMapping http(int httpPort) { + return new HttpPortMapping(httpPort); + } + + @Override + public void init(H http) throws Exception { + http.setSharedObject(PortMapper.class, getPortMapper()); + } + + /** + * Gets the {@link PortMapper} to use. If {@link #portMapper(PortMapper)} + * was not invoked, builds a {@link PortMapperImpl} using the port mappings + * specified with {@link #http(int)}. + * + * @return the {@link PortMapper} to use + */ + private PortMapper getPortMapper() { + if(portMapper == null) { + PortMapperImpl portMapper = new PortMapperImpl(); + portMapper.setPortMappings(httpsPortMappings); + this.portMapper = portMapper; + } + return portMapper; + } + + /** + * Allows specifying the HTTPS port for a given HTTP port when redirecting + * between HTTP and HTTPS. + * + * @author Rob Winch + * @since 3.2 + */ + public final class HttpPortMapping { + private final int httpPort; + + /** + * Creates a new instance + * @param httpPort + * @see PortMapperConfigurer#http(int) + */ + private HttpPortMapping(int httpPort) { + this.httpPort = httpPort; + } + + /** + * Maps the given HTTP port to the provided HTTPS port and vice versa. + * @param httpsPort the HTTPS port to map to + * @return the {@link PortMapperConfigurer} for further customization + */ + public PortMapperConfigurer mapsTo(int httpsPort) { + httpsPortMappings.put(String.valueOf(httpPort), String.valueOf(httpsPort)); + return PortMapperConfigurer.this; + } + } +} diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/RememberMeConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/RememberMeConfigurer.java new file mode 100644 index 0000000000..47defc4c15 --- /dev/null +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/RememberMeConfigurer.java @@ -0,0 +1,352 @@ +/* + * Copyright 2002-2013 the original author or authors. + * + * 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 org.springframework.security.config.annotation.web.configurers; + +import java.util.UUID; + +import org.springframework.security.authentication.RememberMeAuthenticationProvider; +import org.springframework.security.config.annotation.web.HttpSecurityBuilder; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.web.authentication.AuthenticationSuccessHandler; +import org.springframework.security.web.authentication.RememberMeServices; +import org.springframework.security.web.authentication.logout.LogoutHandler; +import org.springframework.security.web.authentication.rememberme.AbstractRememberMeServices; +import org.springframework.security.web.authentication.rememberme.PersistentTokenBasedRememberMeServices; +import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository; +import org.springframework.security.web.authentication.rememberme.RememberMeAuthenticationFilter; +import org.springframework.security.web.authentication.rememberme.TokenBasedRememberMeServices; +import org.springframework.security.web.authentication.ui.DefaultLoginPageViewFilter; + +/** + * Configures Remember Me authentication. This typically involves the user + * checking a box when they enter their username and password that states to + * "Remember Me". + * + *

Security Filters

+ * + * The following Filters are populated + * + *
    + *
  • + * {@link RememberMeAuthenticationFilter}
  • + *
+ * + *

Shared Objects Created

+ * + * The following shared objects are populated + * + *
    + *
  • + * {@link HttpSecurity#authenticationProvider(org.springframework.security.authentication.AuthenticationProvider)} + * is populated with a {@link RememberMeAuthenticationProvider}
  • + *
  • {@link RememberMeServices} is populated as a shared object and available on {@link HttpSecurity#getSharedObject(Class)}
  • + *
  • {@link LogoutConfigurer#addLogoutHandler(LogoutHandler)} is used to add a logout handler to clean up the remember me authentication.
  • + *
+ * + *

Shared Objects Used

+ * + * The following shared objects are used: + * + *
    + *
  • {@link HttpSecurity#getAuthenticationManager()}
  • + *
  • {@link UserDetailsService} if no {@link #userDetailsService(UserDetailsService)} was specified.
  • + *
  • {@link DefaultLoginPageViewFilter} - if present will be populated with information from the configuration
  • + *
+ * + * @author Rob Winch + * @since 3.2 + */ +public final class RememberMeConfigurer> extends AbstractHttpConfigurer { + private AuthenticationSuccessHandler authenticationSuccessHandler; + private String key; + private RememberMeServices rememberMeServices; + private LogoutHandler logoutHandler; + private String rememberMeParameter = "remember-me"; + private String rememberMeCookieName = "remember-me"; + private PersistentTokenRepository tokenRepository; + private UserDetailsService userDetailsService; + private Integer tokenValiditySeconds; + private Boolean useSecureCookie; + + /** + * Creates a new instance + */ + public RememberMeConfigurer() { + } + + /** + * Allows specifying how long (in seconds) a token is valid for + * + * @param tokenValiditySeconds + * @return {@link RememberMeConfigurer} for further customization + * @see AbstractRememberMeServices#setTokenValiditySeconds(int) + */ + public RememberMeConfigurer tokenValiditySeconds(int tokenValiditySeconds) { + this.tokenValiditySeconds = tokenValiditySeconds; + return this; + } + + /** + *Whether the cookie should be flagged as secure or not. Secure cookies can only be sent over an HTTPS connection + * and thus cannot be accidentally submitted over HTTP where they could be intercepted. + *

+ * By default the cookie will be secure if the request is secure. If you only want to use remember-me over + * HTTPS (recommended) you should set this property to {@code true}. + * + * @param useSecureCookie set to {@code true} to always user secure cookies, {@code false} to disable their use. + * @return the {@link RememberMeConfigurer} for further customization + * @see AbstractRememberMeServices#setUseSecureCookie(boolean) + */ + public RememberMeConfigurer useSecureCookie(boolean useSecureCookie) { + this.useSecureCookie = useSecureCookie; + return this; + } + + /** + * Specifies the {@link UserDetailsService} used to look up the + * {@link UserDetails} when a remember me token is valid. The default is to + * use the {@link UserDetailsService} found by invoking + * {@link HttpSecurity#getSharedObject(Class)} which is set when using + * {@link WebSecurityConfigurerAdapter#registerAuthentication(org.springframework.security.config.annotation.authentication.AuthenticationManagerBuilder)}. + * Alternatively, one can populate {@link #rememberMeServices(RememberMeServices)}. + * + * @param userDetailsService + * the {@link UserDetailsService} to configure + * @return the {@link RememberMeConfigurer} for further customization + * @see AbstractRememberMeServices + */ + public RememberMeConfigurer userDetailsService(UserDetailsService userDetailsService) { + this.userDetailsService = userDetailsService; + return this; + } + + /** + * Specifies the {@link PersistentTokenRepository} to use. The default is to + * use {@link TokenBasedRememberMeServices} instead. + * + * @param tokenRepository + * the {@link PersistentTokenRepository} to use + * @return the {@link RememberMeConfigurer} for further customization + */ + public RememberMeConfigurer tokenRepository(PersistentTokenRepository tokenRepository) { + this.tokenRepository = tokenRepository; + return this; + } + + /** + * Sets the key to identify tokens created for remember me authentication. Default is a secure randomly generated + * key. + * + * @param key the key to identify tokens created for remember me authentication + * @return the {@link RememberMeConfigurer} for further customization + */ + public RememberMeConfigurer key(String key) { + this.key = key; + return this; + } + + /** + * Allows control over the destination a remembered user is sent to when they are successfully authenticated. + * By default, the filter will just allow the current request to proceed, but if an + * {@code AuthenticationSuccessHandler} is set, it will be invoked and the {@code doFilter()} method will return + * immediately, thus allowing the application to redirect the user to a specific URL, regardless of what the original + * request was for. + * + * @param authenticationSuccessHandler the strategy to invoke immediately before returning from {@code doFilter()}. + * @return {@link RememberMeConfigurer} for further customization + * @see RememberMeAuthenticationFilter#setAuthenticationSuccessHandler(AuthenticationSuccessHandler) + */ + public RememberMeConfigurer authenticationSuccessHandler(AuthenticationSuccessHandler authenticationSuccessHandler) { + this.authenticationSuccessHandler = authenticationSuccessHandler; + return this; + } + + /** + * Specify the {@link RememberMeServices} to use. + * @param rememberMeServices the {@link RememberMeServices} to use + * @return the {@link RememberMeConfigurer} for further customizations + * @see RememberMeServices + */ + public RememberMeConfigurer rememberMeServices(RememberMeServices rememberMeServices) { + this.rememberMeServices = rememberMeServices; + return this; + } + + @SuppressWarnings("unchecked") + @Override + public void init(H http) throws Exception { + String key = getKey(); + RememberMeServices rememberMeServices = getRememberMeServices(http, key); + http.setSharedObject(RememberMeServices.class, rememberMeServices); + LogoutConfigurer logoutConfigurer = http.getConfigurer(LogoutConfigurer.class); + if(logoutConfigurer != null) { + logoutConfigurer.addLogoutHandler(logoutHandler); + } + + RememberMeAuthenticationProvider authenticationProvider = new RememberMeAuthenticationProvider( + key); + authenticationProvider = postProcess(authenticationProvider); + http.authenticationProvider(authenticationProvider); + + initDefaultLoginFilter(http); + } + + @Override + public void configure(H http) throws Exception { + RememberMeAuthenticationFilter rememberMeFilter = new RememberMeAuthenticationFilter( + http.getAuthenticationManager(), rememberMeServices); + if (authenticationSuccessHandler != null) { + rememberMeFilter + .setAuthenticationSuccessHandler(authenticationSuccessHandler); + } + rememberMeFilter = postProcess(rememberMeFilter); + http.addFilter(rememberMeFilter); + } + + /** + * Returns the HTTP parameter used to indicate to remember the user at time of login. + * @return the HTTP parameter used to indicate to remember the user + */ + private String getRememberMeParameter() { + return rememberMeParameter; + } + + /** + * If available, initializes the {@link DefaultLoginPageViewFilter} shared object. + * + * @param http the {@link HttpSecurityBuilder} to use + */ + private void initDefaultLoginFilter(H http) { + DefaultLoginPageViewFilter loginPageGeneratingFilter = http.getSharedObject(DefaultLoginPageViewFilter.class); + if(loginPageGeneratingFilter != null) { + loginPageGeneratingFilter.setRememberMeParameter(getRememberMeParameter()); + } + } + + /** + * Gets the {@link RememberMeServices} or creates the {@link RememberMeServices}. + * @param http the {@link HttpSecurity} to lookup shared objects + * @param key the {@link #key(String)} + * @return the {@link RememberMeServices} to use + * @throws Exception + */ + private RememberMeServices getRememberMeServices(H http, + String key) throws Exception { + if (rememberMeServices != null) { + if (rememberMeServices instanceof LogoutHandler + && logoutHandler == null) { + this.logoutHandler = (LogoutHandler) rememberMeServices; + } + return rememberMeServices; + } + AbstractRememberMeServices tokenRememberMeServices = createRememberMeServices( + http, key); + tokenRememberMeServices.setParameter(rememberMeParameter); + tokenRememberMeServices.setCookieName(rememberMeCookieName); + if (tokenValiditySeconds != null) { + tokenRememberMeServices + .setTokenValiditySeconds(tokenValiditySeconds); + } + if (useSecureCookie != null) { + tokenRememberMeServices.setUseSecureCookie(useSecureCookie); + } + tokenRememberMeServices.afterPropertiesSet(); + logoutHandler = tokenRememberMeServices; + rememberMeServices = tokenRememberMeServices; + return tokenRememberMeServices; + } + + /** + * Creates the {@link RememberMeServices} to use when none is provided. The + * result is either {@link PersistentTokenRepository} (if a + * {@link PersistentTokenRepository} is specified, else + * {@link TokenBasedRememberMeServices}. + * + * @param http the {@link HttpSecurity} to lookup shared objects + * @param key the {@link #key(String)} + * @return the {@link RememberMeServices} to use + * @throws Exception + */ + private AbstractRememberMeServices createRememberMeServices( + H http, String key) throws Exception { + return tokenRepository == null ? createTokenBasedRememberMeServices( + http, key) : createPersistentRememberMeServices(http, key); + } + + /** + * Creates {@link TokenBasedRememberMeServices} + * + * @param http the {@link HttpSecurity} to lookup shared objects + * @param key the {@link #key(String)} + * @return the {@link TokenBasedRememberMeServices} + */ + private AbstractRememberMeServices createTokenBasedRememberMeServices( + H http, String key) { + UserDetailsService userDetailsService = getUserDetailsService(http); + return new TokenBasedRememberMeServices(key, userDetailsService); + } + + /** + * Creates {@link PersistentTokenBasedRememberMeServices} + * + * @param http the {@link HttpSecurity} to lookup shared objects + * @param key the {@link #key(String)} + * @return the {@link PersistentTokenBasedRememberMeServices} + */ + private AbstractRememberMeServices createPersistentRememberMeServices( + H http, String key) { + UserDetailsService userDetailsService = getUserDetailsService(http); + return new PersistentTokenBasedRememberMeServices(key, + userDetailsService, tokenRepository); + } + + /** + * Gets the {@link UserDetailsService} to use. Either the explicitly + * configure {@link UserDetailsService} from + * {@link #userDetailsService(UserDetailsService)} or a shared object from + * {@link HttpSecurity#getSharedObject(Class)}. + * + * @param http {@link HttpSecurity} to get the shared {@link UserDetailsService} + * @return the {@link UserDetailsService} to use + */ + private UserDetailsService getUserDetailsService(H http) { + if(userDetailsService == null) { + userDetailsService = http.getSharedObject(UserDetailsService.class); + } + if(userDetailsService == null) { + throw new IllegalStateException("userDetailsService cannot be null. Invoke " + + RememberMeConfigurer.class.getSimpleName() + "#userDetailsService(UserDetailsService) or see its javadoc for alternative approaches."); + } + return userDetailsService; + } + + /** + * Gets the key to use for validating remember me tokens. Either the value + * passed into {@link #key(String)}, or a secure random string if none was + * specified. + * + * @return the remember me key to use + */ + private String getKey() { + if (key == null) { + key = UUID.randomUUID().toString(); + } + return key; + } +} \ No newline at end of file diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/RequestCacheConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/RequestCacheConfigurer.java new file mode 100644 index 0000000000..1d57e5664d --- /dev/null +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/RequestCacheConfigurer.java @@ -0,0 +1,98 @@ +/* + * Copyright 2002-2013 the original author or authors. + * + * 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 org.springframework.security.config.annotation.web.configurers; + +import org.springframework.security.config.annotation.web.HttpSecurityBuilder; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.web.savedrequest.HttpSessionRequestCache; +import org.springframework.security.web.savedrequest.RequestCache; +import org.springframework.security.web.savedrequest.RequestCacheAwareFilter; + +/** + * Adds request cache for Spring Security. Specifically this ensures that + * requests that are saved (i.e. after authentication is required) are later + * replayed. All properties have reasonable defaults, so no additional + * configuration is required other than applying this + * {@link org.springframework.security.config.annotation.SecurityConfigurer}. + * + *

Security Filters

+ * + * The following Filters are populated + * + *
    + *
  • {@link RequestCacheAwareFilter}
  • + *
+ * + *

Shared Objects Created

+ * + * No shared objects are created. + * + *

Shared Objects Used

+ * + * The following shared objects are used: + * + *
    + *
  • If no explicit {@link RequestCache}, is provided a {@link RequestCache} + * shared object is used to replay the request after authentication is + * successful
  • + *
+ * + * @author Rob Winch + * @since 3.2 + * @see RequestCache + */ +public final class RequestCacheConfigurer> extends AbstractHttpConfigurer { + + public RequestCacheConfigurer() { + } + + /** + * Allows explicit configuration of the {@link RequestCache} to be used. Defaults to try finding a + * {@link RequestCache} as a shared object. Then falls back to a {@link HttpSessionRequestCache}. + * + * @param requestCache the explicit {@link RequestCache} to use + * @return the {@link RequestCacheConfigurer} for further customization + */ + public RequestCacheConfigurer requestCache(RequestCache requestCache) { + getBuilder().setSharedObject(RequestCache.class, requestCache); + return this; + } + + @Override + public void configure(H http) throws Exception { + RequestCache requestCache = getRequestCache(http); + RequestCacheAwareFilter requestCacheFilter = new RequestCacheAwareFilter(requestCache); + requestCacheFilter = postProcess(requestCacheFilter); + http.addFilter(requestCacheFilter); + } + + /** + * Gets the {@link RequestCache} to use. If one is defined using + * {@link #requestCache(org.springframework.security.web.savedrequest.RequestCache)}, then it is used. Otherwise, an + * attempt to find a {@link RequestCache} shared object is made. If that fails, an {@link HttpSessionRequestCache} + * is used + * + * @param http the {@link HttpSecurity} to attempt to fined the shared object + * @return the {@link RequestCache} to use + */ + private RequestCache getRequestCache(H http) { + RequestCache result = http.getSharedObject(RequestCache.class); + if(result != null) { + return result; + } + return new HttpSessionRequestCache(); + } +} \ No newline at end of file diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/SecurityContextConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/SecurityContextConfigurer.java new file mode 100644 index 0000000000..ec0b185b38 --- /dev/null +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/SecurityContextConfigurer.java @@ -0,0 +1,95 @@ +/* + * Copyright 2002-2013 the original author or authors. + * + * 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 org.springframework.security.config.annotation.web.configurers; + +import org.springframework.security.config.annotation.web.HttpSecurityBuilder; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.web.context.SecurityContextPersistenceFilter; +import org.springframework.security.web.context.SecurityContextRepository; + +/** + * Allows persisting and restoring of the {@link SecurityContext} found on the + * {@link SecurityContextHolder} for each request by configuring the + * {@link SecurityContextPersistenceFilter}. All properties have reasonable + * defaults, so no additional configuration is required other than applying this + * {@link org.springframework.security.config.annotation.SecurityConfigurer}. + * + *

Security Filters

+ * + * The following Filters are populated + * + *
    + *
  • {@link SecurityContextPersistenceFilter}
  • + *
+ * + *

Shared Objects Created

+ * + * No shared objects are created. + * + *

Shared Objects Used

+ * + * The following shared objects are used: + * + *
    + *
  • If {@link SessionManagementConfigurer}, is provided and set to always, + * then the + * {@link SecurityContextPersistenceFilter#setForceEagerSessionCreation(boolean)} + * will be set to true.
  • + *
  • {@link SecurityContextRepository} must be set and is used on + * {@link SecurityContextPersistenceFilter}.
  • + *
+ * + * @author Rob Winch + * @since 3.2 + */ +public final class SecurityContextConfigurer> extends AbstractHttpConfigurer { + + /** + * Creates a new instance + * @see HttpSecurity#securityContext() + */ + public SecurityContextConfigurer() { + } + + /** + * Specifies the shared {@link SecurityContextRepository} that is to be used + * @param securityContextRepository the {@link SecurityContextRepository} to use + * @return the {@link HttpSecurity} for further customizations + */ + public SecurityContextConfigurer securityContextRepository(SecurityContextRepository securityContextRepository) { + getBuilder().setSharedObject(SecurityContextRepository.class, securityContextRepository); + return this; + } + + @Override + @SuppressWarnings("unchecked") + public void configure(H http) throws Exception { + + SecurityContextRepository securityContextRepository = http.getSharedObject(SecurityContextRepository.class); + SecurityContextPersistenceFilter securityContextFilter = new SecurityContextPersistenceFilter( + securityContextRepository); + SessionManagementConfigurer sessionManagement = http.getConfigurer(SessionManagementConfigurer.class); + SessionCreationPolicy sessionCreationPolicy = sessionManagement == null ? null + : sessionManagement.getSessionCreationPolicy(); + if (SessionCreationPolicy.always == sessionCreationPolicy) { + securityContextFilter.setForceEagerSessionCreation(true); + } + securityContextFilter = postProcess(securityContextFilter); + http.addFilter(securityContextFilter); + } +} \ No newline at end of file diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/ServletApiConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/ServletApiConfigurer.java new file mode 100644 index 0000000000..3554662252 --- /dev/null +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/ServletApiConfigurer.java @@ -0,0 +1,75 @@ +/* + * Copyright 2002-2013 the original author or authors. + * + * 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 org.springframework.security.config.annotation.web.configurers; + +import javax.servlet.http.HttpServletRequest; + +import org.springframework.security.config.annotation.web.HttpSecurityBuilder; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter; + +/** + * Implements select methods from the {@link HttpServletRequest} using the {@link SecurityContext} from the {@link SecurityContextHolder}. + * + *

Security Filters

+ * + * The following Filters are populated + * + *
    + *
  • {@link SecurityContextHolderAwareRequestFilter}
  • + *
+ * + *

Shared Objects Created

+ * + * No shared objects are created. + * + *

Shared Objects Used

+ * + * No shared Objects are used. + * + * @author Rob Winch + * @since 3.2 + */ +public final class ServletApiConfigurer> extends AbstractHttpConfigurer { + private SecurityContextHolderAwareRequestFilter securityContextRequestFilter = new SecurityContextHolderAwareRequestFilter(); + + /** + * Creates a new instance + * @see HttpSecurity#servletApi() + */ + public ServletApiConfigurer() { + } + + public ServletApiConfigurer rolePrefix(String rolePrefix) { + securityContextRequestFilter.setRolePrefix(rolePrefix); + return this; + } + + @SuppressWarnings("unchecked") + public H disable() { + getBuilder().removeConfigurer(getClass()); + return getBuilder(); + } + + @Override + public void configure(H builder) + throws Exception { + securityContextRequestFilter = postProcess(securityContextRequestFilter); + builder.addFilter(securityContextRequestFilter); + } +} \ No newline at end of file diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/SessionCreationPolicy.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/SessionCreationPolicy.java new file mode 100644 index 0000000000..6fe03e1ce1 --- /dev/null +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/SessionCreationPolicy.java @@ -0,0 +1,39 @@ +/* + * Copyright 2002-2013 the original author or authors. + * + * 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 org.springframework.security.config.annotation.web.configurers; + +import javax.servlet.http.HttpSession; + +import org.springframework.security.core.context.SecurityContext; + +/** + * Specifies the various session creation policies for Spring Security. + * + * FIXME this should be removed once {@link org.springframework.security.config.http.SessionCreationPolicy} is made public. + * + * @author Rob Winch + * @since 3.2 + */ +public enum SessionCreationPolicy { + /** Always create an {@link HttpSession} */ + always, + /** Spring Security will never create an {@link HttpSession}, but will use the {@link HttpSession} if it already exists */ + never, + /** Spring Security will only create an {@link HttpSession} if required */ + ifRequired, + /** Spring Security will never create an {@link HttpSession} and it will never use it to obtain the {@link SecurityContext} */ + stateless +} \ No newline at end of file diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/SessionManagementConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/SessionManagementConfigurer.java new file mode 100644 index 0000000000..f95f20c793 --- /dev/null +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/SessionManagementConfigurer.java @@ -0,0 +1,331 @@ +/* + * Copyright 2002-2013 the original author or authors. + * + * 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 org.springframework.security.config.annotation.web.configurers; + +import javax.servlet.http.HttpServletResponse; + +import org.springframework.security.config.annotation.web.HttpSecurityBuilder; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.core.session.SessionRegistry; +import org.springframework.security.core.session.SessionRegistryImpl; +import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler; +import org.springframework.security.web.authentication.session.ConcurrentSessionControlStrategy; +import org.springframework.security.web.authentication.session.SessionAuthenticationStrategy; +import org.springframework.security.web.authentication.session.SessionFixationProtectionStrategy; +import org.springframework.security.web.context.HttpSessionSecurityContextRepository; +import org.springframework.security.web.context.NullSecurityContextRepository; +import org.springframework.security.web.context.SecurityContextRepository; +import org.springframework.security.web.savedrequest.NullRequestCache; +import org.springframework.security.web.savedrequest.RequestCache; +import org.springframework.security.web.session.ConcurrentSessionFilter; +import org.springframework.security.web.session.SessionManagementFilter; +import org.springframework.security.web.session.SimpleRedirectInvalidSessionStrategy; +import org.springframework.util.Assert; + +/** + * Allows configuring session management. + * + *

Security Filters

+ * + * The following Filters are populated + * + *
    + *
  • {@link SessionManagementFilter}
  • + *
  • {@link ConcurrentSessionFilter} if there are restrictions on how many concurrent sessions a user can have
  • + *
+ * + *

Shared Objects Created

+ * + * The following shared objects are created: + * + *
    + *
  • {@link RequestCache}
  • + *
  • {@link SecurityContextRepository}
  • + *
  • {@link SessionManagementConfigurer}
  • + *
+ * + *

Shared Objects Used

+ * + *
    + *
  • {@link SecurityContextRepository}
  • + *
+ * + * @author Rob Winch + * @since 3.2 + * @see SessionManagementFilter + * @see ConcurrentSessionFilter + */ +public final class SessionManagementConfigurer> extends AbstractHttpConfigurer { + private SessionAuthenticationStrategy sessionAuthenticationStrategy = new SessionFixationProtectionStrategy(); + private SessionRegistry sessionRegistry = new SessionRegistryImpl(); + private Integer maximumSessions; + private String expiredUrl; + private boolean maxSessionsPreventsLogin; + private SessionCreationPolicy sessionPolicy = SessionCreationPolicy.ifRequired; + private boolean enableSessionUrlRewriting; + private String invalidSessionUrl; + private String sessionAuthenticationErrorUrl; + + /** + * Creates a new instance + * @see HttpSecurity#sessionManagement() + */ + public SessionManagementConfigurer() { + } + + /** + * Setting this attribute will inject the {@link SessionManagementFilter} with a + * {@link SimpleRedirectInvalidSessionStrategy} configured with the attribute value. + * When an invalid session ID is submitted, the strategy will be invoked, + * redirecting to the configured URL. + * + * @param invalidSessionUrl the URL to redirect to when an invalid session is detected + * @return the {@link SessionManagementConfigurer} for further customization + */ + public SessionManagementConfigurer invalidSessionUrl(String invalidSessionUrl) { + this.invalidSessionUrl = invalidSessionUrl; + return this; + } + + /** + * Defines the URL of the error page which should be shown when the + * SessionAuthenticationStrategy raises an exception. If not set, an + * unauthorized (402) error code will be returned to the client. Note that + * this attribute doesn't apply if the error occurs during a form-based + * login, where the URL for authentication failure will take precedence. + * + * @param sessionAuthenticationErrorUrl + * the URL to redirect to + * @return the {@link SessionManagementConfigurer} for further customization + */ + public SessionManagementConfigurer sessionAuthenticationErrorUrl(String sessionAuthenticationErrorUrl) { + this.sessionAuthenticationErrorUrl = sessionAuthenticationErrorUrl; + return this; + } + + /** + * If set to true, allows HTTP sessions to be rewritten in the URLs when + * using {@link HttpServletResponse#encodeRedirectURL(String)} or + * {@link HttpServletResponse#encodeURL(String)}, otherwise disallows HTTP + * sessions to be included in the URL. This prevents leaking information to + * external domains. + * + * @param enableSessionUrlRewriting true if should allow the JSESSIONID to be rewritten into the URLs, else false (default) + * @return the {@link SessionManagementConfigurer} for further customization + * @see HttpSessionSecurityContextRepository#setDisableUrlRewriting(boolean) + */ + public SessionManagementConfigurer enableSessionUrlRewriting(boolean enableSessionUrlRewriting) { + this.enableSessionUrlRewriting = enableSessionUrlRewriting; + return this; + } + + /** + * Allows specifying the {@link SessionCreationPolicy} + * @param sessionCreationPolicy the {@link SessionCreationPolicy} to use. Cannot be null. + * @return the {@link SessionManagementConfigurer} for further customizations + * @see SessionCreationPolicy + * @throws IllegalArgumentException if {@link SessionCreationPolicy} is null. + */ + public SessionManagementConfigurer sessionCreationPolicy(SessionCreationPolicy sessionCreationPolicy) { + Assert.notNull(sessionCreationPolicy, "sessionCreationPolicy cannot be null"); + this.sessionPolicy = sessionCreationPolicy; + return this; + } + + /** + * Allows explicitly specifying the {@link SessionAuthenticationStrategy}. + * The default is to use {@link SessionFixationProtectionStrategy}. If + * restricting the maximum number of sessions is configured, + * {@link ConcurrentSessionControlStrategy} will be used. + * + * @param sessionAuthenticationStrategy + * @return the {@link SessionManagementConfigurer} for further customizations + */ + public SessionManagementConfigurer sessionAuthenticationStrategy(SessionAuthenticationStrategy sessionAuthenticationStrategy) { + this.sessionAuthenticationStrategy = sessionAuthenticationStrategy; + return this; + } + + /** + * Controls the maximum number of sessions for a user. The default is to allow any number of users. + * @param maximumSessions the maximum number of sessions for a user + * @return the {@link SessionManagementConfigurer} for further customizations + */ + public ConcurrencyControlConfigurer maximumSessions(int maximumSessions) { + this.maximumSessions = maximumSessions; + this.sessionAuthenticationStrategy = null; + return new ConcurrencyControlConfigurer(); + } + + /** + * Allows configuring controlling of multiple sessions. + * + * @author Rob Winch + */ + public final class ConcurrencyControlConfigurer { + + /** + * The URL to redirect to if a user tries to access a resource and their + * session has been expired due to too many sessions for the current user. + * The default is to write a simple error message to the response. + * + * @param expiredUrl the URL to redirect to + * @return the {@link ConcurrencyControlConfigurer} for further customizations + */ + public ConcurrencyControlConfigurer expiredUrl(String expiredUrl) { + SessionManagementConfigurer.this.expiredUrl = expiredUrl; + return this; + } + + /** + * If true, prevents a user from authenticating when the + * {@link #maximumSessions(int)} has been reached. Otherwise (default), the user who + * authenticates is allowed access and an existing user's session is + * expired. The user's who's session is forcibly expired is sent to + * {@link #expiredUrl(String)}. The advantage of this approach is if a user + * accidentally does not log out, there is no need for an administrator to + * intervene or wait till their session expires. + * + * @param maxSessionsPreventsLogin true to have an error at time of authentication, else false (default) + * @return the {@link ConcurrencyControlConfigurer} for further customizations + */ + public ConcurrencyControlConfigurer maxSessionsPreventsLogin(boolean maxSessionsPreventsLogin) { + SessionManagementConfigurer.this.maxSessionsPreventsLogin = maxSessionsPreventsLogin; + return this; + } + + /** + * Controls the {@link SessionRegistry} implementation used. The default + * is {@link SessionRegistryImpl} which is an in memory implementation. + * + * @param sessionRegistry the {@link SessionRegistry} to use + * @return the {@link ConcurrencyControlConfigurer} for further customizations + */ + public ConcurrencyControlConfigurer sessionRegistry(SessionRegistry sessionRegistry) { + SessionManagementConfigurer.this.sessionRegistry = sessionRegistry; + return this; + } + + /** + * Used to chain back to the {@link SessionManagementConfigurer} + * + * @return the {@link SessionManagementConfigurer} for further customizations + */ + public SessionManagementConfigurer and() { + return SessionManagementConfigurer.this; + } + + private ConcurrencyControlConfigurer() {} + } + + @Override + public void init(H builder) throws Exception { + SecurityContextRepository securityContextRepository = builder.getSharedObject(SecurityContextRepository.class); + boolean stateless = isStateless(); + + if(securityContextRepository == null) { + if(stateless) { + builder.setSharedObject(SecurityContextRepository.class, new NullSecurityContextRepository()); + } else { + HttpSessionSecurityContextRepository httpSecurityRepository = new HttpSessionSecurityContextRepository(); + httpSecurityRepository.setDisableUrlRewriting(!enableSessionUrlRewriting); + httpSecurityRepository.setAllowSessionCreation(isAllowSessionCreation()); + builder.setSharedObject(SecurityContextRepository.class, httpSecurityRepository); + } + } + + RequestCache requestCache = builder.getSharedObject(RequestCache.class); + if(requestCache == null) { + if(stateless) { + builder.setSharedObject(RequestCache.class, new NullRequestCache()); + } + } + builder.setSharedObject(SessionAuthenticationStrategy.class, getSessionAuthenticationStrategy()); + } + + @Override + public void configure(H http) throws Exception { + SecurityContextRepository securityContextRepository = http.getSharedObject(SecurityContextRepository.class); + SessionManagementFilter sessionManagementFilter = new SessionManagementFilter(securityContextRepository, getSessionAuthenticationStrategy()); + if(sessionAuthenticationErrorUrl != null) { + sessionManagementFilter.setAuthenticationFailureHandler(new SimpleUrlAuthenticationFailureHandler(sessionAuthenticationErrorUrl)); + } + if(invalidSessionUrl != null) { + sessionManagementFilter.setInvalidSessionStrategy(new SimpleRedirectInvalidSessionStrategy(invalidSessionUrl)); + } + sessionManagementFilter = postProcess(sessionManagementFilter); + + http.addFilter(sessionManagementFilter); + if(isConcurrentSessionControlEnabled()) { + ConcurrentSessionFilter concurrentSessionFilter = new ConcurrentSessionFilter(sessionRegistry, expiredUrl); + concurrentSessionFilter = postProcess(concurrentSessionFilter); + http.addFilter(concurrentSessionFilter); + } + } + + /** + * Gets the {@link SessionCreationPolicy}. Can not be null. + * @return the {@link SessionCreationPolicy} + */ + SessionCreationPolicy getSessionCreationPolicy() { + return sessionPolicy; + } + + /** + * Returns true if the {@link SessionCreationPolicy} allows session creation, else false + * @return true if the {@link SessionCreationPolicy} allows session creation + */ + private boolean isAllowSessionCreation() { + return SessionCreationPolicy.always == sessionPolicy || SessionCreationPolicy.ifRequired == sessionPolicy; + } + + /** + * Returns true if the {@link SessionCreationPolicy} is stateless + * @return + */ + private boolean isStateless() { + return SessionCreationPolicy.stateless == sessionPolicy; + } + + /** + * Gets the customized {@link SessionAuthenticationStrategy} if + * {@link #sessionAuthenticationStrategy(SessionAuthenticationStrategy)} was + * specified. Otherwise creates a default + * {@link SessionAuthenticationStrategy}. + * + * @return the {@link SessionAuthenticationStrategy} to use + */ + private SessionAuthenticationStrategy getSessionAuthenticationStrategy() { + if(sessionAuthenticationStrategy != null) { + return sessionAuthenticationStrategy; + } + if(isConcurrentSessionControlEnabled()) { + ConcurrentSessionControlStrategy concurrentSessionControlStrategy = new ConcurrentSessionControlStrategy(sessionRegistry); + concurrentSessionControlStrategy.setMaximumSessions(maximumSessions); + concurrentSessionControlStrategy.setExceptionIfMaximumExceeded(maxSessionsPreventsLogin); + sessionAuthenticationStrategy = concurrentSessionControlStrategy; + } + return sessionAuthenticationStrategy; + } + + /** + * Returns true if the number of concurrent sessions per user should be restricted. + * @return + */ + private boolean isConcurrentSessionControlEnabled() { + return maximumSessions != null; + } +} \ No newline at end of file diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/UrlAuthorizationConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/UrlAuthorizationConfigurer.java new file mode 100644 index 0000000000..f6efedd0c2 --- /dev/null +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/UrlAuthorizationConfigurer.java @@ -0,0 +1,241 @@ +/* + * Copyright 2002-2013 the original author or authors. + * + * 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 org.springframework.security.config.annotation.web.configurers; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import org.springframework.security.access.AccessDecisionManager; +import org.springframework.security.access.AccessDecisionVoter; +import org.springframework.security.access.ConfigAttribute; +import org.springframework.security.access.SecurityConfig; +import org.springframework.security.access.vote.AuthenticatedVoter; +import org.springframework.security.access.vote.RoleVoter; +import org.springframework.security.config.annotation.web.HttpSecurityBuilder; +import org.springframework.security.web.access.intercept.DefaultFilterInvocationSecurityMetadataSource; +import org.springframework.security.web.access.intercept.FilterInvocationSecurityMetadataSource; +import org.springframework.security.web.util.RequestMatcher; +import org.springframework.util.Assert; + + +/** + * Adds URL based authorization using {@link DefaultFilterInvocationSecurityMetadataSource}. At least one + * {@link org.springframework.web.bind.annotation.RequestMapping} needs to be mapped to {@link ConfigAttribute}'s for + * this {@link SecurityContextConfigurer} to have meaning. + *

Security Filters

+ * + * The following Filters are populated + * + *
    + *
  • {@link org.springframework.security.web.access.intercept.FilterSecurityInterceptor}
  • + *
+ * + *

Shared Objects Created

+ * + * The following shared objects are populated to allow other {@link org.springframework.security.config.annotation.SecurityConfigurer}'s to customize: + *
    + *
  • {@link org.springframework.security.web.access.intercept.FilterSecurityInterceptor}
  • + *
+ * + *

Shared Objects Used

+ * + * The following shared objects are used: + * + *
    + *
  • {@link org.springframework.security.config.annotation.web.builders.HttpSecurity#getAuthenticationManager()}
  • + *
+ * + * @param the type of {@link HttpSecurityBuilder} that is being configured + * @param the type of object that is being chained + * + * @author Rob Winch + * @since 3.2 + * @see ExpressionUrlAuthorizationConfigurer + */ +public final class UrlAuthorizationConfigurer, C> extends AbstractInterceptUrlConfigurer.AuthorizedUrl> { + + /** + * Creates the default {@link AccessDecisionVoter} instances used if an + * {@link AccessDecisionManager} was not specified using + * {@link #accessDecisionManager(AccessDecisionManager)}. + */ + @Override + @SuppressWarnings("rawtypes") + final List getDecisionVoters() { + List decisionVoters = new ArrayList(); + decisionVoters.add(new RoleVoter()); + decisionVoters.add(new AuthenticatedVoter()); + return decisionVoters; + } + + /** + * Creates the {@link FilterInvocationSecurityMetadataSource} to use. The + * implementation is a {@link DefaultFilterInvocationSecurityMetadataSource} + * . + */ + @Override + FilterInvocationSecurityMetadataSource createMetadataSource() { + return new DefaultFilterInvocationSecurityMetadataSource(createRequestMap()); + } + + /** + * Chains the {@link RequestMatcher} creation to the {@link AuthorizedUrl} class. + */ + @Override + protected AuthorizedUrl chainRequestMatchersInternal(List requestMatchers) { + return new AuthorizedUrl(requestMatchers); + } + + /** + * Adds a mapping of the {@link RequestMatcher} instances to the {@link ConfigAttribute} instances. + * @param requestMatchers the {@link RequestMatcher} instances that should map to the provided {@link ConfigAttribute} instances + * @param configAttributes the {@link ConfigAttribute} instances that should be mapped by the {@link RequestMatcher} instances + * @return the {@link UrlAuthorizationConfigurer} for further customizations + */ + private UrlAuthorizationConfigurer addMapping(Iterable requestMatchers, Collection configAttributes) { + for(RequestMatcher requestMatcher : requestMatchers) { + addMapping(new UrlMapping(requestMatcher, configAttributes)); + } + return this; + } + + /** + * Creates a String for specifying a user requires a role. + * + * @param role + * the role that should be required which is prepended with ROLE_ + * automatically (i.e. USER, ADMIN, etc). It should not start + * with ROLE_ + * @return the {@link ConfigAttribute} expressed as a String + */ + private static String hasRole(String role) { + Assert.isTrue( + !role.startsWith("ROLE_"), + role + + " should not start with ROLE_ since ROLE_ is automatically prepended when using hasRole. Consider using hasAuthority or access instead."); + return "ROLE_" + role; + } + + /** + * Creates a String for specifying that a user requires one of many roles. + * + * @param roles + * the roles that the user should have at least one of (i.e. + * ADMIN, USER, etc). Each role should not start with ROLE_ since + * it is automatically prepended already. + * @return the {@link ConfigAttribute} expressed as a String + */ + private static String[] hasAnyRole(String... roles) { + for(int i=0;i requestMatchers; + + /** + * Creates a new instance + * @param requestMatchers the {@link RequestMatcher} instances to map to some {@link ConfigAttribute} instances. + * @see UrlAuthorizationConfigurer#chainRequestMatchers(List) + */ + private AuthorizedUrl(List requestMatchers) { + Assert.notEmpty(requestMatchers, "requestMatchers must contain at least one value"); + this.requestMatchers = requestMatchers; + } + + /** + * Specifies a user requires a role. + * + * @param role + * the role that should be required which is prepended with ROLE_ + * automatically (i.e. USER, ADMIN, etc). It should not start + * with ROLE_ + * the {@link UrlAuthorizationConfigurer} for further customization + */ + public UrlAuthorizationConfigurer hasRole(String role) { + return access(UrlAuthorizationConfigurer.hasRole(role)); + } + + /** + * Specifies that a user requires one of many roles. + * + * @param roles + * the roles that the user should have at least one of (i.e. + * ADMIN, USER, etc). Each role should not start with ROLE_ since + * it is automatically prepended already. + * @return the {@link UrlAuthorizationConfigurer} for further customization + */ + public UrlAuthorizationConfigurer hasAnyRole(String... roles) { + return access(UrlAuthorizationConfigurer.hasAnyRole(roles)); + } + + /** + * Specifies a user requires an authority. + * + * @param authority + * the authority that should be required + * @return the {@link UrlAuthorizationConfigurer} for further customization + */ + public UrlAuthorizationConfigurer hasAuthority(String authority) { + return access(authority); + } + + /** + * Specifies that a user requires one of many authorities + * @param authorities the authorities that the user should have at least one of (i.e. ROLE_USER, ROLE_ADMIN, etc). + * @return the {@link UrlAuthorizationConfigurer} for further customization + */ + public UrlAuthorizationConfigurer hasAnyAuthority(String... authorities) { + return access(UrlAuthorizationConfigurer.hasAnyAuthority(authorities)); + } + + /** + * Specifies that an anonymous user is allowed access + * @return the {@link UrlAuthorizationConfigurer} for further customization + */ + public UrlAuthorizationConfigurer anonymous() { + return hasRole("ROLE_ANONYMOUS"); + } + + /** + * Specifies that the user must have the specified {@link ConfigAttribute}'s + * @param attributes the {@link ConfigAttribute}'s that restrict access to a URL + * @return the {@link UrlAuthorizationConfigurer} for further customization + */ + public UrlAuthorizationConfigurer access(String... attributes) { + addMapping(requestMatchers, SecurityConfig.createList(attributes)); + return UrlAuthorizationConfigurer.this; + } + } +} \ No newline at end of file diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/X509Configurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/X509Configurer.java new file mode 100644 index 0000000000..d1a4ed0c80 --- /dev/null +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/X509Configurer.java @@ -0,0 +1,196 @@ +/* + * Copyright 2002-2013 the original author or authors. + * + * 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 org.springframework.security.config.annotation.web.configurers; + +import javax.servlet.http.HttpServletRequest; + +import org.springframework.security.authentication.AuthenticationDetailsSource; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.config.annotation.web.HttpSecurityBuilder; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.userdetails.AuthenticationUserDetailsService; +import org.springframework.security.core.userdetails.UserDetailsByNameServiceWrapper; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.web.AuthenticationEntryPoint; +import org.springframework.security.web.authentication.Http403ForbiddenEntryPoint; +import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationProvider; +import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken; +import org.springframework.security.web.authentication.preauth.PreAuthenticatedGrantedAuthoritiesWebAuthenticationDetails; +import org.springframework.security.web.authentication.preauth.x509.SubjectDnX509PrincipalExtractor; +import org.springframework.security.web.authentication.preauth.x509.X509AuthenticationFilter; + +/** + * Adds X509 based pre authentication to an application. Since validating the + * certificate happens when the client connects, the requesting and validation + * of the client certificate should be performed by the container. Spring Security + * will then use the certificate to look up the {@link Authentication} for the user. + * + *

Security Filters

+ * + * The following Filters are populated + * + *
    + *
  • {@link X509AuthenticationFilter}
  • + *
+ * + *

Shared Objects Created

+ * + * The following shared objects are created + * + *
    + *
  • + * {@link AuthenticationEntryPoint} + * is populated with an {@link Http403ForbiddenEntryPoint}
  • + *
  • A {@link PreAuthenticatedAuthenticationProvider} is populated into + * {@link HttpSecurity#authenticationProvider(org.springframework.security.authentication.AuthenticationProvider)} + *
  • + *
+ * + *

Shared Objects Used

+ * + * The following shared objects are used: + * + *
    + *
  • A {@link UserDetailsService} shared object is used if no {@link AuthenticationUserDetailsService} is specified
  • + *
+ * + * @author Rob Winch + * @since 3.2 + */ +public final class X509Configurer> extends AbstractHttpConfigurer { + private X509AuthenticationFilter x509AuthenticationFilter; + private AuthenticationUserDetailsService authenticationUserDetailsService; + private String subjectPrincipalRegex; + private AuthenticationDetailsSource authenticationDetailsSource; + + /** + * Creates a new instance + * @see HttpSecurity#x509() + */ + public X509Configurer() { + } + + /** + * Allows specifying the entire {@link X509AuthenticationFilter}. If this is + * specified, the properties on {@link X509Configurer} will not be + * populated on the {@link X509AuthenticationFilter}. + * + * @param x509AuthenticationFilter the {@link X509AuthenticationFilter} to use + * @return the {@link X509Configurer} for further customizations + */ + public X509Configurer x509AuthenticationFilter( + X509AuthenticationFilter x509AuthenticationFilter) { + this.x509AuthenticationFilter = x509AuthenticationFilter; + return this; + } + + /** + * Specifies the {@link AuthenticationDetailsSource} + * + * @param authenticationDetailsSource the {@link AuthenticationDetailsSource} to use + * @return the {@link X509Configurer} to use + */ + public X509Configurer authenticationDetailsSource(AuthenticationDetailsSource authenticationDetailsSource) { + this.authenticationDetailsSource = authenticationDetailsSource; + return this; + } + + /** + * Shortcut for invoking {@link #authenticationUserDetailsService(AuthenticationUserDetailsService)} with a {@link UserDetailsByNameServiceWrapper}. + * + * @param userDetailsService the {@link UserDetailsService} to use + * @return the {@link X509Configurer} for further customizations + */ + public X509Configurer userDetailsService( + UserDetailsService userDetailsService) { + UserDetailsByNameServiceWrapper authenticationUserDetailsService = new UserDetailsByNameServiceWrapper(); + authenticationUserDetailsService.setUserDetailsService(userDetailsService); + return authenticationUserDetailsService(authenticationUserDetailsService); + } + + /** + * Specifies the {@link AuthenticationUserDetailsService} to use. If not + * specified, the shared {@link UserDetailsService} will be used to create a + * {@link UserDetailsByNameServiceWrapper}. + * + * @param authenticationUserDetailsService the {@link AuthenticationUserDetailsService} to use + * @return the {@link X509Configurer} for further customizations + */ + public X509Configurer authenticationUserDetailsService( + AuthenticationUserDetailsService authenticationUserDetailsService) { + this.authenticationUserDetailsService = authenticationUserDetailsService; + return this; + } + + /** + * Specifies the regex to extract the principal from the certificate. If not + * specified, the default expression from + * {@link SubjectDnX509PrincipalExtractor} is used. + * + * @param subjectPrincipalRegex + * the regex to extract the user principal from the certificate + * (i.e. "CN=(.*?)(?:,|$)"). + * @return the {@link X509Configurer} for further customizations + */ + public X509Configurer subjectPrincipalRegex(String subjectPrincipalRegex) { + this.subjectPrincipalRegex = subjectPrincipalRegex; + return this; + } + + @Override + public void init(H http) throws Exception { + PreAuthenticatedAuthenticationProvider authenticationProvider = new PreAuthenticatedAuthenticationProvider(); + authenticationProvider.setPreAuthenticatedUserDetailsService(getAuthenticationUserDetailsService(http)); + + http + .authenticationProvider(authenticationProvider) + .setSharedObject(AuthenticationEntryPoint.class,new Http403ForbiddenEntryPoint()); + } + + @Override + public void configure(H http) throws Exception { + X509AuthenticationFilter filter = getFilter(http.getAuthenticationManager()); + http.addFilter(filter); + } + + private X509AuthenticationFilter getFilter( + AuthenticationManager authenticationManager) { + if (x509AuthenticationFilter == null) { + x509AuthenticationFilter = new X509AuthenticationFilter(); + x509AuthenticationFilter.setAuthenticationManager(authenticationManager); + if(subjectPrincipalRegex != null) { + SubjectDnX509PrincipalExtractor principalExtractor = new SubjectDnX509PrincipalExtractor(); + principalExtractor.setSubjectDnRegex(subjectPrincipalRegex); + x509AuthenticationFilter.setPrincipalExtractor(principalExtractor); + } + if(authenticationDetailsSource != null) { + x509AuthenticationFilter.setAuthenticationDetailsSource(authenticationDetailsSource); + } + x509AuthenticationFilter = postProcess(x509AuthenticationFilter); + } + + return x509AuthenticationFilter; + } + + private AuthenticationUserDetailsService getAuthenticationUserDetailsService(H http) { + if(authenticationUserDetailsService == null) { + userDetailsService(http.getSharedObject(UserDetailsService.class)); + } + return authenticationUserDetailsService; + } + +} \ No newline at end of file diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/openid/OpenIDLoginConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/openid/OpenIDLoginConfigurer.java new file mode 100644 index 0000000000..1c38c408c8 --- /dev/null +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/openid/OpenIDLoginConfigurer.java @@ -0,0 +1,463 @@ +/* + * Copyright 2002-2013 the original author or authors. + * + * 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 org.springframework.security.config.annotation.web.configurers.openid; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import javax.servlet.http.HttpServletRequest; + +import org.openid4java.consumer.ConsumerException; +import org.openid4java.consumer.ConsumerManager; +import org.springframework.security.authentication.AuthenticationDetailsSource; +import org.springframework.security.config.annotation.web.HttpSecurityBuilder; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; +import org.springframework.security.config.annotation.web.configurers.AbstractAuthenticationFilterConfigurer; +import org.springframework.security.config.annotation.web.configurers.FormLoginConfigurer; +import org.springframework.security.config.annotation.web.configurers.RememberMeConfigurer; +import org.springframework.security.config.annotation.web.configurers.SessionManagementConfigurer; +import org.springframework.security.config.annotation.web.configurers.openid.OpenIDLoginConfigurer; +import org.springframework.security.core.userdetails.AuthenticationUserDetailsService; +import org.springframework.security.core.userdetails.UserDetailsByNameServiceWrapper; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.openid.AxFetchListFactory; +import org.springframework.security.openid.OpenID4JavaConsumer; +import org.springframework.security.openid.OpenIDAttribute; +import org.springframework.security.openid.OpenIDAuthenticationFilter; +import org.springframework.security.openid.OpenIDAuthenticationProvider; +import org.springframework.security.openid.OpenIDAuthenticationToken; +import org.springframework.security.openid.OpenIDConsumer; +import org.springframework.security.openid.RegexBasedAxFetchListFactory; +import org.springframework.security.web.AuthenticationEntryPoint; +import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint; +import org.springframework.security.web.authentication.RememberMeServices; +import org.springframework.security.web.authentication.session.SessionAuthenticationStrategy; +import org.springframework.security.web.authentication.ui.DefaultLoginPageViewFilter; + +/** + * Adds support for OpenID based authentication. + * + *

Example Configuration

+ * + *
+ *
+ * @Configuration
+ * @EnableWebSecurity
+ * public class OpenIDLoginConfig extends WebSecurityConfigurerAdapter {
+ *
+ * 	@Override
+ * 	protected void configure(HttpSecurity http) {
+ * 		http
+ * 			.authorizeUrls()
+ * 				.antMatchers("/**").hasRole("USER")
+ * 				.and()
+ * 			.openidLogin()
+ * 				.permitAll();
+ * 	}
+ *
+ * 	@Override
+ * 	protected void registerAuthentication(
+ * 			AuthenticationManagerBuilder auth) throws Exception {
+ * 		auth
+ * 			.inMemoryAuthentication()
+ * 				.withUser("https://www.google.com/accounts/o8/id?id=lmkCn9xzPdsxVwG7pjYMuDgNNdASFmobNkcRPaWU")
+ * 					.password("password")
+ * 					.roles("USER");
+ * 	}
+ * }
+ * 
+ * + *

Security Filters

+ * + * The following Filters are populated + * + *
    + *
  • + * {@link OpenIDAuthenticationFilter}
  • + *
+ * + *

Shared Objects Created

+ * + *
    + *
  • + * {@link AuthenticationEntryPoint} + * is populated with a {@link LoginUrlAuthenticationEntryPoint}
  • + *
  • A {@link OpenIDAuthenticationProvider} is populated into + * {@link HttpSecurity#authenticationProvider(org.springframework.security.authentication.AuthenticationProvider)} + *
  • + *
+ * + *

Shared Objects Used

+ * + * The following shared objects are used: + * + *
    + *
  • {@link HttpSecurity#getAuthenticationManager()}
  • + *
  • {@link RememberMeServices} - is optionally used. See + * {@link RememberMeConfigurer}
  • + *
  • {@link SessionAuthenticationStrategy} - is optionally used. See + * {@link SessionManagementConfigurer}
  • + *
+ * + * @author Rob Winch + * @since 3.2 + */ +public final class OpenIDLoginConfigurer> extends AbstractAuthenticationFilterConfigurer,OpenIDAuthenticationFilter> { + private OpenIDConsumer openIDConsumer; + private ConsumerManager consumerManager; + private AuthenticationUserDetailsService authenticationUserDetailsService; + private List attributeExchangeConfigurers = new ArrayList(); + + /** + * Creates a new instance + */ + public OpenIDLoginConfigurer() { + super(new OpenIDAuthenticationFilter(),"/login/openid"); + } + + /** + * Sets up OpenID attribute exchange for OpenID's matching the specified + * pattern. + * + * @param identifierPattern + * the regular expression for matching on OpenID's (i.e. + * "https://www.google.com/.*", ".*yahoo.com.*", etc) + * @return a {@link AttributeExchangeConfigurer} for further customizations of the attribute exchange + */ + public AttributeExchangeConfigurer attributeExchange(String identifierPattern) { + AttributeExchangeConfigurer attributeExchangeConfigurer = new AttributeExchangeConfigurer(identifierPattern); + this.attributeExchangeConfigurers .add(attributeExchangeConfigurer); + return attributeExchangeConfigurer; + } + + /** + * Allows specifying the {@link OpenIDConsumer} to be used. The default is + * using an {@link OpenID4JavaConsumer}. + * + * @param consumer + * the {@link OpenIDConsumer} to be used + * @return the {@link OpenIDLoginConfigurer} for further customizations + */ + public OpenIDLoginConfigurer consumer(OpenIDConsumer consumer) { + this.openIDConsumer = consumer; + return this; + } + + /** + * Allows specifying the {@link ConsumerManager} to be used. If specified, + * will be populated into an {@link OpenID4JavaConsumer}. + * + *

+ * This is a shortcut for specifying the {@link OpenID4JavaConsumer} with a + * specific {@link ConsumerManager} on {@link #consumer(OpenIDConsumer)}. + *

+ * + * @param consumerManager the {@link ConsumerManager} to use. Cannot be null. + * @return the {@link OpenIDLoginConfigurer} for further customizations + */ + public OpenIDLoginConfigurer consumerManager(ConsumerManager consumerManager) { + this.consumerManager = consumerManager; + return this; + } + + /** + * The {@link AuthenticationUserDetailsService} to use. By default a + * {@link UserDetailsByNameServiceWrapper} is used with the + * {@link UserDetailsService} shared object found with + * {@link HttpSecurity#getSharedObject(Class)}. + * + * @param authenticationUserDetailsService the {@link AuthenticationDetailsSource} to use + * @return the {@link OpenIDLoginConfigurer} for further customizations + */ + public OpenIDLoginConfigurer authenticationUserDetailsService(AuthenticationUserDetailsService authenticationUserDetailsService) { + this.authenticationUserDetailsService = authenticationUserDetailsService; + return this; + } + + /** + * Specifies the URL used to authenticate OpenID requests. If the {@link HttpServletRequest} + * matches this URL the {@link OpenIDAuthenticationFilter} will attempt to + * authenticate the request. The default is "/login/openid". + * + * @param loginUrl + * the URL used to perform authentication + * @return the {@link OpenIDLoginConfigurer} for additional customization + */ + public OpenIDLoginConfigurer loginProcessingUrl(String loginProcessingUrl) { + return super.loginProcessingUrl(loginProcessingUrl); + } + + /** + *

+ * Specifies the URL to send users to if login is required. If used with + * {@link WebSecurityConfigurerAdapter} a default login page will be + * generated when this attribute is not specified. + *

+ * + *

+ * If a URL is specified or this is not being used in conjuction with + * {@link WebSecurityConfigurerAdapter}, users are required to process the + * specified URL to generate a login page. + *

+ * + *
    + *
  • It must be an HTTP POST
  • + *
  • It must be submitted to {@link #loginProcessingUrl(String)}
  • + *
  • It should include the OpenID as an HTTP parameter by the name of + * {@link OpenIDAuthenticationFilter#DEFAULT_CLAIMED_IDENTITY_FIELD}
  • + *
+ * + * @param loginPage the login page to redirect to if authentication is required (i.e. "/login") + * @return the {@link FormLoginConfigurer} for additional customization + */ + public OpenIDLoginConfigurer loginPage(String loginPage) { + return super.loginPage(loginPage); + } + + @Override + public void init(H http) throws Exception { + super.init(http); + + OpenIDAuthenticationProvider authenticationProvider = new OpenIDAuthenticationProvider(); + authenticationProvider.setAuthenticationUserDetailsService(getAuthenticationUserDetailsService(http)); + authenticationProvider = postProcess(authenticationProvider); + http.authenticationProvider(authenticationProvider); + + initDefaultLoginFilter(http); + } + + @Override + public void configure(H http) throws Exception { + getAuthenticationFilter().setConsumer(getConsumer()); + super.configure(http); + } + + /** + * Gets the {@link OpenIDConsumer} that was configured or defaults to an {@link OpenID4JavaConsumer}. + * @return the {@link OpenIDConsumer} to use + * @throws ConsumerException + */ + private OpenIDConsumer getConsumer() throws ConsumerException { + if(openIDConsumer == null) { + openIDConsumer = new OpenID4JavaConsumer(getConsumerManager(), attributesToFetchFactory()); + } + return openIDConsumer; + } + + /** + * Gets the {@link ConsumerManager} that was configured or defaults to using a {@link ConsumerManager} with the default constructor. + * @return the {@link ConsumerManager} to use + */ + private ConsumerManager getConsumerManager() { + if(this.consumerManager != null) { + return this.consumerManager; + } + return new ConsumerManager(); + } + + /** + * Creates an {@link RegexBasedAxFetchListFactory} using the attributes + * populated by {@link AttributeExchangeConfigurer} + * + * @return the {@link AxFetchListFactory} to use + */ + private AxFetchListFactory attributesToFetchFactory() { + Map> identityToAttrs = new HashMap>(); + for(AttributeExchangeConfigurer conf : attributeExchangeConfigurers) { + identityToAttrs.put(conf.identifier, conf.getAttributes()); + } + return new RegexBasedAxFetchListFactory(identityToAttrs); + } + + /** + * Gets the {@link AuthenticationUserDetailsService} that was configured or + * defaults to {@link UserDetailsByNameServiceWrapper} that uses a + * {@link UserDetailsService} looked up using + * {@link HttpSecurity#getSharedObject(Class)} + * + * @param http the current {@link HttpSecurity} + * @return the {@link AuthenticationUserDetailsService}. + */ + private AuthenticationUserDetailsService getAuthenticationUserDetailsService( + H http) { + if(authenticationUserDetailsService != null) { + return authenticationUserDetailsService; + } + return new UserDetailsByNameServiceWrapper(http.getSharedObject(UserDetailsService.class)); + } + + /** + * If available, initializes the {@link DefaultLoginPageViewFilter} shared object. + * + * @param http the {@link HttpSecurityBuilder} to use + */ + private void initDefaultLoginFilter(H http) { + DefaultLoginPageViewFilter loginPageGeneratingFilter = http.getSharedObject(DefaultLoginPageViewFilter.class); + if(loginPageGeneratingFilter != null && !isCustomLoginPage()) { + loginPageGeneratingFilter.setOpenIdEnabled(true); + loginPageGeneratingFilter.setOpenIDauthenticationUrl(getLoginProcessingUrl()); + String loginPageUrl = loginPageGeneratingFilter.getLoginPageUrl(); + if(loginPageUrl == null) { + loginPageGeneratingFilter.setLoginPageUrl(getLoginPage()); + loginPageGeneratingFilter.setFailureUrl(getFailureUrl()); + } + loginPageGeneratingFilter.setOpenIDusernameParameter(OpenIDAuthenticationFilter.DEFAULT_CLAIMED_IDENTITY_FIELD); + } + } + + + /** + * A class used to add OpenID attributes to look up + * + * @author Rob Winch + */ + public final class AttributeExchangeConfigurer { + private final String identifier; + private List attributes = new ArrayList(); + private List attributeConfigurers = new ArrayList(); + + /** + * Creates a new instance + * @param identifierPattern the pattern that attempts to match on the OpenID + * @see OpenIDLoginConfigurer#attributeExchange(String) + */ + private AttributeExchangeConfigurer(String identifierPattern) { + this.identifier = identifierPattern; + } + + /** + * Get the {@link OpenIDLoginConfigurer} to customize the OpenID configuration further + * @return the {@link OpenIDLoginConfigurer} + */ + public OpenIDLoginConfigurer and() { + return OpenIDLoginConfigurer.this; + } + + /** + * Adds an {@link OpenIDAttribute} to be obtained for the configured OpenID pattern. + * @param attribute the {@link OpenIDAttribute} to obtain + * @return the {@link AttributeExchangeConfigurer} for further customization of attribute exchange + */ + public AttributeExchangeConfigurer attribute(OpenIDAttribute attribute) { + this.attributes.add(attribute); + return this; + } + + /** + * Adds an {@link OpenIDAttribute} with the given name + * @param name the name of the {@link OpenIDAttribute} to create + * @return an {@link AttributeConfigurer} to further configure the {@link OpenIDAttribute} that should be obtained. + */ + public AttributeConfigurer attribute(String name) { + AttributeConfigurer attributeConfigurer = new AttributeConfigurer(name); + this.attributeConfigurers.add(attributeConfigurer); + return attributeConfigurer; + } + + /** + * Gets the {@link OpenIDAttribute}'s for the configured OpenID pattern + * @return + */ + private List getAttributes() { + for(AttributeConfigurer config : attributeConfigurers) { + attributes.add(config.build()); + } + attributeConfigurers.clear(); + return attributes; + } + + /** + * Configures an {@link OpenIDAttribute} + * + * @author Rob Winch + * @since 3.2 + */ + public final class AttributeConfigurer { + private String name; + private int count = 1; + private boolean required = false; + private String type; + + /** + * Creates a new instance + * @param name the name of the attribute + * @see AttributeExchangeConfigurer#attribute(String) + */ + private AttributeConfigurer(String name) { + this.name = name; + } + + /** + * Specifies the number of attribute values to request. Default is 1. + * @param count the number of attributes to request. + * @return the {@link AttributeConfigurer} for further customization + */ + public AttributeConfigurer count(int count) { + this.count = count; + return this; + } + + /** + * Specifies that this attribute is required. The default is + * false. Note that as outlined in the OpenID + * specification, required attributes are not validated by the + * OpenID Provider. Developers should perform any validation in + * custom code. + * + * @param required specifies the attribute is required + * @return the {@link AttributeConfigurer} for further customization + */ + public AttributeConfigurer required(boolean required) { + this.required = required; + return this; + } + + /** + * The OpenID attribute type. + * @param type + * @return + */ + public AttributeConfigurer type(String type) { + this.type = type; + return this; + } + + /** + * Gets the {@link AttributeExchangeConfigurer} for further + * customization of the attributes + * + * @return the {@link AttributeConfigurer} + */ + public AttributeExchangeConfigurer and() { + return AttributeExchangeConfigurer.this; + } + + /** + * Builds the {@link OpenIDAttribute}. + * @return + */ + private OpenIDAttribute build() { + OpenIDAttribute attribute = new OpenIDAttribute(name, type); + attribute.setCount(count); + attribute.setRequired(required); + return attribute; + } + } + } +} diff --git a/config/src/test/groovy/org/springframework/security/config/annotation/AnyObjectPostProcessor.java b/config/src/test/groovy/org/springframework/security/config/annotation/AnyObjectPostProcessor.java new file mode 100644 index 0000000000..3834c874b4 --- /dev/null +++ b/config/src/test/groovy/org/springframework/security/config/annotation/AnyObjectPostProcessor.java @@ -0,0 +1,25 @@ +/* + * Copyright 2002-2013 the original author or authors. + * + * 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 org.springframework.security.config.annotation; + +/** + * Exists for mocking purposes to ensure that the Type information is found. + * + * @author Rob Winch + */ +public interface AnyObjectPostProcessor extends ObjectPostProcessor { + +} diff --git a/config/src/test/groovy/org/springframework/security/config/annotation/BaseSpringSpec.groovy b/config/src/test/groovy/org/springframework/security/config/annotation/BaseSpringSpec.groovy new file mode 100644 index 0000000000..64ab934970 --- /dev/null +++ b/config/src/test/groovy/org/springframework/security/config/annotation/BaseSpringSpec.groovy @@ -0,0 +1,122 @@ +/* + * Copyright 2002-2013 the original author or authors. + * + * 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 org.springframework.security.config.annotation; + +import javax.servlet.Filter + +import org.springframework.beans.factory.NoSuchBeanDefinitionException +import org.springframework.context.ConfigurableApplicationContext +import org.springframework.context.annotation.AnnotationConfigApplicationContext +import org.springframework.mock.web.MockFilterChain +import org.springframework.mock.web.MockHttpServletRequest +import org.springframework.mock.web.MockHttpServletResponse +import org.springframework.security.authentication.AuthenticationManager +import org.springframework.security.authentication.AuthenticationProvider +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder +import org.springframework.security.core.Authentication +import org.springframework.security.core.authority.AuthorityUtils; +import org.springframework.security.core.context.SecurityContextHolder +import org.springframework.security.core.context.SecurityContextImpl +import org.springframework.security.web.FilterChainProxy +import org.springframework.security.web.access.intercept.FilterSecurityInterceptor +import org.springframework.security.web.context.HttpRequestResponseHolder +import org.springframework.security.web.context.HttpSessionSecurityContextRepository + +import spock.lang.AutoCleanup +import spock.lang.Specification + +/** + * + * @author Rob Winch + */ +abstract class BaseSpringSpec extends Specification { + @AutoCleanup + ConfigurableApplicationContext context + + MockHttpServletRequest request + MockHttpServletResponse response + MockFilterChain chain + + def setup() { + request = new MockHttpServletRequest(method:"GET") + response = new MockHttpServletResponse() + chain = new MockFilterChain() + } + + AuthenticationManagerBuilder authenticationBldr = new AuthenticationManagerBuilder().inMemoryAuthentication().and() + + def cleanup() { + SecurityContextHolder.clearContext() + } + + def loadConfig(Class... configs) { + context = new AnnotationConfigApplicationContext(configs) + context + } + + def findFilter(Class filter, int index = 0) { + filterChain(index).filters.find { filter.isAssignableFrom(it.class)} + } + + def filterChain(int index=0) { + filterChains()[index] + } + + def filterChains() { + context.getBean(FilterChainProxy).filterChains + } + + Filter getSpringSecurityFilterChain() { + context.getBean("springSecurityFilterChain",Filter.class) + } + + AuthenticationManager authenticationManager() { + context.getBean(AuthenticationManager) + } + + AuthenticationManager getAuthenticationManager() { + try { + authenticationManager().delegateBuilder.getObject() + } catch(NoSuchBeanDefinitionException e) {} + findFilter(FilterSecurityInterceptor).authenticationManager + } + + List authenticationProviders() { + List providers = new ArrayList() + AuthenticationManager authenticationManager = getAuthenticationManager() + while(authenticationManager?.providers) { + providers.addAll(authenticationManager.providers) + authenticationManager = authenticationManager.parent + } + providers + } + + AuthenticationProvider findAuthenticationProvider(Class provider) { + authenticationProviders().find { provider.isAssignableFrom(it.class) } + } + + def login(String username="user", String role="ROLE_USER") { + login(new UsernamePasswordAuthenticationToken(username, null, AuthorityUtils.createAuthorityList(role))) + } + + def login(Authentication auth) { + HttpSessionSecurityContextRepository repo = new HttpSessionSecurityContextRepository() + HttpRequestResponseHolder requestResponseHolder = new HttpRequestResponseHolder(request, response) + repo.loadContext(requestResponseHolder) + repo.saveContext(new SecurityContextImpl(authentication:auth), requestResponseHolder.request, requestResponseHolder.response) + } +} diff --git a/config/src/test/groovy/org/springframework/security/config/annotation/BaseWebSpecuritySpec.groovy b/config/src/test/groovy/org/springframework/security/config/annotation/BaseWebSpecuritySpec.groovy new file mode 100644 index 0000000000..4fd702636d --- /dev/null +++ b/config/src/test/groovy/org/springframework/security/config/annotation/BaseWebSpecuritySpec.groovy @@ -0,0 +1,52 @@ +/* + * Copyright 2002-2013 the original author or authors. + * + * 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 org.springframework.security.config.annotation; + +import org.springframework.context.ConfigurableApplicationContext +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.mock.web.MockFilterChain; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.security.core.context.SecurityContextHolder +import org.springframework.security.web.FilterChainProxy; + +import spock.lang.AutoCleanup +import spock.lang.Specification + +/** + * + * @author Rob Winch + */ +abstract class BaseWebSpecuritySpec extends BaseSpringSpec { + FilterChainProxy springSecurityFilterChain + MockHttpServletRequest request + MockHttpServletResponse response + MockFilterChain chain + + def setup() { + request = new MockHttpServletRequest() + response = new MockHttpServletResponse() + chain = new MockFilterChain() + } + + + def loadConfig(Class... configs) { + super.loadConfig(configs) + springSecurityFilterChain = context.getBean(FilterChainProxy) + } + + +} diff --git a/config/src/test/groovy/org/springframework/security/config/annotation/ConcereteSecurityConfigurerAdapter.java b/config/src/test/groovy/org/springframework/security/config/annotation/ConcereteSecurityConfigurerAdapter.java new file mode 100644 index 0000000000..24b72a9bef --- /dev/null +++ b/config/src/test/groovy/org/springframework/security/config/annotation/ConcereteSecurityConfigurerAdapter.java @@ -0,0 +1,37 @@ +/* + * Copyright 2002-2013 the original author or authors. + * + * 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 org.springframework.security.config.annotation; + +import java.util.ArrayList; +import java.util.List; + +/** + * @author Rob Winch + * + */ +class ConcereteSecurityConfigurerAdapter extends SecurityConfigurerAdapter> { + private List list = new ArrayList(); + + @Override + public void configure(SecurityBuilder builder) throws Exception { + list = postProcess(list); + } + + public ConcereteSecurityConfigurerAdapter list(List l) { + this.list = l; + return this; + } +} diff --git a/config/src/test/groovy/org/springframework/security/config/annotation/ObjectPostProcessorTests.java b/config/src/test/groovy/org/springframework/security/config/annotation/ObjectPostProcessorTests.java new file mode 100644 index 0000000000..04cb2d1258 --- /dev/null +++ b/config/src/test/groovy/org/springframework/security/config/annotation/ObjectPostProcessorTests.java @@ -0,0 +1,50 @@ +/* + * Copyright 2002-2013 the original author or authors. + * + * 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 org.springframework.security.config.annotation; + +import static org.fest.assertions.Assertions.assertThat; + +import java.util.ArrayList; +import java.util.LinkedList; +import java.util.List; + +import org.junit.Test; + +/** + * @author Rob Winch + * + */ +public class ObjectPostProcessorTests { + + @Test + public void convertTypes() { + assertThat((Object)PerformConversion.perform(new ArrayList())).isInstanceOf(LinkedList.class); + } +} + +class ListToLinkedListObjectPostProcessor implements ObjectPostProcessor>{ + + @Override + public > O postProcess(O l) { + return (O) new LinkedList(l); + } +} + +class PerformConversion { + public static List perform(ArrayList l) { + return new ListToLinkedListObjectPostProcessor().postProcess(l); + } +} diff --git a/config/src/test/groovy/org/springframework/security/config/annotation/SecurityConfigurerAdapterTests.groovy b/config/src/test/groovy/org/springframework/security/config/annotation/SecurityConfigurerAdapterTests.groovy new file mode 100644 index 0000000000..cf988f4f65 --- /dev/null +++ b/config/src/test/groovy/org/springframework/security/config/annotation/SecurityConfigurerAdapterTests.groovy @@ -0,0 +1,40 @@ +/* + * Copyright 2002-2013 the original author or authors. + * + * 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 org.springframework.security.config.annotation + +import spock.lang.Specification + +/** + * @author Rob Winch + * + */ +class SecurityConfigurerAdapterTests extends Specification { + ConcereteSecurityConfigurerAdapter conf = new ConcereteSecurityConfigurerAdapter() + + def "addPostProcessor closure"() { + setup: + SecurityBuilder builder = Mock() + conf.addObjectPostProcessor({ List l -> + l.add("a") + l + } as ObjectPostProcessor) + when: + conf.init(builder) + conf.configure(builder) + then: + conf.list.contains("a") + } +} diff --git a/config/src/test/groovy/org/springframework/security/config/annotation/authentication/AuthenticationManagerBuilderTests.groovy b/config/src/test/groovy/org/springframework/security/config/annotation/authentication/AuthenticationManagerBuilderTests.groovy new file mode 100644 index 0000000000..1f8b23d211 --- /dev/null +++ b/config/src/test/groovy/org/springframework/security/config/annotation/authentication/AuthenticationManagerBuilderTests.groovy @@ -0,0 +1,90 @@ +/* + * Copyright 2002-2013 the original author or authors. + * + * 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 org.springframework.security.config.annotation.authentication + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.authentication.AuthenticationEventPublisher +import org.springframework.security.authentication.AuthenticationManager +import org.springframework.security.authentication.AuthenticationProvider +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.config.annotation.BaseSpringSpec +import org.springframework.security.config.annotation.ObjectPostProcessor +import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; +import org.springframework.security.core.Authentication; + +/** + * + * @author Rob Winch + * + */ +class AuthenticationManagerBuilderTests extends BaseSpringSpec { + def "add(AuthenticationProvider) does not perform registration"() { + setup: + ObjectPostProcessor opp = Mock() + AuthenticationProvider provider = Mock() + AuthenticationManagerBuilder builder = new AuthenticationManagerBuilder().objectPostProcessor(opp) + when: "Adding an AuthenticationProvider" + builder.authenticationProvider(provider) + builder.build() + then: "AuthenticationProvider is not passed into LifecycleManager (it should be managed externally)" + 0 * opp._(_ as AuthenticationProvider) + } + + // https://github.com/SpringSource/spring-security-javaconfig/issues/132 + def "#132 Custom AuthenticationEventPublisher with Web registerAuthentication"() { + setup: + AuthenticationEventPublisher aep = Mock() + when: + AuthenticationManager am = new AuthenticationManagerBuilder() + .authenticationEventPublisher(aep) + .inMemoryAuthentication() + .and() + .build() + then: + am.eventPublisher == aep + } + + def "authentication-manager support multiple DaoAuthenticationProvider's"() { + setup: + loadConfig(MultiAuthenticationProvidersConfig) + when: + Authentication auth = authenticationManager.authenticate(new UsernamePasswordAuthenticationToken("user","password")) + then: + auth.name == "user" + auth.authorities*.authority == ['ROLE_USER'] + when: + auth = authenticationManager.authenticate(new UsernamePasswordAuthenticationToken("admin","password")) + then: + auth.name == "admin" + auth.authorities*.authority.sort() == ['ROLE_ADMIN','ROLE_USER'] + } + + @EnableWebSecurity + @Configuration + static class MultiAuthenticationProvidersConfig extends WebSecurityConfigurerAdapter { + protected void registerAuthentication(AuthenticationManagerBuilder auth) throws Exception { + auth + .inMemoryAuthentication() + .withUser("user").password("password").roles("USER").and() + .and() + .inMemoryAuthentication() + .withUser("admin").password("password").roles("USER","ADMIN") + } + } +} diff --git a/config/src/test/groovy/org/springframework/security/config/annotation/authentication/BaseAuthenticationConfig.groovy b/config/src/test/groovy/org/springframework/security/config/annotation/authentication/BaseAuthenticationConfig.groovy new file mode 100644 index 0000000000..5deb410ca3 --- /dev/null +++ b/config/src/test/groovy/org/springframework/security/config/annotation/authentication/BaseAuthenticationConfig.groovy @@ -0,0 +1,50 @@ +/* + * Copyright 2002-2013 the original author or authors. + * + * 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 org.springframework.security.config.annotation.authentication + +import java.rmi.registry.Registry; + +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.security.authentication.AuthenticationManager +import org.springframework.security.config.annotation.authentication.AuthenticationManagerBuilder +import org.springframework.security.config.annotation.authentication.configurers.userdetails.UserDetailsServiceConfigurer; +import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; +import org.springframework.security.config.annotation.authentication.configurers.provisioning.InMemoryUserDetailsManagerConfigurer; +import org.springframework.security.core.userdetails.UserDetailsService; + + +/** + * + * @author Rob Winch + */ +@Configuration +class BaseAuthenticationConfig { + protected void registerAuthentication( + AuthenticationManagerBuilder auth) throws Exception { + auth + .inMemoryAuthentication() + .withUser("user").password("password").roles("USER").and() + .withUser("admin").password("password").roles("USER", "ADMIN").and() + } + + @Bean + public AuthenticationManager authenticationManager() { + AuthenticationManagerBuilder registry = new AuthenticationManagerBuilder(); + registerAuthentication(registry); + return registry.build(); + } +} diff --git a/config/src/test/groovy/org/springframework/security/config/annotation/authentication/NamespaceAuthenticationManagerTests.groovy b/config/src/test/groovy/org/springframework/security/config/annotation/authentication/NamespaceAuthenticationManagerTests.groovy new file mode 100644 index 0000000000..385fd1875e --- /dev/null +++ b/config/src/test/groovy/org/springframework/security/config/annotation/authentication/NamespaceAuthenticationManagerTests.groovy @@ -0,0 +1,92 @@ +/* + * Copyright 2002-2013 the original author or authors. + * + * 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 org.springframework.security.config.annotation.authentication + +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.security.authentication.AuthenticationManager +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken +import org.springframework.security.config.annotation.BaseSpringSpec +import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; +import org.springframework.security.core.Authentication + +/** + * + * @author Rob Winch + * + */ +class NamespaceAuthenticationManagerTests extends BaseSpringSpec { + def "authentication-manager@erase-credentials=true (default)"() { + when: + loadConfig(EraseCredentialsTrueDefaultConfig) + Authentication auth = authenticationManager.authenticate(new UsernamePasswordAuthenticationToken("user","password")) + then: + auth.principal.password == null + auth.credentials == null + when: "authenticate the same user" + auth = authenticationManager.authenticate(new UsernamePasswordAuthenticationToken("user","password")) + then: "successfully authenticate again" + noExceptionThrown() + } + + @EnableWebSecurity + @Configuration + static class EraseCredentialsTrueDefaultConfig extends WebSecurityConfigurerAdapter { + protected void registerAuthentication(AuthenticationManagerBuilder auth) throws Exception { + auth + .inMemoryAuthentication() + .withUser("user").password("password").roles("USER") + } + + // Only necessary to have access to verify the AuthenticationManager + @Bean + @Override + public AuthenticationManager authenticationManagerBean() + throws Exception { + return super.authenticationManagerBean(); + } + } + + def "authentication-manager@erase-credentials=false"() { + when: + loadConfig(EraseCredentialsFalseConfig) + Authentication auth = authenticationManager.authenticate(new UsernamePasswordAuthenticationToken("user","password")) + then: + auth.credentials == "password" + auth.principal.password == "password" + } + + @EnableWebSecurity + @Configuration + static class EraseCredentialsFalseConfig extends WebSecurityConfigurerAdapter { + protected void registerAuthentication(AuthenticationManagerBuilder auth) throws Exception { + auth + .eraseCredentials(false) + .inMemoryAuthentication() + .withUser("user").password("password").roles("USER") + } + + // Only necessary to have access to verify the AuthenticationManager + @Bean + @Override + public AuthenticationManager authenticationManagerBean() + throws Exception { + return super.authenticationManagerBean(); + } + } +} diff --git a/config/src/test/groovy/org/springframework/security/config/annotation/authentication/NamespaceAuthenticationProviderTests.groovy b/config/src/test/groovy/org/springframework/security/config/annotation/authentication/NamespaceAuthenticationProviderTests.groovy new file mode 100644 index 0000000000..a0a0b95e95 --- /dev/null +++ b/config/src/test/groovy/org/springframework/security/config/annotation/authentication/NamespaceAuthenticationProviderTests.groovy @@ -0,0 +1,83 @@ +/* + * Copyright 2002-2013 the original author or authors. + * + * 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 org.springframework.security.config.annotation.authentication + +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.security.authentication.AuthenticationManager +import org.springframework.security.authentication.dao.DaoAuthenticationProvider +import org.springframework.security.config.annotation.BaseSpringSpec +import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; +import org.springframework.security.provisioning.InMemoryUserDetailsManager + +/** + * + * @author Rob Winch + * + */ +class NamespaceAuthenticationProviderTests extends BaseSpringSpec { + def "authentication-provider@ref"() { + when: + loadConfig(AuthenticationProviderRefConfig) + then: + authenticationProviders()[1] == AuthenticationProviderRefConfig.expected + } + + @EnableWebSecurity + @Configuration + static class AuthenticationProviderRefConfig extends WebSecurityConfigurerAdapter { + static DaoAuthenticationProvider expected = new DaoAuthenticationProvider() + protected void registerAuthentication(AuthenticationManagerBuilder auth) throws Exception { + auth + .authenticationProvider(expected) + } + + // Only necessary to have access to verify the AuthenticationManager + @Bean + @Override + public AuthenticationManager authenticationManagerBean() + throws Exception { + return super.authenticationManagerBean(); + } + } + + def "authentication-provider@user-service-ref"() { + when: + loadConfig(UserServiceRefConfig) + then: + findAuthenticationProvider(DaoAuthenticationProvider).userDetailsService == UserServiceRefConfig.expected + } + + @EnableWebSecurity + @Configuration + static class UserServiceRefConfig extends WebSecurityConfigurerAdapter { + static InMemoryUserDetailsManager expected = new InMemoryUserDetailsManager([] as Collection) + protected void registerAuthentication(AuthenticationManagerBuilder auth) throws Exception { + auth + .userDetailsService(expected) + } + + // Only necessary to have access to verify the AuthenticationManager + @Bean + @Override + public AuthenticationManager authenticationManagerBean() + throws Exception { + return super.authenticationManagerBean(); + } + } +} diff --git a/config/src/test/groovy/org/springframework/security/config/annotation/authentication/NamespaceJdbcUserServiceTests.groovy b/config/src/test/groovy/org/springframework/security/config/annotation/authentication/NamespaceJdbcUserServiceTests.groovy new file mode 100644 index 0000000000..039ceb9237 --- /dev/null +++ b/config/src/test/groovy/org/springframework/security/config/annotation/authentication/NamespaceJdbcUserServiceTests.groovy @@ -0,0 +1,188 @@ +/* + * Copyright 2002-2013 the original author or authors. + * + * 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 org.springframework.security.config.annotation.authentication + +import javax.sql.DataSource + +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder +import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType +import org.springframework.security.authentication.AuthenticationManager +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken +import org.springframework.security.authentication.dao.DaoAuthenticationProvider +import org.springframework.security.config.annotation.BaseSpringSpec +import org.springframework.security.config.annotation.authentication.NamespaceJdbcUserServiceTests.CustomJdbcUserServiceSampleConfig.CustomUserCache; +import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; +import org.springframework.security.core.Authentication +import org.springframework.security.core.userdetails.UserCache +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.provisioning.JdbcUserDetailsManager + +/** + * + * @author Rob Winch + * + */ +class NamespaceJdbcUserServiceTests extends BaseSpringSpec { + def "jdbc-user-service"() { + when: + loadConfig(DataSourceConfig,JdbcUserServiceConfig) + then: + findAuthenticationProvider(DaoAuthenticationProvider).userDetailsService instanceof JdbcUserDetailsManager + } + + @EnableWebSecurity + @Configuration + static class JdbcUserServiceConfig extends WebSecurityConfigurerAdapter { + @Autowired + private DataSource dataSource; + + protected void registerAuthentication(AuthenticationManagerBuilder auth) throws Exception { + auth + .jdbcAuthentication() + .dataSource(dataSource) // jdbc-user-service@data-source-ref + } + + // Only necessary to have access to verify the AuthenticationManager + @Bean + @Override + public AuthenticationManager authenticationManagerBean() + throws Exception { + return super.authenticationManagerBean(); + } + } + + def "jdbc-user-service in memory testing sample"() { + when: + loadConfig(DataSourceConfig,JdbcUserServiceInMemorySampleConfig) + then: + Authentication auth = authenticationManager.authenticate(new UsernamePasswordAuthenticationToken("user", "password")) + auth.authorities.collect {it.authority} == ['ROLE_USER'] + auth.name == "user" + } + + @EnableWebSecurity + @Configuration + static class JdbcUserServiceInMemorySampleConfig extends WebSecurityConfigurerAdapter { + @Autowired + private DataSource dataSource; + + protected void registerAuthentication(AuthenticationManagerBuilder auth) throws Exception { + auth + .jdbcAuthentication() + .dataSource(dataSource) + // imports the default schema (will fail if already exists) + .withDefaultSchema() + // adds this user automatically (will fail if already exists) + .withUser("user") + .password("password") + .roles("USER") + } + + // Only necessary to have access to verify the AuthenticationManager + @Bean + @Override + public AuthenticationManager authenticationManagerBean() + throws Exception { + return super.authenticationManagerBean(); + } + } + + @Configuration + static class DataSourceConfig { + @Bean + public DataSource dataSource() { + EmbeddedDatabaseBuilder builder = new EmbeddedDatabaseBuilder() + return builder.setType(EmbeddedDatabaseType.HSQL).build(); + } + } + + def "jdbc-user-service custom"() { + when: + loadConfig(CustomDataSourceConfig,CustomJdbcUserServiceSampleConfig) + then: + findAuthenticationProvider(DaoAuthenticationProvider).userDetailsService.userCache instanceof CustomUserCache + when: + Authentication auth = authenticationManager.authenticate(new UsernamePasswordAuthenticationToken("user", "password")) + then: + auth.authorities.collect {it.authority}.sort() == ['ROLE_DBA','ROLE_USER'] + auth.name == 'user' + } + + @EnableWebSecurity + @Configuration + static class CustomJdbcUserServiceSampleConfig extends WebSecurityConfigurerAdapter { + @Autowired + private DataSource dataSource; + + protected void registerAuthentication(AuthenticationManagerBuilder auth) throws Exception { + auth + .jdbcAuthentication() + // jdbc-user-service@dataSource + .dataSource(dataSource) + // jdbc-user-service@cache-ref + .userCache(new CustomUserCache()) + // jdbc-user-service@users-byusername-query + .usersByUsernameQuery("select principal,credentials,true from users where principal = ?") + // jdbc-user-service@authorities-by-username-query + .authoritiesByUsernameQuery("select principal,role from roles where principal = ?") + // jdbc-user-service@group-authorities-by-username-query + .groupAuthoritiesByUsername(JdbcUserDetailsManager.DEF_GROUP_AUTHORITIES_BY_USERNAME_QUERY) + // jdbc-user-service@role-prefix + .rolePrefix("ROLE_") + + } + + // Only necessary to have access to verify the AuthenticationManager + @Bean + @Override + public AuthenticationManager authenticationManagerBean() + throws Exception { + return super.authenticationManagerBean(); + } + + static class CustomUserCache implements UserCache { + + @Override + public UserDetails getUserFromCache(String username) { + return null; + } + + @Override + public void putUserInCache(UserDetails user) { + } + + @Override + public void removeUserFromCache(String username) { + } + } + } + + @Configuration + static class CustomDataSourceConfig { + @Bean + public DataSource dataSource() { + EmbeddedDatabaseBuilder builder = new EmbeddedDatabaseBuilder() + // simulate that the DB already has the schema loaded and users in it + .addScript("CustomJdbcUserServiceSampleConfig.sql") + return builder.setType(EmbeddedDatabaseType.HSQL).build(); + } + } +} diff --git a/config/src/test/groovy/org/springframework/security/config/annotation/authentication/NamespacePasswordEncoderTests.groovy b/config/src/test/groovy/org/springframework/security/config/annotation/authentication/NamespacePasswordEncoderTests.groovy new file mode 100644 index 0000000000..c62a13c1c1 --- /dev/null +++ b/config/src/test/groovy/org/springframework/security/config/annotation/authentication/NamespacePasswordEncoderTests.groovy @@ -0,0 +1,126 @@ +/* + * Copyright 2002-2013 the original author or authors. + * + * 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 org.springframework.security.config.annotation.authentication + +import static org.springframework.security.config.annotation.authentication.PasswordEncoderConfigurerConfigs.* + +import javax.sql.DataSource + +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder +import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType +import org.springframework.ldap.core.support.BaseLdapPathContextSource; +import org.springframework.security.authentication.AuthenticationManager +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken +import org.springframework.security.config.annotation.BaseSpringSpec +import org.springframework.security.config.annotation.authentication.configurers.ldap.LdapAuthenticationProviderConfigurer; +import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; +import org.springframework.security.core.authority.AuthorityUtils +import org.springframework.security.core.userdetails.User +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder +import org.springframework.security.ldap.authentication.LdapAuthenticationProvider; +import org.springframework.security.provisioning.InMemoryUserDetailsManager + +/** + * + * @author Rob Winch + * + */ +class NamespacePasswordEncoderTests extends BaseSpringSpec { + def "password-encoder@ref with in memory"() { + when: + loadConfig(PasswordEncoderWithInMemoryConfig) + then: + authenticationManager.authenticate(new UsernamePasswordAuthenticationToken("user", "password")) + } + + @EnableWebSecurity + @Configuration + static class PasswordEncoderWithInMemoryConfig extends WebSecurityConfigurerAdapter { + @Override + protected void registerAuthentication(AuthenticationManagerBuilder auth) + throws Exception { + + BCryptPasswordEncoder encoder = new BCryptPasswordEncoder() + auth + .inMemoryAuthentication() + .withUser("user").password(encoder.encode("password")).roles("USER").and() + .passwordEncoder(encoder) + } + } + + def "password-encoder@ref with jdbc"() { + when: + loadConfig(PasswordEncoderWithJdbcConfig) + then: + authenticationManager.authenticate(new UsernamePasswordAuthenticationToken("user", "password")) + } + + @EnableWebSecurity + @Configuration + static class PasswordEncoderWithJdbcConfig extends WebSecurityConfigurerAdapter { + @Override + protected void registerAuthentication(AuthenticationManagerBuilder auth) + throws Exception { + + BCryptPasswordEncoder encoder = new BCryptPasswordEncoder() + auth + .jdbcAuthentication() + .withDefaultSchema() + .dataSource(dataSource()) + .withUser("user").password(encoder.encode("password")).roles("USER").and() + .passwordEncoder(encoder) + } + + @Bean + public DataSource dataSource() { + EmbeddedDatabaseBuilder builder = new EmbeddedDatabaseBuilder() + return builder.setType(EmbeddedDatabaseType.HSQL).build(); + } + } + + def "password-encoder@ref with userdetailsservice"() { + when: + loadConfig(PasswordEncoderWithUserDetailsServiceConfig) + then: + authenticationManager.authenticate(new UsernamePasswordAuthenticationToken("user", "password")) + } + + @EnableWebSecurity + @Configuration + static class PasswordEncoderWithUserDetailsServiceConfig extends WebSecurityConfigurerAdapter { + @Override + protected void registerAuthentication(AuthenticationManagerBuilder auth) + throws Exception { + + BCryptPasswordEncoder encoder = new BCryptPasswordEncoder() + User user = new User("user",encoder.encode("password"), AuthorityUtils.createAuthorityList("ROLE_USER")) + InMemoryUserDetailsManager uds = new InMemoryUserDetailsManager([user]) + auth + .userDetailsService(uds) + .passwordEncoder(encoder) + } + + @Bean + public DataSource dataSource() { + EmbeddedDatabaseBuilder builder = new EmbeddedDatabaseBuilder() + return builder.setType(EmbeddedDatabaseType.HSQL).build(); + } + } +} diff --git a/config/src/test/groovy/org/springframework/security/config/annotation/authentication/PasswordEncoderConfigurerConfigs.java b/config/src/test/groovy/org/springframework/security/config/annotation/authentication/PasswordEncoderConfigurerConfigs.java new file mode 100644 index 0000000000..8d9065b150 --- /dev/null +++ b/config/src/test/groovy/org/springframework/security/config/annotation/authentication/PasswordEncoderConfigurerConfigs.java @@ -0,0 +1,83 @@ +/* + * Copyright 2002-2013 the original author or authors. + * + * 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 org.springframework.security.config.annotation.authentication; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; + +/** + * Class Containing the {@link Configuration} for + * {@link PasswordEncoderConfigurerTests}. Separate to ensure the configuration + * compiles in Java (i.e. we are not using hidden methods). + * + * @author Rob Winch + * @since 3.2 + */ +public class PasswordEncoderConfigurerConfigs { + + @EnableWebSecurity + @Configuration + static class PasswordEncoderConfig extends WebSecurityConfigurerAdapter { + protected void registerAuthentication( + AuthenticationManagerBuilder auth) throws Exception { + BCryptPasswordEncoder encoder = passwordEncoder(); + auth + .inMemoryAuthentication() + .withUser("user").password(encoder.encode("password")).roles("USER").and() + .passwordEncoder(encoder); + } + + @Bean + @Override + public AuthenticationManager authenticationManagerBean() + throws Exception { + return super.authenticationManagerBean(); + } + + @Override + protected void configure(HttpSecurity http) throws Exception { + } + + @Bean + public BCryptPasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } + } + + @EnableWebSecurity + @Configuration + static class PasswordEncoderNoAuthManagerLoadsConfig extends WebSecurityConfigurerAdapter { + protected void registerAuthentication( + AuthenticationManagerBuilder auth) throws Exception { + BCryptPasswordEncoder encoder = passwordEncoder(); + auth + .inMemoryAuthentication() + .withUser("user").password(encoder.encode("password")).roles("USER").and() + .passwordEncoder(encoder); + } + + @Bean + public BCryptPasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } + } +} diff --git a/config/src/test/groovy/org/springframework/security/config/annotation/authentication/PasswordEncoderConfigurerTests.groovy b/config/src/test/groovy/org/springframework/security/config/annotation/authentication/PasswordEncoderConfigurerTests.groovy new file mode 100644 index 0000000000..3e45b2e6c2 --- /dev/null +++ b/config/src/test/groovy/org/springframework/security/config/annotation/authentication/PasswordEncoderConfigurerTests.groovy @@ -0,0 +1,61 @@ +/* + * Copyright 2002-2013 the original author or authors. + * + * 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 org.springframework.security.config.annotation.authentication + +import static org.springframework.security.config.annotation.authentication.PasswordEncoderConfigurerConfigs.* + +import org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.ldap.core.ContextSource; +import org.springframework.ldap.core.support.BaseLdapPathContextSource; +import org.springframework.security.authentication.AuthenticationManager +import org.springframework.security.authentication.AuthenticationProvider +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken +import org.springframework.security.config.annotation.BaseSpringSpec +import org.springframework.security.config.annotation.SecurityBuilder; +import org.springframework.security.config.annotation.authentication.AuthenticationManagerBuilder +import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.web.FilterChainProxy; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.test.util.ReflectionTestUtils; + +/** + * + * @author Rob Winch + * + */ +class PasswordEncoderConfigurerTests extends BaseSpringSpec { + def "password-encoder@ref with No AuthenticationManager Bean"() { + when: + loadConfig(PasswordEncoderNoAuthManagerLoadsConfig) + then: + noExceptionThrown() + } + + def "password-encoder@ref with AuthenticationManagerBuilder"() { + when: + loadConfig(PasswordEncoderConfig) + AuthenticationManager authMgr = authenticationManager() + then: + authMgr.authenticate(new UsernamePasswordAuthenticationToken("user", "password")) + } +} diff --git a/config/src/test/groovy/org/springframework/security/config/annotation/configuration/AutowireBeanFactoryObjectPostProcessorTests.groovy b/config/src/test/groovy/org/springframework/security/config/annotation/configuration/AutowireBeanFactoryObjectPostProcessorTests.groovy new file mode 100644 index 0000000000..a602f27991 --- /dev/null +++ b/config/src/test/groovy/org/springframework/security/config/annotation/configuration/AutowireBeanFactoryObjectPostProcessorTests.groovy @@ -0,0 +1,120 @@ +/* + * Copyright 2002-2013 the original author or authors. + * + * 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 org.springframework.security.config.annotation.configuration + +import javax.servlet.ServletConfig +import javax.servlet.ServletContext + +import org.springframework.beans.factory.BeanClassLoaderAware +import org.springframework.beans.factory.BeanFactoryAware +import org.springframework.beans.factory.BeanNameAware +import org.springframework.beans.factory.DisposableBean; +import org.springframework.beans.factory.config.AutowireCapableBeanFactory; +import org.springframework.context.ApplicationContextAware +import org.springframework.context.ApplicationEventPublisherAware +import org.springframework.context.EnvironmentAware +import org.springframework.context.MessageSourceAware +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.mock.web.MockServletConfig +import org.springframework.mock.web.MockServletContext +import org.springframework.security.config.annotation.BaseSpringSpec +import org.springframework.security.config.annotation.ObjectPostProcessor; +import org.springframework.security.config.annotation.configuration.AutowireBeanFactoryObjectPostProcessor; +import org.springframework.web.context.ServletConfigAware +import org.springframework.web.context.ServletContextAware +import org.springframework.web.context.support.AnnotationConfigWebApplicationContext + +/** + * + * @author Rob Winch + */ +class AutowireBeanFactoryObjectPostProcessorTests extends BaseSpringSpec { + + def "Verify All Aware methods are invoked"() { + setup: + ApplicationContextAware contextAware = Mock(ApplicationContextAware) + ApplicationEventPublisherAware publisher = Mock(ApplicationEventPublisherAware) + BeanClassLoaderAware classloader = Mock(BeanClassLoaderAware) + BeanFactoryAware beanFactory = Mock(BeanFactoryAware) + EnvironmentAware environment = Mock(EnvironmentAware) + MessageSourceAware messageSource = Mock(MessageSourceAware) + ServletConfigAware servletConfig = Mock(ServletConfigAware) + ServletContextAware servletContext = Mock(ServletContextAware) + DisposableBean disposable = Mock(DisposableBean) + + context = new AnnotationConfigWebApplicationContext([servletConfig:new MockServletConfig(),servletContext:new MockServletContext()]) + context.register(Config) + context.refresh() + context.start() + + ObjectPostProcessor opp = context.getBean(ObjectPostProcessor) + when: + opp.postProcess(contextAware) + then: + 1 * contextAware.setApplicationContext(!null) + + when: + opp.postProcess(publisher) + then: + 1 * publisher.setApplicationEventPublisher(!null) + + when: + opp.postProcess(classloader) + then: + 1 * classloader.setBeanClassLoader(!null) + + when: + opp.postProcess(beanFactory) + then: + 1 * beanFactory.setBeanFactory(!null) + + when: + opp.postProcess(environment) + then: + 1 * environment.setEnvironment(!null) + + when: + opp.postProcess(messageSource) + then: + 1 * messageSource.setMessageSource(!null) + + when: + opp.postProcess(servletConfig) + then: + 1 * servletConfig.setServletConfig(!null) + + when: + opp.postProcess(servletContext) + then: + 1 * servletContext.setServletContext(!null) + + when: + opp.postProcess(disposable) + context.close() + context = null + then: + 1 * disposable.destroy() + } + + @Configuration + static class Config { + @Bean + public ObjectPostProcessor objectPostProcessor(AutowireCapableBeanFactory beanFactory) { + return new AutowireBeanFactoryObjectPostProcessor(beanFactory); + } + } +} diff --git a/config/src/test/groovy/org/springframework/security/config/annotation/method/configuration/GlobalMethodSecurityConfigurationTests.groovy b/config/src/test/groovy/org/springframework/security/config/annotation/method/configuration/GlobalMethodSecurityConfigurationTests.groovy new file mode 100644 index 0000000000..225027b935 --- /dev/null +++ b/config/src/test/groovy/org/springframework/security/config/annotation/method/configuration/GlobalMethodSecurityConfigurationTests.groovy @@ -0,0 +1,85 @@ +/* + * Copyright 2002-2013 the original author or authors. + * + * 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 org.springframework.security.config.annotation.method.configuration + +import static org.fest.assertions.Assertions.assertThat +import static org.junit.Assert.fail + +import org.aopalliance.intercept.MethodInterceptor; +import org.springframework.context.ApplicationContext +import org.springframework.context.ApplicationListener; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.access.AccessDecisionManager +import org.springframework.security.access.AccessDeniedException +import org.springframework.security.access.ConfigAttribute +import org.springframework.security.authentication.AuthenticationManager +import org.springframework.security.authentication.DefaultAuthenticationEventPublisher +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken +import org.springframework.security.authentication.event.AuthenticationSuccessEvent; +import org.springframework.security.config.annotation.BaseSpringSpec +import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; +import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; +import org.springframework.security.config.annotation.method.configuration.GlobalMethodSecurityConfiguration; +import org.springframework.security.config.annotation.web.WebSecurityConfigurerAdapterTests.InMemoryAuthWithWebSecurityConfigurerAdapter +import org.springframework.security.core.Authentication +import org.springframework.security.core.authority.AuthorityUtils + +/** + * + * @author Rob Winch + */ +public class GlobalMethodSecurityConfigurationTests extends BaseSpringSpec { + def "messages set when using GlobalMethodSecurityConfiguration"() { + when: + loadConfig(InMemoryAuthWithGlobalMethodSecurityConfig) + then: + authenticationManager.messages.messageSource instanceof ApplicationContext + } + + def "AuthenticationEventPublisher is registered GlobalMethodSecurityConfiguration"() { + when: + loadConfig(InMemoryAuthWithGlobalMethodSecurityConfig) + then: + authenticationManager.eventPublisher instanceof DefaultAuthenticationEventPublisher + when: + Authentication auth = new UsernamePasswordAuthenticationToken("user",null,AuthorityUtils.createAuthorityList("ROLE_USER")) + authenticationManager.eventPublisher.publishAuthenticationSuccess(auth) + then: + InMemoryAuthWithGlobalMethodSecurityConfig.EVENT.authentication == auth + } + + @Configuration + @EnableGlobalMethodSecurity(prePostEnabled = true) + public static class InMemoryAuthWithGlobalMethodSecurityConfig extends GlobalMethodSecurityConfiguration implements ApplicationListener { + static AuthenticationSuccessEvent EVENT + + @Override + protected void registerAuthentication(AuthenticationManagerBuilder auth) + throws Exception { + auth + .inMemoryAuthentication() + } + + @Override + public void onApplicationEvent(AuthenticationSuccessEvent e) { + EVENT = e + } + } + + AuthenticationManager getAuthenticationManager() { + context.getBean(MethodInterceptor).authenticationManager + } +} diff --git a/config/src/test/groovy/org/springframework/security/config/annotation/method/configuration/MethodSecurityService.groovy b/config/src/test/groovy/org/springframework/security/config/annotation/method/configuration/MethodSecurityService.groovy new file mode 100644 index 0000000000..7bc1ce0795 --- /dev/null +++ b/config/src/test/groovy/org/springframework/security/config/annotation/method/configuration/MethodSecurityService.groovy @@ -0,0 +1,51 @@ +/* + * Copyright 2002-2013 the original author or authors. + * + * 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 org.springframework.security.config.annotation.method.configuration; + +import javax.annotation.security.DenyAll + +import org.springframework.security.access.annotation.Secured +import org.springframework.security.access.prepost.PostAuthorize; +import org.springframework.security.access.prepost.PreAuthorize +import org.springframework.security.core.Authentication + + +/** + * + * @author Rob Winch + */ +public interface MethodSecurityService { + @PreAuthorize("denyAll") + public String preAuthorize(); + + @Secured("ROLE_ADMIN") + public String secured(); + + @DenyAll + public String jsr250(); + + @Secured(["ROLE_USER","RUN_AS_SUPER"]) + public Authentication runAs(); + + @PreAuthorize("permitAll") + public String preAuthorizePermitAll(); + + @PreAuthorize("hasPermission(#object,'read')") + public String hasPermission(String object); + + @PostAuthorize("hasPermission(#object,'read')") + public String postHasPermission(String object); +} diff --git a/config/src/test/groovy/org/springframework/security/config/annotation/method/configuration/MethodSecurityServiceImpl.groovy b/config/src/test/groovy/org/springframework/security/config/annotation/method/configuration/MethodSecurityServiceImpl.groovy new file mode 100644 index 0000000000..c0af11e624 --- /dev/null +++ b/config/src/test/groovy/org/springframework/security/config/annotation/method/configuration/MethodSecurityServiceImpl.groovy @@ -0,0 +1,62 @@ +/* + * Copyright 2002-2013 the original author or authors. + * + * 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 org.springframework.security.config.annotation.method.configuration; + +import org.springframework.security.core.Authentication +import org.springframework.security.core.context.SecurityContextHolder + +/** + * + * @author Rob Winch + * + */ +public class MethodSecurityServiceImpl implements MethodSecurityService { + + @Override + public String preAuthorize() { + return null; + } + + @Override + public String secured() { + return null; + } + + @Override + public String jsr250() { + return null; + } + + @Override + public Authentication runAs() { + return SecurityContextHolder.getContext().getAuthentication(); + } + + @Override + public String preAuthorizePermitAll() { + return null; + } + + @Override + public String hasPermission(String object) { + return null; + } + + @Override + public String postHasPermission(String object) { + return null; + } +} diff --git a/config/src/test/groovy/org/springframework/security/config/annotation/method/configuration/NamespaceGlobalMethodSecurityExpressionHandlerTests.groovy b/config/src/test/groovy/org/springframework/security/config/annotation/method/configuration/NamespaceGlobalMethodSecurityExpressionHandlerTests.groovy new file mode 100644 index 0000000000..b601673c25 --- /dev/null +++ b/config/src/test/groovy/org/springframework/security/config/annotation/method/configuration/NamespaceGlobalMethodSecurityExpressionHandlerTests.groovy @@ -0,0 +1,93 @@ +/* + * Copyright 2002-2013 the original author or authors. + * + * 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 org.springframework.security.config.annotation.method.configuration + +import static org.fest.assertions.Assertions.assertThat +import static org.junit.Assert.fail + +import java.io.Serializable; + +import org.springframework.context.ConfigurableApplicationContext +import org.springframework.context.annotation.AnnotationConfigApplicationContext +import org.springframework.context.annotation.Configuration +import org.springframework.security.access.AccessDeniedException +import org.springframework.security.access.PermissionEvaluator; +import org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler +import org.springframework.security.access.expression.method.MethodSecurityExpressionHandler +import org.springframework.security.authentication.TestingAuthenticationToken +import org.springframework.security.config.annotation.BaseSpringSpec +import org.springframework.security.config.annotation.method.configuration.NamespaceGlobalMethodSecurityTests.BaseMethodConfig; +import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; +import org.springframework.security.config.annotation.method.configuration.GlobalMethodSecurityConfiguration; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder + +/** + * + * @author Rob Winch + */ +public class NamespaceGlobalMethodSecurityExpressionHandlerTests extends BaseSpringSpec { + def setup() { + SecurityContextHolder.getContext().setAuthentication( + new TestingAuthenticationToken("user", "password","ROLE_USER")) + } + + def "global-method-security/expression-handler @PreAuthorize"() { + setup: + context = new AnnotationConfigApplicationContext(BaseMethodConfig,CustomAccessDecisionManagerConfig) + MethodSecurityService service = context.getBean(MethodSecurityService) + when: + service.hasPermission("granted") + then: + noExceptionThrown() + when: + service.hasPermission("denied") + then: + thrown(AccessDeniedException) + } + + def "global-method-security/expression-handler @PostAuthorize"() { + setup: + context = new AnnotationConfigApplicationContext(BaseMethodConfig,CustomAccessDecisionManagerConfig) + MethodSecurityService service = context.getBean(MethodSecurityService) + when: + service.postHasPermission("granted") + then: + noExceptionThrown() + when: + service.postHasPermission("denied") + then: + thrown(AccessDeniedException) + } + + @Configuration + @EnableGlobalMethodSecurity(prePostEnabled = true) + public static class CustomAccessDecisionManagerConfig extends GlobalMethodSecurityConfiguration { + @Override + protected MethodSecurityExpressionHandler expressionHandler() { + DefaultMethodSecurityExpressionHandler expressionHandler = new DefaultMethodSecurityExpressionHandler() + expressionHandler.permissionEvaluator = new PermissionEvaluator() { + boolean hasPermission(Authentication authentication, Object targetDomainObject, Object permission) { + "granted" == targetDomainObject + } + boolean hasPermission(Authentication authentication, Serializable targetId, String targetType, Object permission) { + throw new UnsupportedOperationException() + } + } + return expressionHandler + } + } +} diff --git a/config/src/test/groovy/org/springframework/security/config/annotation/method/configuration/NamespaceGlobalMethodSecurityTests.groovy b/config/src/test/groovy/org/springframework/security/config/annotation/method/configuration/NamespaceGlobalMethodSecurityTests.groovy new file mode 100644 index 0000000000..4e035d2246 --- /dev/null +++ b/config/src/test/groovy/org/springframework/security/config/annotation/method/configuration/NamespaceGlobalMethodSecurityTests.groovy @@ -0,0 +1,443 @@ +/* + * Copyright 2002-2013 the original author or authors. + * + * 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 org.springframework.security.config.annotation.method.configuration + +import static org.fest.assertions.Assertions.assertThat +import static org.junit.Assert.fail + +import java.lang.reflect.Method + +import org.springframework.aop.aspectj.annotation.AnnotationAwareAspectJAutoProxyCreator +import org.springframework.beans.factory.BeanCreationException +import org.springframework.context.ConfigurableApplicationContext +import org.springframework.context.annotation.AdviceMode +import org.springframework.context.annotation.AnnotationConfigApplicationContext +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.context.annotation.Import +import org.springframework.core.Ordered +import org.springframework.security.access.AccessDecisionManager +import org.springframework.security.access.AccessDeniedException +import org.springframework.security.access.ConfigAttribute +import org.springframework.security.access.SecurityConfig +import org.springframework.security.access.intercept.AfterInvocationManager +import org.springframework.security.access.intercept.RunAsManager +import org.springframework.security.access.intercept.RunAsManagerImpl +import org.springframework.security.access.intercept.aopalliance.MethodSecurityInterceptor +import org.springframework.security.access.intercept.aopalliance.MethodSecurityMetadataSourceAdvisor +import org.springframework.security.access.method.AbstractMethodSecurityMetadataSource +import org.springframework.security.access.method.MethodSecurityMetadataSource +import org.springframework.security.authentication.AuthenticationManager +import org.springframework.security.authentication.TestingAuthenticationToken +import org.springframework.security.config.annotation.BaseSpringSpec +import org.springframework.security.config.annotation.authentication.BaseAuthenticationConfig; +import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; +import org.springframework.security.config.annotation.method.configuration.GlobalMethodSecurityConfiguration; +import org.springframework.security.core.Authentication +import org.springframework.security.core.context.SecurityContextHolder + +/** + * + * @author Rob Winch + */ +public class NamespaceGlobalMethodSecurityTests extends BaseSpringSpec { + def setup() { + SecurityContextHolder.getContext().setAuthentication( + new TestingAuthenticationToken("user", "password","ROLE_USER")) + } + + // --- access-decision-manager-ref --- + + def "custom AccessDecisionManager can be used"() { + setup: "Create an instance with an AccessDecisionManager that always denies access" + context = new AnnotationConfigApplicationContext(BaseMethodConfig,CustomAccessDecisionManagerConfig) + MethodSecurityService service = context.getBean(MethodSecurityService) + when: + service.preAuthorize() + then: + thrown(AccessDeniedException) + when: + service.secured() + then: + thrown(AccessDeniedException) + } + + @Configuration + @EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true) + public static class CustomAccessDecisionManagerConfig extends GlobalMethodSecurityConfiguration { + @Override + protected AccessDecisionManager accessDecisionManager() { + return new DenyAllAccessDecisionManager() + } + + public static class DenyAllAccessDecisionManager implements AccessDecisionManager { + public void decide(Authentication authentication, Object object, Collection configAttributes) { + throw new AccessDeniedException("Always Denied") + } + public boolean supports(ConfigAttribute attribute) { + return true + } + public boolean supports(Class clazz) { + return true + } + } + } + + // --- authentication-manager-ref --- + + def "custom AuthenticationManager can be used"() { + when: + context = new AnnotationConfigApplicationContext(CustomAuthenticationConfig) + MethodSecurityInterceptor interceptor = context.getBean(MethodSecurityInterceptor) + interceptor.authenticationManager.authenticate(SecurityContextHolder.context.authentication) + then: + thrown(UnsupportedOperationException) + } + + @Configuration + @EnableGlobalMethodSecurity + public static class CustomAuthenticationConfig extends GlobalMethodSecurityConfiguration { + @Override + protected AuthenticationManager authenticationManager() { + return new AuthenticationManager() { + Authentication authenticate(Authentication authentication) { + throw new UnsupportedOperationException() + } + } + } + } + + // --- jsr250-annotations --- + + def "enable jsr250"() { + when: + context = new AnnotationConfigApplicationContext(Jsr250Config) + MethodSecurityService service = context.getBean(MethodSecurityService) + then: "@Secured and @PreAuthorize are ignored" + service.secured() == null + service.preAuthorize() == null + + when: "@DenyAll method invoked" + service.jsr250() + then: "access is denied" + thrown(AccessDeniedException) + } + + @EnableGlobalMethodSecurity(jsr250Enabled = true) + @Configuration + public static class Jsr250Config extends BaseMethodConfig { + } + + // --- metadata-source-ref --- + + def "custom MethodSecurityMetadataSource can be used with higher priority than other sources"() { + setup: + context = new AnnotationConfigApplicationContext(BaseMethodConfig,CustomMethodSecurityMetadataSourceConfig) + MethodSecurityService service = context.getBean(MethodSecurityService) + when: + service.preAuthorize() + then: + thrown(AccessDeniedException) + when: + service.secured() + then: + thrown(AccessDeniedException) + when: + service.jsr250() + then: + thrown(AccessDeniedException) + } + + @Configuration + @EnableGlobalMethodSecurity + public static class CustomMethodSecurityMetadataSourceConfig extends GlobalMethodSecurityConfiguration { + @Override + protected MethodSecurityMetadataSource customMethodSecurityMetadataSource() { + return new AbstractMethodSecurityMetadataSource() { + public Collection getAttributes(Method method, Class targetClass) { + // require ROLE_NOBODY for any method on MethodSecurityService class + return MethodSecurityService.isAssignableFrom(targetClass) ? [new SecurityConfig("ROLE_NOBODY")] : [] + } + public Collection getAllConfigAttributes() { + return null + } + } + } + } + + // --- mode --- + + def "aspectj mode works"() { + when: + context = new AnnotationConfigApplicationContext(AspectJModeConfig) + then: + AnnotationAwareAspectJAutoProxyCreator autoProxyCreator = context.getBean(AnnotationAwareAspectJAutoProxyCreator) + autoProxyCreator.proxyTargetClass == true + } + + @Configuration + @EnableGlobalMethodSecurity(mode = AdviceMode.ASPECTJ, proxyTargetClass = true) + public static class AspectJModeConfig extends BaseMethodConfig { + } + + def "aspectj mode works extending GlobalMethodSecurityConfiguration"() { + when: + context = new AnnotationConfigApplicationContext(BaseMethodConfig,AspectJModeExtendsGMSCConfig) + then: + AnnotationAwareAspectJAutoProxyCreator autoProxyCreator = context.getBean(AnnotationAwareAspectJAutoProxyCreator) + autoProxyCreator.proxyTargetClass == false + } + + @Configuration + @EnableGlobalMethodSecurity(mode = AdviceMode.ASPECTJ) + public static class AspectJModeExtendsGMSCConfig extends GlobalMethodSecurityConfiguration { + } + + // --- order --- + + def order() { + when: + context = new AnnotationConfigApplicationContext(CustomOrderConfig) + MethodSecurityMetadataSourceAdvisor advisor = context.getBean(MethodSecurityMetadataSourceAdvisor) + then: + advisor.order == 135 + } + + @Configuration + @EnableGlobalMethodSecurity(order = 135) + public static class CustomOrderConfig extends BaseMethodConfig { + } + + def "order is defaulted to Ordered.LOWEST_PRECEDENCE when using @EnableGlobalMethodSecurity"() { + when: + context = new AnnotationConfigApplicationContext(DefaultOrderConfig) + MethodSecurityMetadataSourceAdvisor advisor = context.getBean(MethodSecurityMetadataSourceAdvisor) + then: + advisor.order == Ordered.LOWEST_PRECEDENCE + } + + @Configuration + @EnableGlobalMethodSecurity + public static class DefaultOrderConfig extends BaseMethodConfig { + } + + def "order is defaulted to Ordered.LOWEST_PRECEDENCE when extending GlobalMethodSecurityConfiguration"() { + when: + context = new AnnotationConfigApplicationContext(BaseMethodConfig,DefaultOrderExtendsMethodSecurityConfig) + MethodSecurityMetadataSourceAdvisor advisor = context.getBean(MethodSecurityMetadataSourceAdvisor) + then: + advisor.order == Ordered.LOWEST_PRECEDENCE + } + + @Configuration + @EnableGlobalMethodSecurity + public static class DefaultOrderExtendsMethodSecurityConfig extends GlobalMethodSecurityConfiguration { + } + + // --- pre-post-annotations --- + + def preAuthorize() { + when: + context = new AnnotationConfigApplicationContext(PreAuthorizeConfig) + MethodSecurityService service = context.getBean(MethodSecurityService) + then: + service.secured() == null + service.jsr250() == null + + when: + service.preAuthorize() + then: + thrown(AccessDeniedException) + } + + @EnableGlobalMethodSecurity(prePostEnabled = true) + @Configuration + public static class PreAuthorizeConfig extends BaseMethodConfig { + } + + def "prePostEnabled extends GlobalMethodSecurityConfiguration"() { + when: + context = new AnnotationConfigApplicationContext(BaseMethodConfig,PreAuthorizeExtendsGMSCConfig) + MethodSecurityService service = context.getBean(MethodSecurityService) + then: + service.secured() == null + service.jsr250() == null + + when: + service.preAuthorize() + then: + thrown(AccessDeniedException) + } + + @EnableGlobalMethodSecurity(prePostEnabled = true) + @Configuration + public static class PreAuthorizeExtendsGMSCConfig extends GlobalMethodSecurityConfiguration { + } + + // --- proxy-target-class --- + + def "proxying classes works"() { + when: + context = new AnnotationConfigApplicationContext(ProxyTargetClass) + MethodSecurityServiceImpl service = context.getBean(MethodSecurityServiceImpl) + then: + noExceptionThrown() + } + + @EnableGlobalMethodSecurity(proxyTargetClass = true) + @Configuration + public static class ProxyTargetClass extends BaseMethodConfig { + } + + def "proxying interfaces works"() { + when: + context = new AnnotationConfigApplicationContext(PreAuthorizeConfig) + MethodSecurityService service = context.getBean(MethodSecurityService) + then: "we get an instance of the interface" + noExceptionThrown() + when: "try to cast to the class" + MethodSecurityServiceImpl serviceImpl = service + then: "we get a class cast exception" + thrown(ClassCastException) + } + + // --- run-as-manager-ref --- + + def "custom RunAsManager"() { + when: + context = new AnnotationConfigApplicationContext(BaseMethodConfig,CustomRunAsManagerConfig) + MethodSecurityService service = context.getBean(MethodSecurityService) + then: + service.runAs().authorities.find { it.authority == "ROLE_RUN_AS_SUPER"} + } + + @Configuration + @EnableGlobalMethodSecurity(securedEnabled = true) + public static class CustomRunAsManagerConfig extends GlobalMethodSecurityConfiguration { + @Override + protected RunAsManager runAsManager() { + RunAsManagerImpl runAsManager = new RunAsManagerImpl() + runAsManager.setKey("some key") + return runAsManager + } + } + + // --- secured-annotation --- + + def "secured enabled"() { + setup: + context = new AnnotationConfigApplicationContext(SecuredConfig) + MethodSecurityService service = context.getBean(MethodSecurityService) + when: + service.secured() + then: + thrown(AccessDeniedException) + service.preAuthorize() == null + service.jsr250() == null + } + + @EnableGlobalMethodSecurity(securedEnabled = true) + @Configuration + public static class SecuredConfig extends BaseMethodConfig { + } + + // --- after-invocation-provider + + def "custom AfterInvocationManager"() { + setup: + context = new AnnotationConfigApplicationContext(BaseMethodConfig,CustomAfterInvocationManagerConfig) + MethodSecurityService service = context.getBean(MethodSecurityService) + when: + service.preAuthorizePermitAll() + then: + AccessDeniedException e = thrown() + e.message == "custom AfterInvocationManager" + } + + @Configuration + @EnableGlobalMethodSecurity(prePostEnabled = true) + public static class CustomAfterInvocationManagerConfig extends GlobalMethodSecurityConfiguration { + @Override + protected AfterInvocationManager afterInvocationManager() { + return new AfterInvocationManagerStub() + } + + public static class AfterInvocationManagerStub implements AfterInvocationManager { + Object decide(Authentication authentication, Object object, Collection attributes, + Object returnedObject) throws AccessDeniedException { + throw new AccessDeniedException("custom AfterInvocationManager") + } + + boolean supports(ConfigAttribute attribute) { + return true + } + boolean supports(Class clazz) { + return true + } + } + } + + // --- misc --- + + def "good error message when no Enable annotation"() { + when: + context = new AnnotationConfigApplicationContext(ExtendsNoEnableAnntotationConfig) + MethodSecurityInterceptor interceptor = context.getBean(MethodSecurityInterceptor) + interceptor.authenticationManager.authenticate(SecurityContextHolder.context.authentication) + then: + BeanCreationException e = thrown() + e.message.contains(EnableGlobalMethodSecurity.class.getName() + " is required") + } + + @Configuration + public static class ExtendsNoEnableAnntotationConfig extends GlobalMethodSecurityConfiguration { + @Override + protected AuthenticationManager authenticationManager() { + return new AuthenticationManager() { + Authentication authenticate(Authentication authentication) { + throw new UnsupportedOperationException() + } + } + } + } + + def "import subclass of GlobalMethodSecurityConfiguration"() { + when: + context = new AnnotationConfigApplicationContext(ImportSubclassGMSCConfig) + MethodSecurityService service = context.getBean(MethodSecurityService) + then: + service.secured() == null + service.jsr250() == null + + when: + service.preAuthorize() + then: + thrown(AccessDeniedException) + } + + @Configuration + @Import(PreAuthorizeExtendsGMSCConfig) + public static class ImportSubclassGMSCConfig extends BaseMethodConfig { + } + + @Configuration + public static class BaseMethodConfig extends BaseAuthenticationConfig { + @Bean + public MethodSecurityService methodSecurityService() { + return new MethodSecurityServiceImpl() + } + } +} diff --git a/config/src/test/groovy/org/springframework/security/config/annotation/method/configuration/SampleEnableGlobalMethodSecurityTests.groovy b/config/src/test/groovy/org/springframework/security/config/annotation/method/configuration/SampleEnableGlobalMethodSecurityTests.groovy new file mode 100644 index 0000000000..01e61314cd --- /dev/null +++ b/config/src/test/groovy/org/springframework/security/config/annotation/method/configuration/SampleEnableGlobalMethodSecurityTests.groovy @@ -0,0 +1,129 @@ +/* + * Copyright 2002-2013 the original author or authors. + * + * 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 org.springframework.security.config.annotation.method.configuration + +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.security.access.AccessDeniedException +import org.springframework.security.access.PermissionEvaluator; +import org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler; +import org.springframework.security.access.expression.method.MethodSecurityExpressionHandler +import org.springframework.security.authentication.AuthenticationManager +import org.springframework.security.authentication.TestingAuthenticationToken +import org.springframework.security.config.annotation.BaseSpringSpec +import org.springframework.security.config.annotation.authentication.AuthenticationManagerBuilder +import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; +import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; +import org.springframework.security.config.annotation.method.configuration.GlobalMethodSecurityConfiguration; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder + +/** + * Demonstrate the samples + * + * @author Rob Winch + * + */ +public class SampleEnableGlobalMethodSecurityTests extends BaseSpringSpec { + def setup() { + SecurityContextHolder.getContext().setAuthentication( + new TestingAuthenticationToken("user", "password","ROLE_USER")) + } + + def preAuthorize() { + when: + loadConfig(SampleWebSecurityConfig) + MethodSecurityService service = context.getBean(MethodSecurityService) + then: + service.secured() == null + service.jsr250() == null + + when: + service.preAuthorize() + then: + thrown(AccessDeniedException) + } + + @Configuration + @EnableGlobalMethodSecurity(prePostEnabled=true) + public static class SampleWebSecurityConfig { + @Bean + public MethodSecurityService methodSecurityService() { + return new MethodSecurityServiceImpl() + } + + @Bean + public AuthenticationManager authenticationManager() throws Exception { + return new AuthenticationManagerBuilder() + .inMemoryAuthentication() + .withUser("user").password("password").roles("USER").and() + .withUser("admin").password("password").roles("USER", "ADMIN").and() + .and() + .build(); + } + } + + def 'custom permission handler'() { + when: + loadConfig(CustomPermissionEvaluatorWebSecurityConfig) + MethodSecurityService service = context.getBean(MethodSecurityService) + then: + service.hasPermission("allowed") == null + + when: + service.hasPermission("denied") == null + then: + thrown(AccessDeniedException) + } + + @Configuration + @EnableGlobalMethodSecurity(prePostEnabled=true) + public static class CustomPermissionEvaluatorWebSecurityConfig extends GlobalMethodSecurityConfiguration { + @Bean + public MethodSecurityService methodSecurityService() { + return new MethodSecurityServiceImpl() + } + + @Override + protected MethodSecurityExpressionHandler expressionHandler() { + DefaultMethodSecurityExpressionHandler expressionHandler = new DefaultMethodSecurityExpressionHandler(); + expressionHandler.setPermissionEvaluator(new CustomPermissionEvaluator()); + return expressionHandler; + } + + @Override + protected void registerAuthentication(AuthenticationManagerBuilder auth) + throws Exception { + auth + .inMemoryAuthentication() + .withUser("user").password("password").roles("USER").and() + .withUser("admin").password("password").roles("USER", "ADMIN"); + } + } + + static class CustomPermissionEvaluator implements PermissionEvaluator { + public boolean hasPermission(Authentication authentication, + Object targetDomainObject, Object permission) { + return !"denied".equals(targetDomainObject); + } + + public boolean hasPermission(Authentication authentication, + Serializable targetId, String targetType, Object permission) { + return !"denied".equals(targetId); + } + + } +} diff --git a/config/src/test/groovy/org/springframework/security/config/annotation/provisioning/UserDetailsManagerConfigurerTests.groovy b/config/src/test/groovy/org/springframework/security/config/annotation/provisioning/UserDetailsManagerConfigurerTests.groovy new file mode 100644 index 0000000000..2fde950985 --- /dev/null +++ b/config/src/test/groovy/org/springframework/security/config/annotation/provisioning/UserDetailsManagerConfigurerTests.groovy @@ -0,0 +1,97 @@ +/* + * Copyright 2002-2013 the original author or authors. + * + * 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 org.springframework.security.config.annotation.provisioning + +import org.springframework.security.config.annotation.authentication.configurers.provisioning.UserDetailsManagerConfigurer; +import org.springframework.security.core.authority.SimpleGrantedAuthority +import org.springframework.security.core.userdetails.UserDetails +import org.springframework.security.provisioning.InMemoryUserDetailsManager + +import spock.lang.Specification + +/** + * + * @author Rob Winch + * + */ +class UserDetailsManagerConfigurerTests extends Specification { + + def "all attributes supported"() { + setup: + InMemoryUserDetailsManager userDetailsManager = new InMemoryUserDetailsManager([]) + when: + UserDetails userDetails = new UserDetailsManagerConfigurer>(userDetailsManager) + .withUser("user") + .password("password") + .roles("USER") + .disabled(true) + .accountExpired(true) + .accountLocked(true) + .credentialsExpired(true) + .build() + then: + userDetails.username == 'user' + userDetails.password == 'password' + userDetails.authorities.collect { it.authority } == ["ROLE_USER"] + !userDetails.accountNonExpired + !userDetails.accountNonLocked + !userDetails.credentialsNonExpired + !userDetails.enabled + } + + def "authorities(GrantedAuthorities...) works"() { + setup: + InMemoryUserDetailsManager userDetailsManager = new InMemoryUserDetailsManager([]) + SimpleGrantedAuthority authority = new SimpleGrantedAuthority("ROLE_USER") + when: + UserDetails userDetails = new UserDetailsManagerConfigurer>(userDetailsManager) + .withUser("user") + .password("password") + .authorities(authority) + .build() + then: + userDetails.authorities == [authority] as Set + } + + def "authorities(String...) works"() { + setup: + InMemoryUserDetailsManager userDetailsManager = new InMemoryUserDetailsManager([]) + String authority = "ROLE_USER" + when: + UserDetails userDetails = new UserDetailsManagerConfigurer>(userDetailsManager) + .withUser("user") + .password("password") + .authorities(authority) + .build() + then: + userDetails.authorities.collect { it.authority } == [authority] + } + + + def "authorities(List) works"() { + setup: + InMemoryUserDetailsManager userDetailsManager = new InMemoryUserDetailsManager([]) + SimpleGrantedAuthority authority = new SimpleGrantedAuthority("ROLE_USER") + when: + UserDetails userDetails = new UserDetailsManagerConfigurer>(userDetailsManager) + .withUser("user") + .password("password") + .authorities([authority]) + .build() + then: + userDetails.authorities == [authority] as Set + } +} diff --git a/config/src/test/groovy/org/springframework/security/config/annotation/web/AbstractConfiguredSecurityBuilderTests.groovy b/config/src/test/groovy/org/springframework/security/config/annotation/web/AbstractConfiguredSecurityBuilderTests.groovy new file mode 100644 index 0000000000..1f15282971 --- /dev/null +++ b/config/src/test/groovy/org/springframework/security/config/annotation/web/AbstractConfiguredSecurityBuilderTests.groovy @@ -0,0 +1,171 @@ +/* + * Copyright 2002-2013 the original author or authors. + * + * 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 org.springframework.security.config.annotation.web + +import org.springframework.security.config.annotation.AbstractConfiguredSecurityBuilder +import org.springframework.security.config.annotation.ObjectPostProcessor; +import org.springframework.security.config.annotation.SecurityConfigurer +import org.springframework.security.config.annotation.SecurityConfigurerAdapter +import org.springframework.test.util.ReflectionTestUtils + +import spock.lang.Specification + +/** + * @author Rob Winch + * + */ +class AbstractConfiguredSecurityBuilderTests extends Specification { + + ConcreteAbstractConfiguredBuilder builder = new ConcreteAbstractConfiguredBuilder() + + def "Null ObjectPostProcessor rejected"() { + when: + new ConcreteAbstractConfiguredBuilder(null) + then: + thrown(IllegalArgumentException) + when: + builder.objectPostProcessor(null); + then: + thrown(IllegalArgumentException) + } + + def "apply null is rejected"() { + when: + builder.apply(null) + then: + thrown(IllegalArgumentException) + } + + def "Duplicate configurer is removed"() { + when: + builder.apply(new ConcreteConfigurer()) + builder.apply(new ConcreteConfigurer()) + then: + ReflectionTestUtils.getField(builder,"configurers").size() == 1 + } + + def "build twice fails"() { + setup: + builder.build() + when: + builder.build() + then: + thrown(IllegalStateException) + } + + def "getObject before build fails"() { + when: + builder.getObject() + then: + thrown(IllegalStateException) + } + + def "Configurer.init can apply another configurer"() { + setup: + DelegateConfigurer.CONF = Mock(SecurityConfigurerAdapter) + when: + builder.apply(new DelegateConfigurer()) + builder.build() + then: + 1 * DelegateConfigurer.CONF.init(builder) + 1 * DelegateConfigurer.CONF.configure(builder) + } + + def "getConfigurer with multi fails"() { + setup: + ConcreteAbstractConfiguredBuilder builder = new ConcreteAbstractConfiguredBuilder(ObjectPostProcessor.QUIESCENT_POSTPROCESSOR, true) + builder.apply(new DelegateConfigurer()) + builder.apply(new DelegateConfigurer()) + when: + builder.getConfigurer(DelegateConfigurer) + then: "Fail due to trying to obtain a single DelegateConfigurer and multiple are provided" + thrown(IllegalStateException) + } + + def "removeConfigurer with multi fails"() { + setup: + ConcreteAbstractConfiguredBuilder builder = new ConcreteAbstractConfiguredBuilder(ObjectPostProcessor.QUIESCENT_POSTPROCESSOR, true) + builder.apply(new DelegateConfigurer()) + builder.apply(new DelegateConfigurer()) + when: + builder.removeConfigurer(DelegateConfigurer) + then: "Fail due to trying to remove and obtain a single DelegateConfigurer and multiple are provided" + thrown(IllegalStateException) + } + + def "removeConfigurers with multi"() { + setup: + DelegateConfigurer c1 = new DelegateConfigurer() + DelegateConfigurer c2 = new DelegateConfigurer() + ConcreteAbstractConfiguredBuilder builder = new ConcreteAbstractConfiguredBuilder(ObjectPostProcessor.QUIESCENT_POSTPROCESSOR, true) + builder.apply(c1) + builder.apply(c2) + when: + def result = builder.removeConfigurers(DelegateConfigurer) + then: + result.size() == 2 + result.contains(c1) + result.contains(c2) + builder.getConfigurers(DelegateConfigurer).empty + } + + def "getConfigurers with multi"() { + setup: + DelegateConfigurer c1 = new DelegateConfigurer() + DelegateConfigurer c2 = new DelegateConfigurer() + ConcreteAbstractConfiguredBuilder builder = new ConcreteAbstractConfiguredBuilder(ObjectPostProcessor.QUIESCENT_POSTPROCESSOR, true) + builder.apply(c1) + builder.apply(c2) + when: + def result = builder.getConfigurers(DelegateConfigurer) + then: + result.size() == 2 + result.contains(c1) + result.contains(c2) + builder.getConfigurers(DelegateConfigurer).size() == 2 + } + + private static class DelegateConfigurer extends SecurityConfigurerAdapter { + private static SecurityConfigurer CONF; + + @Override + public void init(ConcreteAbstractConfiguredBuilder builder) + throws Exception { + builder.apply(CONF); + } + } + + private static class ConcreteConfigurer extends SecurityConfigurerAdapter { } + + private static class ConcreteAbstractConfiguredBuilder extends AbstractConfiguredSecurityBuilder { + + public ConcreteAbstractConfiguredBuilder() { + } + + public ConcreteAbstractConfiguredBuilder(ObjectPostProcessor objectPostProcessor) { + super(objectPostProcessor); + } + + public ConcreteAbstractConfiguredBuilder(ObjectPostProcessor objectPostProcessor, boolean allowMulti) { + super(objectPostProcessor,allowMulti); + } + + public Object performBuild() throws Exception { + return "success"; + } + } + +} diff --git a/config/src/test/groovy/org/springframework/security/config/annotation/web/RequestMatchersTests.groovy b/config/src/test/groovy/org/springframework/security/config/annotation/web/RequestMatchersTests.groovy new file mode 100644 index 0000000000..2bf3316708 --- /dev/null +++ b/config/src/test/groovy/org/springframework/security/config/annotation/web/RequestMatchersTests.groovy @@ -0,0 +1,59 @@ +/* + * Copyright 2002-2013 the original author or authors. + * + * 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 org.springframework.security.config.annotation.web; + +import static org.springframework.security.config.annotation.web.AbstractRequestMatcherConfigurer.RequestMatchers.* + +import org.springframework.http.HttpMethod; +import org.springframework.security.web.util.AntPathRequestMatcher; +import org.springframework.security.web.util.RegexRequestMatcher; + +import spock.lang.Specification; + +/** + * @author Rob Winch + * + */ +class RequestMatchersTests extends Specification { + + def "regexMatchers(GET,'/a.*') uses RegexRequestMatcher"() { + when: + def matchers = regexMatchers(HttpMethod.GET, "/a.*") + then: 'matcher is a RegexRequestMatcher' + matchers.collect {it.class } == [RegexRequestMatcher] + } + + def "regexMatchers('/a.*') uses RegexRequestMatcher"() { + when: + def matchers = regexMatchers("/a.*") + then: 'matcher is a RegexRequestMatcher' + matchers.collect {it.class } == [RegexRequestMatcher] + } + + def "antMatchers(GET,'/a.*') uses AntPathRequestMatcher"() { + when: + def matchers = antMatchers(HttpMethod.GET, "/a.*") + then: 'matcher is a RegexRequestMatcher' + matchers.collect {it.class } == [AntPathRequestMatcher] + } + + def "antMatchers('/a.*') uses AntPathRequestMatcher"() { + when: + def matchers = antMatchers("/a.*") + then: 'matcher is a AntPathRequestMatcher' + matchers.collect {it.class } == [AntPathRequestMatcher] + } +} diff --git a/config/src/test/groovy/org/springframework/security/config/annotation/web/SampleWebSecurityConfigurerAdapterTests.groovy b/config/src/test/groovy/org/springframework/security/config/annotation/web/SampleWebSecurityConfigurerAdapterTests.groovy new file mode 100644 index 0000000000..9a83dd68de --- /dev/null +++ b/config/src/test/groovy/org/springframework/security/config/annotation/web/SampleWebSecurityConfigurerAdapterTests.groovy @@ -0,0 +1,321 @@ +/* + * Copyright 2002-2013 the original author or authors. + * + * 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 org.springframework.security.config.annotation.web + +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.core.annotation.Order +import org.springframework.security.authentication.AuthenticationManager +import org.springframework.security.config.annotation.BaseWebSpecuritySpec +import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder +import org.springframework.security.config.annotation.web.builders.HttpSecurity +import org.springframework.security.config.annotation.web.builders.WebSecurity +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter + +/** + * Demonstrate the samples + * + * @author Rob Winch + * + */ +public class SampleWebSecurityConfigurerAdapterTests extends BaseWebSpecuritySpec { + def "README HelloWorld Sample works"() { + setup: "Sample Config is loaded" + loadConfig(HelloWorldWebSecurityConfigurerAdapter) + when: + springSecurityFilterChain.doFilter(request,response,chain) + then: + response.getRedirectedUrl() == "http://localhost/login" + when: "fail to log in" + super.setup() + request.requestURI = "/login" + request.method = "POST" + springSecurityFilterChain.doFilter(request,response,chain) + then: "sent to login error page" + response.getRedirectedUrl() == "/login?error" + when: "login success" + super.setup() + request.requestURI = "/login" + request.method = "POST" + request.parameters.username = ["user"] as String[] + request.parameters.password = ["password"] as String[] + springSecurityFilterChain.doFilter(request,response,chain) + then: "sent to default succes page" + response.getRedirectedUrl() == "/" + } + + /** + * + * + * + * + * + * login-processing-url="/login" + * password-parameter="password" + * username-parameter="username" + * /> + * + * + * + * + * + * + * + * + * + * @author Rob Winch + */ + @Configuration + @EnableWebSecurity + public static class HelloWorldWebSecurityConfigurerAdapter extends WebSecurityConfigurerAdapter { + @Override + protected void registerAuthentication(AuthenticationManagerBuilder auth) { + auth + .inMemoryAuthentication() + .withUser("user").password("password").roles("USER"); + } + } + + def "README Sample works"() { + setup: "Sample Config is loaded" + loadConfig(SampleWebSecurityConfigurerAdapter) + when: + springSecurityFilterChain.doFilter(request,response,chain) + then: + response.getRedirectedUrl() == "http://localhost/login" + when: "fail to log in" + super.setup() + request.requestURI = "/login" + request.method = "POST" + springSecurityFilterChain.doFilter(request,response,chain) + then: "sent to login error page" + response.getRedirectedUrl() == "/login?error" + when: "login success" + super.setup() + request.requestURI = "/login" + request.method = "POST" + request.parameters.username = ["user"] as String[] + request.parameters.password = ["password"] as String[] + springSecurityFilterChain.doFilter(request,response,chain) + then: "sent to default succes page" + response.getRedirectedUrl() == "/" + } + + /** + * + * + * + * + * + * + * + * + * + * password-parameter="password" + * username-parameter="username" + * /> + * + * + * + * + * + * + * + * + * + * + * @author Rob Winch + */ + @Configuration + @EnableWebSecurity + public static class SampleWebSecurityConfigurerAdapter extends WebSecurityConfigurerAdapter { + + @Override + public void configure(WebSecurity web) throws Exception { + web + .ignoring() + .antMatchers("/resources/**"); + } + + @Override + protected void configure(HttpSecurity http) throws Exception { + http + .authorizeUrls() + .antMatchers("/signup","/about").permitAll() + .anyRequest().hasRole("USER") + .and() + .formLogin() + .loginUrl("/login") + // set permitAll for all URLs associated with Form Login + .permitAll(); + } + + @Override + protected void registerAuthentication(AuthenticationManagerBuilder auth) { + auth + .inMemoryAuthentication() + .withUser("user").password("password").roles("USER").and() + .withUser("admin").password("password").roles("USER", "ADMIN"); + } + } + + def "README Multi http Sample works"() { + setup: + loadConfig(SampleMultiHttpSecurityConfig) + when: + springSecurityFilterChain.doFilter(request,response,chain) + then: + response.getRedirectedUrl() == "http://localhost/login" + when: "fail to log in" + super.setup() + request.requestURI = "/login" + request.method = "POST" + springSecurityFilterChain.doFilter(request,response,chain) + then: "sent to login error page" + response.getRedirectedUrl() == "/login?error" + when: "login success" + super.setup() + request.requestURI = "/login" + request.method = "POST" + request.parameters.username = ["user"] as String[] + request.parameters.password = ["password"] as String[] + springSecurityFilterChain.doFilter(request,response,chain) + then: "sent to default succes page" + response.getRedirectedUrl() == "/" + + when: "request protected API URL" + super.setup() + request.servletPath = "/api/admin/test" + springSecurityFilterChain.doFilter(request,response,chain) + then: "get 403" + response.getStatus() == 403 + + when: "request API for admins with user" + super.setup() + request.servletPath = "/api/admin/test" + request.addHeader("Authorization", "Basic " + "user:password".bytes.encodeBase64().toString()) + springSecurityFilterChain.doFilter(request,response,chain) + then: "get 403" + response.getStatus() == 403 + + when: "request API for admins with admin" + super.setup() + request.servletPath = "/api/admin/test" + request.addHeader("Authorization", "Basic " + "admin:password".bytes.encodeBase64().toString()) + springSecurityFilterChain.doFilter(request,response,chain) + then: "get 200" + response.getStatus() == 200 + } + + + /** + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * password-parameter="password" + * username-parameter="username" + * /> + * + * + * + * + * + * + * + * + * + * + * @author Rob Winch + */ + @Configuration + @EnableWebSecurity + public static class SampleMultiHttpSecurityConfig { + @Bean + public AuthenticationManager authenticationManager() { + return new AuthenticationManagerBuilder() + .inMemoryAuthentication() + .withUser("user").password("password").roles("USER").and() + .withUser("admin").password("password").roles("USER", "ADMIN").and() + .and() + .build(); + } + + @Configuration + @Order(1) + public static class ApiWebSecurityConfigurationAdapter extends WebSecurityConfigurerAdapter { + protected void configure(HttpSecurity http) throws Exception { + http + .antMatcher("/api/**") + .authorizeUrls() + .antMatchers("/api/admin/**").hasRole("ADMIN") + .antMatchers("/api/**").hasRole("USER") + .and() + .httpBasic(); + } + } + + @Configuration + public static class FormLoginWebSecurityConfigurerAdapter extends WebSecurityConfigurerAdapter { + @Override + public void configure(WebSecurity web) throws Exception { + web + .ignoring() + .antMatchers("/resources/**"); + } + + @Override + protected void configure(HttpSecurity http) throws Exception { + http + .authorizeUrls() + .antMatchers("/signup","/about").permitAll() + .anyRequest().hasRole("USER") + .and() + .formLogin() + .loginUrl("/login") + .permitAll(); + } + } + } +} \ No newline at end of file diff --git a/config/src/test/groovy/org/springframework/security/config/annotation/web/WebSecurityConfigurerAdapterTests.groovy b/config/src/test/groovy/org/springframework/security/config/annotation/web/WebSecurityConfigurerAdapterTests.groovy new file mode 100644 index 0000000000..5067affb6d --- /dev/null +++ b/config/src/test/groovy/org/springframework/security/config/annotation/web/WebSecurityConfigurerAdapterTests.groovy @@ -0,0 +1,102 @@ +/* + * Copyright 2002-2013 the original author or authors. + * + * 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 org.springframework.security.config.annotation.web; + +import static org.springframework.security.config.annotation.web.WebSecurityConfigurerAdapterTestsConfigs.* +import static org.junit.Assert.* + +import javax.sql.DataSource + +import org.springframework.context.ApplicationContext +import org.springframework.context.ApplicationListener +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder +import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType +import org.springframework.ldap.core.support.BaseLdapPathContextSource +import org.springframework.security.authentication.AuthenticationManager +import org.springframework.security.authentication.AuthenticationProvider +import org.springframework.security.authentication.DefaultAuthenticationEventPublisher +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken +import org.springframework.security.authentication.event.AuthenticationSuccessEvent +import org.springframework.security.config.annotation.BaseSpringSpec +import org.springframework.security.config.annotation.authentication.configurers.ldap.LdapAuthenticationProviderConfigurer; +import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; +import org.springframework.security.core.Authentication +import org.springframework.security.core.authority.AuthorityUtils +import org.springframework.security.ldap.DefaultSpringSecurityContextSource + +/** + * @author Rob Winch + * + */ +class WebSecurityConfigurerAdapterTests extends BaseSpringSpec { + + def "MessageSources populated on AuthenticationProviders"() { + when: + loadConfig(MessageSourcesPopulatedConfig) + List providers = authenticationProviders() + then: + providers*.messages*.messageSource == [context,context,context,context] + } + + def "messages set when using WebSecurityConfigurerAdapter"() { + when: + loadConfig(InMemoryAuthWithWebSecurityConfigurerAdapter) + then: + authenticationManager.messages.messageSource instanceof ApplicationContext + } + + def "AuthenticationEventPublisher is registered for Web registerAuthentication"() { + when: + loadConfig(InMemoryAuthWithWebSecurityConfigurerAdapter) + then: + authenticationManager.parent.eventPublisher instanceof DefaultAuthenticationEventPublisher + when: + Authentication token = new UsernamePasswordAuthenticationToken("user","password") + authenticationManager.authenticate(token) + then: "We only receive the AuthenticationSuccessEvent once" + InMemoryAuthWithWebSecurityConfigurerAdapter.EVENTS.size() == 1 + InMemoryAuthWithWebSecurityConfigurerAdapter.EVENTS[0].authentication.name == token.principal + } + + @EnableWebSecurity + @Configuration + static class InMemoryAuthWithWebSecurityConfigurerAdapter extends WebSecurityConfigurerAdapter implements ApplicationListener { + static List EVENTS = [] + @Bean + @Override + public AuthenticationManager authenticationManagerBean() + throws Exception { + return super.authenticationManagerBean(); + } + + @Override + protected void registerAuthentication(AuthenticationManagerBuilder auth) + throws Exception { + auth + .inMemoryAuthentication() + .withUser("user").password("password").roles("USER") + } + + @Override + public void onApplicationEvent(AuthenticationSuccessEvent e) { + EVENTS.add(e) + } + } +} diff --git a/config/src/test/groovy/org/springframework/security/config/annotation/web/WebSecurityConfigurerAdapterTestsConfigs.java b/config/src/test/groovy/org/springframework/security/config/annotation/web/WebSecurityConfigurerAdapterTestsConfigs.java new file mode 100644 index 0000000000..975607862b --- /dev/null +++ b/config/src/test/groovy/org/springframework/security/config/annotation/web/WebSecurityConfigurerAdapterTestsConfigs.java @@ -0,0 +1,76 @@ +/* + * Copyright 2002-2013 the original author or authors. + * + * 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 org.springframework.security.config.annotation.web; + +import javax.sql.DataSource; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder; +import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType; +import org.springframework.ldap.core.support.BaseLdapPathContextSource; +import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; +import org.springframework.security.ldap.DefaultSpringSecurityContextSource; + +/** + * @author Rob Winch + * + */ +public class WebSecurityConfigurerAdapterTestsConfigs { + + // necessary because groovy resolves incorrect method when using generics + @Configuration + @EnableWebSecurity + static class MessageSourcesPopulatedConfig extends WebSecurityConfigurerAdapter { + @Override + protected void configure(HttpSecurity http) throws Exception { + http + .antMatcher("/role1/**") + .authorizeUrls() + .anyRequest().hasRole("1"); + } + + @Bean + public BaseLdapPathContextSource contextSource() throws Exception { + DefaultSpringSecurityContextSource contextSource = new DefaultSpringSecurityContextSource("ldap://127.0.0.1:33389/dc=springframework,dc=org"); + contextSource.setUserDn("uid=admin,ou=system"); + contextSource.setPassword("secret"); + return contextSource; + } + + @Bean + public DataSource dataSource() { + EmbeddedDatabaseBuilder builder = new EmbeddedDatabaseBuilder(); + return builder.setType(EmbeddedDatabaseType.HSQL).build(); + } + + @Override + protected void registerAuthentication(AuthenticationManagerBuilder auth) + throws Exception { + auth + .inMemoryAuthentication().and() + .jdbcAuthentication() + .dataSource(dataSource()) + .and() + .ldapAuthentication() + .userDnPatterns("uid={0},ou=people") + .contextSource(contextSource()); + } + } +} diff --git a/config/src/test/groovy/org/springframework/security/config/annotation/web/builders/HttpConfigurationTests.groovy b/config/src/test/groovy/org/springframework/security/config/annotation/web/builders/HttpConfigurationTests.groovy new file mode 100644 index 0000000000..36b4db138d --- /dev/null +++ b/config/src/test/groovy/org/springframework/security/config/annotation/web/builders/HttpConfigurationTests.groovy @@ -0,0 +1,120 @@ +/* + * Copyright 2002-2013 the original author or authors. + * + * 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 org.springframework.security.config.annotation.web.builders + +import javax.servlet.Filter +import javax.servlet.FilterChain +import javax.servlet.ServletException +import javax.servlet.http.HttpServletRequest +import javax.servlet.http.HttpServletResponse + +import org.springframework.beans.factory.BeanCreationException +import org.springframework.context.annotation.Configuration +import org.springframework.mock.web.MockFilterChain +import org.springframework.mock.web.MockHttpServletRequest +import org.springframework.mock.web.MockHttpServletResponse +import org.springframework.security.cas.web.CasAuthenticationFilter +import org.springframework.security.config.annotation.BaseSpringSpec +import org.springframework.security.config.annotation.web.configuration.BaseWebConfig +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter +import org.springframework.web.filter.OncePerRequestFilter + +/** + * HttpSecurity tests + * + * @author Rob Winch + * + */ +public class HttpSecurityTests extends BaseSpringSpec { + def "addFilter with unregistered Filter"() { + when: + loadConfig(UnregisteredFilterConfig) + then: + BeanCreationException success = thrown() + success.message.contains "The Filter class ${UnregisteredFilter.name} does not have a registered order and cannot be added without a specified order. Consider using addFilterBefore or addFilterAfter instead." + } + + @Configuration + static class UnregisteredFilterConfig extends BaseWebConfig { + protected void configure(HttpSecurity http) throws Exception { + http + .addFilter(new UnregisteredFilter()) + } + } + + static class UnregisteredFilter extends OncePerRequestFilter { + @Override + protected void doFilterInternal(HttpServletRequest request, + HttpServletResponse response, FilterChain filterChain) + throws ServletException, IOException { + filterChain.doFilter(request, response); + } + } + + // https://github.com/SpringSource/spring-security-javaconfig/issues/104 + def "#104 addFilter CasAuthenticationFilter"() { + when: + loadConfig(CasAuthenticationFilterConfig) + then: + findFilter(CasAuthenticationFilter) + } + + @Configuration + static class CasAuthenticationFilterConfig extends BaseWebConfig { + protected void configure(HttpSecurity http) throws Exception { + http + .addFilter(new CasAuthenticationFilter()) + } + } + + + def "requestMatchers() javadoc"() { + setup: "load configuration like the config on the requestMatchers() javadoc" + loadConfig(RequestMatcherRegistryConfigs) + when: + super.setup() + request.servletPath = "/oauth/a" + springSecurityFilterChain.doFilter(request, response, chain) + then: + response.status == 403 + where: + servletPath | status + "/oauth/a" | 403 + "/oauth/b" | 403 + "/api/a" | 403 + "/api/b" | 403 + "/oauth2/b" | 200 + "/api2/b" | 200 + } + + @EnableWebSecurity + @Configuration + static class RequestMatcherRegistryConfigs extends BaseWebConfig { + @Override + protected void configure(HttpSecurity http) throws Exception { + http + .requestMatchers() + .antMatchers("/api/**") + .antMatchers("/oauth/**") + .and() + .authorizeUrls() + .antMatchers("/**").hasRole("USER") + .and() + .httpBasic() + } + } +} diff --git a/config/src/test/groovy/org/springframework/security/config/annotation/web/builders/NamespaceHttpTests.groovy b/config/src/test/groovy/org/springframework/security/config/annotation/web/builders/NamespaceHttpTests.groovy new file mode 100644 index 0000000000..1f20f74355 --- /dev/null +++ b/config/src/test/groovy/org/springframework/security/config/annotation/web/builders/NamespaceHttpTests.groovy @@ -0,0 +1,511 @@ +/* + * Copyright 2002-2013 the original author or authors. + * + * 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 org.springframework.security.config.annotation.web.builders + +import javax.servlet.http.HttpServletRequest + +import org.springframework.context.annotation.Configuration +import org.springframework.security.access.AccessDecisionManager +import org.springframework.security.access.ConfigAttribute +import org.springframework.security.access.vote.AuthenticatedVoter +import org.springframework.security.access.vote.RoleVoter +import org.springframework.security.authentication.AuthenticationManager +import org.springframework.security.authentication.BadCredentialsException +import org.springframework.security.config.annotation.BaseSpringSpec +import org.springframework.security.config.annotation.web.builders.NamespaceHttpTests.AuthenticationManagerRefConfig.CustomAuthenticationManager +import org.springframework.security.config.annotation.web.builders.NamespaceHttpTests.RequestMatcherRefConfig.MyRequestMatcher +import org.springframework.security.config.annotation.web.configuration.BaseWebConfig +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity +import org.springframework.security.config.annotation.web.configurers.SessionCreationPolicy +import org.springframework.security.config.annotation.web.configurers.UrlAuthorizationConfigurer +import org.springframework.security.core.Authentication +import org.springframework.security.core.AuthenticationException +import org.springframework.security.web.FilterInvocation +import org.springframework.security.web.access.ExceptionTranslationFilter +import org.springframework.security.web.access.expression.ExpressionBasedFilterInvocationSecurityMetadataSource +import org.springframework.security.web.access.expression.WebExpressionVoter +import org.springframework.security.web.access.intercept.DefaultFilterInvocationSecurityMetadataSource +import org.springframework.security.web.access.intercept.FilterSecurityInterceptor +import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint +import org.springframework.security.web.authentication.www.BasicAuthenticationFilter +import org.springframework.security.web.context.HttpSessionSecurityContextRepository +import org.springframework.security.web.context.NullSecurityContextRepository +import org.springframework.security.web.context.SecurityContextPersistenceFilter +import org.springframework.security.web.jaasapi.JaasApiIntegrationFilter +import org.springframework.security.web.savedrequest.HttpSessionRequestCache +import org.springframework.security.web.savedrequest.NullRequestCache +import org.springframework.security.web.savedrequest.RequestCacheAwareFilter +import org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter +import org.springframework.security.web.session.SessionManagementFilter +import org.springframework.security.web.util.RegexRequestMatcher +import org.springframework.security.web.util.RequestMatcher + +/** + * Tests to verify that all the functionality of attributes is present + * + * @author Rob Winch + * + */ +public class NamespaceHttpTests extends BaseSpringSpec { + def "http@access-decision-manager-ref"() { + setup: + AccessDecisionManagerRefConfig.ACCESS_DECISION_MGR = Mock(AccessDecisionManager) + AccessDecisionManagerRefConfig.ACCESS_DECISION_MGR.supports(FilterInvocation) >> true + AccessDecisionManagerRefConfig.ACCESS_DECISION_MGR.supports(_ as ConfigAttribute) >> true + when: + loadConfig(AccessDecisionManagerRefConfig) + then: + findFilter(FilterSecurityInterceptor).accessDecisionManager == AccessDecisionManagerRefConfig.ACCESS_DECISION_MGR + } + + @Configuration + static class AccessDecisionManagerRefConfig extends BaseWebConfig { + static AccessDecisionManager ACCESS_DECISION_MGR + + protected void configure(HttpSecurity http) throws Exception { + http + .authorizeUrls() + .anyRequest().permitAll() + .accessDecisionManager(ACCESS_DECISION_MGR) + } + } + + def "http@access-denied-page"() { + when: + loadConfig(AccessDeniedPageConfig) + then: + findFilter(ExceptionTranslationFilter).accessDeniedHandler.errorPage == "/AccessDeniedPageConfig" + } + + @Configuration + static class AccessDeniedPageConfig extends BaseWebConfig { + protected void configure(HttpSecurity http) throws Exception { + http + .exceptionHandling() + .accessDeniedPage("/AccessDeniedPageConfig") + } + } + + def "http@authentication-manager-ref"() { + when: "Specify AuthenticationManager" + loadConfig(AuthenticationManagerRefConfig) + then: "Populates the AuthenticationManager" + findFilter(FilterSecurityInterceptor).authenticationManager.parent.class == CustomAuthenticationManager + } + + @Configuration + static class AuthenticationManagerRefConfig extends BaseWebConfig { + // demo authentication-manager-ref (could be any value) + + @Override + protected AuthenticationManager authenticationManager() throws Exception { + return new CustomAuthenticationManager(); + } + + @Override + protected void configure(HttpSecurity http) throws Exception { + http + .authorizeUrls() + .anyRequest().hasRole("USER"); + } + + static class CustomAuthenticationManager implements AuthenticationManager { + public Authentication authenticate(Authentication authentication) + throws AuthenticationException { + throw new BadCredentialsException("This always fails"); + } + } + } + + // Note: There is no http@auto-config equivalent in Java Config + + def "http@create-session=always"() { + when: + loadConfig(IfRequiredConfig) + then: + findFilter(SecurityContextPersistenceFilter).forceEagerSessionCreation == false + findFilter(SecurityContextPersistenceFilter).repo.allowSessionCreation == true + findFilter(SessionManagementFilter).securityContextRepository.allowSessionCreation == true + findFilter(ExceptionTranslationFilter).requestCache.class == HttpSessionRequestCache + } + + @Configuration + static class CreateSessionAlwaysConfig extends BaseWebConfig { + protected void configure(HttpSecurity http) throws Exception { + http + .sessionManagement() + .sessionCreationPolicy(SessionCreationPolicy.always); + } + } + + def "http@create-session=stateless"() { + when: + loadConfig(CreateSessionStatelessConfig) + then: + findFilter(SecurityContextPersistenceFilter).forceEagerSessionCreation == false + findFilter(SecurityContextPersistenceFilter).repo.class == NullSecurityContextRepository + findFilter(SessionManagementFilter).securityContextRepository.class == NullSecurityContextRepository + findFilter(ExceptionTranslationFilter).requestCache.class == NullRequestCache + findFilter(RequestCacheAwareFilter).requestCache.class == NullRequestCache + } + + @Configuration + static class CreateSessionStatelessConfig extends BaseWebConfig { + protected void configure(HttpSecurity http) throws Exception { + http + .sessionManagement() + .sessionCreationPolicy(SessionCreationPolicy.stateless); + } + } + + def "http@create-session=ifRequired"() { + when: + loadConfig(IfRequiredConfig) + then: + findFilter(SecurityContextPersistenceFilter).forceEagerSessionCreation == false + findFilter(SecurityContextPersistenceFilter).repo.allowSessionCreation == true + findFilter(SessionManagementFilter).securityContextRepository.allowSessionCreation == true + } + + @Configuration + static class IfRequiredConfig extends BaseWebConfig { + protected void configure(HttpSecurity http) throws Exception { + http + .sessionManagement() + .sessionCreationPolicy(SessionCreationPolicy.ifRequired); + } + } + + def "http@create-session defaults to ifRequired"() { + when: + loadConfig(IfRequiredConfig) + then: + findFilter(SecurityContextPersistenceFilter).forceEagerSessionCreation == false + findFilter(SecurityContextPersistenceFilter).repo.allowSessionCreation == true + findFilter(SessionManagementFilter).securityContextRepository.allowSessionCreation == true + } + + def "http@create-session=never"() { + when: + loadConfig(CreateSessionNeverConfig) + then: + findFilter(SecurityContextPersistenceFilter).forceEagerSessionCreation == false + findFilter(SecurityContextPersistenceFilter).repo.allowSessionCreation == false + findFilter(SessionManagementFilter).securityContextRepository.allowSessionCreation == false + } + + @Configuration + static class CreateSessionNeverConfig extends BaseWebConfig { + protected void configure(HttpSecurity http) throws Exception { + http + .sessionManagement() + .sessionCreationPolicy(SessionCreationPolicy.never); + } + } + + @Configuration + static class DefaultCreateSessionConfig extends BaseWebConfig { + protected void configure(HttpSecurity http) throws Exception { + } + } + + def "http@disable-url-rewriting = true (default for Java Config)"() { + when: + loadConfig(DefaultUrlRewritingConfig) + then: + findFilter(SecurityContextPersistenceFilter).repo.disableUrlRewriting + } + + @Configuration + static class DefaultUrlRewritingConfig extends BaseWebConfig { + protected void configure(HttpSecurity http) throws Exception { + } + } + + // http@disable-url-rewriting is on by default to disable it create a custom HttpSecurityContextRepository and use security-context-repository-ref + + def "http@disable-url-rewriting = false"() { + when: + loadConfig(EnableUrlRewritingConfig) + then: + findFilter(SecurityContextPersistenceFilter).repo.disableUrlRewriting == false + } + + @Configuration + static class EnableUrlRewritingConfig extends BaseWebConfig { + protected void configure(HttpSecurity http) throws Exception { + HttpSessionSecurityContextRepository repository = new HttpSessionSecurityContextRepository() + repository.disableUrlRewriting = false // explicitly configured (not necessary due to default values) + + http. + securityContext() + .securityContextRepository(repository) + } + } + + def "http@entry-point-ref"() { + when: + loadConfig(EntryPointRefConfig) + then: + findFilter(ExceptionTranslationFilter).authenticationEntryPoint.loginFormUrl == "/EntryPointRefConfig" + } + + @Configuration + static class EntryPointRefConfig extends BaseWebConfig { + protected void configure(HttpSecurity http) throws Exception { + http + .exceptionHandling() + .authenticationEntryPoint(new LoginUrlAuthenticationEntryPoint("/EntryPointRefConfig")) + } + } + + def "http@jaas-api-provision"() { + when: + loadConfig(JaasApiProvisionConfig) + then: + findFilter(JaasApiIntegrationFilter) + } + + @Configuration + static class JaasApiProvisionConfig extends BaseWebConfig { + protected void configure(HttpSecurity http) throws Exception { + http + .addFilter(new JaasApiIntegrationFilter()) + } + } + + // http@name is not available since it can be done w/ standard bean configuration easily + + def "http@once-per-request=true"() { + when: + loadConfig(OncePerRequestConfig) + then: + findFilter(FilterSecurityInterceptor).observeOncePerRequest + } + + @Configuration + static class OncePerRequestConfig extends BaseWebConfig { + @Override + protected void configure(HttpSecurity http) throws Exception { + http + .authorizeUrls() + .anyRequest().hasRole("USER"); + } + } + + def "http@once-per-request=false"() { + when: + loadConfig(OncePerRequestFalseConfig) + then: + !findFilter(FilterSecurityInterceptor).observeOncePerRequest + } + + @Configuration + static class OncePerRequestFalseConfig extends BaseWebConfig { + @Override + protected void configure(HttpSecurity http) throws Exception { + http. + authorizeUrls() + .filterSecurityInterceptorOncePerRequest(false) + .antMatchers("/users**","/sessions/**").hasRole("ADMIN") + .antMatchers("/signup").permitAll() + .anyRequest().hasRole("USER"); + } + } + + // http@path-type is not available (instead request matcher instances are used) + + // http@pattern is not available (instead see the tests http@request-matcher-ref ant or http@request-matcher-ref regex) + + def "http@realm"() { + when: + loadConfig(RealmConfig) + then: + findFilter(BasicAuthenticationFilter).authenticationEntryPoint.realmName == "RealmConfig" + } + + @Configuration + static class RealmConfig extends BaseWebConfig { + protected void configure(HttpSecurity http) throws Exception { + http + .httpBasic().realmName("RealmConfig") + } + } + + // http@request-matcher is not available (instead request matcher instances are used) + + def "http@request-matcher-ref ant"() { + when: + loadConfig(RequestMatcherAntConfig) + then: + filterChain(0).requestMatcher.pattern == "/api/**" + } + + @Configuration + static class RequestMatcherAntConfig extends BaseWebConfig { + protected void configure(HttpSecurity http) throws Exception { + http + .antMatcher("/api/**") + } + } + + def "http@request-matcher-ref regex"() { + when: + loadConfig(RequestMatcherRegexConfig) + then: + filterChain(0).requestMatcher.class == RegexRequestMatcher + filterChain(0).requestMatcher.pattern.matcher("/regex/a") + filterChain(0).requestMatcher.pattern.matcher("/regex/b") + !filterChain(0).requestMatcher.pattern.matcher("/regex1/b") + } + + @Configuration + static class RequestMatcherRegexConfig extends BaseWebConfig { + protected void configure(HttpSecurity http) throws Exception { + http + .regexMatcher("/regex/.*") + } + } + + def "http@request-matcher-ref"() { + when: + loadConfig(RequestMatcherRefConfig) + then: + filterChain(0).requestMatcher.class == MyRequestMatcher + } + + @Configuration + static class RequestMatcherRefConfig extends BaseWebConfig { + protected void configure(HttpSecurity http) throws Exception { + http + .requestMatcher(new MyRequestMatcher()); + } + static class MyRequestMatcher implements RequestMatcher { + public boolean matches(HttpServletRequest request) { + return true; + } + } + } + + def "http@security=none"() { + when: + loadConfig(SecurityNoneConfig) + then: + filterChain(0).requestMatcher.pattern == "/resources/**" + filterChain(0).filters.empty + filterChain(1).requestMatcher.pattern == "/public/**" + filterChain(1).filters.empty + } + + @Configuration + static class SecurityNoneConfig extends BaseWebConfig { + + @Override + public void configure(WebSecurity web) + throws Exception { + web + .ignoring() + .antMatchers("/resources/**","/public/**") + } + + @Override + protected void configure(HttpSecurity http) throws Exception {} + + } + + def "http@security-context-repository-ref"() { + when: + loadConfig(SecurityContextRepoConfig) + then: + findFilter(SecurityContextPersistenceFilter).repo.class == NullSecurityContextRepository + } + + @Configuration + static class SecurityContextRepoConfig extends BaseWebConfig { + protected void configure(HttpSecurity http) throws Exception { + http + .securityContext() + .securityContextRepository(new NullSecurityContextRepository()) // security-context-repository-ref + } + } + + def "http@servlet-api-provision=false"() { + when: + loadConfig(ServletApiProvisionConfig) + then: + findFilter(SecurityContextHolderAwareRequestFilter) == null + } + + @Configuration + static class ServletApiProvisionConfig extends BaseWebConfig { + protected void configure(HttpSecurity http) throws Exception { + http.servletApi().disable() + } + } + + def "http@servlet-api-provision defaults to true"() { + when: + loadConfig(ServletApiProvisionDefaultsConfig) + then: + findFilter(SecurityContextHolderAwareRequestFilter) != null + } + + @Configuration + static class ServletApiProvisionDefaultsConfig extends BaseWebConfig { + protected void configure(HttpSecurity http) throws Exception { + } + } + + def "http@use-expressions=true"() { + when: + loadConfig(UseExpressionsConfig) + then: + findFilter(FilterSecurityInterceptor).securityMetadataSource.class == ExpressionBasedFilterInvocationSecurityMetadataSource + findFilter(FilterSecurityInterceptor).accessDecisionManager.decisionVoters.collect { it.class } == [WebExpressionVoter] + } + + @Configuration + @EnableWebSecurity + static class UseExpressionsConfig extends BaseWebConfig { + protected void configure(HttpSecurity http) throws Exception { + http + .authorizeUrls() + .antMatchers("/users**","/sessions/**").hasRole("USER") + .antMatchers("/signup").permitAll() + .anyRequest().hasRole("USER") + } + } + + def "http@use-expressions=false"() { + when: + loadConfig(DisableUseExpressionsConfig) + then: + findFilter(FilterSecurityInterceptor).securityMetadataSource.class == DefaultFilterInvocationSecurityMetadataSource + findFilter(FilterSecurityInterceptor).accessDecisionManager.decisionVoters.collect { it.class } == [RoleVoter, AuthenticatedVoter] + } + + @Configuration + @EnableWebSecurity + static class DisableUseExpressionsConfig extends BaseWebConfig { + protected void configure(HttpSecurity http) throws Exception { + http + .apply(new UrlAuthorizationConfigurer()) + .antMatchers("/users**","/sessions/**").hasRole("USER") + .antMatchers("/signup").hasRole("ANONYMOUS") + .anyRequest().hasRole("USER") + } + } +} diff --git a/config/src/test/groovy/org/springframework/security/config/annotation/web/configuration/BaseWebConfig.groovy b/config/src/test/groovy/org/springframework/security/config/annotation/web/configuration/BaseWebConfig.groovy new file mode 100644 index 0000000000..a9557a64fd --- /dev/null +++ b/config/src/test/groovy/org/springframework/security/config/annotation/web/configuration/BaseWebConfig.groovy @@ -0,0 +1,42 @@ +/* + * Copyright 2002-2013 the original author or authors. + * + * 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 org.springframework.security.config.annotation.web.configuration; + +import org.springframework.context.annotation.Configuration +import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder + +/** + * + * @author Rob Winch + */ +@Configuration +@EnableWebSecurity +public abstract class BaseWebConfig extends WebSecurityConfigurerAdapter { + BaseWebConfig(boolean disableDefaults) { + super(disableDefaults) + } + + BaseWebConfig() { + } + + protected void registerAuthentication( + AuthenticationManagerBuilder auth) throws Exception { + auth + .inMemoryAuthentication() + .withUser("user").password("password").roles("USER").and() + .withUser("admin").password("password").roles("USER", "ADMIN"); + } +} \ No newline at end of file diff --git a/config/src/test/groovy/org/springframework/security/config/annotation/web/configuration/EnableWebSecurityTests.groovy b/config/src/test/groovy/org/springframework/security/config/annotation/web/configuration/EnableWebSecurityTests.groovy new file mode 100644 index 0000000000..723784a886 --- /dev/null +++ b/config/src/test/groovy/org/springframework/security/config/annotation/web/configuration/EnableWebSecurityTests.groovy @@ -0,0 +1,88 @@ +/* + * Copyright 2002-2013 the original author or authors. + * + * 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 org.springframework.security.config.annotation.web.configuration; + +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.mock.web.MockHttpServletRequest +import org.springframework.security.authentication.AnonymousAuthenticationToken +import org.springframework.security.authentication.AuthenticationManager +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken +import org.springframework.security.config.annotation.BaseSpringSpec +import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder +import org.springframework.security.config.annotation.web.builders.DebugFilter; +import org.springframework.security.config.annotation.web.builders.HttpSecurity +import org.springframework.security.web.authentication.AnonymousAuthenticationFilter + +class EnableWebSecurityTests extends BaseSpringSpec { + + def "@Bean(BeanIds.AUTHENTICATION_MANAGER) includes HttpSecurity's AuthenticationManagerBuilder"() { + when: + loadConfig(SecurityConfig) + AuthenticationManager authenticationManager = context.getBean(AuthenticationManager) + AnonymousAuthenticationToken anonymousAuthToken = findFilter(AnonymousAuthenticationFilter).createAuthentication(new MockHttpServletRequest()) + then: + authenticationManager.authenticate(new UsernamePasswordAuthenticationToken("user", "password")) + authenticationManager.authenticate(anonymousAuthToken) + + } + + + @EnableWebSecurity + @Configuration + static class SecurityConfig extends WebSecurityConfigurerAdapter { + @Override + protected void registerAuthentication(AuthenticationManagerBuilder auth) + throws Exception { + auth + .inMemoryAuthentication() + .withUser("user").password("password").roles("USER"); + } + + @Bean + @Override + public AuthenticationManager authenticationManagerBean() + throws Exception { + return super.authenticationManagerBean(); + } + + @Override + protected void configure(HttpSecurity http) throws Exception { + http + .authorizeUrls() + .antMatchers("/*").hasRole("USER") + .and() + .formLogin(); + } + } + + def "@EnableWebSecurity on superclass"() { + when: + loadConfig(ChildSecurityConfig) + then: + context.getBean("springSecurityFilterChain", DebugFilter) + } + + @Configuration + static class ChildSecurityConfig extends DebugSecurityConfig { + } + + @Configuration + @EnableWebSecurity(debug=true) + static class DebugSecurityConfig extends WebSecurityConfigurerAdapter { + + } +} diff --git a/config/src/test/groovy/org/springframework/security/config/annotation/web/configuration/WebSecurityConfigurationTests.groovy b/config/src/test/groovy/org/springframework/security/config/annotation/web/configuration/WebSecurityConfigurationTests.groovy new file mode 100644 index 0000000000..e0d0d8742e --- /dev/null +++ b/config/src/test/groovy/org/springframework/security/config/annotation/web/configuration/WebSecurityConfigurationTests.groovy @@ -0,0 +1,217 @@ +/* + * Copyright 2002-2013 the original author or authors. + * + * 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 org.springframework.security.config.annotation.web.configuration; + +import static org.junit.Assert.* + +import java.util.List; + +import org.springframework.beans.factory.BeanCreationException +import org.springframework.context.ApplicationListener; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration +import org.springframework.core.annotation.Order +import org.springframework.security.authentication.AuthenticationManager +import org.springframework.security.authentication.event.AuthenticationSuccessEvent; +import org.springframework.security.config.annotation.BaseSpringSpec +import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder +import org.springframework.security.config.annotation.web.builders.HttpSecurity +import org.springframework.security.config.annotation.web.builders.WebSecurity +import org.springframework.security.web.FilterChainProxy +import org.springframework.security.web.SecurityFilterChain +import org.springframework.security.web.access.WebInvocationPrivilegeEvaluator; +import org.springframework.security.web.access.expression.WebSecurityExpressionHandler; +import org.springframework.security.web.util.AnyRequestMatcher + +/** + * @author Rob Winch + * + */ +class WebSecurityConfigurationTests extends BaseSpringSpec { + + def "WebSecurityConfigurers are sorted"() { + when: + loadConfig(SortedWebSecurityConfigurerAdaptersConfig); + List filterChains = context.getBean(FilterChainProxy).filterChains + then: + filterChains[0].requestMatcher.pattern == "/ignore1" + filterChains[0].filters.empty + filterChains[1].requestMatcher.pattern == "/ignore2" + filterChains[1].filters.empty + + filterChains[2].requestMatcher.pattern == "/role1/**" + filterChains[3].requestMatcher.pattern == "/role2/**" + filterChains[4].requestMatcher.pattern == "/role3/**" + filterChains[5].requestMatcher.class == AnyRequestMatcher + } + + + @Configuration + @EnableWebSecurity + static class SortedWebSecurityConfigurerAdaptersConfig { + public AuthenticationManager authenticationManager() throws Exception { + return new AuthenticationManagerBuilder() + .inMemoryAuthentication() + .withUser("marissa").password("koala").roles("USER").and() + .withUser("paul").password("emu").roles("USER").and() + .and() + .build(); + } + + @Configuration + @Order(1) + public static class WebConfigurer1 extends WebSecurityConfigurerAdapter { + @Override + public void configure(WebSecurity web) throws Exception { + web + .ignoring() + .antMatchers("/ignore1","/ignore2"); + } + + @Override + protected void configure(HttpSecurity http) throws Exception { + http + .antMatcher("/role1/**") + .authorizeUrls() + .anyRequest().hasRole("1"); + } + } + + @Configuration + @Order(2) + public static class WebConfigurer2 extends WebSecurityConfigurerAdapter { + @Override + protected void configure(HttpSecurity http) throws Exception { + http + .antMatcher("/role2/**") + .authorizeUrls() + .anyRequest().hasRole("2"); + } + } + + @Configuration + @Order(3) + public static class WebConfigurer3 extends WebSecurityConfigurerAdapter { + @Override + protected void configure(HttpSecurity http) throws Exception { + http + .antMatcher("/role3/**") + .authorizeUrls() + .anyRequest().hasRole("3"); + } + } + + @Configuration + public static class WebConfigurer4 extends WebSecurityConfigurerAdapter { + + @Override + protected void configure(HttpSecurity http) throws Exception { + http + .authorizeUrls() + .anyRequest().hasRole("4"); + } + } + } + + def "WebSecurityConfigurers fails with duplicate order"() { + when: + loadConfig(DuplicateOrderConfig); + then: + BeanCreationException e = thrown() + e.message.contains "@Order on WebSecurityConfigurers must be unique" + } + + + @Configuration + @EnableWebSecurity + static class DuplicateOrderConfig { + public AuthenticationManager authenticationManager() throws Exception { + return new AuthenticationManagerBuilder() + .inMemoryAuthentication() + .withUser("user").password("password").roles("USER").and() + .and() + .build(); + } + + @Configuration + public static class WebConfigurer1 extends WebSecurityConfigurerAdapter { + + @Override + protected void configure(HttpSecurity http) throws Exception { + http + .antMatcher("/role1/**") + .authorizeUrls() + .anyRequest().hasRole("1"); + } + } + + @Configuration + public static class WebConfigurer2 extends WebSecurityConfigurerAdapter { + @Override + protected void configure(HttpSecurity http) throws Exception { + http + .antMatcher("/role2/**") + .authorizeUrls() + .anyRequest().hasRole("2"); + } + } + } + + def "Override privilegeEvaluator"() { + setup: + WebInvocationPrivilegeEvaluator privilegeEvaluator = Mock() + PrivilegeEvaluatorConfigurerAdapterConfig.PE = privilegeEvaluator + when: + loadConfig(PrivilegeEvaluatorConfigurerAdapterConfig) + then: + context.getBean(WebInvocationPrivilegeEvaluator) == privilegeEvaluator + } + + @EnableWebSecurity + @Configuration + static class PrivilegeEvaluatorConfigurerAdapterConfig extends WebSecurityConfigurerAdapter { + static WebInvocationPrivilegeEvaluator PE + + @Override + public void configure(WebSecurity web) throws Exception { + web + .privilegeEvaluator(PE) + } + } + + def "Override webSecurityExpressionHandler"() { + setup: + WebSecurityExpressionHandler expressionHandler = Mock() + WebSecurityExpressionHandlerConfig.EH = expressionHandler + when: + loadConfig(WebSecurityExpressionHandlerConfig) + then: + context.getBean(WebSecurityExpressionHandler) == expressionHandler + } + + @EnableWebSecurity + @Configuration + static class WebSecurityExpressionHandlerConfig extends WebSecurityConfigurerAdapter { + @SuppressWarnings("deprecation") + static WebSecurityExpressionHandler EH + + @Override + public void configure(WebSecurity web) throws Exception { + web + .expressionHandler(EH) + } + } +} diff --git a/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/AbstractRequestMatcherMappingConfigurerTests.groovy b/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/AbstractRequestMatcherMappingConfigurerTests.groovy new file mode 100644 index 0000000000..b2c36c999b --- /dev/null +++ b/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/AbstractRequestMatcherMappingConfigurerTests.groovy @@ -0,0 +1,76 @@ +/* + * Copyright 2002-2013 the original author or authors. + * + * 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 org.springframework.security.config.annotation.web.configurers; + +import java.util.List; + +import org.springframework.http.HttpMethod; +import org.springframework.security.access.AccessDecisionVoter; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configurers.AbstractRequestMatcherMappingConfigurer; +import org.springframework.security.web.DefaultSecurityFilterChain; +import org.springframework.security.web.access.intercept.FilterInvocationSecurityMetadataSource; +import org.springframework.security.web.util.AntPathRequestMatcher; +import org.springframework.security.web.util.RegexRequestMatcher; +import org.springframework.security.web.util.RequestMatcher; + +import spock.lang.Specification; + +/** + * @author Rob Winch + * + */ +class AbstractRequestMatcherMappingConfigurerTests extends Specification { + ConcreteAbstractRequestMatcherMappingConfigurer registry = new ConcreteAbstractRequestMatcherMappingConfigurer() + + def "regexMatchers(GET,'/a.*') uses RegexRequestMatcher"() { + when: + def matchers = registry.regexMatchers(HttpMethod.GET,"/a.*") + then: 'matcher is a RegexRequestMatcher' + matchers.collect {it.class } == [RegexRequestMatcher] + } + + def "regexMatchers('/a.*') uses RegexRequestMatcher"() { + when: + def matchers = registry.regexMatchers("/a.*") + then: 'matcher is a RegexRequestMatcher' + matchers.collect {it.class } == [RegexRequestMatcher] + } + + def "antMatchers(GET,'/a.*') uses AntPathRequestMatcher"() { + when: + def matchers = registry.antMatchers(HttpMethod.GET, "/a.*") + then: 'matcher is a RegexRequestMatcher' + matchers.collect {it.class } == [AntPathRequestMatcher] + } + + def "antMatchers('/a.*') uses AntPathRequestMatcher"() { + when: + def matchers = registry.antMatchers("/a.*") + then: 'matcher is a AntPathRequestMatcher' + matchers.collect {it.class } == [AntPathRequestMatcher] + } + + static class ConcreteAbstractRequestMatcherMappingConfigurer extends AbstractRequestMatcherMappingConfigurer,DefaultSecurityFilterChain> { + List decisionVoters() { + return null; + } + + List chainRequestMatchersInternal(List requestMatchers) { + return requestMatchers; + } + } +} diff --git a/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/ChannelSecurityConfigurerTests.groovy b/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/ChannelSecurityConfigurerTests.groovy new file mode 100644 index 0000000000..e46c22eca8 --- /dev/null +++ b/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/ChannelSecurityConfigurerTests.groovy @@ -0,0 +1,53 @@ +/* + * Copyright 2002-2013 the original author or authors. + * + * 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 org.springframework.security.config.annotation.web.configurers + +import org.springframework.security.config.annotation.AnyObjectPostProcessor +import org.springframework.security.config.annotation.BaseSpringSpec +import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.web.access.channel.ChannelDecisionManagerImpl +import org.springframework.security.web.access.channel.ChannelProcessingFilter +import org.springframework.security.web.access.channel.InsecureChannelProcessor +import org.springframework.security.web.access.channel.SecureChannelProcessor + +/** + * + * @author Rob Winch + */ +class ChannelSecurityConfigurerTests extends BaseSpringSpec { + + def "requiresChannel ObjectPostProcessor"() { + setup: "initialize the AUTH_FILTER as a mock" + AnyObjectPostProcessor objectPostProcessor = Mock() + when: + HttpSecurity http = new HttpSecurity(objectPostProcessor, authenticationBldr, [:]) + http + .requiresChannel() + .anyRequest().requiresSecure() + .and() + .build() + + then: "InsecureChannelProcessor is registered with LifecycleManager" + 1 * objectPostProcessor.postProcess(_ as InsecureChannelProcessor) >> {InsecureChannelProcessor o -> o} + and: "SecureChannelProcessor is registered with LifecycleManager" + 1 * objectPostProcessor.postProcess(_ as SecureChannelProcessor) >> {SecureChannelProcessor o -> o} + and: "ChannelDecisionManagerImpl is registered with LifecycleManager" + 1 * objectPostProcessor.postProcess(_ as ChannelDecisionManagerImpl) >> {ChannelDecisionManagerImpl o -> o} + and: "ChannelProcessingFilter is registered with LifecycleManager" + 1 * objectPostProcessor.postProcess(_ as ChannelProcessingFilter) >> {ChannelProcessingFilter o -> o} + } +} diff --git a/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/DefaultFiltersTests.groovy b/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/DefaultFiltersTests.groovy new file mode 100644 index 0000000000..9487657f87 --- /dev/null +++ b/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/DefaultFiltersTests.groovy @@ -0,0 +1,159 @@ +/* + * Copyright 2002-2013 the original author or authors. + * + * 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 org.springframework.security.config.annotation.web.configurers + +import org.springframework.beans.factory.BeanCreationException +import org.springframework.context.annotation.AnnotationConfigApplicationContext +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.mock.web.MockFilterChain +import org.springframework.mock.web.MockHttpServletRequest +import org.springframework.mock.web.MockHttpServletResponse +import org.springframework.security.config.annotation.BaseSpringSpec +import org.springframework.security.config.annotation.web.WebSecurityConfigurer; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.builders.WebSecurity; +import org.springframework.security.config.annotation.web.configuration.BaseWebConfig; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; +import org.springframework.security.web.DefaultSecurityFilterChain +import org.springframework.security.web.FilterChainProxy +import org.springframework.security.web.access.ExceptionTranslationFilter +import org.springframework.security.web.access.intercept.FilterSecurityInterceptor +import org.springframework.security.web.authentication.AnonymousAuthenticationFilter +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter +import org.springframework.security.web.authentication.logout.LogoutFilter +import org.springframework.security.web.context.SecurityContextPersistenceFilter +import org.springframework.security.web.savedrequest.RequestCacheAwareFilter +import org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter +import org.springframework.security.web.session.SessionManagementFilter; +import org.springframework.security.web.util.AnyRequestMatcher + +/** + * + * @author Rob Winch + */ +class DefaultFiltersTests extends BaseSpringSpec { + def missingConfigMessage = "At least one non-null instance of "+ WebSecurityConfigurer.class.getSimpleName()+" must be exposed as a @Bean when using @EnableWebSecurity. Hint try extending "+ WebSecurityConfigurerAdapter.class.getSimpleName() + + def "DefaultSecurityFilterChainBuilder cannot be null"() { + when: + context = new AnnotationConfigApplicationContext(FilterChainProxyBuilderMissingConfig) + then: + BeanCreationException e = thrown() + e.message.contains missingConfigMessage + } + + @Configuration + @EnableWebSecurity + static class FilterChainProxyBuilderMissingConfig { } + + def "FilterChainProxyBuilder no DefaultSecurityFilterChainBuilder specified"() { + when: + context = new AnnotationConfigApplicationContext(FilterChainProxyBuilderNoSecurityFilterBuildersConfig) + then: + BeanCreationException e = thrown() + e.message.contains missingConfigMessage + } + + @Configuration + @EnableWebSecurity + static class FilterChainProxyBuilderNoSecurityFilterBuildersConfig { + @Bean + public WebSecurity filterChainProxyBuilder() { + new WebSecurity() + .ignoring() + .antMatchers("/resources/**") + } + } + + def "null WebInvocationPrivilegeEvaluator"() { + when: + context = new AnnotationConfigApplicationContext(NullWebInvocationPrivilegeEvaluatorConfig) + then: + List filterChains = context.getBean(FilterChainProxy).filterChains + filterChains.size() == 1 + filterChains[0].requestMatcher instanceof AnyRequestMatcher + filterChains[0].filters.size() == 1 + filterChains[0].filters.find { it instanceof UsernamePasswordAuthenticationFilter } + } + + @Configuration + @EnableWebSecurity + static class NullWebInvocationPrivilegeEvaluatorConfig extends BaseWebConfig { + NullWebInvocationPrivilegeEvaluatorConfig() { + super(true) + } + + protected void configure(HttpSecurity http) { + http.formLogin() + } + } + + def "FilterChainProxyBuilder ignoring resources"() { + when: + context = new AnnotationConfigApplicationContext(FilterChainProxyBuilderIgnoringConfig) + then: + List filterChains = context.getBean(FilterChainProxy).filterChains + filterChains.size() == 2 + filterChains[0].requestMatcher.pattern == '/resources/**' + filterChains[0].filters.empty + filterChains[1].requestMatcher instanceof AnyRequestMatcher + filterChains[1].filters.collect { it.class } == + [SecurityContextPersistenceFilter, LogoutFilter, RequestCacheAwareFilter, + SecurityContextHolderAwareRequestFilter, AnonymousAuthenticationFilter, SessionManagementFilter, + ExceptionTranslationFilter, FilterSecurityInterceptor ] + } + + @Configuration + @EnableWebSecurity + static class FilterChainProxyBuilderIgnoringConfig extends BaseWebConfig { + @Override + public void configure(WebSecurity builder) throws Exception { + builder + .ignoring() + .antMatchers("/resources/**"); + } + @Override + protected void configure(HttpSecurity http) { + http + .authorizeUrls() + .anyRequest().hasRole("USER"); + } + } + + def "DefaultFilters.permitAll()"() { + when: + context = new AnnotationConfigApplicationContext(DefaultFiltersConfigPermitAll) + then: + FilterChainProxy filterChain = context.getBean(FilterChainProxy) + + expect: + MockHttpServletResponse response = new MockHttpServletResponse() + filterChain.doFilter(new MockHttpServletRequest(servletPath : uri, queryString: query), response, new MockFilterChain()) + response.redirectedUrl == null + where: + uri | query + "/logout" | null + } + + @Configuration + @EnableWebSecurity + static class DefaultFiltersConfigPermitAll extends BaseWebConfig { + protected void configure(HttpSecurity http) { + } + } +} diff --git a/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/DefaultLoginPageConfigurerTests.groovy b/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/DefaultLoginPageConfigurerTests.groovy new file mode 100644 index 0000000000..28b3a2b15c --- /dev/null +++ b/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/DefaultLoginPageConfigurerTests.groovy @@ -0,0 +1,343 @@ + + +/* + * Copyright 2002-2013 the original author or authors. + * + * 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 org.springframework.security.config.annotation.web.configurers + +import javax.servlet.http.HttpSession + +import org.springframework.context.annotation.Configuration +import org.springframework.mock.web.MockFilterChain +import org.springframework.mock.web.MockHttpServletRequest +import org.springframework.mock.web.MockHttpServletResponse +import org.springframework.security.config.annotation.AnyObjectPostProcessor +import org.springframework.security.config.annotation.BaseSpringSpec +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.BaseWebConfig; +import org.springframework.security.config.annotation.web.configurers.DefaultLoginPageConfigurer; +import org.springframework.security.web.FilterChainProxy +import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter +import org.springframework.security.web.authentication.logout.SimpleUrlLogoutSuccessHandler +import org.springframework.security.web.authentication.ui.DefaultLoginPageViewFilter; + +/** + * Tests to verify that {@link DefaultLoginPageConfigurer} works + * + * @author Rob Winch + * + */ +public class DefaultLoginPageConfigurerTests extends BaseSpringSpec { + FilterChainProxy springSecurityFilterChain + MockHttpServletRequest request + MockHttpServletResponse response + MockFilterChain chain + + def setup() { + request = new MockHttpServletRequest(method:"GET") + response = new MockHttpServletResponse() + chain = new MockFilterChain() + } + + def "http/form-login default login generating page"() { + setup: + loadConfig(DefaultLoginPageConfig) + springSecurityFilterChain = context.getBean(FilterChainProxy) + when: + springSecurityFilterChain.doFilter(request,response,chain) + then: + findFilter(DefaultLoginPageViewFilter) + response.getRedirectedUrl() == "http://localhost/login" + when: "request the login page" + setup() + request.requestURI = "/login" + springSecurityFilterChain.doFilter(request,response,chain) + then: + response.getContentAsString() == """Login Page +

Login with Username and Password

+ + + + +
User:
Password:
+
""" + when: "fail to log in" + setup() + request.requestURI = "/login" + request.method = "POST" + springSecurityFilterChain.doFilter(request,response,chain) + then: "sent to login error page" + response.getRedirectedUrl() == "/login?error" + when: "request the error page" + HttpSession session = request.session + setup() + request.session = session + request.requestURI = "/login" + request.queryString = "error" + springSecurityFilterChain.doFilter(request,response,chain) + then: + response.getContentAsString() == """Login Page +

Your login attempt was not successful, try again.

Reason: Bad credentials

Login with Username and Password

+ + + + +
User:
Password:
+
""" + when: "login success" + setup() + request.requestURI = "/login" + request.method = "POST" + request.parameters.username = ["user"] as String[] + request.parameters.password = ["password"] as String[] + springSecurityFilterChain.doFilter(request,response,chain) + then: "sent to default succes page" + response.getRedirectedUrl() == "/" + } + + def "logout success renders"() { + setup: + loadConfig(DefaultLoginPageConfig) + springSecurityFilterChain = context.getBean(FilterChainProxy) + when: "logout success" + request.requestURI = "/login" + request.queryString = "logout" + request.method = "GET" + springSecurityFilterChain.doFilter(request,response,chain) + then: "sent to default success page" + response.getContentAsString() == """Login Page +

You have been logged out

Login with Username and Password

+ + + + +
User:
Password:
+
""" + } + + @Configuration + static class DefaultLoginPageConfig extends BaseWebConfig { + @Override + protected void configure(HttpSecurity http) { + http + .authorizeUrls() + .anyRequest().hasRole("USER") + .and() + .formLogin() + } + } + + def "custom logout success handler prevents rendering"() { + setup: + loadConfig(DefaultLoginPageCustomLogoutSuccessHandlerConfig) + springSecurityFilterChain = context.getBean(FilterChainProxy) + when: "logout success" + request.requestURI = "/login" + request.queryString = "logout" + request.method = "GET" + springSecurityFilterChain.doFilter(request,response,chain) + then: "default success page is NOT rendered (application is in charge of it)" + response.getContentAsString() == "" + } + + @Configuration + static class DefaultLoginPageCustomLogoutSuccessHandlerConfig extends BaseWebConfig { + @Override + protected void configure(HttpSecurity http) { + http + .authorizeUrls() + .anyRequest().hasRole("USER") + .and() + .logout() + .logoutSuccessHandler(new SimpleUrlLogoutSuccessHandler()) + .and() + .formLogin() + } + } + + def "custom logout success url prevents rendering"() { + setup: + loadConfig(DefaultLoginPageCustomLogoutConfig) + springSecurityFilterChain = context.getBean(FilterChainProxy) + when: "logout success" + request.requestURI = "/login" + request.queryString = "logout" + request.method = "GET" + springSecurityFilterChain.doFilter(request,response,chain) + then: "default success page is NOT rendered (application is in charge of it)" + response.getContentAsString() == "" + } + + @Configuration + static class DefaultLoginPageCustomLogoutConfig extends BaseWebConfig { + @Override + protected void configure(HttpSecurity http) { + http + .authorizeUrls() + .anyRequest().hasRole("USER") + .and() + .logout() + .logoutSuccessUrl("/login?logout") + .and() + .formLogin() + } + } + + def "http/form-login default login with remember me"() { + setup: + loadConfig(DefaultLoginPageWithRememberMeConfig) + springSecurityFilterChain = context.getBean(FilterChainProxy) + when: "request the login page" + setup() + request.requestURI = "/login" + springSecurityFilterChain.doFilter(request,response,chain) + then: + response.getContentAsString() == """Login Page +

Login with Username and Password

+ + + + + +
User:
Password:
Remember me on this computer.
+
""" + } + + @Configuration + static class DefaultLoginPageWithRememberMeConfig extends BaseWebConfig { + @Override + protected void configure(HttpSecurity http) { + http + .authorizeUrls() + .anyRequest().hasRole("USER") + .and() + .formLogin() + .and() + .rememberMe() + } + } + + def "http/form-login default login with openid"() { + setup: + loadConfig(DefaultLoginPageWithOpenIDConfig) + springSecurityFilterChain = context.getBean(FilterChainProxy) + when: "request the login page" + request.requestURI = "/login" + springSecurityFilterChain.doFilter(request,response,chain) + then: + response.getContentAsString() == """Login Page

Login with OpenID Identity

+ + + +
Identity:
+
""" + } + + @Configuration + static class DefaultLoginPageWithOpenIDConfig extends BaseWebConfig { + @Override + protected void configure(HttpSecurity http) { + http + .authorizeUrls() + .anyRequest().hasRole("USER") + .and() + .openidLogin() + } + } + + def "http/form-login default login with openid, form login, and rememberme"() { + setup: + loadConfig(DefaultLoginPageWithFormLoginOpenIDRememberMeConfig) + springSecurityFilterChain = context.getBean(FilterChainProxy) + when: "request the login page" + request.requestURI = "/login" + springSecurityFilterChain.doFilter(request,response,chain) + then: + response.getContentAsString() == """Login Page +

Login with Username and Password

+ + + + + +
User:
Password:
Remember me on this computer.
+

Login with OpenID Identity

+ + + + +
Identity:
Remember me on this computer.
+
""" + } + + @Configuration + static class DefaultLoginPageWithFormLoginOpenIDRememberMeConfig extends BaseWebConfig { + @Override + protected void configure(HttpSecurity http) { + http + .authorizeUrls() + .anyRequest().hasRole("USER") + .and() + .rememberMe() + .and() + .formLogin() + .and() + .openidLogin() + } + } + + def "default login with custom AuthenticationEntryPoint"() { + when: + loadConfig(DefaultLoginWithCustomAuthenticationEntryPointConfig) + then: + !findFilter(DefaultLoginPageViewFilter) + } + + @Configuration + static class DefaultLoginWithCustomAuthenticationEntryPointConfig extends BaseWebConfig { + @Override + protected void configure(HttpSecurity http) { + http + .exceptionHandling() + .authenticationEntryPoint(new LoginUrlAuthenticationEntryPoint("/login")) + .and() + .authorizeUrls() + .anyRequest().hasRole("USER") + .and() + .formLogin() + } + } + + def "DefaultLoginPage ObjectPostProcessor"() { + setup: + AnyObjectPostProcessor objectPostProcessor = Mock() + when: + HttpSecurity http = new HttpSecurity(objectPostProcessor, authenticationBldr, [:]) + DefaultLoginPageConfigurer defaultLoginConfig = new DefaultLoginPageConfigurer([builder:http]) + defaultLoginConfig.addObjectPostProcessor(objectPostProcessor) + http + // must set builder manually due to groovy not selecting correct method + .apply(defaultLoginConfig).and() + .formLogin() + .and() + .build() + + then: "DefaultLoginPageGeneratingFilter is registered with LifecycleManager" + 1 * objectPostProcessor.postProcess(_ as DefaultLoginPageViewFilter) >> {DefaultLoginPageViewFilter o -> o} + 1 * objectPostProcessor.postProcess(_ as UsernamePasswordAuthenticationFilter) >> {UsernamePasswordAuthenticationFilter o -> o} + 1 * objectPostProcessor.postProcess(_ as LoginUrlAuthenticationEntryPoint) >> {LoginUrlAuthenticationEntryPoint o -> o} + } +} diff --git a/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/ExceptionHandlingConfigurerTests.groovy b/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/ExceptionHandlingConfigurerTests.groovy new file mode 100644 index 0000000000..e7f2f39464 --- /dev/null +++ b/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/ExceptionHandlingConfigurerTests.groovy @@ -0,0 +1,43 @@ +/* + * Copyright 2002-2013 the original author or authors. + * + * 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 org.springframework.security.config.annotation.web.configurers + +import org.springframework.security.config.annotation.AnyObjectPostProcessor +import org.springframework.security.config.annotation.BaseSpringSpec +import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.web.access.ExceptionTranslationFilter + +/** + * + * @author Rob Winch + */ +class ExceptionHandlingConfigurerTests extends BaseSpringSpec { + + def "exception ObjectPostProcessor"() { + setup: "initialize the AUTH_FILTER as a mock" + AnyObjectPostProcessor opp = Mock() + when: + HttpSecurity http = new HttpSecurity(opp, authenticationBldr, [:]) + http + .exceptionHandling() + .and() + .build() + + then: "ExceptionTranslationFilter is registered with LifecycleManager" + 1 * opp.postProcess(_ as ExceptionTranslationFilter) >> {ExceptionTranslationFilter o -> o} + } +} diff --git a/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/ExpressionUrlAuthorizationsTests.groovy b/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/ExpressionUrlAuthorizationsTests.groovy new file mode 100644 index 0000000000..6cf6a4fdf1 --- /dev/null +++ b/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/ExpressionUrlAuthorizationsTests.groovy @@ -0,0 +1,413 @@ +/* + * Copyright 2002-2013 the original author or authors. + * + * 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 org.springframework.security.config.annotation.web.configurers; + +import org.springframework.beans.factory.BeanCreationException; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.access.vote.AffirmativeBased; +import org.springframework.security.authentication.RememberMeAuthenticationToken +import org.springframework.security.config.annotation.BaseSpringSpec +import org.springframework.security.config.annotation.SecurityExpressions.* +import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; +import org.springframework.security.config.annotation.web.configurers.ExpressionUrlAuthorizationConfigurer; +import org.springframework.security.core.authority.AuthorityUtils; +import org.springframework.security.web.access.intercept.FilterSecurityInterceptor; + +public class ExpressionUrlAuthorizationConfigurerTests extends BaseSpringSpec { + + def "hasAnyAuthority('ROLE_USER')"() { + when: + def expression = ExpressionUrlAuthorizationConfigurer.hasAnyAuthority("ROLE_USER") + then: + expression == "hasAnyAuthority('ROLE_USER')" + } + + def "hasAnyAuthority('ROLE_USER','ROLE_ADMIN')"() { + when: + def expression = ExpressionUrlAuthorizationConfigurer.hasAnyAuthority("ROLE_USER","ROLE_ADMIN") + then: + expression == "hasAnyAuthority('ROLE_USER','ROLE_ADMIN')" + } + + def "hasRole('ROLE_USER') is rejected due to starting with ROLE_"() { + when: + def expression = ExpressionUrlAuthorizationConfigurer.hasRole("ROLE_USER") + then: + IllegalArgumentException e = thrown() + e.message == "role should not start with 'ROLE_' since it is automatically inserted. Got 'ROLE_USER'" + } + + def "authorizeUrls() uses AffirmativeBased AccessDecisionManager"() { + when: "Load Config with no specific AccessDecisionManager" + loadConfig(NoSpecificAccessDecessionManagerConfig) + then: "AccessDecessionManager matches the HttpSecurityBuilder's default" + findFilter(FilterSecurityInterceptor).accessDecisionManager.class == AffirmativeBased + } + + @EnableWebSecurity + @Configuration + static class NoSpecificAccessDecessionManagerConfig extends WebSecurityConfigurerAdapter { + protected void configure(HttpSecurity http) throws Exception { + http + .authorizeUrls() + .anyRequest().hasRole("USER") + } + } + + def "authorizeUrls() no requests"() { + when: "Load Config with no requests" + loadConfig(NoRequestsConfig) + then: "A meaningful exception is thrown" + BeanCreationException success = thrown() + success.message.contains "At least one mapping is required (i.e. authorizeUrls().anyRequest.authenticated())" + } + + @EnableWebSecurity + @Configuration + static class NoRequestsConfig extends WebSecurityConfigurerAdapter { + protected void configure(HttpSecurity http) throws Exception { + http + .authorizeUrls() + } + } + + def "authorizeUrls() incomplete mapping"() { + when: "Load Config with incomplete mapping" + loadConfig(IncompleteMappingConfig) + then: "A meaningful exception is thrown" + BeanCreationException success = thrown() + success.message.contains "An incomplete mapping was found for " + } + + @EnableWebSecurity + @Configuration + static class IncompleteMappingConfig extends WebSecurityConfigurerAdapter { + protected void configure(HttpSecurity http) throws Exception { + http + .authorizeUrls() + .antMatchers("/a").authenticated() + .anyRequest() + } + } + + def "authorizeUrls() hasAuthority"() { + setup: + loadConfig(HasAuthorityConfig) + when: + springSecurityFilterChain.doFilter(request,response,chain) + then: + response.status == 403 + when: + super.setup() + login() + springSecurityFilterChain.doFilter(request,response,chain) + then: + response.status == 200 + when: + super.setup() + login("user","ROLE_INVALID") + springSecurityFilterChain.doFilter(request,response,chain) + then: + response.status == 403 + } + + @EnableWebSecurity + @Configuration + static class HasAuthorityConfig extends WebSecurityConfigurerAdapter { + protected void configure(HttpSecurity http) throws Exception { + http + .httpBasic() + .and() + .authorizeUrls() + .anyRequest().hasAuthority("ROLE_USER") + } + } + + def "authorizeUrls() hasAnyAuthority"() { + setup: + loadConfig(HasAnyAuthorityConfig) + when: + springSecurityFilterChain.doFilter(request,response,chain) + then: + response.status == 403 + when: + super.setup() + login("user","ROLE_ADMIN") + springSecurityFilterChain.doFilter(request,response,chain) + then: + response.status == 200 + when: + super.setup() + login("user","ROLE_DBA") + springSecurityFilterChain.doFilter(request,response,chain) + then: + response.status == 200 + when: + super.setup() + login("user","ROLE_INVALID") + springSecurityFilterChain.doFilter(request,response,chain) + then: + response.status == 403 + } + + @EnableWebSecurity + @Configuration + static class HasAnyAuthorityConfig extends WebSecurityConfigurerAdapter { + protected void configure(HttpSecurity http) throws Exception { + http + .httpBasic() + .and() + .authorizeUrls() + .anyRequest().hasAnyAuthority("ROLE_ADMIN","ROLE_DBA") + } + } + + def "authorizeUrls() hasIpAddress"() { + setup: + loadConfig(HasIpAddressConfig) + when: + request.remoteAddr = "192.168.1.1" + springSecurityFilterChain.doFilter(request,response,chain) + then: + response.status == 403 + when: + super.setup() + request.remoteAddr = "192.168.1.0" + springSecurityFilterChain.doFilter(request,response,chain) + then: + response.status == 200 + } + + @EnableWebSecurity + @Configuration + static class HasIpAddressConfig extends WebSecurityConfigurerAdapter { + protected void configure(HttpSecurity http) throws Exception { + http + .httpBasic() + .and() + .authorizeUrls() + .anyRequest().hasIpAddress("192.168.1.0") + } + } + + def "authorizeUrls() anonymous"() { + setup: + loadConfig(AnonymousConfig) + when: + springSecurityFilterChain.doFilter(request,response,chain) + then: + response.status == 200 + when: + super.setup() + login() + springSecurityFilterChain.doFilter(request,response,chain) + then: + response.status == 403 + } + + @EnableWebSecurity + @Configuration + static class AnonymousConfig extends WebSecurityConfigurerAdapter { + protected void configure(HttpSecurity http) throws Exception { + http + .httpBasic() + .and() + .authorizeUrls() + .anyRequest().anonymous() + } + } + + def "authorizeUrls() rememberMe"() { + setup: + loadConfig(RememberMeConfig) + when: + springSecurityFilterChain.doFilter(request,response,chain) + then: + response.status == 403 + when: + super.setup() + login(new RememberMeAuthenticationToken("key", "user", AuthorityUtils.createAuthorityList("ROLE_USER"))) + springSecurityFilterChain.doFilter(request,response,chain) + then: + response.status == 200 + } + + @EnableWebSecurity + @Configuration + static class RememberMeConfig extends WebSecurityConfigurerAdapter { + protected void configure(HttpSecurity http) throws Exception { + http + .rememberMe() + .and() + .httpBasic() + .and() + .authorizeUrls() + .anyRequest().rememberMe() + } + + @Override + protected void registerAuthentication(AuthenticationManagerBuilder auth) + throws Exception { + auth + .inMemoryAuthentication() + .withUser("user").password("password").roles("USER") + } + } + + def "authorizeUrls() denyAll"() { + setup: + loadConfig(DenyAllConfig) + when: + springSecurityFilterChain.doFilter(request,response,chain) + then: + response.status == 403 + when: + super.setup() + login(new RememberMeAuthenticationToken("key", "user", AuthorityUtils.createAuthorityList("ROLE_USER"))) + springSecurityFilterChain.doFilter(request,response,chain) + then: + response.status == 403 + } + + @EnableWebSecurity + @Configuration + static class DenyAllConfig extends WebSecurityConfigurerAdapter { + protected void configure(HttpSecurity http) throws Exception { + http + .httpBasic() + .and() + .authorizeUrls() + .anyRequest().denyAll() + } + } + + def "authorizeUrls() not denyAll"() { + setup: + loadConfig(NotDenyAllConfig) + when: + springSecurityFilterChain.doFilter(request,response,chain) + then: + response.status == 200 + when: + super.setup() + login(new RememberMeAuthenticationToken("key", "user", AuthorityUtils.createAuthorityList("ROLE_USER"))) + springSecurityFilterChain.doFilter(request,response,chain) + then: + response.status == 200 + } + + @EnableWebSecurity + @Configuration + static class NotDenyAllConfig extends WebSecurityConfigurerAdapter { + protected void configure(HttpSecurity http) throws Exception { + http + .httpBasic() + .and() + .authorizeUrls() + .anyRequest().not().denyAll() + } + } + + def "authorizeUrls() fullyAuthenticated"() { + setup: + loadConfig(FullyAuthenticatedConfig) + when: + springSecurityFilterChain.doFilter(request,response,chain) + then: + response.status == 403 + when: + super.setup() + login(new RememberMeAuthenticationToken("key", "user", AuthorityUtils.createAuthorityList("ROLE_USER"))) + springSecurityFilterChain.doFilter(request,response,chain) + then: + response.status == 403 + when: + super.setup() + login() + springSecurityFilterChain.doFilter(request,response,chain) + then: + response.status == 200 + } + + @EnableWebSecurity + @Configuration + static class FullyAuthenticatedConfig extends WebSecurityConfigurerAdapter { + protected void configure(HttpSecurity http) throws Exception { + http + .rememberMe() + .and() + .httpBasic() + .and() + .authorizeUrls() + .anyRequest().fullyAuthenticated() + } + + @Override + protected void registerAuthentication(AuthenticationManagerBuilder auth) + throws Exception { + auth + .inMemoryAuthentication() + .withUser("user").password("password").roles("USER") + } + } + + def "authorizeUrls() access"() { + setup: + loadConfig(AccessConfig) + when: + springSecurityFilterChain.doFilter(request,response,chain) + then: "Access is granted due to GET" + response.status == 200 + when: + super.setup() + login() + request.method = "POST" + springSecurityFilterChain.doFilter(request,response,chain) + then: "Access is granted due to role" + response.status == 200 + when: + super.setup() + request.method = "POST" + springSecurityFilterChain.doFilter(request,response,chain) + then: "Access is denied" + response.status == 403 + } + + @EnableWebSecurity + @Configuration + static class AccessConfig extends WebSecurityConfigurerAdapter { + protected void configure(HttpSecurity http) throws Exception { + http + .rememberMe() + .and() + .httpBasic() + .and() + .authorizeUrls() + .anyRequest().access("hasRole('ROLE_USER') or request.method == 'GET'") + } + + @Override + protected void registerAuthentication(AuthenticationManagerBuilder auth) + throws Exception { + auth + .inMemoryAuthentication() + .withUser("user").password("password").roles("USER") + } + } +} diff --git a/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/FormLoginConfigurerTests.groovy b/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/FormLoginConfigurerTests.groovy new file mode 100644 index 0000000000..c488aec8b9 --- /dev/null +++ b/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/FormLoginConfigurerTests.groovy @@ -0,0 +1,214 @@ +/* + * Copyright 2002-2013 the original author or authors. + * + * 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 org.springframework.security.config.annotation.web.configurers + +import org.springframework.context.annotation.AnnotationConfigApplicationContext +import org.springframework.context.annotation.Configuration +import org.springframework.mock.web.MockFilterChain +import org.springframework.mock.web.MockHttpServletRequest +import org.springframework.mock.web.MockHttpServletResponse +import org.springframework.security.authentication.BadCredentialsException +import org.springframework.security.config.annotation.AnyObjectPostProcessor +import org.springframework.security.config.annotation.BaseSpringSpec +import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder +import org.springframework.security.config.annotation.web.builders.HttpSecurity +import org.springframework.security.config.annotation.web.builders.WebSecurity +import org.springframework.security.config.annotation.web.configuration.BaseWebConfig +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity +import org.springframework.security.web.AuthenticationEntryPoint +import org.springframework.security.web.FilterChainProxy +import org.springframework.security.web.PortMapper +import org.springframework.security.web.access.ExceptionTranslationFilter +import org.springframework.security.web.access.intercept.FilterSecurityInterceptor +import org.springframework.security.web.authentication.AnonymousAuthenticationFilter +import org.springframework.security.web.authentication.AuthenticationFailureHandler +import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter +import org.springframework.security.web.authentication.logout.LogoutFilter +import org.springframework.security.web.authentication.session.SessionFixationProtectionStrategy +import org.springframework.security.web.context.SecurityContextPersistenceFilter +import org.springframework.security.web.savedrequest.RequestCacheAwareFilter +import org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter +import org.springframework.security.web.session.SessionManagementFilter +import org.springframework.security.web.util.AnyRequestMatcher +import org.springframework.test.util.ReflectionTestUtils + +/** + * + * @author Rob Winch + */ +class FormLoginConfigurerTests extends BaseSpringSpec { + def "Form Login"() { + when: "load formLogin()" + context = new AnnotationConfigApplicationContext(FormLoginConfig) + + then: "FilterChains configured correctly" + def filterChains = filterChains() + filterChains.size() == 2 + filterChains[0].requestMatcher.pattern == '/resources/**' + filterChains[0].filters.empty + filterChains[1].requestMatcher instanceof AnyRequestMatcher + filterChains[1].filters.collect { it.class.name.contains('$') ? it.class.superclass : it.class } == + [SecurityContextPersistenceFilter, LogoutFilter, UsernamePasswordAuthenticationFilter, + RequestCacheAwareFilter, SecurityContextHolderAwareRequestFilter, + AnonymousAuthenticationFilter, SessionManagementFilter, ExceptionTranslationFilter, FilterSecurityInterceptor ] + + and: "UsernamePasswordAuthentictionFilter is configured correctly" + UsernamePasswordAuthenticationFilter authFilter = findFilter(UsernamePasswordAuthenticationFilter,1) + authFilter.usernameParameter == "username" + authFilter.passwordParameter == "password" + authFilter.failureHandler.defaultFailureUrl == "/login?error" + authFilter.successHandler.defaultTargetUrl == "/" + authFilter.requiresAuthentication(new MockHttpServletRequest(requestURI : "/login", method: "POST"), new MockHttpServletResponse()) + !authFilter.requiresAuthentication(new MockHttpServletRequest(requestURI : "/login", method: "GET"), new MockHttpServletResponse()) + + and: "SessionFixationProtectionStrategy is configured correctly" + SessionFixationProtectionStrategy sessionStrategy = ReflectionTestUtils.getField(authFilter,"sessionStrategy") + sessionStrategy.migrateSessionAttributes + + and: "Exception handling is configured correctly" + AuthenticationEntryPoint authEntryPoint = filterChains[1].filters.find { it instanceof ExceptionTranslationFilter}.authenticationEntryPoint + MockHttpServletResponse response = new MockHttpServletResponse() + authEntryPoint.commence(new MockHttpServletRequest(requestURI: "/private/"), response, new BadCredentialsException("")) + response.redirectedUrl == "http://localhost/login" + } + + @Configuration + @EnableWebSecurity + static class FormLoginConfig extends BaseWebConfig { + @Override + public void configure(WebSecurity web) throws Exception { + web + .ignoring() + .antMatchers("/resources/**"); + } + + @Override + protected void configure(HttpSecurity http) { + http + .authorizeUrls() + .anyRequest().hasRole("USER") + .and() + .formLogin() + .loginUrl("/login") + } + } + + def "FormLogin.permitAll()"() { + when: "load formLogin() with permitAll" + context = new AnnotationConfigApplicationContext(FormLoginConfigPermitAll) + + then: "the formLogin URLs are granted access" + FilterChainProxy filterChain = context.getBean(FilterChainProxy) + MockHttpServletResponse response = new MockHttpServletResponse() + filterChain.doFilter(new MockHttpServletRequest(servletPath : servletPath, requestURI: servletPath, queryString: query, method: method), response, new MockFilterChain()) + response.redirectedUrl == redirectUrl + + where: + servletPath | method | query | redirectUrl + "/login" | "GET" | null | null + "/login" | "POST" | null | "/login?error" + "/login" | "GET" | "error" | null + } + + @Configuration + @EnableWebSecurity + static class FormLoginConfigPermitAll extends BaseWebConfig { + + @Override + protected void configure(HttpSecurity http) { + http + .authorizeUrls() + .anyRequest().hasRole("USER") + .and() + .formLogin() + .permitAll() + } + } + + def "FormLogin uses PortMapper"() { + when: "load formLogin() with permitAll" + FormLoginUsesPortMapperConfig.PORT_MAPPER = Mock(PortMapper) + loadConfig(FormLoginUsesPortMapperConfig) + then: "the formLogin URLs are granted access" + findFilter(ExceptionTranslationFilter).authenticationEntryPoint.portMapper == FormLoginUsesPortMapperConfig.PORT_MAPPER + } + + @Configuration + @EnableWebSecurity + static class FormLoginUsesPortMapperConfig extends BaseWebConfig { + static PortMapper PORT_MAPPER + + @Override + protected void configure(HttpSecurity http) { + http + .authorizeUrls() + .anyRequest().hasRole("USER") + .and() + .formLogin() + .permitAll() + .and() + .portMapper() + .portMapper(PORT_MAPPER) + } + } + + def "FormLogin permitAll ignores failureUrl when failureHandler set"() { + setup: + PermitAllIgnoresFailureHandlerConfig.FAILURE_HANDLER = Mock(AuthenticationFailureHandler) + loadConfig(PermitAllIgnoresFailureHandlerConfig) + FilterChainProxy springSecurityFilterChain = context.getBean(FilterChainProxy) + when: "access default failureUrl and configured explicit FailureHandler" + MockHttpServletRequest request = new MockHttpServletRequest(requestURI:"/login",queryString:"error") + MockHttpServletResponse response = new MockHttpServletResponse() + springSecurityFilterChain.doFilter(request,response,new MockFilterChain()) + then: "access is not granted to the failure handler (sent to login page)" + response.status == 302 + } + + @EnableWebSecurity + @Configuration + static class PermitAllIgnoresFailureHandlerConfig extends BaseWebConfig { + static AuthenticationFailureHandler FAILURE_HANDLER + + @Override + protected void configure(HttpSecurity http) { + http + .authorizeUrls() + .anyRequest().hasRole("USER") + .and() + .formLogin() + .failureHandler(FAILURE_HANDLER) + .permitAll() + } + } + + def "formLogin ObjectPostProcessor"() { + setup: "initialize the AUTH_FILTER as a mock" + AnyObjectPostProcessor opp = Mock() + HttpSecurity http = new HttpSecurity(opp, authenticationBldr, [:]) + when: + http + .formLogin() + .and() + .build() + + then: "UsernamePasswordAuthenticationFilter is registered with LifecycleManager" + 1 * opp.postProcess(_ as UsernamePasswordAuthenticationFilter) >> {UsernamePasswordAuthenticationFilter o -> o} + and: "LoginUrlAuthenticationEntryPoint is registered with LifecycleManager" + 1 * opp.postProcess(_ as LoginUrlAuthenticationEntryPoint) >> {LoginUrlAuthenticationEntryPoint o -> o} + } +} diff --git a/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/HttpBasicConfigurerTests.groovy b/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/HttpBasicConfigurerTests.groovy new file mode 100644 index 0000000000..fc89846053 --- /dev/null +++ b/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/HttpBasicConfigurerTests.groovy @@ -0,0 +1,43 @@ +/* + * Copyright 2002-2013 the original author or authors. + * + * 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 org.springframework.security.config.annotation.web.configurers + +import org.springframework.security.config.annotation.AnyObjectPostProcessor +import org.springframework.security.config.annotation.BaseSpringSpec +import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.web.authentication.www.BasicAuthenticationFilter + +/** + * + * @author Rob Winch + */ +class HttpBasicConfigurerTests extends BaseSpringSpec { + + def "httBasic ObjectPostProcessor"() { + setup: + AnyObjectPostProcessor opp = Mock() + HttpSecurity http = new HttpSecurity(opp, authenticationBldr, [:]) + when: + http + .httpBasic() + .and() + .build() + + then: "ExceptionTranslationFilter is registered with LifecycleManager" + 1 * opp.postProcess(_ as BasicAuthenticationFilter) >> {BasicAuthenticationFilter o -> o} + } +} diff --git a/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/Issue55Tests.groovy b/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/Issue55Tests.groovy new file mode 100644 index 0000000000..d9c804eee6 --- /dev/null +++ b/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/Issue55Tests.groovy @@ -0,0 +1,114 @@ +/* + * Copyright 2002-2013 the original author or authors. + * + * 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 org.springframework.security.config.annotation.web.configurers + +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.core.annotation.Order; +import org.springframework.security.authentication.AuthenticationManager +import org.springframework.security.config.annotation.BaseSpringSpec +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; +import org.springframework.security.core.Authentication +import org.springframework.security.core.AuthenticationException +import org.springframework.security.web.FilterChainProxy +import org.springframework.security.web.access.intercept.FilterSecurityInterceptor +import org.springframework.stereotype.Component + +/** + * + * @author Rob Winch + */ +class Issue55Tests extends BaseSpringSpec { + + def "WebSecurityConfigurerAdapter defaults to @Autowired"() { + when: + loadConfig(WebSecurityConfigurerAdapterDefaultsAuthManagerConfig) + then: + context.getBean(FilterChainProxy) + findFilter(FilterSecurityInterceptor).authenticationManager.parent.class == CustomAuthenticationManager + } + + @Configuration + @EnableWebSecurity + static class WebSecurityConfigurerAdapterDefaultsAuthManagerConfig { + @Component + public static class WebSecurityAdapter extends WebSecurityConfigurerAdapter { + + @Override + protected void configure(HttpSecurity http) throws Exception { + http + .authorizeUrls() + .anyRequest().hasRole("USER"); + } + } + @Configuration + public static class AuthenticationManagerConfiguration { + @Bean + public AuthenticationManager authenticationManager() throws Exception { + return new CustomAuthenticationManager(); + } + } + } + + def "multi http WebSecurityConfigurerAdapter defaults to @Autowired"() { + when: + loadConfig(MultiWebSecurityConfigurerAdapterDefaultsAuthManagerConfig) + then: + context.getBean(FilterChainProxy) + findFilter(FilterSecurityInterceptor).authenticationManager.parent.class == CustomAuthenticationManager + findFilter(FilterSecurityInterceptor,1).authenticationManager.parent.class == CustomAuthenticationManager + } + + @Configuration + @EnableWebSecurity + static class MultiWebSecurityConfigurerAdapterDefaultsAuthManagerConfig { + @Component + @Order(1) + public static class ApiWebSecurityAdapter extends WebSecurityConfigurerAdapter { + @Override + protected void configure(HttpSecurity http) throws Exception { + http + .antMatcher("/api/**") + .authorizeUrls() + .anyRequest().hasRole("USER"); + } + } + @Component + public static class WebSecurityAdapter extends WebSecurityConfigurerAdapter { + @Override + protected void configure(HttpSecurity http) throws Exception { + http + .authorizeUrls() + .anyRequest().hasRole("USER"); + } + } + @Configuration + public static class AuthenticationManagerConfiguration { + @Bean + public AuthenticationManager authenticationManager() throws Exception { + return new CustomAuthenticationManager(); + } + } + } + + static class CustomAuthenticationManager implements AuthenticationManager { + public Authentication authenticate(Authentication authentication) throws AuthenticationException { + return null; + } + } +} diff --git a/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/JeeConfigurerTests.groovy b/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/JeeConfigurerTests.groovy new file mode 100644 index 0000000000..aaa9bf5678 --- /dev/null +++ b/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/JeeConfigurerTests.groovy @@ -0,0 +1,46 @@ +/* + * Copyright 2002-2013 the original author or authors. + * + * 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 org.springframework.security.config.annotation.web.configurers + +import org.springframework.security.config.annotation.AnyObjectPostProcessor +import org.springframework.security.config.annotation.BaseSpringSpec +import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.web.authentication.preauth.j2ee.J2eeBasedPreAuthenticatedWebAuthenticationDetailsSource +import org.springframework.security.web.authentication.preauth.j2ee.J2eePreAuthenticatedProcessingFilter + +/** + * + * @author Rob Winch + */ +class JeeConfigurerTests extends BaseSpringSpec { + + def "jee ObjectPostProcessor"() { + setup: + AnyObjectPostProcessor opp = Mock() + HttpSecurity http = new HttpSecurity(opp, authenticationBldr, [:]) + when: + http + .jee() + .and() + .build() + + then: "J2eePreAuthenticatedProcessingFilter is registered with LifecycleManager" + 1 * opp.postProcess(_ as J2eePreAuthenticatedProcessingFilter) >> {J2eePreAuthenticatedProcessingFilter o -> o} + and: "J2eeBasedPreAuthenticatedWebAuthenticationDetailsSource is registered with LifecycleManager" + 1 * opp.postProcess(_ as J2eeBasedPreAuthenticatedWebAuthenticationDetailsSource) >> {J2eeBasedPreAuthenticatedWebAuthenticationDetailsSource o -> o} + } +} diff --git a/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/LogoutConfigurerTests.groovy b/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/LogoutConfigurerTests.groovy new file mode 100644 index 0000000000..7d7a2fe1b6 --- /dev/null +++ b/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/LogoutConfigurerTests.groovy @@ -0,0 +1,43 @@ +/* + * Copyright 2002-2013 the original author or authors. + * + * 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 org.springframework.security.config.annotation.web.configurers + +import org.springframework.security.config.annotation.AnyObjectPostProcessor +import org.springframework.security.config.annotation.BaseSpringSpec +import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.web.authentication.logout.LogoutFilter + +/** + * + * @author Rob Winch + */ +class LogoutConfigurerTests extends BaseSpringSpec { + + def "logout ObjectPostProcessor"() { + setup: + AnyObjectPostProcessor opp = Mock() + HttpSecurity http = new HttpSecurity(opp, authenticationBldr, [:]) + when: + http + .logout() + .and() + .build() + + then: "LogoutFilter is registered with LifecycleManager" + 1 * opp.postProcess(_ as LogoutFilter) >> {LogoutFilter o -> o} + } +} diff --git a/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/NamespaceDebugTests.groovy b/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/NamespaceDebugTests.groovy new file mode 100644 index 0000000000..6aceb9a947 --- /dev/null +++ b/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/NamespaceDebugTests.groovy @@ -0,0 +1,78 @@ +/* + * Copyright 2002-2013 the original author or authors. + * + * 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 org.springframework.security.config.annotation.web.configurers; + +import java.io.IOException; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.security.access.AccessDecisionManager +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.access.ConfigAttribute +import org.springframework.security.authentication.AnonymousAuthenticationToken +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.config.annotation.BaseSpringSpec +import org.springframework.security.config.annotation.web.builders.DebugFilter; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.core.authority.AuthorityUtils; +import org.springframework.security.web.AuthenticationEntryPoint +import org.springframework.security.web.FilterInvocation +import org.springframework.security.web.access.AccessDeniedHandler; +import org.springframework.security.web.access.AccessDeniedHandlerImpl; +import org.springframework.security.web.access.ExceptionTranslationFilter +import org.springframework.security.web.access.intercept.FilterSecurityInterceptor +import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter; +import org.springframework.security.web.authentication.AnonymousAuthenticationFilter; +import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; +import org.springframework.security.web.authentication.www.BasicAuthenticationFilter; +import org.springframework.security.web.context.HttpSessionSecurityContextRepository; +import org.springframework.security.web.context.NullSecurityContextRepository; +import org.springframework.security.web.context.SecurityContextPersistenceFilter +import org.springframework.security.web.jaasapi.JaasApiIntegrationFilter; +import org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter; +import org.springframework.security.web.util.AntPathRequestMatcher +import org.springframework.security.web.util.AnyRequestMatcher; +import org.springframework.security.web.util.RequestMatcher + +import spock.lang.Ignore; + +/** + * Tests to verify that all the functionality of attributes is present + * + * @author Rob Winch + * + */ +public class NamespaceDebugTests extends BaseSpringSpec { + def "debug=true"() { + when: "Load configuraiton with debug enabled" + loadConfig(DebugWebSecurity) + then: "The DebugFilter is present" + context.getBean("springSecurityFilterChain").class == DebugFilter + } + + @Configuration + @EnableWebSecurity(debug=true) + static class DebugWebSecurity extends WebSecurityConfigurerAdapter { + } +} diff --git a/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/NamespaceHttpAccessDeniedHandlerTests.groovy b/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/NamespaceHttpAccessDeniedHandlerTests.groovy new file mode 100644 index 0000000000..2e48003260 --- /dev/null +++ b/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/NamespaceHttpAccessDeniedHandlerTests.groovy @@ -0,0 +1,80 @@ +/* + * Copyright 2002-2013 the original author or authors. + * + * 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 org.springframework.security.config.annotation.web.configurers; + +import java.io.IOException; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.config.annotation.BaseSpringSpec; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.BaseWebConfig; +import org.springframework.security.web.access.AccessDeniedHandler; +import org.springframework.security.web.access.ExceptionTranslationFilter; + +/** + * Tests to verify that all the functionality of attributes is present + * + * @author Rob Winch + * + */ +public class NamespaceHttpAccessDeniedHandlerTests extends BaseSpringSpec { + def "http/access-denied-handler@error-page"() { + when: + loadConfig(AccessDeniedPageConfig) + then: + findFilter(ExceptionTranslationFilter).accessDeniedHandler.errorPage == "/AccessDeniedPageConfig" + } + + @Configuration + static class AccessDeniedPageConfig extends BaseWebConfig { + protected void configure(HttpSecurity http) { + http. + exceptionHandling() + .accessDeniedPage("/AccessDeniedPageConfig") + } + } + + def "http/access-denied-handler@ref"() { + when: + loadConfig(AccessDeniedHandlerRefConfig) + then: + findFilter(ExceptionTranslationFilter).accessDeniedHandler.class == AccessDeniedHandlerRefConfig.CustomAccessDeniedHandler + } + + @Configuration + static class AccessDeniedHandlerRefConfig extends BaseWebConfig { + protected void configure(HttpSecurity http) { + CustomAccessDeniedHandler accessDeniedHandler = new CustomAccessDeniedHandler() + http. + exceptionHandling() + .accessDeniedHandler(accessDeniedHandler) + } + + static class CustomAccessDeniedHandler implements AccessDeniedHandler { + public void handle(HttpServletRequest request, + HttpServletResponse response, + AccessDeniedException accessDeniedException) + throws IOException, ServletException { + } + } + } +} diff --git a/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/NamespaceHttpAnonymousTests.groovy b/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/NamespaceHttpAnonymousTests.groovy new file mode 100644 index 0000000000..bac648119a --- /dev/null +++ b/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/NamespaceHttpAnonymousTests.groovy @@ -0,0 +1,129 @@ +/* + * Copyright 2002-2013 the original author or authors. + * + * 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 org.springframework.security.config.annotation.web.configurers; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.authentication.AnonymousAuthenticationToken +import org.springframework.security.config.annotation.BaseSpringSpec; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.BaseWebConfig; +import org.springframework.security.core.authority.AuthorityUtils; +import org.springframework.security.web.access.intercept.FilterSecurityInterceptor; +import org.springframework.security.web.authentication.AnonymousAuthenticationFilter; + +/** + * Tests to verify that all the functionality of attributes is present + * + * @author Rob Winch + * + */ +public class NamespaceHttpAnonymousTests extends BaseSpringSpec { + def "http/anonymous@enabled = true (default)"() { + when: + loadConfig(AnonymousConfig) + then: + def filter = findFilter(AnonymousAuthenticationFilter) + filter != null + def authManager = findFilter(FilterSecurityInterceptor).authenticationManager + authManager.authenticate(new AnonymousAuthenticationToken(filter.key, filter.principal, filter.authorities)).authenticated + } + + @Configuration + static class AnonymousConfig extends BaseWebConfig { + @Override + protected void configure(HttpSecurity http) { + http + .authorizeUrls() + .anyRequest().hasRole("USER"); + } + } + + def "http/anonymous@enabled = false"() { + when: + loadConfig(AnonymousDisabledConfig) + then: + findFilter(AnonymousAuthenticationFilter) == null + } + + @Configuration + static class AnonymousDisabledConfig extends BaseWebConfig { + protected void configure(HttpSecurity http) { + http.anonymous().disable() + } + } + + def "http/anonymous@granted-authority"() { + when: + loadConfig(AnonymousGrantedAuthorityConfig) + then: + findFilter(AnonymousAuthenticationFilter).authorities == AuthorityUtils.createAuthorityList("ROLE_ANON") + } + + @Configuration + static class AnonymousGrantedAuthorityConfig extends BaseWebConfig { + protected void configure(HttpSecurity http) { + http + .anonymous() + .authorities("ROLE_ANON") + } + } + + + def "http/anonymous@key"() { + when: + loadConfig(AnonymousKeyConfig) + then: + def filter = findFilter(AnonymousAuthenticationFilter) + filter != null + filter.key == "AnonymousKeyConfig" + def authManager = findFilter(FilterSecurityInterceptor).authenticationManager + authManager.authenticate(new AnonymousAuthenticationToken(filter.key, filter.principal, filter.authorities)).authenticated + } + + @Configuration + static class AnonymousKeyConfig extends BaseWebConfig { + protected void configure(HttpSecurity http) { + http + .authorizeUrls() + .anyRequest().hasRole("USER") + .and() + .anonymous().key("AnonymousKeyConfig") + } + } + + def "http/anonymous@username"() { + when: + loadConfig(AnonymousUsernameConfig) + then: + def filter = findFilter(AnonymousAuthenticationFilter) + filter != null + filter.principal == "AnonymousUsernameConfig" + def authManager = findFilter(FilterSecurityInterceptor).authenticationManager + authManager.authenticate(new AnonymousAuthenticationToken(filter.key, filter.principal, filter.authorities)).principal == "AnonymousUsernameConfig" + } + + @Configuration + static class AnonymousUsernameConfig extends BaseWebConfig { + protected void configure(HttpSecurity http) { + http + .authorizeUrls() + .anyRequest().hasRole("USER") + .and() + .anonymous().principal("AnonymousUsernameConfig") + } + } +} diff --git a/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/NamespaceHttpBasicTests.groovy b/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/NamespaceHttpBasicTests.groovy new file mode 100644 index 0000000000..57e73c2bb6 --- /dev/null +++ b/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/NamespaceHttpBasicTests.groovy @@ -0,0 +1,169 @@ +/* + * Copyright 2002-2013 the original author or authors. + * + * 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 org.springframework.security.config.annotation.web.configurers; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.mock.web.MockFilterChain; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.security.config.annotation.BaseSpringSpec; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.BaseWebConfig; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.AuthenticationEntryPoint; +import org.springframework.security.web.FilterChainProxy; +import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; +import org.springframework.security.web.authentication.www.BasicAuthenticationFilter; + +/** + * Tests to verify that all the functionality of attributes is present + * + * @author Rob Winch + * + */ +public class NamespaceHttpBasicTests extends BaseSpringSpec { + FilterChainProxy springSecurityFilterChain + MockHttpServletRequest request + MockHttpServletResponse response + MockFilterChain chain + + def setup() { + request = new MockHttpServletRequest() + response = new MockHttpServletResponse() + chain = new MockFilterChain() + } + + def "http/http-basic"() { + setup: + loadConfig(HttpBasicConfig) + springSecurityFilterChain = context.getBean(FilterChainProxy) + when: + springSecurityFilterChain.doFilter(request,response,chain) + then: + response.status == HttpServletResponse.SC_FORBIDDEN + when: "fail to log in" + setup() + login("user","invalid") + springSecurityFilterChain.doFilter(request,response,chain) + then: "unauthorized" + response.status == HttpServletResponse.SC_UNAUTHORIZED + response.getHeader("WWW-Authenticate") == 'Basic realm="Spring Security Application"' + when: "login success" + setup() + login() + then: "sent to default succes page" + !response.committed + } + + @Configuration + static class HttpBasicConfig extends BaseWebConfig { + protected void configure(HttpSecurity http) { + http + .authorizeUrls() + .anyRequest().hasRole("USER") + .and() + .httpBasic(); + } + } + + def "http@realm"() { + setup: + loadConfig(CustomHttpBasicConfig) + springSecurityFilterChain = context.getBean(FilterChainProxy) + when: + login("user","invalid") + springSecurityFilterChain.doFilter(request,response,chain) + then: "unauthorized" + response.status == HttpServletResponse.SC_UNAUTHORIZED + response.getHeader("WWW-Authenticate") == 'Basic realm="Custom Realm"' + } + + @Configuration + static class CustomHttpBasicConfig extends BaseWebConfig { + protected void configure(HttpSecurity http) { + http + .authorizeUrls() + .anyRequest().hasRole("USER") + .and() + .httpBasic().realmName("Custom Realm"); + } + } + + def "http-basic@authentication-details-source-ref"() { + when: + loadConfig(AuthenticationDetailsSourceHttpBasicConfig) + springSecurityFilterChain = context.getBean(FilterChainProxy) + then: + findFilter(BasicAuthenticationFilter).authenticationDetailsSource.class == CustomAuthenticationDetailsSource + } + + @Configuration + static class AuthenticationDetailsSourceHttpBasicConfig extends BaseWebConfig { + protected void configure(HttpSecurity http) { + http + .httpBasic() + .authenticationDetailsSource(new CustomAuthenticationDetailsSource()) + } + } + + static class CustomAuthenticationDetailsSource extends WebAuthenticationDetailsSource {} + + def "http-basic@entry-point-ref"() { + setup: + loadConfig(EntryPointRefHttpBasicConfig) + springSecurityFilterChain = context.getBean(FilterChainProxy) + when: + springSecurityFilterChain.doFilter(request,response,chain) + then: + response.status == HttpServletResponse.SC_FORBIDDEN + when: "fail to log in" + setup() + login("user","invalid") + springSecurityFilterChain.doFilter(request,response,chain) + then: "custom" + response.status == HttpServletResponse.SC_INTERNAL_SERVER_ERROR + when: "login success" + setup() + login() + then: "sent to default succes page" + !response.committed + } + + @Configuration + static class EntryPointRefHttpBasicConfig extends BaseWebConfig { + protected void configure(HttpSecurity http) { + http + .authorizeUrls() + .anyRequest().hasRole("USER") + .and() + .httpBasic() + .authenticationEntryPoint(new AuthenticationEntryPoint() { + public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) { + response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR) + } + }) + } + } + + def login(String username="user",String password="password") { + def credentials = username + ":" + password + request.addHeader("Authorization", "Basic " + credentials.bytes.encodeBase64()) + } +} diff --git a/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/NamespaceHttpCustomFilterTests.groovy b/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/NamespaceHttpCustomFilterTests.groovy new file mode 100644 index 0000000000..cc477a246a --- /dev/null +++ b/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/NamespaceHttpCustomFilterTests.groovy @@ -0,0 +1,170 @@ +/* + * Copyright 2002-2013 the original author or authors. + * + * 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 org.springframework.security.config.annotation.web.configurers; + +import java.io.IOException; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.security.access.AccessDecisionManager +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.access.ConfigAttribute +import org.springframework.security.authentication.AnonymousAuthenticationToken +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.config.annotation.BaseSpringSpec +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.BaseWebConfig; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.core.authority.AuthorityUtils; +import org.springframework.security.web.AuthenticationEntryPoint +import org.springframework.security.web.FilterInvocation +import org.springframework.security.web.access.AccessDeniedHandler; +import org.springframework.security.web.access.AccessDeniedHandlerImpl; +import org.springframework.security.web.access.ExceptionTranslationFilter +import org.springframework.security.web.access.intercept.FilterSecurityInterceptor +import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter; +import org.springframework.security.web.authentication.AnonymousAuthenticationFilter; +import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; +import org.springframework.security.web.authentication.www.BasicAuthenticationFilter; +import org.springframework.security.web.context.HttpSessionSecurityContextRepository; +import org.springframework.security.web.context.NullSecurityContextRepository; +import org.springframework.security.web.context.SecurityContextPersistenceFilter +import org.springframework.security.web.jaasapi.JaasApiIntegrationFilter; +import org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter; +import org.springframework.security.web.util.AntPathRequestMatcher +import org.springframework.security.web.util.AnyRequestMatcher; +import org.springframework.security.web.util.RequestMatcher + +import spock.lang.Ignore; + +/** + * Tests to verify that all the functionality of attributes is present + * + * @author Rob Winch + * + */ +public class NamespaceHttpCustomFilterTests extends BaseSpringSpec { + def "http/custom-filter@before"() { + when: + loadConfig(CustomFilterBeforeConfig) + then: + filterChain().filters[0].class == CustomFilter + } + + @Configuration + static class CustomFilterBeforeConfig extends BaseWebConfig { + CustomFilterBeforeConfig() { + // do not add the default filters to make testing easier + super(true) + } + + protected void configure(HttpSecurity http) { + http + .addFilterBefore(new CustomFilter(), UsernamePasswordAuthenticationFilter.class) + .formLogin() + } + } + + def "http/custom-filter@after"() { + when: + loadConfig(CustomFilterAfterConfig) + then: + filterChain().filters[1].class == CustomFilter + } + + @Configuration + static class CustomFilterAfterConfig extends BaseWebConfig { + CustomFilterAfterConfig() { + // do not add the default filters to make testing easier + super(true) + } + + protected void configure(HttpSecurity http) { + http + .addFilterAfter(new CustomFilter(), UsernamePasswordAuthenticationFilter.class) + .formLogin() + } + } + + def "http/custom-filter@position"() { + when: + loadConfig(CustomFilterPositionConfig) + then: + filterChain().filters.collect { it.class } == [CustomFilter] + } + + @Configuration + static class CustomFilterPositionConfig extends BaseWebConfig { + CustomFilterPositionConfig() { + // do not add the default filters to make testing easier + super(true) + } + + protected void configure(HttpSecurity http) { + http + // this works so long as the CustomFilter extends one of the standard filters + // if not, use addFilterBefore or addFilterAfter + .addFilter(new CustomFilter()) + } + + } + + def "http/custom-filter no AuthenticationManager in HttpSecurity"() { + when: + loadConfig(NoAuthenticationManagerInHtppConfigurationConfig) + then: + filterChain().filters[0].class == CustomFilter + } + + @Configuration + @EnableWebSecurity + static class NoAuthenticationManagerInHtppConfigurationConfig extends WebSecurityConfigurerAdapter { + NoAuthenticationManagerInHtppConfigurationConfig() { + super(true) + } + + protected AuthenticationManager authenticationManager() + throws Exception { + return new CustomAuthenticationManager(); + } + + @Override + protected void configure(HttpSecurity http) { + http + .authorizeUrls() + .anyRequest().hasRole("USER") + .and() + .addFilterBefore(new CustomFilter(), UsernamePasswordAuthenticationFilter.class) + } + } + + static class CustomFilter extends UsernamePasswordAuthenticationFilter {} + + static class CustomAuthenticationManager implements AuthenticationManager { + public Authentication authenticate(Authentication authentication) + throws AuthenticationException { + return null; + } + } +} diff --git a/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/NamespaceHttpExpressionHandlerTests.groovy b/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/NamespaceHttpExpressionHandlerTests.groovy new file mode 100644 index 0000000000..819d3ef0ae --- /dev/null +++ b/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/NamespaceHttpExpressionHandlerTests.groovy @@ -0,0 +1,57 @@ +/* + * Copyright 2002-2013 the original author or authors. + * + * 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 org.springframework.security.config.annotation.web.configurers + +import org.springframework.context.annotation.Configuration +import org.springframework.expression.spel.standard.SpelExpressionParser +import org.springframework.security.access.expression.SecurityExpressionHandler +import org.springframework.security.config.annotation.BaseSpringSpec +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.BaseWebConfig; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; + +/** + * Tests to verify that all the functionality of attributes is present + * + * @author Rob Winch + * + */ +public class NamespaceHttpExpressionHandlerTests extends BaseSpringSpec { + def "http/expression-handler@ref"() { + when: + def parser = new SpelExpressionParser() + ExpressionHandlerConfig.EXPRESSION_HANDLER = Mock(SecurityExpressionHandler.class) + ExpressionHandlerConfig.EXPRESSION_HANDLER.getExpressionParser() >> parser + loadConfig(ExpressionHandlerConfig) + then: + noExceptionThrown() + } + + @Configuration + @EnableWebSecurity + static class ExpressionHandlerConfig extends BaseWebConfig { + static EXPRESSION_HANDLER; + + protected void configure(HttpSecurity http) { + http + .authorizeUrls() + .expressionHandler(EXPRESSION_HANDLER) + .antMatchers("/users**","/sessions/**").hasRole("ADMIN") + .antMatchers("/signup").permitAll() + .anyRequest().hasRole("USER") + } + } +} diff --git a/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/NamespaceHttpFirewallTests.groovy b/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/NamespaceHttpFirewallTests.groovy new file mode 100644 index 0000000000..9d41c3efb6 --- /dev/null +++ b/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/NamespaceHttpFirewallTests.groovy @@ -0,0 +1,110 @@ +/* + * Copyright 2002-2013 the original author or authors. + * + * 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 org.springframework.security.config.annotation.web.configurers; + +import javax.servlet.http.HttpServletRequest +import javax.servlet.http.HttpServletResponse + +import org.springframework.context.annotation.Configuration +import org.springframework.mock.web.MockFilterChain +import org.springframework.mock.web.MockHttpServletRequest +import org.springframework.mock.web.MockHttpServletResponse +import org.springframework.security.config.annotation.BaseSpringSpec +import org.springframework.security.config.annotation.web.builders.HttpSecurity +import org.springframework.security.config.annotation.web.builders.WebSecurity +import org.springframework.security.config.annotation.web.configuration.BaseWebConfig +import org.springframework.security.web.FilterChainProxy +import org.springframework.security.web.firewall.DefaultHttpFirewall +import org.springframework.security.web.firewall.FirewalledRequest +import org.springframework.security.web.firewall.RequestRejectedException + +/** + * Tests to verify that all the functionality of attributes is present + * + * @author Rob Winch + * + */ +public class NamespaceHttpFirewallTests extends BaseSpringSpec { + FilterChainProxy springSecurityFilterChain + MockHttpServletRequest request + MockHttpServletResponse response + MockFilterChain chain + + def setup() { + request = new MockHttpServletRequest() + response = new MockHttpServletResponse() + chain = new MockFilterChain() + } + + def "http-firewall"() { + setup: + loadConfig(HttpFirewallConfig) + springSecurityFilterChain = context.getBean(FilterChainProxy) + request.setPathInfo("/public/../private/") + when: + springSecurityFilterChain.doFilter(request,response,chain) + then: "the default firewall is used" + thrown(RequestRejectedException) + } + + @Configuration + static class HttpFirewallConfig extends BaseWebConfig { + protected void configure(HttpSecurity http) { + } + } + + def "http-firewall@ref"() { + setup: + loadConfig(CustomHttpFirewallConfig) + springSecurityFilterChain = context.getBean(FilterChainProxy) + request.setParameter("deny", "true") + when: + springSecurityFilterChain.doFilter(request,response,chain) + then: "the custom firewall is used" + thrown(RequestRejectedException) + } + + @Configuration + static class CustomHttpFirewallConfig extends BaseWebConfig { + @Override + protected void configure(HttpSecurity http) { } + + @Override + public void configure(WebSecurity builder) throws Exception { + builder + .httpFirewall(new CustomHttpFirewall()) + } + } + + static class CustomHttpFirewall extends DefaultHttpFirewall { + + @Override + public FirewalledRequest getFirewalledRequest(HttpServletRequest request) + throws RequestRejectedException { + if(request.getParameter("deny")) { + throw new RequestRejectedException("custom rejection") + } + return super.getFirewalledRequest(request) + } + + @Override + public HttpServletResponse getFirewalledResponse( + HttpServletResponse response) { + return super.getFirewalledRequest(response) + } + + } +} diff --git a/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/NamespaceHttpFormLoginTests.groovy b/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/NamespaceHttpFormLoginTests.groovy new file mode 100644 index 0000000000..35ad0324c0 --- /dev/null +++ b/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/NamespaceHttpFormLoginTests.groovy @@ -0,0 +1,170 @@ + + +/* + * Copyright 2002-2013 the original author or authors. + * + * 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 org.springframework.security.config.annotation.web.configurers + +import org.springframework.context.annotation.Configuration +import org.springframework.mock.web.MockFilterChain +import org.springframework.mock.web.MockHttpServletRequest +import org.springframework.mock.web.MockHttpServletResponse +import org.springframework.security.config.annotation.BaseSpringSpec +import org.springframework.security.config.annotation.web.builders.HttpSecurity +import org.springframework.security.config.annotation.web.builders.WebSecurity +import org.springframework.security.config.annotation.web.configuration.BaseWebConfig +import org.springframework.security.web.FilterChainProxy +import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler +import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter +import org.springframework.security.web.authentication.WebAuthenticationDetailsSource + +/** + * Tests to verify that all the functionality of attributes is present + * + * @author Rob Winch + * + */ +public class NamespaceHttpFormLoginTests extends BaseSpringSpec { + FilterChainProxy springSecurityFilterChain + + def "http/form-login"() { + setup: + loadConfig(FormLoginConfig) + springSecurityFilterChain = context.getBean(FilterChainProxy) + when: + springSecurityFilterChain.doFilter(request,response,chain) + then: + response.getRedirectedUrl() == "http://localhost/login" + when: "fail to log in" + super.setup() + request.requestURI = "/login" + request.method = "POST" + springSecurityFilterChain.doFilter(request,response,chain) + then: "sent to login error page" + response.getRedirectedUrl() == "/login?error" + when: "login success" + super.setup() + request.requestURI = "/login" + request.method = "POST" + request.parameters.username = ["user"] as String[] + request.parameters.password = ["password"] as String[] + springSecurityFilterChain.doFilter(request,response,chain) + then: "sent to default succes page" + response.getRedirectedUrl() == "/" + } + + @Configuration + static class FormLoginConfig extends BaseWebConfig { + + @Override + public void configure(WebSecurity web) throws Exception { + web + .ignoring() + .antMatchers("/resources/**"); + } + + @Override + protected void configure(HttpSecurity http) { + http + .authorizeUrls() + .anyRequest().hasRole("USER") + .and() + .formLogin() + } + } + + def "http/form-login custom"() { + setup: + loadConfig(FormLoginCustomConfig) + springSecurityFilterChain = context.getBean(FilterChainProxy) + when: + springSecurityFilterChain.doFilter(request,response,chain) + then: + response.getRedirectedUrl() == "http://localhost/authentication/login" + when: "fail to log in" + super.setup() + request.requestURI = "/authentication/login/process" + request.method = "POST" + springSecurityFilterChain.doFilter(request,response,chain) + then: "sent to login error page" + response.getRedirectedUrl() == "/authentication/login?failed" + when: "login success" + super.setup() + request.requestURI = "/authentication/login/process" + request.method = "POST" + request.parameters.j_username = ["user"] as String[] + request.parameters.j_password = ["password"] as String[] + springSecurityFilterChain.doFilter(request,response,chain) + then: "sent to default succes page" + response.getRedirectedUrl() == "/default" + } + + @Configuration + static class FormLoginCustomConfig extends BaseWebConfig { + protected void configure(HttpSecurity http) throws Exception { + boolean alwaysUseDefaultSuccess = true; + http + .authorizeUrls() + .anyRequest().hasRole("USER") + .and() + .formLogin() + .usernameParameter("j_username") // form-login@username-parameter + .passwordParameter("j_password") // form-login@password-parameter + .loginPage("/authentication/login") // form-login@login-page + .failureUrl("/authentication/login?failed") // form-login@authentication-failure-url + .loginProcessingUrl("/authentication/login/process") // form-login@login-processing-url + .defaultSuccessUrl("/default", alwaysUseDefaultSuccess) // form-login@default-target-url / form-login@always-use-default-target + } + } + + def "http/form-login custom refs"() { + when: + loadConfig(FormLoginCustomRefsConfig) + springSecurityFilterChain = context.getBean(FilterChainProxy) + then: "CustomWebAuthenticationDetailsSource is used" + findFilter(UsernamePasswordAuthenticationFilter).authenticationDetailsSource.class == CustomWebAuthenticationDetailsSource + when: "fail to log in" + request.requestURI = "/login" + request.method = "POST" + springSecurityFilterChain.doFilter(request,response,chain) + then: "sent to login error page" + response.getRedirectedUrl() == "/custom/failure" + when: "login success" + super.setup() + request.requestURI = "/login" + request.method = "POST" + request.parameters.username = ["user"] as String[] + request.parameters.password = ["password"] as String[] + springSecurityFilterChain.doFilter(request,response,chain) + then: "sent to default succes page" + response.getRedirectedUrl() == "/custom/targetUrl" + } + + @Configuration + static class FormLoginCustomRefsConfig extends BaseWebConfig { + protected void configure(HttpSecurity http) throws Exception { + http + .formLogin() + .loginPage("/login") + .failureHandler(new SimpleUrlAuthenticationFailureHandler("/custom/failure")) // form-login@authentication-failure-handler-ref + .successHandler(new SavedRequestAwareAuthenticationSuccessHandler( defaultTargetUrl : "/custom/targetUrl" )) // form-login@authentication-success-handler-ref + .authenticationDetailsSource(new CustomWebAuthenticationDetailsSource()) // form-login@authentication-details-source-ref + .and(); + } + } + + static class CustomWebAuthenticationDetailsSource extends WebAuthenticationDetailsSource {} +} diff --git a/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/NamespaceHttpInterceptUrlTests.groovy b/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/NamespaceHttpInterceptUrlTests.groovy new file mode 100644 index 0000000000..1461eef069 --- /dev/null +++ b/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/NamespaceHttpInterceptUrlTests.groovy @@ -0,0 +1,171 @@ +/* + * Copyright 2002-2013 the original author or authors. + * + * 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 org.springframework.security.config.annotation.web.configurers + +import javax.servlet.http.HttpServletResponse + +import org.springframework.context.annotation.Configuration +import org.springframework.http.HttpMethod; +import org.springframework.mock.web.MockFilterChain +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken +import org.springframework.security.config.annotation.BaseSpringSpec +import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; +import org.springframework.security.core.authority.AuthorityUtils; +import org.springframework.security.core.context.SecurityContextImpl +import org.springframework.security.web.FilterChainProxy +import org.springframework.security.web.context.HttpRequestResponseHolder +import org.springframework.security.web.context.HttpSessionSecurityContextRepository + +/** + * Tests to verify that all the functionality of attributes is present + * + * @author Rob Winch + * + */ +public class NamespaceHttpInterceptUrlTests extends BaseSpringSpec { + + def "http/intercept-url denied when not logged in"() { + setup: + loadConfig(HttpInterceptUrlConfig) + request.servletPath == "/users" + when: + springSecurityFilterChain.doFilter(request,response,chain) + then: + response.status == HttpServletResponse.SC_FORBIDDEN + } + + def "http/intercept-url denied when logged in"() { + setup: + loadConfig(HttpInterceptUrlConfig) + login() + request.setServletPath("/users") + when: + springSecurityFilterChain.doFilter(request,response,chain) + then: + response.status == HttpServletResponse.SC_FORBIDDEN + } + + def "http/intercept-url allowed when logged in"() { + setup: + loadConfig(HttpInterceptUrlConfig) + login("admin","ROLE_ADMIN") + request.setServletPath("/users") + when: + springSecurityFilterChain.doFilter(request,response,chain) + then: + response.status == HttpServletResponse.SC_OK + !response.isCommitted() + } + + def "http/intercept-url@method=POST"() { + setup: + loadConfig(HttpInterceptUrlConfig) + when: + login() + request.setServletPath("/admin/post") + springSecurityFilterChain.doFilter(request,response,chain) + then: + response.status == HttpServletResponse.SC_OK + !response.isCommitted() + when: + super.setup() + login() + request.setServletPath("/admin/post") + request.setMethod("POST") + springSecurityFilterChain.doFilter(request,response,chain) + then: + response.status == HttpServletResponse.SC_FORBIDDEN + when: + super.setup() + login("admin","ROLE_ADMIN") + request.setServletPath("/admin/post") + request.setMethod("POST") + springSecurityFilterChain.doFilter(request,response,chain) + then: + response.status == HttpServletResponse.SC_OK + !response.committed + } + + def "http/intercept-url@requires-channel"() { + setup: + loadConfig(HttpInterceptUrlConfig) + when: + request.setServletPath("/login") + request.setRequestURI("/login") + springSecurityFilterChain.doFilter(request,response,chain) + then: + response.redirectedUrl == "https://localhost/login" + when: + super.setup() + request.setServletPath("/secured/a") + request.setRequestURI("/secured/a") + springSecurityFilterChain.doFilter(request,response,chain) + then: + response.redirectedUrl == "https://localhost/secured/a" + when: + super.setup() + request.setSecure(true) + request.setScheme("https") + request.setServletPath("/user") + request.setRequestURI("/user") + springSecurityFilterChain.doFilter(request,response,chain) + then: + response.redirectedUrl == "http://localhost/user" + } + + @Configuration + @EnableWebSecurity + static class HttpInterceptUrlConfig extends WebSecurityConfigurerAdapter { + + @Override + protected void configure(HttpSecurity http) throws Exception { + http + .authorizeUrls() + // the line below is similar to intercept-url@pattern: + // + // + .antMatchers("/users**","/sessions/**").hasRole("ADMIN") + // the line below is similar to intercept-url@method: + // + // + .antMatchers(HttpMethod.POST, "/admin/post","/admin/another-post/**").hasRole("ADMIN") + .antMatchers("/signup").permitAll() + .anyRequest().hasRole("USER") + .and() + .requiresChannel() + // NOTE: channel security is configured separately of authorization (i.e. intercept-url@access + // the line below is similar to intercept-url@requires-channel="https": + // + // + .antMatchers("/login","/secured/**").requiresSecure() + // the line below is similar to intercept-url@requires-channel="http": + // + .anyRequest().requiresInsecure() + } + protected void registerAuthentication( + AuthenticationManagerBuilder auth) throws Exception { + auth + .inMemoryAuthentication() + .withUser("user").password("password").roles("USER").and() + .withUser("admin").password("password").roles("USER", "ADMIN") + } + } +} diff --git a/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/NamespaceHttpJeeTests.groovy b/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/NamespaceHttpJeeTests.groovy new file mode 100644 index 0000000000..adad677ef4 --- /dev/null +++ b/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/NamespaceHttpJeeTests.groovy @@ -0,0 +1,145 @@ +/* + * Copyright 2002-2013 the original author or authors. + * + * 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 org.springframework.security.config.annotation.web.configurers; + +import java.io.IOException; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.mock.web.MockFilterChain +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse +import org.springframework.security.access.AccessDecisionManager +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.access.ConfigAttribute +import org.springframework.security.authentication.AnonymousAuthenticationToken +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.config.annotation.BaseSpringSpec +import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.core.authority.AuthorityUtils; +import org.springframework.security.core.userdetails.AuthenticationUserDetailsService; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.security.crypto.codec.Base64; +import org.springframework.security.web.AuthenticationEntryPoint +import org.springframework.security.web.FilterChainProxy +import org.springframework.security.web.FilterInvocation +import org.springframework.security.web.access.AccessDeniedHandler; +import org.springframework.security.web.access.AccessDeniedHandlerImpl; +import org.springframework.security.web.access.ExceptionTranslationFilter +import org.springframework.security.web.access.intercept.FilterSecurityInterceptor +import org.springframework.security.web.authentication.AnonymousAuthenticationFilter; +import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint; +import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler +import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; +import org.springframework.security.web.authentication.WebAuthenticationDetailsSource +import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationProvider; +import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken; +import org.springframework.security.web.authentication.preauth.PreAuthenticatedGrantedAuthoritiesUserDetailsService; +import org.springframework.security.web.authentication.preauth.j2ee.J2eeBasedPreAuthenticatedWebAuthenticationDetailsSource; +import org.springframework.security.web.authentication.preauth.j2ee.J2eePreAuthenticatedProcessingFilter; +import org.springframework.security.web.authentication.www.BasicAuthenticationFilter; +import org.springframework.security.web.context.HttpSessionSecurityContextRepository; +import org.springframework.security.web.context.NullSecurityContextRepository; +import org.springframework.security.web.context.SecurityContextPersistenceFilter +import org.springframework.security.web.jaasapi.JaasApiIntegrationFilter; +import org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter; +import org.springframework.security.web.util.AntPathRequestMatcher +import org.springframework.security.web.util.AnyRequestMatcher; +import org.springframework.security.web.util.RequestMatcher +import org.springframework.test.util.ReflectionTestUtils; + +/** + * Tests to verify that all the functionality of attributes is present + * + * @author Rob Winch + * + */ +public class NamespaceHttpJeeTests extends BaseSpringSpec { + + def "http/jee@mappable-roles"() { + when: + loadConfig(JeeMappableRolesConfig) + J2eePreAuthenticatedProcessingFilter filter = findFilter(J2eePreAuthenticatedProcessingFilter) + AuthenticationManager authenticationManager = ReflectionTestUtils.getField(filter,"authenticationManager") + then: + authenticationManager + filter.authenticationDetailsSource.class == J2eeBasedPreAuthenticatedWebAuthenticationDetailsSource + filter.authenticationDetailsSource.j2eeMappableRoles == ["ROLE_USER", "ROLE_ADMIN"] as Set + authenticationManager.providers.find { it instanceof PreAuthenticatedAuthenticationProvider }.preAuthenticatedUserDetailsService.class == PreAuthenticatedGrantedAuthoritiesUserDetailsService + } + + @Configuration + @EnableWebSecurity + public static class JeeMappableRolesConfig extends WebSecurityConfigurerAdapter { + + @Override + protected void configure(HttpSecurity http) throws Exception { + http + .authorizeUrls() + .anyRequest().hasRole("USER") + .and() + .jee() + .mappableRoles("USER","ADMIN"); + } + } + + def "http/jee@user-service-ref"() { + when: + loadConfig(JeeUserServiceRefConfig) + J2eePreAuthenticatedProcessingFilter filter = findFilter(J2eePreAuthenticatedProcessingFilter) + AuthenticationManager authenticationManager = ReflectionTestUtils.getField(filter,"authenticationManager") + then: + authenticationManager + filter.authenticationDetailsSource.class == J2eeBasedPreAuthenticatedWebAuthenticationDetailsSource + filter.authenticationDetailsSource.j2eeMappableRoles == ["ROLE_USER", "ROLE_ADMIN"] as Set + authenticationManager.providers.find { it instanceof PreAuthenticatedAuthenticationProvider }.preAuthenticatedUserDetailsService.class == CustomUserService + } + + @Configuration + @EnableWebSecurity + public static class JeeUserServiceRefConfig extends WebSecurityConfigurerAdapter { + + @Override + protected void configure(HttpSecurity http) throws Exception { + http + .authorizeUrls() + .anyRequest().hasRole("USER") + .and() + .jee() + .mappableAuthorities("ROLE_USER","ROLE_ADMIN") + .authenticatedUserDetailsService(new CustomUserService()); + } + } + + static class CustomUserService implements AuthenticationUserDetailsService { + public UserDetails loadUserDetails( + PreAuthenticatedAuthenticationToken token) + throws UsernameNotFoundException { + return null; + } + } +} diff --git a/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/NamespaceHttpLogoutTests.groovy b/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/NamespaceHttpLogoutTests.groovy new file mode 100644 index 0000000000..42525a2900 --- /dev/null +++ b/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/NamespaceHttpLogoutTests.groovy @@ -0,0 +1,171 @@ +/* + * Copyright 2002-2013 the original author or authors. + * + * 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 org.springframework.security.config.annotation.web.configurers; + +import java.io.IOException; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.mock.web.MockFilterChain +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse +import org.springframework.security.access.AccessDecisionManager +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.access.ConfigAttribute +import org.springframework.security.authentication.AnonymousAuthenticationToken +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken +import org.springframework.security.config.annotation.BaseSpringSpec +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.BaseWebConfig; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.core.authority.AuthorityUtils; +import org.springframework.security.core.context.SecurityContextImpl +import org.springframework.security.crypto.codec.Base64; +import org.springframework.security.web.AuthenticationEntryPoint +import org.springframework.security.web.FilterChainProxy +import org.springframework.security.web.FilterInvocation +import org.springframework.security.web.access.AccessDeniedHandler; +import org.springframework.security.web.access.AccessDeniedHandlerImpl; +import org.springframework.security.web.access.ExceptionTranslationFilter +import org.springframework.security.web.access.intercept.FilterSecurityInterceptor +import org.springframework.security.web.authentication.AnonymousAuthenticationFilter; +import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint; +import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler +import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; +import org.springframework.security.web.authentication.WebAuthenticationDetailsSource +import org.springframework.security.web.authentication.logout.SimpleUrlLogoutSuccessHandler; +import org.springframework.security.web.authentication.www.BasicAuthenticationFilter; +import org.springframework.security.web.context.HttpRequestResponseHolder +import org.springframework.security.web.context.HttpSessionSecurityContextRepository; +import org.springframework.security.web.context.NullSecurityContextRepository; +import org.springframework.security.web.context.SecurityContextPersistenceFilter +import org.springframework.security.web.jaasapi.JaasApiIntegrationFilter; +import org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter; +import org.springframework.security.web.util.AntPathRequestMatcher +import org.springframework.security.web.util.AnyRequestMatcher; +import org.springframework.security.web.util.RequestMatcher + +/** + * Tests to verify that all the functionality of attributes is present + * + * @author Rob Winch + * + */ +public class NamespaceHttpLogoutTests extends BaseSpringSpec { + FilterChainProxy springSecurityFilterChain + MockHttpServletRequest request + MockHttpServletResponse response + MockFilterChain chain + + def setup() { + request = new MockHttpServletRequest() + response = new MockHttpServletResponse() + chain = new MockFilterChain() + } + + def "http/logout"() { + setup: + loadConfig(HttpLogoutConfig) + springSecurityFilterChain = context.getBean(FilterChainProxy) + login() + request.setRequestURI("/logout") + when: + springSecurityFilterChain.doFilter(request,response,chain) + then: + !authenticated() + !request.getSession(false) + response.redirectedUrl == "/login?logout" + !response.getCookies() + } + + @Configuration + static class HttpLogoutConfig extends BaseWebConfig { + protected void configure(HttpSecurity http) throws Exception { + } + } + + def "http/logout custom"() { + setup: + loadConfig(CustomHttpLogoutConfig) + springSecurityFilterChain = context.getBean(FilterChainProxy) + login() + request.setRequestURI("/custom-logout") + when: + springSecurityFilterChain.doFilter(request,response,chain) + then: + !authenticated() + request.getSession(false) + response.redirectedUrl == "/logout-success" + response.getCookies().length == 1 + response.getCookies()[0].name == "remove" + response.getCookies()[0].maxAge == 0 + } + + @Configuration + static class CustomHttpLogoutConfig extends BaseWebConfig { + protected void configure(HttpSecurity http) throws Exception { + http + .logout() + .deleteCookies("remove") // logout@delete-cookies + .invalidateHttpSession(false) // logout@invalidate-session=false (default is true) + .logoutUrl("/custom-logout") // logout@logout-url (default is /logout) + .logoutSuccessUrl("/logout-success") // logout@success-url (default is /login?logout) + } + } + + def "http/logout@success-handler-ref"() { + setup: + loadConfig(SuccessHandlerRefHttpLogoutConfig) + springSecurityFilterChain = context.getBean(FilterChainProxy) + login() + request.setRequestURI("/logout") + when: + springSecurityFilterChain.doFilter(request,response,chain) + then: + !authenticated() + !request.getSession(false) + response.redirectedUrl == "/SuccessHandlerRefHttpLogoutConfig" + !response.getCookies() + } + + @Configuration + static class SuccessHandlerRefHttpLogoutConfig extends BaseWebConfig { + protected void configure(HttpSecurity http) throws Exception { + SimpleUrlLogoutSuccessHandler logoutSuccessHandler = new SimpleUrlLogoutSuccessHandler(defaultTargetUrl:"/SuccessHandlerRefHttpLogoutConfig") + http + .logout() + .logoutSuccessHandler(logoutSuccessHandler) + } + } + + def login(String username="user", String role="ROLE_USER") { + HttpSessionSecurityContextRepository repo = new HttpSessionSecurityContextRepository() + HttpRequestResponseHolder requestResponseHolder = new HttpRequestResponseHolder(request, response) + repo.loadContext(requestResponseHolder) + repo.saveContext(new SecurityContextImpl(authentication: new UsernamePasswordAuthenticationToken(username, null, AuthorityUtils.createAuthorityList(role))), requestResponseHolder.request, requestResponseHolder.response) + } + + def authenticated() { + HttpRequestResponseHolder requestResponseHolder = new HttpRequestResponseHolder(request, response) + new HttpSessionSecurityContextRepository().loadContext(requestResponseHolder)?.authentication?.authenticated + } +} diff --git a/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/NamespaceHttpOpenIDLoginTests.groovy b/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/NamespaceHttpOpenIDLoginTests.groovy new file mode 100644 index 0000000000..cdf4ef5cf8 --- /dev/null +++ b/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/NamespaceHttpOpenIDLoginTests.groovy @@ -0,0 +1,243 @@ + + +/* + * Copyright 2002-2013 the original author or authors. + * + * 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 org.springframework.security.config.annotation.web.configurers + +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.mock.web.MockFilterChain +import org.springframework.mock.web.MockHttpServletRequest +import org.springframework.mock.web.MockHttpServletResponse +import org.springframework.security.authentication.AuthenticationManager +import org.springframework.security.config.annotation.BaseSpringSpec +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.BaseWebConfig; +import org.springframework.security.core.userdetails.AuthenticationUserDetailsService +import org.springframework.security.core.userdetails.UserDetailsByNameServiceWrapper; +import org.springframework.security.openid.OpenID4JavaConsumer +import org.springframework.security.openid.OpenIDAuthenticationFilter +import org.springframework.security.openid.OpenIDAuthenticationProvider +import org.springframework.security.openid.OpenIDAuthenticationToken; +import org.springframework.security.web.FilterChainProxy +import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler +import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler +import org.springframework.security.web.authentication.WebAuthenticationDetailsSource + +/** + * Tests to verify that all the functionality of attributes is present + * + * @author Rob Winch + * + */ +public class NamespaceHttpOpenIDLoginTests extends BaseSpringSpec { + FilterChainProxy springSecurityFilterChain + MockHttpServletRequest request + MockHttpServletResponse response + MockFilterChain chain + + def setup() { + request = new MockHttpServletRequest() + response = new MockHttpServletResponse() + chain = new MockFilterChain() + } + + def "http/openid-login"() { + when: + loadConfig(OpenIDLoginConfig) + springSecurityFilterChain = context.getBean(FilterChainProxy) + then: + findFilter(OpenIDAuthenticationFilter).consumer.class == OpenID4JavaConsumer + when: + springSecurityFilterChain.doFilter(request,response,chain) + then: + response.getRedirectedUrl() == "http://localhost/login" + when: "fail to log in" + setup() + request.requestURI = "/login/openid" + request.method = "POST" + springSecurityFilterChain.doFilter(request,response,chain) + then: "sent to login error page" + response.getRedirectedUrl() == "/login?error" + } + + @Configuration + static class OpenIDLoginConfig extends BaseWebConfig { + protected void configure(HttpSecurity http) { + http + .authorizeUrls() + .anyRequest().hasRole("USER") + .and() + .openidLogin() + .permitAll(); + } + } + + def "http/openid-login/attribute-exchange"() { + when: + loadConfig(OpenIDLoginAttributeExchangeConfig) + springSecurityFilterChain = context.getBean(FilterChainProxy) + OpenID4JavaConsumer consumer = findFilter(OpenIDAuthenticationFilter).consumer + then: + consumer.class == OpenID4JavaConsumer + + def googleAttrs = consumer.attributesToFetchFactory.createAttributeList("https://www.google.com/1") + googleAttrs[0].name == "email" + googleAttrs[0].type == "http://axschema.org/contact/email" + googleAttrs[0].required + googleAttrs[1].name == "firstname" + googleAttrs[1].type == "http://axschema.org/namePerson/first" + googleAttrs[1].required + googleAttrs[2].name == "lastname" + googleAttrs[2].type == "http://axschema.org/namePerson/last" + googleAttrs[2].required + + def yahooAttrs = consumer.attributesToFetchFactory.createAttributeList("https://rwinch.yahoo.com/rwinch/id") + yahooAttrs[0].name == "email" + yahooAttrs[0].type == "http://schema.openid.net/contact/email" + yahooAttrs[0].required + yahooAttrs[1].name == "fullname" + yahooAttrs[1].type == "http://axschema.org/namePerson" + yahooAttrs[1].required + when: + springSecurityFilterChain.doFilter(request,response,chain) + then: + response.getRedirectedUrl() == "http://localhost/login" + when: "fail to log in" + setup() + request.requestURI = "/login/openid" + request.method = "POST" + springSecurityFilterChain.doFilter(request,response,chain) + then: "sent to login error page" + response.getRedirectedUrl() == "/login?error" + } + + @Configuration + static class OpenIDLoginAttributeExchangeConfig extends BaseWebConfig { + protected void configure(HttpSecurity http) { + http + .authorizeUrls() + .anyRequest().hasRole("USER") + .and() + .openidLogin() + .attributeExchange("https://www.google.com/.*") // attribute-exchange@identifier-match + .attribute("email") // openid-attribute@name + .type("http://axschema.org/contact/email") // openid-attribute@type + .required(true) // openid-attribute@required + .count(1) // openid-attribute@count + .and() + .attribute("firstname") + .type("http://axschema.org/namePerson/first") + .required(true) + .and() + .attribute("lastname") + .type("http://axschema.org/namePerson/last") + .required(true) + .and() + .and() + .attributeExchange(".*yahoo.com.*") + .attribute("email") + .type("http://schema.openid.net/contact/email") + .required(true) + .and() + .attribute("fullname") + .type("http://axschema.org/namePerson") + .required(true) + .and() + .and() + .permitAll(); + } + } + + def "http/openid-login custom"() { + setup: + loadConfig(OpenIDLoginCustomConfig) + springSecurityFilterChain = context.getBean(FilterChainProxy) + when: + springSecurityFilterChain.doFilter(request,response,chain) + then: + response.getRedirectedUrl() == "http://localhost/authentication/login" + when: "fail to log in" + setup() + request.requestURI = "/authentication/login/process" + request.method = "POST" + springSecurityFilterChain.doFilter(request,response,chain) + then: "sent to login error page" + response.getRedirectedUrl() == "/authentication/login?failed" + } + + @Configuration + static class OpenIDLoginCustomConfig extends BaseWebConfig { + protected void configure(HttpSecurity http) throws Exception { + boolean alwaysUseDefaultSuccess = true; + http + .authorizeUrls() + .anyRequest().hasRole("USER") + .and() + .openidLogin() + .permitAll() + .loginPage("/authentication/login") // openid-login@login-page + .failureUrl("/authentication/login?failed") // openid-login@authentication-failure-url + .loginProcessingUrl("/authentication/login/process") // openid-login@login-processing-url + .defaultSuccessUrl("/default", alwaysUseDefaultSuccess) // openid-login@default-target-url / openid-login@always-use-default-target + } + } + + def "http/openid-login custom refs"() { + when: + OpenIDLoginCustomRefsConfig.AUDS = Mock(AuthenticationUserDetailsService) + loadConfig(OpenIDLoginCustomRefsConfig) + springSecurityFilterChain = context.getBean(FilterChainProxy) + then: "CustomWebAuthenticationDetailsSource is used" + findFilter(OpenIDAuthenticationFilter).authenticationDetailsSource.class == CustomWebAuthenticationDetailsSource + findAuthenticationProvider(OpenIDAuthenticationProvider).userDetailsService == OpenIDLoginCustomRefsConfig.AUDS + when: "fail to log in" + request.requestURI = "/login/openid" + request.method = "POST" + springSecurityFilterChain.doFilter(request,response,chain) + then: "sent to login error page" + response.getRedirectedUrl() == "/custom/failure" + } + + @Configuration + static class OpenIDLoginCustomRefsConfig extends BaseWebConfig { + static AuthenticationUserDetailsService AUDS + + protected void configure(HttpSecurity http) throws Exception { + http + .authorizeUrls() + .anyRequest().hasRole("USER") + .and() + .openidLogin() + // if using UserDetailsService wrap with new UserDetailsByNameServiceWrapper() + .authenticationUserDetailsService(AUDS) // openid-login@user-service-ref + .failureHandler(new SimpleUrlAuthenticationFailureHandler("/custom/failure")) // openid-login@authentication-failure-handler-ref + .successHandler(new SavedRequestAwareAuthenticationSuccessHandler( defaultTargetUrl : "/custom/targetUrl" )) // openid-login@authentication-success-handler-ref + .authenticationDetailsSource(new CustomWebAuthenticationDetailsSource()); // openid-login@authentication-details-source-ref + } + + // only necessary to have easy access to the AuthenticationManager for testing/verification + @Bean + @Override + public AuthenticationManager authenticationManagerBean() + throws Exception { + return super.authenticationManagerBean(); + } + + } + + static class CustomWebAuthenticationDetailsSource extends WebAuthenticationDetailsSource {} +} diff --git a/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/NamespaceHttpPortMappingsTests.groovy b/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/NamespaceHttpPortMappingsTests.groovy new file mode 100644 index 0000000000..ca8b57536e --- /dev/null +++ b/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/NamespaceHttpPortMappingsTests.groovy @@ -0,0 +1,117 @@ +/* + * Copyright 2002-2013 the original author or authors. + * + * 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 org.springframework.security.config.annotation.web.configurers + +import org.springframework.context.annotation.Configuration +import org.springframework.mock.web.MockFilterChain +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken +import org.springframework.security.config.annotation.BaseSpringSpec +import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; +import org.springframework.security.core.authority.AuthorityUtils; +import org.springframework.security.core.context.SecurityContextImpl +import org.springframework.security.web.FilterChainProxy +import org.springframework.security.web.context.HttpRequestResponseHolder +import org.springframework.security.web.context.HttpSessionSecurityContextRepository + +/** + * Tests to verify that all the functionality of attributes is present + * + * @author Rob Winch + * + */ +public class NamespaceHttpPortMappingsTests extends BaseSpringSpec { + FilterChainProxy springSecurityFilterChain + MockHttpServletRequest request + MockHttpServletResponse response + MockFilterChain chain + + def setup() { + request = new MockHttpServletRequest() + request.setMethod("GET") + response = new MockHttpServletResponse() + chain = new MockFilterChain() + } + + def "http/port-mapper works with http/intercept-url@requires-channel"() { + setup: + loadConfig(HttpInterceptUrlWithPortMapperConfig) + springSecurityFilterChain = context.getBean(FilterChainProxy) + when: + request.setServletPath("/login") + request.setRequestURI("/login") + request.setServerPort(9080); + springSecurityFilterChain.doFilter(request,response,chain) + then: + response.redirectedUrl == "https://localhost:9443/login" + when: + setup() + request.setServletPath("/secured/a") + request.setRequestURI("/secured/a") + request.setServerPort(9080); + springSecurityFilterChain.doFilter(request,response,chain) + then: + response.redirectedUrl == "https://localhost:9443/secured/a" + when: + setup() + request.setSecure(true) + request.setScheme("https") + request.setServerPort(9443); + request.setServletPath("/user") + request.setRequestURI("/user") + springSecurityFilterChain.doFilter(request,response,chain) + then: + response.redirectedUrl == "http://localhost:9080/user" + } + + @Configuration + @EnableWebSecurity + static class HttpInterceptUrlWithPortMapperConfig extends WebSecurityConfigurerAdapter { + + @Override + protected void configure(HttpSecurity http) throws Exception { + http + .authorizeUrls() + .anyRequest().hasRole("USER") + .and() + .portMapper() + .http(9080).mapsTo(9443) + .and() + .requiresChannel() + .antMatchers("/login","/secured/**").requiresSecure() + .anyRequest().requiresInsecure() + } + + protected void registerAuthentication( + AuthenticationManagerBuilder auth) throws Exception { + auth + .inMemoryAuthentication() + .withUser("user").password("password").roles("USER").and() + .withUser("admin").password("password").roles("USER", "ADMIN") + } + } + + def login(String username="user", String role="ROLE_USER") { + HttpSessionSecurityContextRepository repo = new HttpSessionSecurityContextRepository() + HttpRequestResponseHolder requestResponseHolder = new HttpRequestResponseHolder(request, response) + repo.loadContext(requestResponseHolder) + repo.saveContext(new SecurityContextImpl(authentication: new UsernamePasswordAuthenticationToken(username, null, AuthorityUtils.createAuthorityList(role))), requestResponseHolder.request, requestResponseHolder.response) + } +} diff --git a/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/NamespaceHttpRequestCacheTests.groovy b/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/NamespaceHttpRequestCacheTests.groovy new file mode 100644 index 0000000000..a71a61f081 --- /dev/null +++ b/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/NamespaceHttpRequestCacheTests.groovy @@ -0,0 +1,73 @@ +/* + * Copyright 2002-2013 the original author or authors. + * + * 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 org.springframework.security.config.annotation.web.configurers; + +import java.io.IOException; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.config.annotation.BaseSpringSpec; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.BaseWebConfig; +import org.springframework.security.web.access.AccessDeniedHandler; +import org.springframework.security.web.access.ExceptionTranslationFilter; +import org.springframework.security.web.savedrequest.HttpSessionRequestCache; +import org.springframework.security.web.savedrequest.RequestCache; + +/** + * Tests to verify that all the functionality of attributes is present + * + * @author Rob Winch + * + */ +public class NamespaceHttpRequestCacheTests extends BaseSpringSpec { + def "http/request-cache@ref"() { + setup: + RequestCacheRefConfig.REQUEST_CACHE = Mock(RequestCache) + when: + loadConfig(RequestCacheRefConfig) + then: + findFilter(ExceptionTranslationFilter).requestCache == RequestCacheRefConfig.REQUEST_CACHE + } + + @Configuration + static class RequestCacheRefConfig extends BaseWebConfig { + static RequestCache REQUEST_CACHE + protected void configure(HttpSecurity http) { + http. + requestCache() + .requestCache(REQUEST_CACHE) + } + } + + def "http/request-cache@ref defaults to HttpSessionRequestCache"() { + when: + loadConfig(DefaultRequestCacheRefConfig) + then: + findFilter(ExceptionTranslationFilter).requestCache.class == HttpSessionRequestCache + } + + @Configuration + static class DefaultRequestCacheRefConfig extends BaseWebConfig { + protected void configure(HttpSecurity http) { + } + } +} diff --git a/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/NamespaceHttpX509Tests.groovy b/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/NamespaceHttpX509Tests.groovy new file mode 100644 index 0000000000..8ae6d445aa --- /dev/null +++ b/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/NamespaceHttpX509Tests.groovy @@ -0,0 +1,275 @@ +/* + * Copyright 2002-2013 the original author or authors. + * + * 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 org.springframework.security.config.annotation.web.configurers; + +import java.security.cert.CertificateFactory +import java.security.cert.X509Certificate + +import javax.servlet.http.HttpServletRequest + +import org.springframework.context.annotation.Configuration +import org.springframework.mock.web.MockFilterChain +import org.springframework.mock.web.MockHttpServletRequest +import org.springframework.mock.web.MockHttpServletResponse +import org.springframework.security.authentication.AuthenticationDetailsSource +import org.springframework.security.authentication.AuthenticationManager +import org.springframework.security.config.annotation.BaseSpringSpec +import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; +import org.springframework.security.core.authority.AuthorityUtils +import org.springframework.security.core.userdetails.AuthenticationUserDetailsService +import org.springframework.security.core.userdetails.User +import org.springframework.security.core.userdetails.UserDetails +import org.springframework.security.core.userdetails.UserDetailsByNameServiceWrapper +import org.springframework.security.core.userdetails.UserDetailsService +import org.springframework.security.core.userdetails.UsernameNotFoundException +import org.springframework.security.web.FilterChainProxy +import org.springframework.security.web.authentication.WebAuthenticationDetailsSource +import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationProvider +import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken +import org.springframework.security.web.authentication.preauth.PreAuthenticatedGrantedAuthoritiesWebAuthenticationDetails +import org.springframework.security.web.authentication.preauth.x509.X509AuthenticationFilter +import org.springframework.security.web.context.HttpRequestResponseHolder +import org.springframework.security.web.context.HttpSessionSecurityContextRepository +import org.springframework.test.util.ReflectionTestUtils + +/** + * Tests to verify that all the functionality of attributes is present + * + * @author Rob Winch + * + */ +public class NamespaceHttpX509Tests extends BaseSpringSpec { + FilterChainProxy springSecurityFilterChain + MockHttpServletRequest request + MockHttpServletResponse response + MockFilterChain chain + + def setup() { + request = new MockHttpServletRequest() + response = new MockHttpServletResponse() + chain = new MockFilterChain() + } + + def "http/x509 can authenticate"() { + setup: + X509Certificate certificate = loadCert("rod.cer") + loadConfig(X509Config) + springSecurityFilterChain = context.getBean(FilterChainProxy) + when: + request.setAttribute("javax.servlet.request.X509Certificate", [certificate] as X509Certificate[] ) + springSecurityFilterChain.doFilter(request, response, chain); + then: + response.status == 200 + authentication().name == 'rod' + } + + def "http/x509"() { + when: + loadConfig(X509Config) + X509AuthenticationFilter filter = findFilter(X509AuthenticationFilter) + AuthenticationManager authenticationManager = ReflectionTestUtils.getField(filter,"authenticationManager") + then: + authenticationManager + filter.authenticationDetailsSource.class == WebAuthenticationDetailsSource + authenticationManager.providers.find { it instanceof PreAuthenticatedAuthenticationProvider }.preAuthenticatedUserDetailsService.class == UserDetailsByNameServiceWrapper + } + + @Configuration + @EnableWebSecurity + public static class X509Config extends WebSecurityConfigurerAdapter { + @Override + protected void registerAuthentication(AuthenticationManagerBuilder auth) throws Exception { + auth. + inMemoryAuthentication() + .withUser("rod").password("password").roles("USER","ADMIN"); + } + + @Override + protected void configure(HttpSecurity http) throws Exception { + http + .authorizeUrls() + .anyRequest().hasRole("USER") + .and() + .x509(); + } + } + + def "http/x509@authentication-details-source-ref"() { + setup: + AuthenticationDetailsSourceRefConfig.AUTHENTICATION_DETAILS_SOURCE = Mock(AuthenticationDetailsSource) + when: + loadConfig(AuthenticationDetailsSourceRefConfig) + X509AuthenticationFilter filter = findFilter(X509AuthenticationFilter) + AuthenticationManager authenticationManager = ReflectionTestUtils.getField(filter,"authenticationManager") + then: + authenticationManager + filter.authenticationDetailsSource == AuthenticationDetailsSourceRefConfig.AUTHENTICATION_DETAILS_SOURCE + authenticationManager.providers.find { it instanceof PreAuthenticatedAuthenticationProvider }.preAuthenticatedUserDetailsService.class == UserDetailsByNameServiceWrapper + } + + @Configuration + @EnableWebSecurity + public static class AuthenticationDetailsSourceRefConfig extends WebSecurityConfigurerAdapter { + static AuthenticationDetailsSource AUTHENTICATION_DETAILS_SOURCE + + protected void registerAuthentication(AuthenticationManagerBuilder auth) throws Exception { + auth. + inMemoryAuthentication() + .withUser("rod").password("password").roles("USER","ADMIN"); + } + + protected void configure(HttpSecurity http) throws Exception { + http + .authorizeUrls() + .anyRequest().hasRole("USER") + .and() + .x509() + .authenticationDetailsSource(AUTHENTICATION_DETAILS_SOURCE); + } + } + + def "http/x509@subject-principal-regex"() { + setup: + X509Certificate certificate = loadCert("rodatexampledotcom.cer") + loadConfig(SubjectPrincipalRegexConfig) + springSecurityFilterChain = context.getBean(FilterChainProxy) + when: + request.setAttribute("javax.servlet.request.X509Certificate", [certificate] as X509Certificate[] ) + springSecurityFilterChain.doFilter(request, response, chain); + then: + response.status == 200 + authentication().name == 'rod' + } + + @Configuration + @EnableWebSecurity + public static class SubjectPrincipalRegexConfig extends WebSecurityConfigurerAdapter { + @Override + protected void registerAuthentication(AuthenticationManagerBuilder auth) throws Exception { + auth. + inMemoryAuthentication() + .withUser("rod").password("password").roles("USER","ADMIN"); + } + + @Override + protected void configure(HttpSecurity http) throws Exception { + http + .authorizeUrls() + .anyRequest().hasRole("USER") + .and() + .x509() + .subjectPrincipalRegex('CN=(.*?)@example.com(?:,|$)'); + } + } + + def "http/x509@user-service-ref"() { + setup: + X509Certificate certificate = loadCert("rodatexampledotcom.cer") + loadConfig(UserDetailsServiceRefConfig) + springSecurityFilterChain = context.getBean(FilterChainProxy) + when: + request.setAttribute("javax.servlet.request.X509Certificate", [certificate] as X509Certificate[] ) + springSecurityFilterChain.doFilter(request, response, chain); + then: + response.status == 200 + authentication().name == 'customuser' + } + + @Configuration + @EnableWebSecurity + public static class UserDetailsServiceRefConfig extends WebSecurityConfigurerAdapter { + @Override + protected void registerAuthentication(AuthenticationManagerBuilder auth) throws Exception { + auth. + inMemoryAuthentication() + .withUser("rod").password("password").roles("USER","ADMIN"); + } + + @Override + protected void configure(HttpSecurity http) throws Exception { + http + .authorizeUrls() + .anyRequest().hasRole("USER") + .and() + .x509() + .userDetailsService(new CustomUserDetailsService()); + } + } + + def "http/x509 custom AuthenticationUserDetailsService"() { + setup: + X509Certificate certificate = loadCert("rodatexampledotcom.cer") + loadConfig(AuthenticationUserDetailsServiceConfig) + springSecurityFilterChain = context.getBean(FilterChainProxy) + when: + request.setAttribute("javax.servlet.request.X509Certificate", [certificate] as X509Certificate[] ) + springSecurityFilterChain.doFilter(request, response, chain); + then: + response.status == 200 + authentication().name == 'customuser' + } + + @Configuration + @EnableWebSecurity + public static class AuthenticationUserDetailsServiceConfig extends WebSecurityConfigurerAdapter { + static AuthenticationDetailsSource AUTHENTICATION_DETAILS_SOURCE + + protected void registerAuthentication(AuthenticationManagerBuilder auth) throws Exception { + auth. + inMemoryAuthentication() + .withUser("rod").password("password").roles("USER","ADMIN"); + } + + protected void configure(HttpSecurity http) throws Exception { + http + .authorizeUrls() + .anyRequest().hasRole("USER") + .and() + .x509() + .userDetailsService(new CustomUserDetailsService()); + } + } + + def loadCert(String location) { + CertificateFactory certFactory = CertificateFactory.getInstance("X.509"); + certFactory.generateCertificate(Thread.currentThread().contextClassLoader.getResourceAsStream(location)) + } + + static class CustomUserDetailsService implements UserDetailsService { + + public UserDetails loadUserByUsername(String username) + throws UsernameNotFoundException { + return new User("customuser", "password", AuthorityUtils.createAuthorityList("ROLE_USER")); + } + + } + + static class CustomAuthenticationUserDetailsService implements AuthenticationUserDetailsService { + public UserDetails loadUserDetails( + PreAuthenticatedAuthenticationToken token) + throws UsernameNotFoundException { + return new User("customuser", "password", AuthorityUtils.createAuthorityList("ROLE_USER")); + } + } + + def authentication() { + HttpRequestResponseHolder requestResponseHolder = new HttpRequestResponseHolder(request, response) + new HttpSessionSecurityContextRepository().loadContext(requestResponseHolder)?.authentication + } +} diff --git a/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/NamespaceRememberMeTests.groovy b/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/NamespaceRememberMeTests.groovy new file mode 100644 index 0000000000..e9bdac60a0 --- /dev/null +++ b/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/NamespaceRememberMeTests.groovy @@ -0,0 +1,345 @@ +/* + * Copyright 2002-2013 the original author or authors. + * + * 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 org.springframework.security.config.annotation.web.configurers + +import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.BaseWebConfig; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; + +import javax.servlet.http.Cookie + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration +import org.springframework.mock.web.MockFilterChain +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse +import org.springframework.mock.web.MockHttpSession +import org.springframework.security.authentication.AuthenticationManager +import org.springframework.security.authentication.RememberMeAuthenticationToken; +import org.springframework.security.config.annotation.BaseSpringSpec +import org.springframework.security.core.authority.AuthorityUtils; +import org.springframework.security.core.context.SecurityContext +import org.springframework.security.core.userdetails.UserDetailsService +import org.springframework.security.web.FilterChainProxy +import org.springframework.security.web.authentication.AuthenticationSuccessHandler +import org.springframework.security.web.authentication.RememberMeServices +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter +import org.springframework.security.web.authentication.rememberme.AbstractRememberMeServices +import org.springframework.security.web.authentication.rememberme.PersistentTokenBasedRememberMeServices; +import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository; +import org.springframework.security.web.authentication.rememberme.RememberMeAuthenticationFilter +import org.springframework.security.web.context.HttpRequestResponseHolder +import org.springframework.security.web.context.HttpSessionSecurityContextRepository +import org.springframework.test.util.ReflectionTestUtils; + +/** + * Tests to verify that all the functionality of attributes is present + * + * @author Rob Winch + * + */ +public class NamespaceRememberMeTests extends BaseSpringSpec { + FilterChainProxy springSecurityFilterChain + MockHttpServletRequest request + MockHttpServletResponse response + MockFilterChain chain + + def setup() { + request = new MockHttpServletRequest() + response = new MockHttpServletResponse() + chain = new MockFilterChain() + } + + def "http/remember-me"() { + setup: + loadConfig(RememberMeConfig) + springSecurityFilterChain = context.getBean(FilterChainProxy) + when: "login with remember me" + setup() + request.requestURI = "/login" + request.method = "POST" + request.parameters.username = ["user"] as String[] + request.parameters.password = ["password"] as String[] + request.parameters.'remember-me' = ["true"] as String[] + springSecurityFilterChain.doFilter(request,response,chain) + Cookie rememberMeCookie = getRememberMeCookie() + then: "response contains remember me cookie" + rememberMeCookie != null + when: "session expires" + setup() + request.setCookies(rememberMeCookie) + request.requestURI = "/abc" + springSecurityFilterChain.doFilter(request,response,chain) + MockHttpSession session = request.getSession() + then: "initialized to RememberMeAuthenticationToken" + SecurityContext context = new HttpSessionSecurityContextRepository().loadContext(new HttpRequestResponseHolder(request, response)) + context.getAuthentication() instanceof RememberMeAuthenticationToken + when: "logout" + setup() + request.setSession(session) + request.setCookies(rememberMeCookie) + request.requestURI = "/logout" + springSecurityFilterChain.doFilter(request,response,chain) + rememberMeCookie = getRememberMeCookie() + then: "logout cookie expired" + response.getRedirectedUrl() == "/login?logout" + rememberMeCookie.maxAge == 0 + when: "use remember me after logout" + setup() + request.setCookies(rememberMeCookie) + request.requestURI = "/abc" + springSecurityFilterChain.doFilter(request,response,chain) + then: "sent to default login page" + response.getRedirectedUrl() == "http://localhost/login" + } + + @Configuration + static class RememberMeConfig extends BaseWebConfig { + protected void configure(HttpSecurity http) throws Exception { + http + .authorizeUrls() + .anyRequest().hasRole("USER") + .and() + .formLogin() + .and() + .rememberMe() + } + } + + def "http/remember-me@services-ref"() { + setup: + RememberMeServicesRefConfig.REMEMBER_ME_SERVICES = Mock(RememberMeServices) + when: "use custom remember-me services" + loadConfig(RememberMeServicesRefConfig) + then: "custom remember-me services used" + findFilter(RememberMeAuthenticationFilter).rememberMeServices == RememberMeServicesRefConfig.REMEMBER_ME_SERVICES + findFilter(UsernamePasswordAuthenticationFilter).rememberMeServices == RememberMeServicesRefConfig.REMEMBER_ME_SERVICES + } + + @Configuration + static class RememberMeServicesRefConfig extends BaseWebConfig { + static RememberMeServices REMEMBER_ME_SERVICES + protected void configure(HttpSecurity http) throws Exception { + http + .formLogin() + .and() + .rememberMe() + .rememberMeServices(REMEMBER_ME_SERVICES) + } + } + + def "http/remember-me@authentication-success-handler-ref"() { + setup: + AuthSuccessConfig.SUCCESS_HANDLER = Mock(AuthenticationSuccessHandler) + when: "use custom success handler" + loadConfig(AuthSuccessConfig) + then: "custom remember-me success handler is used" + findFilter(RememberMeAuthenticationFilter).successHandler == AuthSuccessConfig.SUCCESS_HANDLER + } + + @Configuration + static class AuthSuccessConfig extends BaseWebConfig { + static AuthenticationSuccessHandler SUCCESS_HANDLER + protected void configure(HttpSecurity http) throws Exception { + http + .formLogin() + .and() + .rememberMe() + .authenticationSuccessHandler(SUCCESS_HANDLER) + } + } + + // http/remember-me@data-source-ref is not supported directly. Instead use http/remember-me@token-repository-ref example + + def "http/remember-me@key"() { + when: "use custom key" + loadConfig(KeyConfig) + AuthenticationManager authManager = context.getBean(AuthenticationManager) + then: "custom key services used" + findFilter(RememberMeAuthenticationFilter).rememberMeServices.key == "KeyConfig" + authManager.authenticate(new RememberMeAuthenticationToken("KeyConfig", "user", AuthorityUtils.createAuthorityList("ROLE_USER"))) + } + + @Configuration + static class KeyConfig extends BaseWebConfig { + protected void configure(HttpSecurity http) throws Exception { + http + .formLogin() + .and() + .rememberMe() + .key("KeyConfig") + } + + @Bean + @Override + public AuthenticationManager authenticationManagerBean() + throws Exception { + return super.authenticationManagerBean(); + } + } + + // http/remember-me@services-alias is not supported use standard aliasing instead (i.e. @Bean("alias")) + + def "http/remember-me@token-repository-ref"() { + setup: + TokenRepositoryRefConfig.TOKEN_REPOSITORY = Mock(PersistentTokenRepository) + when: "use custom token services" + loadConfig(TokenRepositoryRefConfig) + then: "custom token services used with PersistentTokenBasedRememberMeServices" + PersistentTokenBasedRememberMeServices rememberMeServices = findFilter(RememberMeAuthenticationFilter).rememberMeServices + findFilter(RememberMeAuthenticationFilter).rememberMeServices.tokenRepository == TokenRepositoryRefConfig.TOKEN_REPOSITORY + } + + @Configuration + static class TokenRepositoryRefConfig extends BaseWebConfig { + static PersistentTokenRepository TOKEN_REPOSITORY + protected void configure(HttpSecurity http) throws Exception { + // JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl() + // tokenRepository.setDataSource(dataSource); + http + .formLogin() + .and() + .rememberMe() + .tokenRepository(TOKEN_REPOSITORY) + } + } + + def "http/remember-me@token-validity-seconds"() { + when: "use token validity" + loadConfig(TokenValiditySecondsConfig) + then: "custom token validity used" + findFilter(RememberMeAuthenticationFilter).rememberMeServices.tokenValiditySeconds == 1 + } + + @Configuration + static class TokenValiditySecondsConfig extends BaseWebConfig { + protected void configure(HttpSecurity http) throws Exception { + http + .formLogin() + .and() + .rememberMe() + .tokenValiditySeconds(1) + } + } + + def "http/remember-me@token-validity-seconds default"() { + when: "use token validity" + loadConfig(DefaultTokenValiditySecondsConfig) + then: "custom token validity used" + findFilter(RememberMeAuthenticationFilter).rememberMeServices.tokenValiditySeconds == AbstractRememberMeServices.TWO_WEEKS_S + } + + @Configuration + static class DefaultTokenValiditySecondsConfig extends BaseWebConfig { + protected void configure(HttpSecurity http) throws Exception { + http + .formLogin() + .and() + .rememberMe() + } + } + + def "http/remember-me@use-secure-cookie"() { + when: "use secure cookies = true" + loadConfig(UseSecureCookieConfig) + then: "secure cookies will be used" + ReflectionTestUtils.getField(findFilter(RememberMeAuthenticationFilter).rememberMeServices, "useSecureCookie") == true + } + + @Configuration + static class UseSecureCookieConfig extends BaseWebConfig { + protected void configure(HttpSecurity http) throws Exception { + http + .formLogin() + .and() + .rememberMe() + .useSecureCookie(true) + } + } + + def "http/remember-me@use-secure-cookie defaults"() { + when: "use secure cookies not specified" + loadConfig(DefaultUseSecureCookieConfig) + then: "secure cookies will be null (use secure if the request is secure)" + ReflectionTestUtils.getField(findFilter(RememberMeAuthenticationFilter).rememberMeServices, "useSecureCookie") == null + } + + @Configuration + static class DefaultUseSecureCookieConfig extends BaseWebConfig { + protected void configure(HttpSecurity http) throws Exception { + http + .formLogin() + .and() + .rememberMe() + } + } + + def "http/remember-me defaults UserDetailsService with custom UserDetailsService"() { + setup: + DefaultsUserDetailsServiceWithDaoConfig.USERDETAILS_SERVICE = Mock(UserDetailsService) + when: "use secure cookies not specified" + loadConfig(DefaultsUserDetailsServiceWithDaoConfig) + then: "RememberMeServices defaults to the custom UserDetailsService" + ReflectionTestUtils.getField(findFilter(RememberMeAuthenticationFilter).rememberMeServices, "userDetailsService") == DefaultsUserDetailsServiceWithDaoConfig.USERDETAILS_SERVICE + } + + @Configuration + @EnableWebSecurity + static class DefaultsUserDetailsServiceWithDaoConfig extends WebSecurityConfigurerAdapter { + static UserDetailsService USERDETAILS_SERVICE + + protected void configure(HttpSecurity http) throws Exception { + http + .formLogin() + .and() + .rememberMe() + } + + protected void registerAuthentication( + AuthenticationManagerBuilder auth) throws Exception { + auth + .userDetailsService(USERDETAILS_SERVICE); + } + } + + def "http/remember-me@user-service-ref"() { + setup: + UserServiceRefConfig.USERDETAILS_SERVICE = Mock(UserDetailsService) + when: "use custom UserDetailsService" + loadConfig(UserServiceRefConfig) + then: "custom UserDetailsService is used" + ReflectionTestUtils.getField(findFilter(RememberMeAuthenticationFilter).rememberMeServices, "userDetailsService") == UserServiceRefConfig.USERDETAILS_SERVICE + } + + @Configuration + static class UserServiceRefConfig extends BaseWebConfig { + static UserDetailsService USERDETAILS_SERVICE + protected void configure(HttpSecurity http) throws Exception { + http + .formLogin() + .and() + .rememberMe() + .userDetailsService(USERDETAILS_SERVICE) + } + } + + + Cookie getRememberMeCookie(String cookieName="remember-me") { + response.getCookie(cookieName) + } +} diff --git a/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/NamespaceSessionManagementTests.groovy b/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/NamespaceSessionManagementTests.groovy new file mode 100644 index 0000000000..e6e95a5af6 --- /dev/null +++ b/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/NamespaceSessionManagementTests.groovy @@ -0,0 +1,160 @@ +/* + * Copyright 2002-2013 the original author or authors. + * + * 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 org.springframework.security.config.annotation.web.configurers + +import org.springframework.context.annotation.Configuration +import org.springframework.security.config.annotation.BaseSpringSpec +import org.springframework.security.config.annotation.ObjectPostProcessor +import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; +import org.springframework.security.core.session.SessionRegistry +import org.springframework.security.web.authentication.session.NullAuthenticatedSessionStrategy; +import org.springframework.security.web.authentication.session.SessionAuthenticationStrategy; +import org.springframework.security.web.authentication.session.SessionFixationProtectionStrategy +import org.springframework.security.web.context.SecurityContextPersistenceFilter +import org.springframework.security.web.context.SecurityContextRepository +import org.springframework.security.web.session.ConcurrentSessionFilter +import org.springframework.security.web.session.SessionManagementFilter + +/** + * + * @author Rob Winch + */ +class NamespaceSessionManagementTests extends BaseSpringSpec { + + def "http/session-management"() { + when: + loadConfig(SessionManagementConfig) + then: + findFilter(SessionManagementFilter).sessionAuthenticationStrategy instanceof SessionFixationProtectionStrategy + } + + @EnableWebSecurity + @Configuration + static class SessionManagementConfig extends WebSecurityConfigurerAdapter { + @Override + protected void configure(HttpSecurity http) throws Exception { + // enabled by default + } + } + + def "http/session-management custom"() { + setup: + CustomSessionManagementConfig.SR = Mock(SessionRegistry) + when: + loadConfig(CustomSessionManagementConfig) + then: + findFilter(SessionManagementFilter).invalidSessionStrategy.destinationUrl == "/invalid-session" + findFilter(SessionManagementFilter).failureHandler.defaultFailureUrl == "/session-auth-error" + findFilter(SessionManagementFilter).sessionAuthenticationStrategy.maximumSessions == 1 + findFilter(SessionManagementFilter).sessionAuthenticationStrategy.exceptionIfMaximumExceeded + findFilter(SessionManagementFilter).sessionAuthenticationStrategy.sessionRegistry == CustomSessionManagementConfig.SR + findFilter(ConcurrentSessionFilter).expiredUrl == "/expired-session" + } + + @EnableWebSecurity + @Configuration + static class CustomSessionManagementConfig extends WebSecurityConfigurerAdapter { + static SessionRegistry SR + @Override + protected void configure(HttpSecurity http) throws Exception { + http + .sessionManagement() + .invalidSessionUrl("/invalid-session") // session-management@invalid-session-url + .sessionAuthenticationErrorUrl("/session-auth-error") // session-management@session-authentication-error-url + .maximumSessions(1) // session-management/concurrency-control@max-sessions + .maxSessionsPreventsLogin(true) // session-management/concurrency-control@error-if-maximum-exceeded + .expiredUrl("/expired-session") // session-management/concurrency-control@expired-url + .sessionRegistry(SR) // session-management/concurrency-control@session-registry-ref + } + } + + def "http/session-management refs"() { + setup: + RefsSessionManagementConfig.SAS = Mock(SessionAuthenticationStrategy) + when: + loadConfig(RefsSessionManagementConfig) + then: + findFilter(SessionManagementFilter).sessionAuthenticationStrategy == RefsSessionManagementConfig.SAS + } + + @EnableWebSecurity + @Configuration + static class RefsSessionManagementConfig extends WebSecurityConfigurerAdapter { + static SessionAuthenticationStrategy SAS + @Override + protected void configure(HttpSecurity http) throws Exception { + http + .sessionManagement() + .sessionAuthenticationStrategy(SAS) // session-management@session-authentication-strategy-ref + } + } + + def "http/session-management@session-fixation-protection=none"() { + when: + loadConfig(SFPNoneSessionManagementConfig) + then: + findFilter(SessionManagementFilter).sessionAuthenticationStrategy.class == NullAuthenticatedSessionStrategy + } + + @EnableWebSecurity + @Configuration + static class SFPNoneSessionManagementConfig extends WebSecurityConfigurerAdapter { + @Override + protected void configure(HttpSecurity http) throws Exception { + http + .sessionManagement() + .sessionAuthenticationStrategy(new NullAuthenticatedSessionStrategy()) + } + } + + def "http/session-management@session-fixation-protection=migrateSession (default)"() { + when: + loadConfig(SFPMigrateSessionManagementConfig) + then: + findFilter(SessionManagementFilter).sessionAuthenticationStrategy.migrateSessionAttributes + } + + @EnableWebSecurity + @Configuration + static class SFPMigrateSessionManagementConfig extends WebSecurityConfigurerAdapter { + @Override + protected void configure(HttpSecurity http) throws Exception { + http + .sessionManagement() + } + } + + def "http/session-management@session-fixation-protection=newSession"() { + when: + loadConfig(SFPNewSessionSessionManagementConfig) + then: + !findFilter(SessionManagementFilter).sessionAuthenticationStrategy.migrateSessionAttributes + } + + @EnableWebSecurity + @Configuration + static class SFPNewSessionSessionManagementConfig extends WebSecurityConfigurerAdapter { + @Override + protected void configure(HttpSecurity http) throws Exception { + http + .sessionManagement() + .sessionAuthenticationStrategy(new SessionFixationProtectionStrategy(migrateSessionAttributes : false)) + } + } +} diff --git a/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/PermitAllSupportTests.groovy b/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/PermitAllSupportTests.groovy new file mode 100644 index 0000000000..a784354ffa --- /dev/null +++ b/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/PermitAllSupportTests.groovy @@ -0,0 +1,71 @@ +/* + * Copyright 2002-2013 the original author or authors. + * + * 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 org.springframework.security.config.annotation.web.configurers + +import org.springframework.beans.factory.BeanCreationException; +import org.springframework.context.annotation.Configuration +import org.springframework.mock.web.MockHttpServletRequest +import org.springframework.security.config.annotation.BaseSpringSpec +import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; +import org.springframework.security.web.util.RequestMatcher + +/** + * @author Rob Winch + * + */ +class PermitAllSupportTests extends BaseSpringSpec { + def "PermitAllSupport.ExactUrlRequestMatcher"() { + expect: + RequestMatcher matcher = new PermitAllSupport.ExactUrlRequestMatcher(processUrl) + matcher.matches(new MockHttpServletRequest(requestURI:requestURI,contextPath:contextPath,queryString: query)) == matches + where: + processUrl | requestURI | contextPath | query | matches + "/login" | "/sample/login" | "/sample" | null | true + "/login" | "/sample/login" | "/sample" | "error" | false + "/login?error" | "/sample/login" | "/sample" | "error" | true + } + + def "PermitAllSupport throws Exception when authorizedUrls() not invoked"() { + when: + loadConfig(NoAuthorizedUrlsConfig) + then: + BeanCreationException e = thrown() + e.message.contains "permitAll only works with HttpSecurity.authorizeUrls" + + } + + @EnableWebSecurity + @Configuration + static class NoAuthorizedUrlsConfig extends WebSecurityConfigurerAdapter { + + @Override + protected void registerAuthentication(AuthenticationManagerBuilder auth) + throws Exception { + auth + .inMemoryAuthentication() + } + + @Override + protected void configure(HttpSecurity http) throws Exception { + http + .formLogin() + .permitAll() + } + } +} diff --git a/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/RememberMeConfigurerTests.groovy b/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/RememberMeConfigurerTests.groovy new file mode 100644 index 0000000000..63c5e9fb7e --- /dev/null +++ b/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/RememberMeConfigurerTests.groovy @@ -0,0 +1,72 @@ +/* + * Copyright 2002-2013 the original author or authors. + * + * 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 org.springframework.security.config.annotation.web.configurers + +import org.springframework.context.annotation.Configuration +import org.springframework.security.config.annotation.AnyObjectPostProcessor +import org.springframework.security.config.annotation.BaseSpringSpec +import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; +import org.springframework.security.core.userdetails.UserDetailsService +import org.springframework.security.web.authentication.rememberme.RememberMeAuthenticationFilter + +/** + * Tests for RememberMeConfigurer that flex edge cases. {@link NamespaceRememberMeTests} demonstrate mapping of the XML namespace to Java Config. + * + * @author Rob Winch + */ +public class RememberMeConfigurerTests extends BaseSpringSpec { + + def "rememberMe() null UserDetailsService provides meaningful error"() { + when: "Load Config without UserDetailsService specified" + loadConfig(NullUserDetailsConfig) + then: "A good error message is provided" + Exception success = thrown() + success.message.contains "Invoke RememberMeConfigurer#userDetailsService(UserDetailsService) or see its javadoc for alternative approaches." + } + + @EnableWebSecurity + @Configuration + static class NullUserDetailsConfig extends WebSecurityConfigurerAdapter { + protected void configure(HttpSecurity http) throws Exception { + http + .authorizeUrls() + .anyRequest().hasRole("USER") + .and() + .formLogin() + .and() + .rememberMe() + } + } + + def "rememberMe ObjectPostProcessor"() { + setup: + AnyObjectPostProcessor opp = Mock() + HttpSecurity http = new HttpSecurity(opp, authenticationBldr, [:]) + UserDetailsService uds = authenticationBldr.getDefaultUserDetailsService() + when: + http + .rememberMe() + .userDetailsService(authenticationBldr.getDefaultUserDetailsService()) + .and() + .build() + + then: "RememberMeAuthenticationFilter is registered with LifecycleManager" + 1 * opp.postProcess(_ as RememberMeAuthenticationFilter) >> {RememberMeAuthenticationFilter o -> o} + } +} diff --git a/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/RequestCacheConfigurerTests.groovy b/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/RequestCacheConfigurerTests.groovy new file mode 100644 index 0000000000..8e722f0bba --- /dev/null +++ b/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/RequestCacheConfigurerTests.groovy @@ -0,0 +1,43 @@ +/* + * Copyright 2002-2013 the original author or authors. + * + * 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 org.springframework.security.config.annotation.web.configurers + +import org.springframework.security.config.annotation.AnyObjectPostProcessor +import org.springframework.security.config.annotation.BaseSpringSpec +import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.web.savedrequest.RequestCacheAwareFilter + +/** + * + * @author Rob Winch + */ +class RequestCacheConfigurerTests extends BaseSpringSpec { + + def "requestCache ObjectPostProcessor"() { + setup: + AnyObjectPostProcessor opp = Mock() + HttpSecurity http = new HttpSecurity(opp, authenticationBldr, [:]) + when: + http + .requestCache() + .and() + .build() + + then: "RequestCacheAwareFilter is registered with LifecycleManager" + 1 * opp.postProcess(_ as RequestCacheAwareFilter) >> {RequestCacheAwareFilter o -> o} + } +} diff --git a/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/SecurityContextConfigurerTests.groovy b/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/SecurityContextConfigurerTests.groovy new file mode 100644 index 0000000000..51a9c6af82 --- /dev/null +++ b/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/SecurityContextConfigurerTests.groovy @@ -0,0 +1,43 @@ +/* + * Copyright 2002-2013 the original author or authors. + * + * 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 org.springframework.security.config.annotation.web.configurers + +import org.springframework.security.config.annotation.AnyObjectPostProcessor +import org.springframework.security.config.annotation.BaseSpringSpec +import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.web.context.SecurityContextPersistenceFilter + +/** + * + * @author Rob Winch + */ +class SecurityContextConfigurerTests extends BaseSpringSpec { + + def "securityContext ObjectPostProcessor"() { + setup: + AnyObjectPostProcessor opp = Mock() + HttpSecurity http = new HttpSecurity(opp, authenticationBldr, [:]) + when: + http + .securityContext() + .and() + .build() + + then: "SecurityContextPersistenceFilter is registered with LifecycleManager" + 1 * opp.postProcess(_ as SecurityContextPersistenceFilter) >> {SecurityContextPersistenceFilter o -> o} + } +} diff --git a/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/ServletApiConfigurerTests.groovy b/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/ServletApiConfigurerTests.groovy new file mode 100644 index 0000000000..5e35acdf59 --- /dev/null +++ b/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/ServletApiConfigurerTests.groovy @@ -0,0 +1,43 @@ +/* + * Copyright 2002-2013 the original author or authors. + * + * 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 org.springframework.security.config.annotation.web.configurers + +import org.springframework.security.config.annotation.AnyObjectPostProcessor +import org.springframework.security.config.annotation.BaseSpringSpec +import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter + +/** + * + * @author Rob Winch + */ +class ServletApiConfigurerTests extends BaseSpringSpec { + + def "servletApi ObjectPostProcessor"() { + setup: + AnyObjectPostProcessor opp = Mock() + HttpSecurity http = new HttpSecurity(opp, authenticationBldr, [:]) + when: + http + .servletApi() + .and() + .build() + + then: "SecurityContextHolderAwareRequestFilter is registered with LifecycleManager" + 1 * opp.postProcess(_ as SecurityContextHolderAwareRequestFilter) >> {SecurityContextHolderAwareRequestFilter o -> o} + } +} diff --git a/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/SessionManagementConfigurerTests.groovy b/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/SessionManagementConfigurerTests.groovy new file mode 100644 index 0000000000..a9ec6ac3bd --- /dev/null +++ b/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/SessionManagementConfigurerTests.groovy @@ -0,0 +1,108 @@ +/* + * Copyright 2002-2013 the original author or authors. + * + * 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 org.springframework.security.config.annotation.web.configurers + +import org.springframework.context.annotation.Configuration +import org.springframework.security.config.annotation.AnyObjectPostProcessor +import org.springframework.security.config.annotation.BaseSpringSpec +import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; +import org.springframework.security.config.annotation.web.configurers.SessionCreationPolicy; +import org.springframework.security.web.access.ExceptionTranslationFilter +import org.springframework.security.web.context.SecurityContextPersistenceFilter +import org.springframework.security.web.context.SecurityContextRepository +import org.springframework.security.web.savedrequest.RequestCache +import org.springframework.security.web.session.ConcurrentSessionFilter +import org.springframework.security.web.session.SessionManagementFilter + +/** + * + * @author Rob Winch + */ +class SessionManagementConfigurerTests extends BaseSpringSpec { + + def "sessionManagement does not override explicit RequestCache"() { + setup: + SessionManagementDoesNotOverrideExplicitRequestCacheConfig.REQUEST_CACHE = Mock(RequestCache) + when: + loadConfig(SessionManagementDoesNotOverrideExplicitRequestCacheConfig) + then: + findFilter(ExceptionTranslationFilter).requestCache == SessionManagementDoesNotOverrideExplicitRequestCacheConfig.REQUEST_CACHE + } + + @EnableWebSecurity + @Configuration + static class SessionManagementDoesNotOverrideExplicitRequestCacheConfig extends WebSecurityConfigurerAdapter { + static RequestCache REQUEST_CACHE + + @Override + protected void configure(HttpSecurity http) throws Exception { + http + .requestCache() + .requestCache(REQUEST_CACHE) + .and() + .sessionManagement() + .sessionCreationPolicy(SessionCreationPolicy.stateless) + } + + } + + def "sessionManagement does not override explict SecurityContextRepository"() { + setup: + SessionManagementDoesNotOverrideExplicitSecurityContextRepositoryConfig.SECURITY_CONTEXT_REPO = Mock(SecurityContextRepository) + when: + loadConfig(SessionManagementDoesNotOverrideExplicitSecurityContextRepositoryConfig) + then: + findFilter(SecurityContextPersistenceFilter).repo == SessionManagementDoesNotOverrideExplicitSecurityContextRepositoryConfig.SECURITY_CONTEXT_REPO + } + + @Configuration + @EnableWebSecurity + static class SessionManagementDoesNotOverrideExplicitSecurityContextRepositoryConfig extends WebSecurityConfigurerAdapter { + static SecurityContextRepository SECURITY_CONTEXT_REPO + + @Override + protected void configure(HttpSecurity http) throws Exception { + http + .securityContext() + .securityContextRepository(SECURITY_CONTEXT_REPO) + .and() + .sessionManagement() + .sessionCreationPolicy(SessionCreationPolicy.stateless) + } + + } + + def "sessionManagement ObjectPostProcessor"() { + setup: + AnyObjectPostProcessor opp = Mock() + HttpSecurity http = new HttpSecurity(opp, authenticationBldr, [:]) + when: + http + .sessionManagement() + .maximumSessions(1) + .and() + .and() + .build() + + then: "SessionManagementFilter is registered with LifecycleManager" + 1 * opp.postProcess(_ as SessionManagementFilter) >> {SessionManagementFilter o -> o} + and: "ConcurrentSessionFilter is registered with LifecycleManager" + 1 * opp.postProcess(_ as ConcurrentSessionFilter) >> {ConcurrentSessionFilter o -> o} + } +} diff --git a/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/UrlAuthorizationsTests.groovy b/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/UrlAuthorizationsTests.groovy new file mode 100644 index 0000000000..ebbc0fa836 --- /dev/null +++ b/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/UrlAuthorizationsTests.groovy @@ -0,0 +1,79 @@ +/* + * Copyright 2002-2013 the original author or authors. + * + * 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 org.springframework.security.config.annotation.web.configurers; + +import org.springframework.context.annotation.Configuration +import org.springframework.security.access.vote.AffirmativeBased +import org.springframework.security.config.annotation.BaseSpringSpec +import org.springframework.security.config.annotation.SecurityExpressions.* +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; +import org.springframework.security.config.annotation.web.configurers.UrlAuthorizationConfigurer; +import org.springframework.security.web.access.intercept.FilterSecurityInterceptor + +/** + * + * @author Rob Winch + * + */ +public class UrlAuthorizationsTests extends BaseSpringSpec { + + def "hasAnyAuthority('ROLE_USER')"() { + when: + def expression = UrlAuthorizationConfigurer.hasAnyAuthority("ROLE_USER") + then: + expression == ["ROLE_USER"] + } + + def "hasAnyAuthority('ROLE_USER','ROLE_ADMIN')"() { + when: + def expression = UrlAuthorizationConfigurer.hasAnyAuthority("ROLE_USER","ROLE_ADMIN") + then: + expression == ["ROLE_USER","ROLE_ADMIN"] + } + + def "hasAnyRole('USER')"() { + when: + def expression = UrlAuthorizationConfigurer.hasAnyRole("USER") + then: + expression == ["ROLE_USER"] + } + + def "hasAnyRole('ROLE_USER','ROLE_ADMIN')"() { + when: + def expression = UrlAuthorizationConfigurer.hasAnyRole("USER","ADMIN") + then: + expression == ["ROLE_USER","ROLE_ADMIN"] + } + + def "uses AffirmativeBased AccessDecisionManager"() { + when: "Load Config with no specific AccessDecisionManager" + loadConfig(NoSpecificAccessDecessionManagerConfig) + then: "AccessDecessionManager matches the HttpSecurityBuilder's default" + findFilter(FilterSecurityInterceptor).accessDecisionManager.class == AffirmativeBased + } + + @EnableWebSecurity + @Configuration + static class NoSpecificAccessDecessionManagerConfig extends WebSecurityConfigurerAdapter { + protected void configure(HttpSecurity http) throws Exception { + http + .apply(new UrlAuthorizationConfigurer()) + .anyRequest().hasRole("USER") + } + } +} diff --git a/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/X509ConfigurerTests.groovy b/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/X509ConfigurerTests.groovy new file mode 100644 index 0000000000..4b8b9bc97a --- /dev/null +++ b/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/X509ConfigurerTests.groovy @@ -0,0 +1,43 @@ +/* + * Copyright 2002-2013 the original author or authors. + * + * 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 org.springframework.security.config.annotation.web.configurers + +import org.springframework.security.config.annotation.AnyObjectPostProcessor +import org.springframework.security.config.annotation.BaseSpringSpec +import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.web.authentication.preauth.x509.X509AuthenticationFilter + +/** + * + * @author Rob Winch + */ +class X509ConfigurerTests extends BaseSpringSpec { + + def "x509 ObjectPostProcessor"() { + setup: + AnyObjectPostProcessor opp = Mock() + HttpSecurity http = new HttpSecurity(opp, authenticationBldr, [:]) + when: + http + .x509() + .and() + .build() + + then: "X509AuthenticationFilter is registered with LifecycleManager" + 1 * opp.postProcess(_ as X509AuthenticationFilter) >> {X509AuthenticationFilter o -> o} + } +} diff --git a/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/openid/OpenIDLoginConfigurerTests.groovy b/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/openid/OpenIDLoginConfigurerTests.groovy new file mode 100644 index 0000000000..f16ea46ca7 --- /dev/null +++ b/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/openid/OpenIDLoginConfigurerTests.groovy @@ -0,0 +1,51 @@ +/* + * Copyright 2002-2013 the original author or authors. + * + * 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 org.springframework.security.config.annotation.web.configurers.openid + +import org.springframework.security.config.annotation.AnyObjectPostProcessor +import org.springframework.security.config.annotation.BaseSpringSpec +import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.core.userdetails.UserDetailsByNameServiceWrapper +import org.springframework.security.core.userdetails.UserDetailsService +import org.springframework.security.openid.OpenIDAuthenticationFilter +import org.springframework.security.openid.OpenIDAuthenticationProvider +import org.springframework.security.openid.OpenIDAuthenticationToken + +/** + * + * @author Rob Winch + */ +class OpenIDLoginConfigurerTests extends BaseSpringSpec { + + def "openidLogin ObjectPostProcessor"() { + setup: + AnyObjectPostProcessor opp = Mock() + HttpSecurity http = new HttpSecurity(opp, authenticationBldr, [:]) + UserDetailsService uds = authenticationBldr.getDefaultUserDetailsService() + when: + http + .openidLogin() + .authenticationUserDetailsService(new UserDetailsByNameServiceWrapper(uds)) + .and() + .build() + + then: "OpenIDAuthenticationFilter is registered with LifecycleManager" + 1 * opp.postProcess(_ as OpenIDAuthenticationFilter) >> {OpenIDAuthenticationFilter o -> o} + and: "OpenIDAuthenticationProvider is registered with LifecycleManager" + 1 * opp.postProcess(_ as OpenIDAuthenticationProvider) >> {OpenIDAuthenticationProvider o -> o} + } +} diff --git a/config/src/test/resources/CustomJdbcUserServiceSampleConfig.sql b/config/src/test/resources/CustomJdbcUserServiceSampleConfig.sql new file mode 100644 index 0000000000..a63dc8afd6 --- /dev/null +++ b/config/src/test/resources/CustomJdbcUserServiceSampleConfig.sql @@ -0,0 +1,13 @@ +create table users(principal varchar_ignorecase(50) not null primary key,credentials varchar_ignorecase(50) not null); +create table roles (principal varchar_ignorecase(50) not null,role varchar_ignorecase(50) not null,constraint fk_roles_users foreign key(principal) references users(principal)); +create unique index ix_auth_principal on roles (principal,role); +create table groups (id bigint generated by default as identity(start with 0) primary key,group_name varchar_ignorecase(50) not null); +create table group_authorities (group_id bigint not null,authority varchar(50) not null,constraint fk_group_authorities_group foreign key(group_id) references groups(id)); +create table group_members (id bigint generated by default as identity(start with 0) primary key,username varchar(50) not null,group_id bigint not null,constraint fk_group_members_group foreign key(group_id) references groups(id)); + +insert into users values('user','password'); +insert into roles values('user','USER'); + +insert into groups values(1,'OPERATIONS'); +insert into group_authorities values(1,'DBA'); +insert into group_members values(1,'user',1); \ No newline at end of file diff --git a/config/src/test/resources/rod.cer b/config/src/test/resources/rod.cer new file mode 100644 index 0000000000..c897d370a1 Binary files /dev/null and b/config/src/test/resources/rod.cer differ diff --git a/config/src/test/resources/rodatexampledotcom.cer b/config/src/test/resources/rodatexampledotcom.cer new file mode 100644 index 0000000000..5b72c932a3 Binary files /dev/null and b/config/src/test/resources/rodatexampledotcom.cer differ diff --git a/core/src/main/java/org/springframework/security/core/SpringSecurityCoreVersion.java b/core/src/main/java/org/springframework/security/core/SpringSecurityCoreVersion.java index b54eed093f..ce087bcf82 100644 --- a/core/src/main/java/org/springframework/security/core/SpringSecurityCoreVersion.java +++ b/core/src/main/java/org/springframework/security/core/SpringSecurityCoreVersion.java @@ -22,7 +22,7 @@ public class SpringSecurityCoreVersion { static final String SPRING_MAJOR_VERSION = "3"; - static final String MIN_SPRING_VERSION = "3.2.0.RELEASE"; + static final String MIN_SPRING_VERSION = "3.2.3.RELEASE"; static { // Check Spring Compatibility diff --git a/core/src/main/resources/org/springframework/security/core/userdetails/jdbc/users.ddl b/core/src/main/resources/org/springframework/security/core/userdetails/jdbc/users.ddl new file mode 100644 index 0000000000..4950a1c61f --- /dev/null +++ b/core/src/main/resources/org/springframework/security/core/userdetails/jdbc/users.ddl @@ -0,0 +1,3 @@ +create table users(username varchar_ignorecase(50) not null primary key,password varchar_ignorecase(500) not null,enabled boolean not null); +create table authorities (username varchar_ignorecase(50) not null,authority varchar_ignorecase(50) not null,constraint fk_authorities_users foreign key(username) references users(username)); +create unique index ix_auth_username on authorities (username,authority); \ No newline at end of file diff --git a/gradle/javaprojects.gradle b/gradle/javaprojects.gradle index f34018b5d4..554d66e4f9 100644 --- a/gradle/javaprojects.gradle +++ b/gradle/javaprojects.gradle @@ -4,7 +4,7 @@ apply plugin: 'eclipse' sourceCompatibility = 1.5 targetCompatibility = 1.5 -ext.springVersion = '3.2.0.RELEASE' +ext.springVersion = '3.2.3.RELEASE' ext.springLdapVersion = '1.3.1.RELEASE' ext.ehcacheVersion = '1.6.2' ext.aspectjVersion = '1.6.10' @@ -27,6 +27,15 @@ ext.powerMockDependencies = [ "org.powermock:powermock-reflect:$powerMockVersion" ] +ext.apacheds_libs = [ + "org.apache.directory.server:apacheds-core:$apacheDsVersion", + "org.apache.directory.server:apacheds-core-entry:$apacheDsVersion", + "org.apache.directory.server:apacheds-protocol-shared:$apacheDsVersion", + "org.apache.directory.server:apacheds-protocol-ldap:$apacheDsVersion", + "org.apache.directory.server:apacheds-server-jndi:$apacheDsVersion", + 'org.apache.directory.shared:shared-ldap:0.9.15' +] + ext.bundlorProperties = [ version: version, secRange: "[$version, 3.3.0)", diff --git a/gradle/maven-deployment.gradle b/gradle/maven-deployment.gradle index 83267918b3..a1af433944 100644 --- a/gradle/maven-deployment.gradle +++ b/gradle/maven-deployment.gradle @@ -47,7 +47,15 @@ def customizePom(pom, gradleProject) { // Hack for specific case of config module if (p.artifactId == 'spring-security-config') { + p.dependencies.find { dep -> dep.artifactId == 'spring-security-ldap'}.optional = true + p.dependencies.find { dep -> dep.artifactId == 'spring-ldap-core'}.optional = true + p.dependencies.find { dep -> dep.groupId.startsWith "org.apache.directory" }*.optional = true p.dependencies.find { dep -> dep.artifactId == 'spring-security-web'}.optional = true + p.dependencies.find { dep -> dep.artifactId == 'spring-security-openid'}.optional = true + p.dependencies.find { dep -> dep.artifactId == 'guice'}.optional = true + p.dependencies.find { dep -> dep.artifactId == 'openid4java-nodeps'}.optional = true + p.dependencies.find { dep -> dep.artifactId == 'spring-jdbc'}.optional = true + p.dependencies.find { dep -> dep.artifactId == 'spring-tx'}.optional = true p.dependencies.find { dep -> dep.artifactId == 'spring-web'}.optional = true } diff --git a/ldap/ldap.gradle b/ldap/ldap.gradle index 8594baa37b..2410e2d226 100644 --- a/ldap/ldap.gradle +++ b/ldap/ldap.gradle @@ -1,14 +1,5 @@ // Ldap build file -def apacheds_libs = [ - "org.apache.directory.server:apacheds-core:$apacheDsVersion", - "org.apache.directory.server:apacheds-core-entry:$apacheDsVersion", - "org.apache.directory.server:apacheds-protocol-shared:$apacheDsVersion", - "org.apache.directory.server:apacheds-protocol-ldap:$apacheDsVersion", - "org.apache.directory.server:apacheds-server-jndi:$apacheDsVersion", - 'org.apache.directory.shared:shared-ldap:0.9.15' -] - dependencies { compile project(':spring-security-core'), "org.springframework:spring-beans:$springVersion", diff --git a/web/src/main/java/org/springframework/security/web/authentication/ui/DefaultLoginPageViewFilter.java b/web/src/main/java/org/springframework/security/web/authentication/ui/DefaultLoginPageViewFilter.java new file mode 100644 index 0000000000..443d560fe6 --- /dev/null +++ b/web/src/main/java/org/springframework/security/web/authentication/ui/DefaultLoginPageViewFilter.java @@ -0,0 +1,226 @@ +/* + * Copyright 2002-2013 the original author or authors. + * + * 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 org.springframework.security.web.authentication.ui; + +import java.io.IOException; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpSession; + +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.WebAttributes; +import org.springframework.web.filter.GenericFilterBean; + +/** + * This class generates a default login page if one was not specified. The class is quite similar + * + * @author Rob Winch + * @since 3.2 + */ +public class DefaultLoginPageViewFilter extends GenericFilterBean { + private String loginPageUrl; + private String logoutSuccessUrl; + private String failureUrl; + private boolean formLoginEnabled; + private boolean openIdEnabled; + private String authenticationUrl; + private String usernameParameter; + private String passwordParameter; + private String rememberMeParameter; + private String openIDauthenticationUrl; + private String openIDusernameParameter; + private String openIDrememberMeParameter; + + public boolean isEnabled() { + return formLoginEnabled || openIdEnabled; + } + + public void setLogoutSuccessUrl(String logoutSuccessUrl) { + this.logoutSuccessUrl = logoutSuccessUrl; + } + + public String getLoginPageUrl() { + return loginPageUrl; + } + + public void setLoginPageUrl(String loginPageUrl) { + this.loginPageUrl = loginPageUrl; + } + + public void setFailureUrl(String failureUrl) { + this.failureUrl = failureUrl; + } + + public void setFormLoginEnabled(boolean formLoginEnabled) { + this.formLoginEnabled = formLoginEnabled; + } + + public void setOpenIdEnabled(boolean openIdEnabled) { + this.openIdEnabled = openIdEnabled; + } + + public void setAuthenticationUrl(String authenticationUrl) { + this.authenticationUrl = authenticationUrl; + } + + public void setUsernameParameter(String usernameParameter) { + this.usernameParameter = usernameParameter; + } + + public void setPasswordParameter(String passwordParameter) { + this.passwordParameter = passwordParameter; + } + + public void setRememberMeParameter(String rememberMeParameter) { + this.rememberMeParameter = rememberMeParameter; + this.openIDrememberMeParameter = rememberMeParameter; + } + + public void setOpenIDauthenticationUrl(String openIDauthenticationUrl) { + this.openIDauthenticationUrl = openIDauthenticationUrl; + } + + public void setOpenIDusernameParameter(String openIDusernameParameter) { + this.openIDusernameParameter = openIDusernameParameter; + } + + public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) + throws IOException, ServletException { + HttpServletRequest request = (HttpServletRequest) req; + HttpServletResponse response = (HttpServletResponse) res; + + boolean loginError = isErrorPage(request); + boolean logoutSuccess = isLogoutSuccess(request); + if (isLoginUrlRequest(request) || loginError || logoutSuccess) { + String loginPageHtml = generateLoginPageHtml(request, loginError, logoutSuccess); + response.setContentType("text/html;charset=UTF-8"); + response.setContentLength(loginPageHtml.length()); + response.getWriter().write(loginPageHtml); + + return; + } + + chain.doFilter(request, response); + } + + private String generateLoginPageHtml(HttpServletRequest request, boolean loginError, boolean logoutSuccess) { + String errorMsg = "none"; + + if (loginError) { + HttpSession session = request.getSession(false); + + if(session != null) { + AuthenticationException ex = (AuthenticationException) session.getAttribute(WebAttributes.AUTHENTICATION_EXCEPTION); + errorMsg = ex != null ? ex.getMessage() : "none"; + } + } + + StringBuilder sb = new StringBuilder(); + + sb.append("Login Page"); + + if (formLoginEnabled) { + sb.append("\n"); + } + + if (loginError) { + sb.append("

Your login attempt was not successful, try again.

Reason: "); + sb.append(errorMsg); + sb.append("

"); + } + + if (logoutSuccess) { + sb.append("

You have been logged out

"); + } + + if (formLoginEnabled) { + sb.append("

Login with Username and Password

"); + sb.append("
\n"); + sb.append(" \n"); + sb.append(" \n"); + sb.append(" \n"); + + if (rememberMeParameter != null) { + sb.append(" \n"); + } + + sb.append(" \n"); + sb.append("
User:
Password:
Remember me on this computer.
\n"); + sb.append("
"); + } + + if(openIdEnabled) { + sb.append("

Login with OpenID Identity

"); + sb.append("
\n"); + sb.append(" \n"); + sb.append(" \n"); + + if (openIDrememberMeParameter != null) { + sb.append(" \n"); + } + + sb.append(" \n"); + sb.append("
Identity:
Remember me on this computer.
\n"); + sb.append("
"); + } + + sb.append(""); + + return sb.toString(); + } + + private boolean isLogoutSuccess(HttpServletRequest request) { + return logoutSuccessUrl != null && matches(request, logoutSuccessUrl); + } + + private boolean isLoginUrlRequest(HttpServletRequest request) { + return matches(request, loginPageUrl); + } + + private boolean isErrorPage(HttpServletRequest request) { + return matches(request, failureUrl); + } + + private boolean matches(HttpServletRequest request, String url) { + if(!"GET".equals(request.getMethod())) { + return false; + } + String uri = request.getRequestURI(); + int pathParamIndex = uri.indexOf(';'); + + if (pathParamIndex > 0) { + // strip everything after the first semi-colon + uri = uri.substring(0, pathParamIndex); + } + + if(request.getQueryString() != null) { + uri += "?" + request.getQueryString(); + } + + if ("".equals(request.getContextPath())) { + return uri.endsWith(url); + } + + return uri.endsWith(request.getContextPath() + url); + } +} diff --git a/web/src/main/java/org/springframework/security/web/context/AbstractSecurityWebApplicationInitializer.java b/web/src/main/java/org/springframework/security/web/context/AbstractSecurityWebApplicationInitializer.java new file mode 100644 index 0000000000..0ecc09e22c --- /dev/null +++ b/web/src/main/java/org/springframework/security/web/context/AbstractSecurityWebApplicationInitializer.java @@ -0,0 +1,254 @@ +/* + * Copyright 2002-2013 the original author or authors. + * + * 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 org.springframework.security.web.context; + +import java.util.Arrays; +import java.util.EnumSet; + +import javax.servlet.DispatcherType; +import javax.servlet.Filter; +import javax.servlet.FilterRegistration.Dynamic; +import javax.servlet.ServletContext; +import javax.servlet.ServletException; + +import org.springframework.context.ApplicationContext; +import org.springframework.core.Conventions; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; +import org.springframework.security.web.session.HttpSessionEventPublisher; +import org.springframework.util.Assert; +import org.springframework.web.WebApplicationInitializer; +import org.springframework.web.context.AbstractContextLoaderInitializer; +import org.springframework.web.context.WebApplicationContext; +import org.springframework.web.filter.DelegatingFilterProxy; + +/** + * Registers the {@link DelegatingFilterProxy} to use the + * springSecurityFilterChain before any other registered {@link Filter}. This + * class is typically used in addition to a subclass of + * {@link AbstractContextLoaderInitializer}. + * + *

+ * By default the {@link DelegatingFilterProxy} is registered without support, + * but can be enabled by overriding {@link #isAsyncSecuritySupported()} and + * {@link #getSecurityDispatcherTypes()}. + *

+ * + *

+ * Additional configuration before and after the springSecurityFilterChain can + * be added by overriding + * {@link #beforeSpringSecurityFilterChain(ServletContext)} and + * {@link #afterSpringSecurityFilterChain(ServletContext)}. + *

+ * + * + *

Caveats

+ *

+ * Subclasses of AbstractDispatcherServletInitializer will register their + * filters before any other {@link Filter}. This means that you will typically + * want to ensure subclasses of AbstractDispatcherServletInitializer are invoked + * first. This can be done by ensuring the {@link Order} or {@link Ordered} of + * AbstractDispatcherServletInitializer are sooner than subclasses of + * {@link AbstractSecurityWebApplicationInitializer}. + *

+ * + * @author Rob Winch + */ +public abstract class AbstractSecurityWebApplicationInitializer implements WebApplicationInitializer { + + private static final String SERVLET_CONTEXT_PREFIX = "org.springframework.web.servlet.FrameworkServlet.CONTEXT."; + + /* (non-Javadoc) + * @see org.springframework.web.WebApplicationInitializer#onStartup(javax.servlet.ServletContext) + */ + @Override + public final void onStartup(ServletContext servletContext) + throws ServletException { + if(enableHttpSessionEventPublisher()) { + servletContext.addListener(HttpSessionEventPublisher.class); + } + insertSpringSecurityFilterChain(servletContext); + afterSpringSecurityFilterChain(servletContext); + } + + /** + * Override this if {@link HttpSessionEventPublisher} should be added as a + * listener. This should be true, if session management has specified a + * maximum number of sessions. + * + * @return true to add {@link HttpSessionEventPublisher}, else false + */ + protected boolean enableHttpSessionEventPublisher() { + return false; + } + + /** + * Registers the springSecurityFilterChain + * @param servletContext the {@link ServletContext} + */ + private void insertSpringSecurityFilterChain(ServletContext servletContext) { + String filterName = "springSecurityFilterChain"; + DelegatingFilterProxy springSecurityFilterChain = new DelegatingFilterProxy(filterName); + String contextAttribute = getWebApplicationContextAttribute(); + if(contextAttribute != null) { + springSecurityFilterChain.setContextAttribute(contextAttribute); + } + registerFilter(servletContext, true, filterName, springSecurityFilterChain); + } + + /** + * Inserts the provided {@link Filter}s before existing {@link Filter}s + * using default generated names, {@link #getSecurityDispatcherTypes()}, and + * {@link #isAsyncSecuritySupported()}. + * + * @param servletContext + * the {@link ServletContext} to use + * @param filters + * the {@link Filter}s to register + */ + protected final void insertFilters(ServletContext servletContext,Filter... filters) { + registerFilters(servletContext, true, filters); + } + + /** + * Inserts the provided {@link Filter}s after existing {@link Filter}s + * using default generated names, {@link #getSecurityDispatcherTypes()}, and + * {@link #isAsyncSecuritySupported()}. + * + * @param servletContext + * the {@link ServletContext} to use + * @param filters + * the {@link Filter}s to register + */ + protected final void appendFilters(ServletContext servletContext,Filter... filters) { + registerFilters(servletContext, false, filters); + } + + /** + * Registers the provided {@link Filter}s using default generated names, + * {@link #getSecurityDispatcherTypes()}, and + * {@link #isAsyncSecuritySupported()}. + * + * @param servletContext + * the {@link ServletContext} to use + * @param insertBeforeOtherFilters + * if true, will insert the provided {@link Filter}s before other + * {@link Filter}s. Otherwise, will insert the {@link Filter}s + * after other {@link Filter}s. + * @param filters + * the {@link Filter}s to register + */ + private void registerFilters(ServletContext servletContext, boolean insertBeforeOtherFilters, Filter... filters) { + Assert.notEmpty(filters, "filters cannot be null or empty"); + + for(Filter filter : filters) { + if(filter == null) { + throw new IllegalArgumentException("filters cannot contain null values. Got " + Arrays.asList(filters)); + } + String filterName = Conventions.getVariableName(filter); + registerFilter(servletContext, insertBeforeOtherFilters, filterName, filter); + } + } + + /** + * Registers the provided filter using the {@link #isAsyncSecuritySupported()} and {@link #getSecurityDispatcherTypes()}. + * + * @param servletContext + * @param insertBeforeOtherFilters should this Filter be inserted before or after other {@link Filter} + * @param filterName + * @param filter + */ + private final void registerFilter(ServletContext servletContext, boolean insertBeforeOtherFilters, String filterName, Filter filter) { + Dynamic registration = servletContext.addFilter(filterName, filter); + if(registration == null) { + throw new IllegalStateException("Duplicate Filter registration for '" + filterName +"'. Check to ensure the Filter is only configured once."); + } + registration.setAsyncSupported(isAsyncSecuritySupported()); + EnumSet dispatcherTypes = getSecurityDispatcherTypes(); + registration.addMappingForUrlPatterns(dispatcherTypes, !insertBeforeOtherFilters, "/*"); + } + + /** + * Returns the {@link DelegatingFilterProxy#getContextAttribute()} or null + * if the parent {@link ApplicationContext} should be used. The default + * behavior is to use the parent {@link ApplicationContext}. + * + *

+ * If {@link #getDispatcherWebApplicationContextSuffix()} is non-null the + * {@link WebApplicationContext} for the Dispatcher will be used. This means + * the child {@link ApplicationContext} is used to look up the + * springSecurityFilterChain bean. + *

+ * + * @return the {@link DelegatingFilterProxy#getContextAttribute()} or null + * if the parent {@link ApplicationContext} should be used + */ + private String getWebApplicationContextAttribute() { + String dispatcherServletName = getDispatcherWebApplicationContextSuffix(); + if(dispatcherServletName == null) { + return null; + } + return SERVLET_CONTEXT_PREFIX + dispatcherServletName; + } + + /** + * Return the to use the DispatcherServlet's + * {@link WebApplicationContext} to find the {@link DelegatingFilterProxy} + * or null to use the parent {@link ApplicationContext}. + * + *

+ * For example, if you are using AbstractDispatcherServletInitializer or + * AbstractAnnotationConfigDispatcherServletInitializer and using the + * provided Servlet name, you can return "dispatcher" from this method to + * use the DispatcherServlet's {@link WebApplicationContext}. + *

+ * + * @return the of the DispatcherServlet to use its + * {@link WebApplicationContext} or null (default) to use the parent + * {@link ApplicationContext}. + */ + protected String getDispatcherWebApplicationContextSuffix() { + return null; + } + + /** + * Invoked after the springSecurityFilterChain is added. + * @param servletContext the {@link ServletContext} + */ + protected void afterSpringSecurityFilterChain(ServletContext servletContext) { + + } + + /** + * Get the {@link DispatcherType} for the springSecurityFilterChain. + * @return + */ + protected EnumSet getSecurityDispatcherTypes() { + return EnumSet.of(DispatcherType.REQUEST, DispatcherType.ERROR); + } + + /** + * Determine if the springSecurityFilterChain should be marked as supporting + * asynch. Default is true. + * + * @return true if springSecurityFilterChain should be marked as supporting + * asynch + */ + protected boolean isAsyncSecuritySupported() { + return true; + } + +}