diff --git a/core/pom.xml b/core/pom.xml index 8e0ea3288b..d128b428d7 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -116,19 +116,22 @@ org.apache.directory.server apacheds-core 1.0.2 - test + compile + true org.apache.directory.server apacheds-server-jndi 1.0.2 - test + compile + true org.slf4j slf4j-log4j12 1.4.3 - test + runtime + true jmock diff --git a/core/src/main/java/org/springframework/security/config/ApacheDSStartStopBean.java b/core/src/main/java/org/springframework/security/config/ApacheDSStartStopBean.java new file mode 100644 index 0000000000..2b6797539f --- /dev/null +++ b/core/src/main/java/org/springframework/security/config/ApacheDSStartStopBean.java @@ -0,0 +1,122 @@ +package org.springframework.security.config; + +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.DisposableBean; +import org.springframework.beans.BeansException; +import org.springframework.context.ApplicationContextAware; +import org.springframework.context.ApplicationContext; +import org.springframework.core.io.Resource; +import org.springframework.ldap.core.ContextSource; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.apache.directory.server.configuration.MutableServerStartupConfiguration; +import org.apache.directory.server.jndi.ServerContextFactory; +import org.apache.directory.server.protocol.shared.store.LdifFileLoader; +import org.apache.directory.server.core.configuration.ShutdownConfiguration; + +import javax.naming.Context; +import javax.naming.InitialContext; +import javax.naming.directory.DirContext; +import javax.naming.directory.InitialDirContext; +import java.util.Properties; +import java.io.File; + +/** + * Starts and stops the embedded apacheDS server defined by the supplied configuration. + * Used by {@link LdapBeanDefinitionParser}. + * + * @author Luke Taylor + * @version $Id$ + */ +class ApacheDSStartStopBean implements InitializingBean, DisposableBean, ApplicationContextAware { + private Log logger = LogFactory.getLog(getClass()); + + private MutableServerStartupConfiguration configuration; + private ApplicationContext ctxt; + private File workingDir; + + public ApacheDSStartStopBean(MutableServerStartupConfiguration configuration) { + this.configuration = configuration; + } + + public void afterPropertiesSet() throws Exception { + Properties env = new Properties(); + String apacheWorkDir = System.getProperty("apacheDSWorkDir"); + + if (apacheWorkDir == null) { + apacheWorkDir = System.getProperty("java.io.tmpdir") + File.separator + "apacheds-spring-security"; + } + + workingDir = new File(apacheWorkDir); + +// if (workingDir.exists()) { +// logger.info("Deleting existing working directory " + workingDir.getAbsolutePath()); +// deleteDir(workingDir); +// } + + configuration.setWorkingDirectory(workingDir); + + env.put(Context.INITIAL_CONTEXT_FACTORY, ServerContextFactory.class.getName()); + env.setProperty(Context.SECURITY_AUTHENTICATION, "simple"); + env.setProperty(Context.SECURITY_PRINCIPAL, "uid=admin,ou=system"); + env.setProperty(Context.SECURITY_CREDENTIALS, "secret"); + env.putAll(configuration.toJndiEnvironment()); + + DirContext serverContext = new InitialDirContext(env); + + // Import any ldif files + Resource[] ldifs = ctxt.getResources("classpath:*.ldif"); + + + DirContext dirContext = ((ContextSource)ctxt.getBean("contextSource")).getReadWriteContext(); + + if(ldifs != null && ldifs.length > 0) { + try { + String ldifFile = ldifs[0].getFile().getAbsolutePath(); + LdifFileLoader loader = new LdifFileLoader(dirContext, ldifFile); + loader.execute(); + } finally { + dirContext.close(); + } + } + + } + + public void destroy() throws Exception { + Properties env = new Properties(); + env.setProperty(Context.INITIAL_CONTEXT_FACTORY, ServerContextFactory.class.getName()); + env.setProperty(Context.SECURITY_AUTHENTICATION, "simple"); + env.setProperty(Context.SECURITY_PRINCIPAL, "uid=admin,ou=system"); + env.setProperty(Context.SECURITY_CREDENTIALS, "secret"); + + ShutdownConfiguration shutdown = new ShutdownConfiguration(); + env.putAll(shutdown.toJndiEnvironment()); + + logger.info("Shutting down server..."); + new InitialContext(env); + + if (workingDir.exists()) { + logger.info("Deleting working directory after shutting down " + workingDir.getAbsolutePath()); + deleteDir(workingDir); + } + + } + + public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { + ctxt = applicationContext; + } + + public static boolean deleteDir(File dir) { + if (dir.isDirectory()) { + String[] children = dir.list(); + for (int i=0; i < children.length; i++) { + boolean success = deleteDir(new File(dir, children[i])); + if (!success) { + return false; + } + } + } + + return dir.delete(); + } +} diff --git a/core/src/main/java/org/springframework/security/config/LdapBeanDefinitionParser.java b/core/src/main/java/org/springframework/security/config/LdapBeanDefinitionParser.java new file mode 100644 index 0000000000..42485f8b4e --- /dev/null +++ b/core/src/main/java/org/springframework/security/config/LdapBeanDefinitionParser.java @@ -0,0 +1,182 @@ +package org.springframework.security.config; + +import org.springframework.beans.factory.xml.ParserContext; +import org.springframework.beans.factory.xml.AbstractBeanDefinitionParser; +import org.springframework.beans.factory.support.AbstractBeanDefinition; +import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.beans.factory.BeanDefinitionStoreException; +import org.springframework.util.StringUtils; +import org.springframework.security.ldap.DefaultInitialDirContextFactory; +import org.springframework.security.providers.ldap.LdapAuthenticationProvider; +import org.springframework.security.providers.ldap.populator.DefaultLdapAuthoritiesPopulator; +import org.springframework.security.providers.ldap.authenticator.BindAuthenticator; +import org.springframework.ldap.core.DirContextAdapter; +import org.w3c.dom.Element; +import org.apache.directory.server.configuration.MutableServerStartupConfiguration; +import org.apache.directory.server.core.partition.impl.btree.MutableBTreePartitionConfiguration; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import javax.naming.NamingException; +import java.util.HashSet; + +/** + * Experimental "security:ldap" namespace configuration. + * + * + * @author Luke Taylor + * @version $Id$ + * @since 2.0 + */ +public class LdapBeanDefinitionParser extends AbstractBeanDefinitionParser { + private Log logger = LogFactory.getLog(getClass()); + + /** Defines the Url of the ldap server to use. If not specified, an embedded apache DS instance will be created */ + private static final String URL_ATTRIBUTE = "url"; + private static final String AUTH_TYPE_ATTRIBUTE = "auth"; + // TODO: Setting login/passwords for non embedded server. + private static final String PRINCIPAL_ATTRIBUTE = "managerDn"; + private static final String PASSWORD_ATTRIBUTE = "managerPassword"; + + // Properties which apply to embedded server only - when no Url is set + + /** sets the configuration suffix (default is "dc=springframework,dc=org"). */ + public static final String ROOT_SUFFIX_ATTRIBUTE = "root"; + + /** + * Optionally defines an ldif resource to be loaded. Otherwise an attempt will be made to load all ldif files + * found on the classpath. + */ + public static final String LDIF_FILE_ATTRIBUTE = "ldif"; + + // Defaults + private static final String DEFAULT_ROOT_SUFFIX = "dc=springframework,dc=org"; + + private static final String DEFAULT_PROVIDER_BEAN_ID = "_ldapProvider"; + + private static final String DEFAULT_DN_PATTERN = "uid={0},ou=people"; + + private static final String DEFAULT_GROUP_CONTEXT = "ou=groups"; + + + protected AbstractBeanDefinition parseInternal(Element elt, ParserContext parserContext) { + String url = elt.getAttribute(URL_ATTRIBUTE); + + RootBeanDefinition initialDirContextFactory; + + if (!StringUtils.hasText(url)) { + initialDirContextFactory = createEmbeddedServer(elt, parserContext); + } else { + initialDirContextFactory = new RootBeanDefinition(DefaultInitialDirContextFactory.class); + initialDirContextFactory.getConstructorArgumentValues().addIndexedArgumentValue(0, url); + } + + // TODO: Make these default values for 2.0 + initialDirContextFactory.getPropertyValues().addPropertyValue("useLdapContext", Boolean.TRUE); + initialDirContextFactory.getPropertyValues().addPropertyValue("dirObjectFactory", "org.springframework.ldap.core.support.DefaultDirObjectFactory"); + + String id = elt.getAttribute(ID_ATTRIBUTE); + String contextSourceId = "contextSource"; + + if (StringUtils.hasText(id)) { + contextSourceId = id + "." + contextSourceId; + } + + if (parserContext.getRegistry().containsBeanDefinition(contextSourceId)) { + logger.warn("Bean already exists with Id '" + contextSourceId + "'"); + } + + parserContext.getRegistry().registerBeanDefinition(contextSourceId, initialDirContextFactory); + + RootBeanDefinition bindAuthenticator = new RootBeanDefinition(BindAuthenticator.class); + bindAuthenticator.getConstructorArgumentValues().addGenericArgumentValue(initialDirContextFactory); + bindAuthenticator.getPropertyValues().addPropertyValue("userDnPatterns", new String[] {DEFAULT_DN_PATTERN}); + RootBeanDefinition authoritiesPopulator = new RootBeanDefinition(DefaultLdapAuthoritiesPopulator.class); + authoritiesPopulator.getConstructorArgumentValues().addGenericArgumentValue(initialDirContextFactory); + authoritiesPopulator.getConstructorArgumentValues().addGenericArgumentValue(DEFAULT_GROUP_CONTEXT); + + RootBeanDefinition ldapProvider = new RootBeanDefinition(LdapAuthenticationProvider.class); + ldapProvider.getConstructorArgumentValues().addGenericArgumentValue(bindAuthenticator); + ldapProvider.getConstructorArgumentValues().addGenericArgumentValue(authoritiesPopulator); + + return ldapProvider; + } + + + /** + * Will be called if no url attribute is supplied. + * + * Registers beans to create an embedded apache directory server. + * + * @param element + * @param parserContext + * + * @return the BeanDefinition for the ContextSource for the embedded server. + */ + private RootBeanDefinition createEmbeddedServer(Element element, ParserContext parserContext) { + MutableServerStartupConfiguration configuration = new MutableServerStartupConfiguration(); + MutableBTreePartitionConfiguration partition = new MutableBTreePartitionConfiguration(); + + partition.setName("springsecurity"); + + DirContextAdapter rootContext = new DirContextAdapter(); + rootContext.setAttributeValues("objectClass", new String[] {"top", "domain", "extensibleObject"}); + rootContext.setAttributeValue("dc", "springsecurity"); + + partition.setContextEntry(rootContext.getAttributes()); + + String suffix = element.getAttribute(ROOT_SUFFIX_ATTRIBUTE); + + if (!StringUtils.hasText(suffix)) { + suffix = DEFAULT_ROOT_SUFFIX; + } + + try { + partition.setSuffix(suffix); + } catch (NamingException e) { + // TODO: What exception should we be throwing here ? + + logger.error("Failed to set root name suffix to " + suffix, e); + } + + HashSet partitions = new HashSet(1); + partitions.add(partition); + + //TODO: Allow port configuration + configuration.setLdapPort(3389); + configuration.setContextPartitionConfigurations(partitions); + + RootBeanDefinition initialDirContextFactory = new RootBeanDefinition(DefaultInitialDirContextFactory.class); + initialDirContextFactory.getConstructorArgumentValues().addIndexedArgumentValue(0, "ldap://127.0.0.1:3389/" + suffix); + + initialDirContextFactory.getPropertyValues().addPropertyValue("managerDn", "uid=admin,ou=system"); + initialDirContextFactory.getPropertyValues().addPropertyValue("managerPassword", "secret"); + + RootBeanDefinition apacheDSStartStop = new RootBeanDefinition(ApacheDSStartStopBean.class); + apacheDSStartStop.getConstructorArgumentValues().addGenericArgumentValue(configuration); + + if (parserContext.getRegistry().containsBeanDefinition("_apacheDSStartStopBean")) { + //TODO: Appropriate exception + throw new IllegalArgumentException("Only one embedded server bean is allowed per application context"); + } + + parserContext.getRegistry().registerBeanDefinition("_apacheDSStartStopBean", apacheDSStartStop); + + + return initialDirContextFactory; + } + + + protected String resolveId(Element element, AbstractBeanDefinition definition, ParserContext parserContext) throws BeanDefinitionStoreException { + String id = super.resolveId(element, definition, parserContext); + + if (StringUtils.hasText(id)) { + return id; + } + + // TODO: Check for duplicate using default id here. + + return DEFAULT_PROVIDER_BEAN_ID; + } +} diff --git a/core/src/main/java/org/springframework/security/config/SecurityNamespaceHandler.java b/core/src/main/java/org/springframework/security/config/SecurityNamespaceHandler.java new file mode 100644 index 0000000000..ec7264ba82 --- /dev/null +++ b/core/src/main/java/org/springframework/security/config/SecurityNamespaceHandler.java @@ -0,0 +1,18 @@ +package org.springframework.security.config; + +import org.springframework.beans.factory.xml.NamespaceHandlerSupport; + + +/** + * Registers the bean definition parsers for the "security" namespace (http://www.springframework.org/schema/security). + * + * @author Luke Taylor + * @version $Id$ + */ +public class SecurityNamespaceHandler extends NamespaceHandlerSupport { + public void init() { + registerBeanDefinitionParser("ldap", new LdapBeanDefinitionParser()); + + + } +} 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 new file mode 100644 index 0000000000..0e2ef5b2ff --- /dev/null +++ b/core/src/main/resources/org/springframework/security/config/spring-security-2.0.xsd @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/core/src/test/java/org/springframework/security/config/LdapBeanDefinitionParserTests.java b/core/src/test/java/org/springframework/security/config/LdapBeanDefinitionParserTests.java new file mode 100644 index 0000000000..874df2e60a --- /dev/null +++ b/core/src/test/java/org/springframework/security/config/LdapBeanDefinitionParserTests.java @@ -0,0 +1,44 @@ +package org.springframework.security.config; + +import org.springframework.security.ldap.InitialDirContextFactory; +import org.springframework.ldap.core.LdapTemplate; +import org.springframework.context.support.ClassPathXmlApplicationContext; + +import static org.junit.Assert.*; +import org.junit.BeforeClass; +import org.junit.AfterClass; +import org.junit.Test; + + +/** + * @author luke + * @version $Id$ + */ +public class LdapBeanDefinitionParserTests { + private static ClassPathXmlApplicationContext appContext; + + @BeforeClass + public static void loadContext() { + appContext = new ClassPathXmlApplicationContext("org/springframework/security/config/ldap-embedded-default.xml"); + } + + @AfterClass + public static void closeContext() { + // Make sure apache ds shuts down + appContext.close(); + } + + @Test + public void testContextContainsExpectedBeansAndData() { + InitialDirContextFactory idcf = (InitialDirContextFactory) appContext.getBean("contextSource"); + + assertEquals("dc=springframework,dc=org", idcf.getRootDn()); + + // Check data is loaded + LdapTemplate template = new LdapTemplate(idcf); + + template.lookup("uid=ben,ou=people"); + + + } +} diff --git a/core/src/test/resources/log4j.properties b/core/src/test/resources/log4j.properties index 58c3bbb44a..916f223139 100644 --- a/core/src/test/resources/log4j.properties +++ b/core/src/test/resources/log4j.properties @@ -2,7 +2,7 @@ # # $Id$ -log4j.rootCategory=WARN, stdout +log4j.rootCategory=INFO, stdout log4j.appender.stdout=org.apache.log4j.ConsoleAppender log4j.appender.stdout.layout=org.apache.log4j.PatternLayout diff --git a/core/src/test/resources/org/springframework/security/config/ldap-embedded-default.xml b/core/src/test/resources/org/springframework/security/config/ldap-embedded-default.xml new file mode 100644 index 0000000000..48aead85d3 --- /dev/null +++ b/core/src/test/resources/org/springframework/security/config/ldap-embedded-default.xml @@ -0,0 +1,14 @@ + + + + + + + + + + \ No newline at end of file diff --git a/core/src/test/resources/test-server.ldif b/core/src/test/resources/test-server.ldif new file mode 100644 index 0000000000..f2caf17ca8 --- /dev/null +++ b/core/src/test/resources/test-server.ldif @@ -0,0 +1,56 @@ +dn: ou=groups,dc=springframework,dc=org +objectclass: top +objectclass: organizationalUnit +ou: groups + +dn: ou=subgroups,ou=groups,dc=springframework,dc=org +objectclass: top +objectclass: organizationalUnit +ou: subgroups + +dn: ou=people,dc=springframework,dc=org +objectclass: top +objectclass: organizationalUnit +ou: people + +dn: uid=ben,ou=people,dc=springframework,dc=org +objectclass: top +objectclass: person +objectclass: organizationalPerson +objectclass: inetOrgPerson +cn: Ben Alex +sn: Alex +uid: ben +userPassword: {SHA}nFCebWjxfaLbHHG1Qk5UU4trbvQ= + +dn: uid=bob,ou=people,dc=springframework,dc=org +objectclass: top +objectclass: person +objectclass: organizationalPerson +objectclass: inetOrgPerson +cn: Bob Hamilton +sn: Hamilton +uid: bob +userPassword: bobspassword + +dn: cn=developers,ou=groups,dc=springframework,dc=org +objectclass: top +objectclass: groupOfNames +cn: developers +ou: developer +member: uid=ben,ou=people,dc=springframework,dc=org +member: uid=bob,ou=people,dc=springframework,dc=org + +dn: cn=managers,ou=groups,dc=springframework,dc=org +objectclass: top +objectclass: groupOfNames +cn: managers +ou: manager +member: uid=ben,ou=people,dc=springframework,dc=org + +dn: cn=submanagers,ou=subgroups,ou=groups,dc=springframework,dc=org +objectclass: top +objectclass: groupOfNames +cn: submanagers +ou: submanager +member: uid=ben,ou=people,dc=springframework,dc=org diff --git a/src/docbkx/springsecurity.xml b/src/docbkx/springsecurity.xml index fef9cff7a3..d307c483dc 100644 --- a/src/docbkx/springsecurity.xml +++ b/src/docbkx/springsecurity.xml @@ -3062,10 +3062,10 @@ key: A private key to prevent modification of the remember-me token linkend="ldap-dircontextfactory">connecting to the LDAP server for more information on this). For example, if you are using an LDAP server specified by the URL - ldap://monkeymachine.co.uk/dc=acegisecurity,dc=org, + ldap://monkeymachine.co.uk/dc=springframework,dc=org, and have a pattern uid={0},ou=greatapes, then a login name of "gorilla" will map to a DN - uid=gorilla,ou=greatapes,dc=acegisecurity,dc=org. + uid=gorilla,ou=greatapes,dc=springframework,dc=org. Each configured DN pattern will be tried in turn until a match is found. For information on using a search, see the section on search objects below. A @@ -3168,8 +3168,8 @@ key: A private key to prevent modification of the remember-me token above, might look like this: <bean id="initialDirContextFactory" class="org.springframework.security.ldap.DefaultInitialDirContextFactory"> - <constructor-arg value="ldap://monkeymachine:389/dc=acegisecurity,dc=org"/> - <property name="managerDn"><value>cn=manager,dc=acegisecurity,dc=org</value></property> + <constructor-arg value="ldap://monkeymachine:389/dc=springframework,dc=org"/> + <property name="managerDn"><value>cn=manager,dc=springframework,dc=org</value></property> <property name="managerPassword"><value>password</value></property> </bean> @@ -3208,12 +3208,12 @@ key: A private key to prevent modification of the remember-me token This would set up the provider to access an LDAP server with URL - ldap://monkeymachine:389/dc=acegisecurity,dc=org. + ldap://monkeymachine:389/dc=springframework,dc=org. Authentication will be performed by attempting to bind with the DN - uid=<user-login-name>,ou=people,dc=acegisecurity,dc=org. + uid=<user-login-name>,ou=people,dc=springframework,dc=org. After successful authentication, roles will be assigned to the user by searching under the DN - ou=groups,dc=acegisecurity,dc=org with the default + ou=groups,dc=springframework,dc=org with the default filter (member=<user's-DN>). The role name will be taken from the ou attribute of each match.