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.

This commit is contained in:
Luke Taylor 2007-10-02 10:46:38 +00:00
parent 1a7b00106d
commit 77b6503e2e
10 changed files with 472 additions and 11 deletions

View File

@ -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>

View File

@ -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();
}
}

View File

@ -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;
}
}

View File

@ -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());
}
}

View File

@ -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>

View File

@ -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");
}
}

View File

@ -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

View File

@ -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>

View File

@ -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

View File

@ -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>