From 5738a5104046d34c3bfc51766a93ff70c5e96925 Mon Sep 17 00:00:00 2001 From: Luke Taylor Date: Mon, 28 Jan 2008 00:47:34 +0000 Subject: [PATCH] SEC-651: Support for ldap-user-service bean. --- .../security/config/Elements.java | 1 + .../security/config/LdapConfigUtils.java | 51 ++++++++++++ .../LdapProviderBeanDefinitionParser.java | 52 ++----------- .../LdapUserServiceBeanDefinitionParser.java | 78 +++++++++++++++++++ .../config/SecurityNamespaceHandler.java | 2 + .../security/config/spring-security-2.0.rnc | 21 ++++- .../security/config/spring-security-2.0.xsd | 35 ++++++++- ...pUserServiceBeanDefinitionParserTests.java | 44 +++++++++++ .../applicationContext-security-ns.xml | 5 ++ 9 files changed, 241 insertions(+), 48 deletions(-) create mode 100644 core/src/main/java/org/springframework/security/config/LdapConfigUtils.java create mode 100644 core/src/main/java/org/springframework/security/config/LdapUserServiceBeanDefinitionParser.java create mode 100644 core/src/test/java/org/springframework/security/config/LdapUserServiceBeanDefinitionParserTests.java diff --git a/core/src/main/java/org/springframework/security/config/Elements.java b/core/src/main/java/org/springframework/security/config/Elements.java index d6319c65d7..2099d52c92 100644 --- a/core/src/main/java/org/springframework/security/config/Elements.java +++ b/core/src/main/java/org/springframework/security/config/Elements.java @@ -16,6 +16,7 @@ abstract class Elements { public static final String HTTP = "http"; public static final String LDAP_PROVIDER = "ldap-authentication-provider"; public static final String LDAP_SERVER = "ldap-server"; + public static final String LDAP_USER_SERVICE = "ldap-user-service"; public static final String PROTECT = "protect"; public static final String CONCURRENT_SESSIONS = "concurrent-session-control"; public static final String LOGOUT = "logout"; diff --git a/core/src/main/java/org/springframework/security/config/LdapConfigUtils.java b/core/src/main/java/org/springframework/security/config/LdapConfigUtils.java new file mode 100644 index 0000000000..3435ffb92c --- /dev/null +++ b/core/src/main/java/org/springframework/security/config/LdapConfigUtils.java @@ -0,0 +1,51 @@ +package org.springframework.security.config; + +import org.springframework.security.ldap.SpringSecurityContextSource; +import org.springframework.beans.factory.config.BeanFactoryPostProcessor; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.beans.BeansException; +import org.springframework.core.Ordered; + +import java.util.Map; + +/** + * @author Luke Taylor + * @version $Id$ + * @since 2.0 + */ +class LdapConfigUtils { + + /** Checks for the presence of a ContextSource instance */ + private static class ContextSourceSettingPostProcessor implements BeanFactoryPostProcessor, Ordered { + public void postProcessBeanFactory(ConfigurableListableBeanFactory bf) throws BeansException { + Map beans = bf.getBeansOfType(SpringSecurityContextSource.class); + + if (beans.size() == 0) { + throw new SecurityConfigurationException("No SpringSecurityContextSource instances found. Have you " + + "added an <" + Elements.LDAP_SERVER + " /> element to your application context?"); + } + +// else if (beans.size() > 1) { +// throw new SecurityConfigurationException("More than one SpringSecurityContextSource instance found. " + +// "Please specify a specific server id when configuring your <" + Elements.LDAP_PROVIDER + "> " + +// "or <" + Elements.LDAP_USER_SERVICE + ">."); +// } + } + + public int getOrder() { + return LOWEST_PRECEDENCE; + } + } + + static void registerPostProcessorIfNecessary(BeanDefinitionRegistry registry) { + if (registry.containsBeanDefinition(BeanIds.CONTEXT_SOURCE_SETTING_POST_PROCESSOR)) { + return; + } + + registry.registerBeanDefinition(BeanIds.CONTEXT_SOURCE_SETTING_POST_PROCESSOR, + new RootBeanDefinition(ContextSourceSettingPostProcessor.class)); + } + +} diff --git a/core/src/main/java/org/springframework/security/config/LdapProviderBeanDefinitionParser.java b/core/src/main/java/org/springframework/security/config/LdapProviderBeanDefinitionParser.java index 129586d5a6..09b15ec3a0 100644 --- a/core/src/main/java/org/springframework/security/config/LdapProviderBeanDefinitionParser.java +++ b/core/src/main/java/org/springframework/security/config/LdapProviderBeanDefinitionParser.java @@ -1,34 +1,22 @@ package org.springframework.security.config; import org.springframework.security.ldap.populator.DefaultLdapAuthoritiesPopulator; -import org.springframework.security.ldap.SpringSecurityContextSource; import org.springframework.security.providers.ldap.LdapAuthenticationProvider; import org.springframework.security.providers.ldap.authenticator.BindAuthenticator; -import org.springframework.security.intercept.method.aopalliance.MethodSecurityInterceptor; -import org.springframework.security.ui.rememberme.RememberMeServices; import org.springframework.beans.factory.config.BeanDefinition; -import org.springframework.beans.factory.config.BeanFactoryPostProcessor; -import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; import org.springframework.beans.factory.config.RuntimeBeanReference; import org.springframework.beans.factory.support.RootBeanDefinition; -import org.springframework.beans.factory.support.BeanDefinitionRegistry; import org.springframework.beans.factory.xml.BeanDefinitionParser; import org.springframework.beans.factory.xml.ParserContext; -import org.springframework.beans.BeansException; -import org.springframework.core.Ordered; -import org.springframework.ldap.core.ContextSource; import org.springframework.util.StringUtils; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.w3c.dom.Element; -import java.util.Map; - /** * Experimental "security:ldap" namespace configuration. * - * * @author Luke Taylor * @version $Id$ * @since 2.0 @@ -40,7 +28,8 @@ public class LdapProviderBeanDefinitionParser implements BeanDefinitionParser { private static final String ATT_SERVER = "server-ref"; private static final String OPT_DEFAULT_DN_PATTERN = "uid={0},ou=people"; - private static final String DEFAULT_GROUP_CONTEXT = "ou=groups"; + private static final String DEF_GROUP_CONTEXT = "ou=groups"; + private static final String DEF_GROUP_SEARCH_FILTER = "(uniqueMember={0})"; public BeanDefinition parse(Element elt, ParserContext parserContext) { @@ -57,47 +46,18 @@ public class LdapProviderBeanDefinitionParser implements BeanDefinitionParser { bindAuthenticator.getPropertyValues().addPropertyValue("userDnPatterns", new String[] {OPT_DEFAULT_DN_PATTERN}); RootBeanDefinition authoritiesPopulator = new RootBeanDefinition(DefaultLdapAuthoritiesPopulator.class); authoritiesPopulator.getConstructorArgumentValues().addGenericArgumentValue(contextSource); - authoritiesPopulator.getConstructorArgumentValues().addGenericArgumentValue(DEFAULT_GROUP_CONTEXT); + authoritiesPopulator.getConstructorArgumentValues().addGenericArgumentValue(DEF_GROUP_CONTEXT); + // TODO: Change to using uniqueMember as default +// authoritiesPopulator.getPropertyValues().addPropertyValue("groupSearchFilter", DEF_GROUP_SEARCH_FILTER); RootBeanDefinition ldapProvider = new RootBeanDefinition(LdapAuthenticationProvider.class); ldapProvider.getConstructorArgumentValues().addGenericArgumentValue(bindAuthenticator); ldapProvider.getConstructorArgumentValues().addGenericArgumentValue(authoritiesPopulator); - registerPostProcessorIfNecessary(parserContext.getRegistry()); + LdapConfigUtils.registerPostProcessorIfNecessary(parserContext.getRegistry()); ConfigUtils.getRegisteredProviders(parserContext).add(ldapProvider); return null; } - - // Todo: Move to utility class when we add ldap-user-service, as this check will be needed even if no - // provider is added. - private static class ContextSourceSettingPostProcessor implements BeanFactoryPostProcessor, Ordered { - - public void postProcessBeanFactory(ConfigurableListableBeanFactory bf) throws BeansException { - Map beans = bf.getBeansOfType(SpringSecurityContextSource.class); - - if (beans.size() == 0) { - throw new SecurityConfigurationException("No SpringSecurityContextSource instances found. Have you " + - "added an <" + Elements.LDAP_SERVER + " /> element to your application context?"); - } else if (beans.size() > 1) { - throw new SecurityConfigurationException("More than one SpringSecurityContextSource instance found. " + - "Please specify a specific server id when configuring your <" + Elements.LDAP_PROVIDER + ">"); - } - } - - public int getOrder() { - return LOWEST_PRECEDENCE; - } - - } - - public void registerPostProcessorIfNecessary(BeanDefinitionRegistry registry) { - if (registry.containsBeanDefinition(BeanIds.CONTEXT_SOURCE_SETTING_POST_PROCESSOR)) { - return; - } - - registry.registerBeanDefinition(BeanIds.CONTEXT_SOURCE_SETTING_POST_PROCESSOR, - new RootBeanDefinition(LdapProviderBeanDefinitionParser.ContextSourceSettingPostProcessor.class)); - } } diff --git a/core/src/main/java/org/springframework/security/config/LdapUserServiceBeanDefinitionParser.java b/core/src/main/java/org/springframework/security/config/LdapUserServiceBeanDefinitionParser.java new file mode 100644 index 0000000000..aa7157843e --- /dev/null +++ b/core/src/main/java/org/springframework/security/config/LdapUserServiceBeanDefinitionParser.java @@ -0,0 +1,78 @@ +package org.springframework.security.config; + +import org.springframework.security.userdetails.ldap.LdapUserDetailsService; +import org.springframework.security.ldap.search.FilterBasedLdapUserSearch; +import org.springframework.security.ldap.populator.DefaultLdapAuthoritiesPopulator; +import org.springframework.beans.factory.xml.ParserContext; +import org.springframework.beans.factory.support.BeanDefinitionBuilder; +import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.beans.factory.config.RuntimeBeanReference; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.util.StringUtils; +import org.springframework.util.Assert; + +import org.w3c.dom.Element; + +/** + * @author Luke Taylor + * @version $Id$ + * @since 2.0 + */ +public class LdapUserServiceBeanDefinitionParser extends AbstractUserDetailsServiceBeanDefinitionParser { + private static final String ATT_SERVER = "server-ref"; + public static final String ATT_USER_SEARCH_FILTER = "user-search-filter"; + public static final String ATT_USER_SEARCH_BASE = "user-search-base"; + public static final String DEF_USER_SEARCH_BASE = ""; + + public static final String ATT_GROUP_SEARCH_FILTER = "group-search-filter"; + public static final String ATT_GROUP_SEARCH_BASE = "group-search-base"; + public static final String DEF_GROUP_SEARCH_FILTER = "(uniqueMember={0})"; + public static final String DEF_GROUP_SEARCH_BASE = "ou=groups"; + + protected Class getBeanClass(Element element) { + return LdapUserDetailsService.class; + } + + protected void doParse(Element elt, ParserContext parserContext, BeanDefinitionBuilder builder) { + String server = elt.getAttribute(ATT_SERVER); + + if (!StringUtils.hasText(server)) { + server = BeanIds.CONTEXT_SOURCE; + } + + String userSearchFilter = elt.getAttribute(ATT_USER_SEARCH_FILTER); + Assert.hasText(userSearchFilter, "User search filter must be supplied"); + String userSearchBase = elt.getAttribute(ATT_USER_SEARCH_BASE); + + if (!StringUtils.hasText(userSearchBase)) { + userSearchBase = DEF_USER_SEARCH_BASE; + } + + String groupSearchFilter = elt.getAttribute(ATT_GROUP_SEARCH_FILTER); + String groupSearchBase = elt.getAttribute(ATT_GROUP_SEARCH_BASE); + + if (!StringUtils.hasText(groupSearchFilter)) { + groupSearchFilter = DEF_GROUP_SEARCH_FILTER; + } + + if (!StringUtils.hasText(groupSearchBase)) { + groupSearchBase = DEF_GROUP_SEARCH_BASE; + } + + RuntimeBeanReference contextSource = new RuntimeBeanReference(server); + BeanDefinition search = new RootBeanDefinition(FilterBasedLdapUserSearch.class); + search.getConstructorArgumentValues().addIndexedArgumentValue(0, userSearchBase); + search.getConstructorArgumentValues().addIndexedArgumentValue(1, userSearchFilter); + search.getConstructorArgumentValues().addIndexedArgumentValue(2, contextSource); + + BeanDefinition populator = new RootBeanDefinition(DefaultLdapAuthoritiesPopulator.class); + populator.getConstructorArgumentValues().addIndexedArgumentValue(0, contextSource); + populator.getConstructorArgumentValues().addIndexedArgumentValue(1, groupSearchBase); + populator.getPropertyValues().addPropertyValue("groupSearchFilter", groupSearchFilter); + + builder.addConstructorArg(search); + builder.addConstructorArg(populator); + + LdapConfigUtils.registerPostProcessorIfNecessary(parserContext.getRegistry()); + } +} diff --git a/core/src/main/java/org/springframework/security/config/SecurityNamespaceHandler.java b/core/src/main/java/org/springframework/security/config/SecurityNamespaceHandler.java index af175ce2ed..a53a8119a2 100644 --- a/core/src/main/java/org/springframework/security/config/SecurityNamespaceHandler.java +++ b/core/src/main/java/org/springframework/security/config/SecurityNamespaceHandler.java @@ -7,6 +7,7 @@ import org.springframework.beans.factory.xml.NamespaceHandlerSupport; * * @author Luke Taylor * @author Ben Alex + * @since 2.0 * @version $Id$ */ public class SecurityNamespaceHandler extends NamespaceHandlerSupport { @@ -15,6 +16,7 @@ public class SecurityNamespaceHandler extends NamespaceHandlerSupport { // Parsers registerBeanDefinitionParser(Elements.LDAP_PROVIDER, new LdapProviderBeanDefinitionParser()); registerBeanDefinitionParser(Elements.LDAP_SERVER, new LdapServerBeanDefinitionParser()); + registerBeanDefinitionParser(Elements.LDAP_USER_SERVICE, new LdapUserServiceBeanDefinitionParser()); registerBeanDefinitionParser(Elements.HTTP, new HttpSecurityBeanDefinitionParser()); registerBeanDefinitionParser(Elements.USER_SERVICE, new UserServiceBeanDefinitionParser()); registerBeanDefinitionParser(Elements.JDBC_USER_SERVICE, new JdbcUserServiceBeanDefinitionParser()); diff --git a/core/src/main/resources/org/springframework/security/config/spring-security-2.0.rnc b/core/src/main/resources/org/springframework/security/config/spring-security-2.0.rnc index c9819bbf77..7d4ee361c0 100644 --- a/core/src/main/resources/org/springframework/security/config/spring-security-2.0.rnc +++ b/core/src/main/resources/org/springframework/security/config/spring-security-2.0.rnc @@ -66,6 +66,25 @@ ldap-server.attlist &= ## Optional root suffix for the embedded LDAP server. Default is "dc=springframework,dc=org" attribute root { xsd:string }? +ldap-user-service = + element ldap-user-service {ldap-us.attlist} +ldap-us.attlist &= id? +ldap-us.attlist &= + ## The optional server to use. If omitted, and a default LDAP server is registered (using with no Id), that server will be used. + attribute server-ref {xsd:string}? +ldap-us.attlist &= + attribute user-search-filter {xsd:string} +ldap-us.attlist &= + ## Search base for user searches. Defaults to "". + attribute user-search-base {xsd:string}? +ldap-us.attlist &= + ## Group search filter. Defaults to (uniqueMember={0}). + attribute group-search-filter {xsd:string}? +ldap-us.attlist &= + ## Search base for group membership searches. Defaults to "ou=groups". + attribute group-search-base {xsd:string}? + + ldap-authentication-provider = ## Sets up an ldap authentication provider @@ -249,7 +268,7 @@ x509.attlist &= authentication-provider = ## Indicates that the contained user-service should be used as an authentication source. - element authentication-provider {ap.attlist & (user-service | jdbc-user-service) & password-encoder} + element authentication-provider {ap.attlist & (user-service | jdbc-user-service | ldap-user-service) & password-encoder} ap.attlist &= ## Specifies a reference to a separately configured UserDetailsService from which to obtain authentication data. user-service-ref? diff --git a/core/src/main/resources/org/springframework/security/config/spring-security-2.0.xsd b/core/src/main/resources/org/springframework/security/config/spring-security-2.0.xsd index 721e68bcc0..32a1598262 100644 --- a/core/src/main/resources/org/springframework/security/config/spring-security-2.0.xsd +++ b/core/src/main/resources/org/springframework/security/config/spring-security-2.0.xsd @@ -186,6 +186,39 @@ + + + + + + + + + A bean identifier, used for referring to the bean elsewhere in the context. + + + + + The optional server to use. If omitted, and a default LDAP server is registered (using <ldap-server> with no Id), that server will be used. + + + + + + Search base for user searches. Defaults to "". + + + + + Group search filter. Defaults to (uniqueMember={0}). + + + + + Search base for group membership searches. Defaults to "ou=groups". + + + Sets up an ldap authentication provider @@ -578,7 +611,7 @@ - + The regular expression used to obtain the username from the certificate's subject. Defaults to matching on the common name using the pattern "CN=(.*?),". diff --git a/core/src/test/java/org/springframework/security/config/LdapUserServiceBeanDefinitionParserTests.java b/core/src/test/java/org/springframework/security/config/LdapUserServiceBeanDefinitionParserTests.java new file mode 100644 index 0000000000..3dbf5efd15 --- /dev/null +++ b/core/src/test/java/org/springframework/security/config/LdapUserServiceBeanDefinitionParserTests.java @@ -0,0 +1,44 @@ +package org.springframework.security.config; + +import org.springframework.security.util.InMemoryXmlApplicationContext; +import org.springframework.security.userdetails.UserDetailsService; +import org.springframework.security.userdetails.UserDetails; + +import org.junit.Test; +import org.junit.After; +import static org.junit.Assert.assertEquals; + +/** + * @author Luke Taylor + * @version $Id$ + */ +public class LdapUserServiceBeanDefinitionParserTests { + private InMemoryXmlApplicationContext appCtx; + + @After + public void closeAppContext() { + if (appCtx != null) { + appCtx.close(); + appCtx = null; + } + } + + @Test + public void minimalConfigurationIsParsedOk() throws Exception { + setContext(""); + } + + @Test + public void userServiceReturnsExpectedData() throws Exception { + setContext(""); + + UserDetailsService uds = (UserDetailsService) appCtx.getBean("ldapUDS"); + UserDetails ben = uds.loadUserByUsername("ben"); + + assertEquals(2, ben.getAuthorities().length); + } + + private void setContext(String context) { + appCtx = new InMemoryXmlApplicationContext(context); + } +} diff --git a/samples/tutorial/src/main/webapp/WEB-INF/applicationContext-security-ns.xml b/samples/tutorial/src/main/webapp/WEB-INF/applicationContext-security-ns.xml index 2df27f8d4f..93d97696b0 100644 --- a/samples/tutorial/src/main/webapp/WEB-INF/applicationContext-security-ns.xml +++ b/samples/tutorial/src/main/webapp/WEB-INF/applicationContext-security-ns.xml @@ -57,8 +57,12 @@ scott/wombat --> @@ -69,4 +73,5 @@ + \ No newline at end of file