Explorar el Código

SEC-271: Added namespace handler class and experimental LDAP parser. The latter creates an embedded Apache DS server if no server URL is supplied, so changed dependencies on the latter to compile-time/optional.

Luke Taylor hace 18 años
padre
commit
77b6503e2e

+ 6 - 3
core/pom.xml

@@ -116,19 +116,22 @@
 			<groupId>org.apache.directory.server</groupId>
 			<artifactId>apacheds-core</artifactId>
 			<version>1.0.2</version>
-			<scope>test</scope>
+			<scope>compile</scope>
+            <optional>true</optional>
 		</dependency>
 		<dependency>
 			<groupId>org.apache.directory.server</groupId>
 			<artifactId>apacheds-server-jndi</artifactId>
 			<version>1.0.2</version>
-			<scope>test</scope>
+			<scope>compile</scope>
+            <optional>true</optional>
 		</dependency>
         <dependency>
 			<groupId>org.slf4j</groupId>
 			<artifactId>slf4j-log4j12</artifactId>
 			<version>1.4.3</version>
-			<scope>test</scope>
+			<scope>runtime</scope>
+            <optional>true</optional>
 		</dependency>
 		<dependency>
 			<groupId>jmock</groupId>

+ 122 - 0
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();
+    }
+}

+ 182 - 0
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;
+    }
+}

+ 18 - 0
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());
+
+
+    }
+}

+ 22 - 0
core/src/main/resources/org/springframework/security/config/spring-security-2.0.xsd

@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+
+<xsd:schema xmlns="http://www.springframework.org/schema/security"
+	xmlns:xsd="http://www.w3.org/2001/XMLSchema"
+	targetNamespace="http://www.springframework.org/schema/security"
+	xmlns:beans="http://www.springframework.org/schema/beans"
+	elementFormDefault="qualified" attributeFormDefault="unqualified">
+
+
+    <xsd:import namespace="http://www.springframework.org/schema/beans" />
+
+	<xsd:element name="autoconfig" />
+
+
+	<xsd:element name="ldap">
+		<xsd:complexType>
+			<xsd:attribute name="url" type="xsd:string" />            
+			<xsd:attribute name="ldif" default="classpath:*.ldif"/>			
+		</xsd:complexType>
+	</xsd:element>    
+
+</xsd:schema>

+ 44 - 0
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");
+
+
+    }
+}

+ 1 - 1
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

+ 14 - 0
core/src/test/resources/org/springframework/security/config/ldap-embedded-default.xml

@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<beans xmlns="http://www.springframework.org/schema/beans"
+    xmlns:security="http://www.springframework.org/schema/security"
+    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+    xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.0.xsd
+http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security-2.0.xsd">
+
+
+    <security:ldap />
+    
+
+
+</beans>

+ 56 - 0
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

+ 7 - 7
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</link> for more information on this). For example, if you
             are using an LDAP server specified by the URL
-            <literal>ldap://monkeymachine.co.uk/dc=acegisecurity,dc=org</literal>,
+            <literal>ldap://monkeymachine.co.uk/dc=springframework,dc=org</literal>,
             and have a pattern <literal>uid={0},ou=greatapes</literal>, then a
             login name of "gorilla" will map to a DN
-            <literal>uid=gorilla,ou=greatapes,dc=acegisecurity,dc=org</literal>.
+            <literal>uid=gorilla,ou=greatapes,dc=springframework,dc=org</literal>.
             Each configured DN pattern will be tried in turn until a match is
             found. For information on using a search, see the section on <link
             linkend="ldap-searchobjects">search objects</link> below. A
@@ -3168,8 +3168,8 @@ key:              A private key to prevent modification of the remember-me token
         above, might look like this: <programlisting>
     &lt;bean id="initialDirContextFactory"
             class="org.springframework.security.ldap.DefaultInitialDirContextFactory"&gt;
-      &lt;constructor-arg value="ldap://monkeymachine:389/dc=acegisecurity,dc=org"/&gt;
-      &lt;property name="managerDn"&gt;&lt;value&gt;cn=manager,dc=acegisecurity,dc=org&lt;/value&gt;&lt;/property&gt;
+      &lt;constructor-arg value="ldap://monkeymachine:389/dc=springframework,dc=org"/&gt;
+      &lt;property name="managerDn"&gt;&lt;value&gt;cn=manager,dc=springframework,dc=org&lt;/value&gt;&lt;/property&gt;
       &lt;property name="managerPassword"&gt;&lt;value&gt;password&lt;/value&gt;&lt;/property&gt;
     &lt;/bean&gt;
 
@@ -3208,12 +3208,12 @@ key:              A private key to prevent modification of the remember-me token
 
           </programlisting> This would set up the provider to access an LDAP
         server with URL
-        <literal>ldap://monkeymachine:389/dc=acegisecurity,dc=org</literal>.
+        <literal>ldap://monkeymachine:389/dc=springframework,dc=org</literal>.
         Authentication will be performed by attempting to bind with the DN
-        <literal>uid=&lt;user-login-name&gt;,ou=people,dc=acegisecurity,dc=org</literal>.
+        <literal>uid=&lt;user-login-name&gt;,ou=people,dc=springframework,dc=org</literal>.
         After successful authentication, roles will be assigned to the user by
         searching under the DN
-        <literal>ou=groups,dc=acegisecurity,dc=org</literal> with the default
+        <literal>ou=groups,dc=springframework,dc=org</literal> with the default
         filter <literal>(member=&lt;user's-DN&gt;)</literal>. The role name
         will be taken from the <quote>ou</quote> attribute of each
         match.</para>