Преглед изворни кода

SEC-705: Extend ldap-authentication-provider namespace elt to support user searches and multiple authentication strategies
http://jira.springframework.org/browse/SEC-705

Luke Taylor пре 17 година
родитељ
комит
f7ae070b2f

+ 1 - 0
core/src/main/java/org/springframework/security/config/Elements.java

@@ -35,4 +35,5 @@ abstract class Elements {
     public static final String CUSTOM_AUTH_RPOVIDER = "custom-authentication-provider";
     public static final String X509 = "x509";
 	public static final String FILTER_INVOCATION_DEFINITION_SOURCE = "filter-invocation-definition-source";
+    public static final String LDAP_PASSWORD_COMPARE = "password-compare";
 }

+ 58 - 28
core/src/main/java/org/springframework/security/config/LdapProviderBeanDefinitionParser.java

@@ -1,21 +1,23 @@
 package org.springframework.security.config;
 
-import org.springframework.security.ldap.populator.DefaultLdapAuthoritiesPopulator;
+import org.springframework.security.ldap.search.FilterBasedLdapUserSearch;
 import org.springframework.security.providers.ldap.LdapAuthenticationProvider;
 import org.springframework.security.providers.ldap.authenticator.BindAuthenticator;
+import org.springframework.security.providers.ldap.authenticator.PasswordComparisonAuthenticator;
 import org.springframework.beans.factory.config.BeanDefinition;
 import org.springframework.beans.factory.config.RuntimeBeanReference;
 import org.springframework.beans.factory.support.RootBeanDefinition;
 import org.springframework.beans.factory.xml.BeanDefinitionParser;
 import org.springframework.beans.factory.xml.ParserContext;
 import org.springframework.util.StringUtils;
+import org.springframework.util.xml.DomUtils;
 
 import org.apache.commons.logging.Log;
 import org.apache.commons.logging.LogFactory;
 import org.w3c.dom.Element;
 
 /**
- * Experimental "security:ldap" namespace configuration.
+ * Ldap authentication provider namespace configuration.
  *
  * @author Luke Taylor
  * @version $Id$
@@ -23,36 +25,64 @@ import org.w3c.dom.Element;
  */
 public class LdapProviderBeanDefinitionParser implements BeanDefinitionParser {
     private Log logger = LogFactory.getLog(getClass());
-
-    private static final String ATT_AUTH_TYPE = "auth-type";
-    private static final String ATT_SERVER = "server-ref";
-
-    private static final String OPT_DEFAULT_DN_PATTERN = "uid={0},ou=people";
-    private static final String DEF_GROUP_CONTEXT = "ou=groups";
-    private static final String DEF_GROUP_SEARCH_FILTER = "(uniqueMember={0})";
-
+  
+    private static final String ATT_USER_DN_PATTERN = "user-dn-pattern";
+    private static final String ATT_USER_PASSWORD= "password-attribute";
+    
+    private static final String DEF_USER_SEARCH_FILTER="uid={0}";
 
     public BeanDefinition parse(Element elt, ParserContext parserContext) {
-        String server = elt.getAttribute(ATT_SERVER);
-
-        if (!StringUtils.hasText(server)) {
-            server = BeanIds.CONTEXT_SOURCE;
+        RuntimeBeanReference contextSource = LdapUserServiceBeanDefinitionParser.parseServerReference(elt, parserContext);
+        
+        RootBeanDefinition searchBean = LdapUserServiceBeanDefinitionParser.parseSearchBean(elt, parserContext);
+        String userDnPattern = elt.getAttribute(ATT_USER_DN_PATTERN);
+        
+        String[] userDnPatternArray = new String[0];
+        
+        if (StringUtils.hasText(userDnPattern)) {
+            userDnPatternArray = new String[] {userDnPattern};
+            // TODO: Validate the pattern and make sure it is a valid DN.
+        } else if (searchBean == null) {
+            logger.info("No search information or DN pattern specified. Using default search filter '" + DEF_USER_SEARCH_FILTER + "'");
+            searchBean = new RootBeanDefinition(FilterBasedLdapUserSearch.class);
+            searchBean.setSource(elt);
+            searchBean.getConstructorArgumentValues().addIndexedArgumentValue(0, "");
+            searchBean.getConstructorArgumentValues().addIndexedArgumentValue(1, DEF_USER_SEARCH_FILTER);
+            searchBean.getConstructorArgumentValues().addIndexedArgumentValue(2, contextSource);
         }
-
-        RuntimeBeanReference contextSource = new RuntimeBeanReference(server);
-
-        RootBeanDefinition bindAuthenticator = new RootBeanDefinition(BindAuthenticator.class);
-        bindAuthenticator.getConstructorArgumentValues().addGenericArgumentValue(contextSource);
-        bindAuthenticator.getPropertyValues().addPropertyValue("userDnPatterns", new String[] {OPT_DEFAULT_DN_PATTERN});
-        RootBeanDefinition authoritiesPopulator = new RootBeanDefinition(DefaultLdapAuthoritiesPopulator.class);
-        authoritiesPopulator.getConstructorArgumentValues().addGenericArgumentValue(contextSource);
-        authoritiesPopulator.getConstructorArgumentValues().addGenericArgumentValue(DEF_GROUP_CONTEXT);
-        // TODO: Change to using uniqueMember as default
-//        authoritiesPopulator.getPropertyValues().addPropertyValue("groupSearchFilter", DEF_GROUP_SEARCH_FILTER);
-
+        
+        RootBeanDefinition authenticator = new RootBeanDefinition(BindAuthenticator.class); 
+        Element passwordCompareElt = DomUtils.getChildElementByTagName(elt, Elements.LDAP_PASSWORD_COMPARE);
+        if (passwordCompareElt != null) {
+            authenticator = new RootBeanDefinition(PasswordComparisonAuthenticator.class);
+            
+            String passwordAttribute = passwordCompareElt.getAttribute(ATT_USER_PASSWORD);
+            if (StringUtils.hasText(passwordAttribute)) {
+                authenticator.getPropertyValues().addPropertyValue("passwordAttributeName", passwordAttribute);
+            }
+            
+            Element passwordEncoderElement = DomUtils.getChildElementByTagName(passwordCompareElt, Elements.PASSWORD_ENCODER);
+            
+            if (passwordEncoderElement != null) {
+                PasswordEncoderParser pep = new PasswordEncoderParser(passwordEncoderElement, parserContext);
+                authenticator.getPropertyValues().addPropertyValue("passwordEncoder", pep.getPasswordEncoder());
+                
+                if (pep.getSaltSource() != null) {
+                    parserContext.getReaderContext().warning("Salt source information isn't valid when used with LDAP", passwordEncoderElement);
+                }
+            }
+        }
+        
+        authenticator.getConstructorArgumentValues().addGenericArgumentValue(contextSource);
+        authenticator.getPropertyValues().addPropertyValue("userDnPatterns", userDnPatternArray);
+        
+        if (searchBean != null) {
+            authenticator.getPropertyValues().addPropertyValue("userSearch", searchBean);
+        }
+                
         RootBeanDefinition ldapProvider = new RootBeanDefinition(LdapAuthenticationProvider.class);
-        ldapProvider.getConstructorArgumentValues().addGenericArgumentValue(bindAuthenticator);
-        ldapProvider.getConstructorArgumentValues().addGenericArgumentValue(authoritiesPopulator);
+        ldapProvider.getConstructorArgumentValues().addGenericArgumentValue(authenticator);
+        ldapProvider.getConstructorArgumentValues().addGenericArgumentValue(LdapUserServiceBeanDefinitionParser.parseAuthoritiesPopulator(elt, parserContext));
 
         LdapConfigUtils.registerPostProcessorIfNecessary(parserContext.getRegistry());
 

+ 54 - 27
core/src/main/java/org/springframework/security/config/LdapUserServiceBeanDefinitionParser.java

@@ -17,13 +17,14 @@ import org.w3c.dom.Element;
  * @since 2.0
  */
 public class LdapUserServiceBeanDefinitionParser extends AbstractUserDetailsServiceBeanDefinitionParser {
-    private static final String ATT_SERVER = "server-ref";
+    public 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 ATT_GROUP_ROLE_ATTRIBUTE = "group-role-attribute";    
     public static final String DEF_GROUP_SEARCH_FILTER = "(uniqueMember={0})";
     public static final String DEF_GROUP_SEARCH_BASE = "ou=groups";
 
@@ -32,26 +33,60 @@ public class LdapUserServiceBeanDefinitionParser extends AbstractUserDetailsServ
     }
 
     protected void doParse(Element elt, ParserContext parserContext, BeanDefinitionBuilder builder) {
-        String server = elt.getAttribute(ATT_SERVER);
 
-        if (!StringUtils.hasText(server)) {
-            server = BeanIds.CONTEXT_SOURCE;
+        if (!StringUtils.hasText(elt.getAttribute(ATT_USER_SEARCH_FILTER))) {
+            parserContext.getReaderContext().error("User search filter must be supplied", elt);
         }
+        
+        builder.addConstructorArg(parseSearchBean(elt, parserContext));
+        builder.addConstructorArg(parseAuthoritiesPopulator(elt, parserContext));
 
+        LdapConfigUtils.registerPostProcessorIfNecessary(parserContext.getRegistry());
+    }
+    
+    static RootBeanDefinition parseSearchBean(Element elt, ParserContext parserContext) {
         String userSearchFilter = elt.getAttribute(ATT_USER_SEARCH_FILTER);
-
+        String userSearchBase = elt.getAttribute(ATT_USER_SEARCH_BASE);
+        Object source = parserContext.extractSource(elt);
+        
+        if (StringUtils.hasText(userSearchBase)) {
+            if(!StringUtils.hasText(userSearchFilter)) {
+                parserContext.getReaderContext().error(ATT_USER_SEARCH_BASE + " cannot be used without a " + ATT_USER_SEARCH_FILTER, source);
+            }
+        } else {
+            userSearchBase = DEF_USER_SEARCH_BASE;
+        }        
+        
         if (!StringUtils.hasText(userSearchFilter)) {
-            parserContext.getReaderContext().error("User search filter must be supplied", elt);
+            return null;
         }
+        
+        RootBeanDefinition search = new RootBeanDefinition(FilterBasedLdapUserSearch.class);
+        search.setSource(source);
+        search.getConstructorArgumentValues().addIndexedArgumentValue(0, userSearchBase);
+        search.getConstructorArgumentValues().addIndexedArgumentValue(1, userSearchFilter);
+        search.getConstructorArgumentValues().addIndexedArgumentValue(2, parseServerReference(elt, parserContext));
+        
+        return search;
+    }
+    
+    static RuntimeBeanReference parseServerReference(Element elt, ParserContext parserContext) {
+        String server = elt.getAttribute(ATT_SERVER);
 
-        String userSearchBase = elt.getAttribute(ATT_USER_SEARCH_BASE);
-
-        if (!StringUtils.hasText(userSearchBase)) {
-            userSearchBase = DEF_USER_SEARCH_BASE;
+        if (!StringUtils.hasText(server)) {
+            server = BeanIds.CONTEXT_SOURCE;
         }
 
+        RuntimeBeanReference contextSource = new RuntimeBeanReference(server);
+        contextSource.setSource(parserContext.extractSource(elt));
+        
+        return contextSource;
+    }
+    
+    static RootBeanDefinition parseAuthoritiesPopulator(Element elt, ParserContext parserContext) {
         String groupSearchFilter = elt.getAttribute(ATT_GROUP_SEARCH_FILTER);
         String groupSearchBase = elt.getAttribute(ATT_GROUP_SEARCH_BASE);
+        String groupRoleAttribute = elt.getAttribute(ATT_GROUP_ROLE_ATTRIBUTE);
 
         if (!StringUtils.hasText(groupSearchFilter)) {
             groupSearchFilter = DEF_GROUP_SEARCH_FILTER;
@@ -60,25 +95,17 @@ public class LdapUserServiceBeanDefinitionParser extends AbstractUserDetailsServ
         if (!StringUtils.hasText(groupSearchBase)) {
             groupSearchBase = DEF_GROUP_SEARCH_BASE;
         }
-
-        Object source = parserContext.extractSource(elt);
-
-        RuntimeBeanReference contextSource = new RuntimeBeanReference(server);
-        RootBeanDefinition search = new RootBeanDefinition(FilterBasedLdapUserSearch.class);
-        search.setSource(source);
-        search.getConstructorArgumentValues().addIndexedArgumentValue(0, userSearchBase);
-        search.getConstructorArgumentValues().addIndexedArgumentValue(1, userSearchFilter);
-        search.getConstructorArgumentValues().addIndexedArgumentValue(2, contextSource);
-
+        
         RootBeanDefinition populator = new RootBeanDefinition(DefaultLdapAuthoritiesPopulator.class);
-        populator.setSource(source);
-        populator.getConstructorArgumentValues().addIndexedArgumentValue(0, contextSource);
+        populator.setSource(parserContext.extractSource(elt));
+        populator.getConstructorArgumentValues().addIndexedArgumentValue(0, parseServerReference(elt, parserContext));
         populator.getConstructorArgumentValues().addIndexedArgumentValue(1, groupSearchBase);
         populator.getPropertyValues().addPropertyValue("groupSearchFilter", groupSearchFilter);
-
-        builder.addConstructorArg(search);
-        builder.addConstructorArg(populator);
-
-        LdapConfigUtils.registerPostProcessorIfNecessary(parserContext.getRegistry());
+        
+        if (StringUtils.hasLength(groupRoleAttribute)) {
+            populator.getPropertyValues().addPropertyValue("groupRoleAttribute", groupRoleAttribute);
+        }
+        
+        return populator;
     }
 }

+ 3 - 0
core/src/main/java/org/springframework/security/config/PasswordEncoderParser.java

@@ -2,6 +2,7 @@ package org.springframework.security.config;
 
 import org.springframework.security.providers.encoding.Md4PasswordEncoder;
 import org.springframework.security.providers.encoding.Md5PasswordEncoder;
+import org.springframework.security.providers.encoding.PlaintextPasswordEncoder;
 import org.springframework.security.providers.encoding.ShaPasswordEncoder;
 import org.springframework.security.providers.encoding.BaseDigestPasswordEncoder;
 import org.springframework.security.providers.ldap.authenticator.LdapShaPasswordEncoder;
@@ -32,6 +33,7 @@ public class PasswordEncoderParser {
     static final String ATT_REF = "ref";
     static final String ATT_HASH = "hash";
     static final String ATT_BASE_64 = "base64";
+    static final String OPT_HASH_PLAINTEXT = "plaintext";    
     static final String OPT_HASH_SHA = "sha";
     static final String OPT_HASH_MD4 = "md4";
     static final String OPT_HASH_MD5 = "md5";
@@ -41,6 +43,7 @@ public class PasswordEncoderParser {
 
     static {
         ENCODER_CLASSES = new HashMap();
+        ENCODER_CLASSES.put(OPT_HASH_PLAINTEXT, PlaintextPasswordEncoder.class);
         ENCODER_CLASSES.put(OPT_HASH_SHA, ShaPasswordEncoder.class);
         ENCODER_CLASSES.put(OPT_HASH_MD4, Md4PasswordEncoder.class);
         ENCODER_CLASSES.put(OPT_HASH_MD5, Md5PasswordEncoder.class);

+ 53 - 15
core/src/main/resources/org/springframework/security/config/spring-security-2.0.rnc

@@ -10,7 +10,7 @@ start = http | ldap-server | authentication-provider | ldap-authentication-provi
 
 hash =
     ## Defines the hashing algorithm used on user passwords. We recommend strongly against using MD4, as it is a very weak hashing algorithm.
-    attribute hash {"sha" | "md5" | "md4" | "{sha}" | "{ssha}"}
+    attribute hash {"plaintext" | "sha" | "md5" | "md4" | "{sha}" | "{ssha}"}
 base64 = 
     ## Whether a string should be base64 encoded
     attribute base64 {"true" | "false"}
@@ -66,33 +66,71 @@ ldap-server.attlist &=
     ## Optional root suffix for the embedded LDAP server. Default is "dc=springframework,dc=org"
     attribute root { xsd:string }?
 
+ldap-server-ref-attribute =
+    ## 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. 
+    attribute server-ref {xsd:string}
+
+
+group-search-filter-attribute = 
+    ## Group search filter. Defaults to (uniqueMember={0}). The substituted parameter is the DN of the user.
+    attribute group-search-filter {xsd:string}
+group-search-base-attribute = 
+    ## Search base for group membership searches. Defaults to "ou=groups".
+    attribute group-search-base {xsd:string}
+user-search-filter-attribute =
+    attribute user-search-filter {xsd:string}
+user-search-base-attribute =
+    ## Search base for user searches. Defaults to "".
+    attribute user-search-base {xsd:string}?
+group-role-attribute-attribute =
+    ## The LDAP attribute name which contains the role name which will be used within Spring Security. Defaults to "cn".
+    attribute group-role-attribute {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 <ldap-server> with no Id), that server will be used. 
-    attribute server-ref {xsd:string}?
+    ldap-server-ref-attribute?
 ldap-us.attlist &=
-    attribute user-search-filter {xsd:string}
+    user-search-filter-attribute?
 ldap-us.attlist &=
-    ## Search base for user searches. Defaults to "".
-    attribute user-search-base {xsd:string}?
+    user-search-base-attribute?
 ldap-us.attlist &=
-    ## Group search filter. Defaults to (uniqueMember={0}).
-    attribute group-search-filter {xsd:string}?
+    group-search-filter-attribute?
 ldap-us.attlist &=
-    ## Search base for group membership searches. Defaults to "ou=groups".
-    attribute group-search-base {xsd:string}?
-
-
+    group-search-base-attribute?
+ldap-us.attlist &=
+    group-role-attribute-attribute?
 
 ldap-authentication-provider =
     ## Sets up an ldap authentication provider
-    element ldap-authentication-provider {ldap-ap.attlist, empty}
+    element ldap-authentication-provider {ldap-ap.attlist, password-compare-element?}
+ldap-ap.attlist &=
+    ldap-server-ref-attribute?
 ldap-ap.attlist &=
-    ## The server to authenticate against.  
-    attribute server-ref {xsd:string}?
+    user-search-base-attribute?
+ldap-ap.attlist &=
+    user-search-filter-attribute?
+ldap-ap.attlist &=
+    group-search-base-attribute?
+ldap-ap.attlist &= 
+    group-search-filter-attribute?
+ldap-ap.attlist &=
+    group-role-attribute-attribute?
+ldap-ap.attlist &=
+    ## 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.
+    attribute user-dn-pattern {xsd:string}?
 
+password-compare-element =
+    ## Specifies that an LDAP provider should use an LDAP compare operation of the user's password to authenticate the user
+    element password-compare {password-compare.attlist, password-encoder?}
+    
+password-compare.attlist &=
+    ## The attribute in the directory which contains the user password. Defaults to "userPassword".
+    attribute password-attribute {xsd:string}?
+password-compare.attlist &=
+    hash?
 
 intercept-methods =
     ## Can be used inside a bean definition to add a security interceptor to the bean and set up access configuration attributes for the bean's methods

+ 107 - 8
core/src/main/resources/org/springframework/security/config/spring-security-2.0.xsd

@@ -7,6 +7,7 @@
       </xs:annotation>
       <xs:simpleType>
         <xs:restriction base="xs:token">
+          <xs:enumeration value="plaintext"/>
           <xs:enumeration value="sha"/>
           <xs:enumeration value="md5"/>
           <xs:enumeration value="md4"/>
@@ -96,6 +97,7 @@
         </xs:annotation>
         <xs:simpleType>
           <xs:restriction base="xs:token">
+            <xs:enumeration value="plaintext"/>
             <xs:enumeration value="sha"/>
             <xs:enumeration value="md5"/>
             <xs:enumeration value="md4"/>
@@ -186,6 +188,44 @@
       </xs:annotation>
     </xs:attribute>
   </xs:attributeGroup>
+  <xs:attributeGroup name="ldap-server-ref-attribute">
+    <xs:attribute name="server-ref" use="required" type="xs:string">
+      <xs:annotation>
+        <xs:documentation>The optional server to use. If omitted, and a default LDAP server is registered (using &lt;ldap-server&gt; with no Id), that server will be used. </xs:documentation>
+      </xs:annotation>
+    </xs:attribute>
+  </xs:attributeGroup>
+  <xs:attributeGroup name="group-search-filter-attribute">
+    <xs:attribute name="group-search-filter" use="required" type="xs:string">
+      <xs:annotation>
+        <xs:documentation>Group search filter. Defaults to (uniqueMember={0}). The substituted parameter is the DN of the user.</xs:documentation>
+      </xs:annotation>
+    </xs:attribute>
+  </xs:attributeGroup>
+  <xs:attributeGroup name="group-search-base-attribute">
+    <xs:attribute name="group-search-base" use="required" type="xs:string">
+      <xs:annotation>
+        <xs:documentation>Search base for group membership searches. Defaults to "ou=groups".</xs:documentation>
+      </xs:annotation>
+    </xs:attribute>
+  </xs:attributeGroup>
+  <xs:attributeGroup name="user-search-filter-attribute">
+    <xs:attribute name="user-search-filter" use="required" type="xs:string"/>
+  </xs:attributeGroup>
+  <xs:attributeGroup name="user-search-base-attribute">
+    <xs:attribute name="user-search-base" type="xs:string">
+      <xs:annotation>
+        <xs:documentation>Search base for user searches. Defaults to "".</xs:documentation>
+      </xs:annotation>
+    </xs:attribute>
+  </xs:attributeGroup>
+  <xs:attributeGroup name="group-role-attribute-attribute">
+    <xs:attribute name="group-role-attribute" use="required" type="xs:string">
+      <xs:annotation>
+        <xs:documentation>The LDAP attribute name which contains the role name which will be used within Spring Security. Defaults to "cn".</xs:documentation>
+      </xs:annotation>
+    </xs:attribute>
+  </xs:attributeGroup>
   <xs:element name="ldap-user-service">
     <xs:complexType>
       <xs:attributeGroup ref="security:ldap-us.attlist"/>
@@ -202,15 +242,11 @@
         <xs:documentation>The optional server to use. If omitted, and a default LDAP server is registered (using &lt;ldap-server&gt; with no Id), that server will be used. </xs:documentation>
       </xs:annotation>
     </xs:attribute>
-    <xs:attribute name="user-search-filter" use="required" type="xs:string"/>
-    <xs:attribute name="user-search-base" type="xs:string">
-      <xs:annotation>
-        <xs:documentation>Search base for user searches. Defaults to "".</xs:documentation>
-      </xs:annotation>
-    </xs:attribute>
+    <xs:attribute name="user-search-filter" type="xs:string"/>
+    <xs:attributeGroup ref="security:user-search-base-attribute"/>
     <xs:attribute name="group-search-filter" type="xs:string">
       <xs:annotation>
-        <xs:documentation>Group search filter. Defaults to (uniqueMember={0}).</xs:documentation>
+        <xs:documentation>Group search filter. Defaults to (uniqueMember={0}). The substituted parameter is the DN of the user.</xs:documentation>
       </xs:annotation>
     </xs:attribute>
     <xs:attribute name="group-search-base" type="xs:string">
@@ -218,21 +254,84 @@
         <xs:documentation>Search base for group membership searches. Defaults to "ou=groups".</xs:documentation>
       </xs:annotation>
     </xs:attribute>
+    <xs:attribute name="group-role-attribute" type="xs:string">
+      <xs:annotation>
+        <xs:documentation>The LDAP attribute name which contains the role name which will be used within Spring Security. Defaults to "cn".</xs:documentation>
+      </xs:annotation>
+    </xs:attribute>
   </xs:attributeGroup>
   <xs:element name="ldap-authentication-provider">
     <xs:annotation>
       <xs:documentation>Sets up an ldap authentication provider</xs:documentation>
     </xs:annotation>
     <xs:complexType>
+      <xs:sequence>
+        <xs:element minOccurs="0" ref="security:password-compare"/>
+      </xs:sequence>
       <xs:attributeGroup ref="security:ldap-ap.attlist"/>
     </xs:complexType>
   </xs:element>
   <xs:attributeGroup name="ldap-ap.attlist">
     <xs:attribute name="server-ref" type="xs:string">
       <xs:annotation>
-        <xs:documentation>The server to authenticate against.  </xs:documentation>
+        <xs:documentation>The optional server to use. If omitted, and a default LDAP server is registered (using &lt;ldap-server&gt; with no Id), that server will be used. </xs:documentation>
       </xs:annotation>
     </xs:attribute>
+    <xs:attributeGroup ref="security:user-search-base-attribute"/>
+    <xs:attribute name="user-search-filter" type="xs:string"/>
+    <xs:attribute name="group-search-base" type="xs:string">
+      <xs:annotation>
+        <xs:documentation>Search base for group membership searches. Defaults to "ou=groups".</xs:documentation>
+      </xs:annotation>
+    </xs:attribute>
+    <xs:attribute name="group-search-filter" type="xs:string">
+      <xs:annotation>
+        <xs:documentation>Group search filter. Defaults to (uniqueMember={0}). The substituted parameter is the DN of the user.</xs:documentation>
+      </xs:annotation>
+    </xs:attribute>
+    <xs:attribute name="group-role-attribute" type="xs:string">
+      <xs:annotation>
+        <xs:documentation>The LDAP attribute name which contains the role name which will be used within Spring Security. Defaults to "cn".</xs:documentation>
+      </xs:annotation>
+    </xs:attribute>
+    <xs:attribute name="user-dn-pattern" type="xs:string">
+      <xs:annotation>
+        <xs:documentation>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.</xs:documentation>
+      </xs:annotation>
+    </xs:attribute>
+  </xs:attributeGroup>
+  <xs:element name="password-compare">
+    <xs:annotation>
+      <xs:documentation>Specifies that an LDAP provider should use an LDAP compare operation of the user's password to authenticate the user</xs:documentation>
+    </xs:annotation>
+    <xs:complexType>
+      <xs:sequence>
+        <xs:element minOccurs="0" ref="security:password-encoder"/>
+      </xs:sequence>
+      <xs:attributeGroup ref="security:password-compare.attlist"/>
+    </xs:complexType>
+  </xs:element>
+  <xs:attributeGroup name="password-compare.attlist">
+    <xs:attribute name="password-attribute" type="xs:string">
+      <xs:annotation>
+        <xs:documentation>The attribute in the directory which contains the user password. Defaults to "userPassword".</xs:documentation>
+      </xs:annotation>
+    </xs:attribute>
+    <xs:attribute name="hash">
+      <xs:annotation>
+        <xs:documentation>Defines the hashing algorithm used on user passwords. We recommend strongly against using MD4, as it is a very weak hashing algorithm.</xs:documentation>
+      </xs:annotation>
+      <xs:simpleType>
+        <xs:restriction base="xs:token">
+          <xs:enumeration value="plaintext"/>
+          <xs:enumeration value="sha"/>
+          <xs:enumeration value="md5"/>
+          <xs:enumeration value="md4"/>
+          <xs:enumeration value="{sha}"/>
+          <xs:enumeration value="{ssha}"/>
+        </xs:restriction>
+      </xs:simpleType>
+    </xs:attribute>
   </xs:attributeGroup>
   <xs:element name="intercept-methods">
     <xs:annotation>

+ 32 - 10
core/src/test/java/org/springframework/security/config/LdapProviderBeanDefinitionParserTests.java

@@ -1,5 +1,6 @@
 package org.springframework.security.config;
 
+import org.springframework.beans.factory.parsing.BeanDefinitionParsingException;
 import org.springframework.security.providers.ProviderManager;
 import org.springframework.security.providers.UsernamePasswordAuthenticationToken;
 import org.springframework.security.providers.ldap.LdapAuthenticationProvider;
@@ -12,7 +13,7 @@ import org.junit.After;
 
 
 /**
- * @author luke
+ * @author Luke Taylor
  * @version $Id$
  */
 public class LdapProviderBeanDefinitionParserTests {
@@ -28,22 +29,43 @@ public class LdapProviderBeanDefinitionParserTests {
 
     @Test
     public void simpleProviderAuthenticatesCorrectly() {
-        appCtx = new InMemoryXmlApplicationContext("<ldap-server /> <ldap-authentication-provider />");
+        setContext("<ldap-server /> <ldap-authentication-provider group-search-filter='member={0}' />");
 
-        ProviderManager authManager = (ProviderManager) appCtx.getBean(BeanIds.AUTHENTICATION_MANAGER);
-
-        assertEquals(1, authManager.getProviders().size());
-
-        LdapAuthenticationProvider provider = (LdapAuthenticationProvider) authManager.getProviders().get(0);
+        LdapAuthenticationProvider provider = getProvider();
         Authentication auth = provider.authenticate(new UsernamePasswordAuthenticationToken("ben", "benspassword"));
         LdapUserDetailsImpl ben = (LdapUserDetailsImpl) auth.getPrincipal();
 
         assertEquals(2, ben.getAuthorities().length);
-    }
-
+    }  
+    
     @Test(expected = SecurityConfigurationException.class)
     public void missingServerEltCausesConfigException() {
-        appCtx = new InMemoryXmlApplicationContext("<ldap-authentication-provider />");
+        setContext("<ldap-authentication-provider />");
+    }
+    
+    @Test
+    public void supportsPasswordComparisonAuthentication() {
+        setContext("<ldap-server /> " +
+        		"<ldap-authentication-provider user-dn-pattern='uid={0},ou=people'>" +
+        		"    <password-compare password-attribute='uid'>" +
+        		"        <password-encoder hash='plaintext'/>" +
+        		"    </password-compare>" +
+        		"</ldap-authentication-provider>");
+        LdapAuthenticationProvider provider = getProvider();
+        provider.authenticate(new UsernamePasswordAuthenticationToken("ben", "ben"));        
     }
 
+    private void setContext(String context) {
+        appCtx = new InMemoryXmlApplicationContext(context);
+    }    
+
+
+    private LdapAuthenticationProvider getProvider() {
+        ProviderManager authManager = (ProviderManager) appCtx.getBean(BeanIds.AUTHENTICATION_MANAGER);
+
+        assertEquals(1, authManager.getProviders().size());
+
+        LdapAuthenticationProvider provider = (LdapAuthenticationProvider) authManager.getProviders().get(0);
+        return provider;
+    }    
 }

+ 21 - 2
core/src/test/java/org/springframework/security/config/LdapUserServiceBeanDefinitionParserTests.java

@@ -1,12 +1,16 @@
 package org.springframework.security.config;
 
+import java.util.Set;
+
+import org.springframework.security.GrantedAuthorityImpl;
+import org.springframework.security.util.AuthorityUtils;
 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;
+import static org.junit.Assert.*;
 
 /**
  * @author Luke Taylor
@@ -35,7 +39,9 @@ public class LdapUserServiceBeanDefinitionParserTests {
         UserDetailsService uds = (UserDetailsService) appCtx.getBean("ldapUDS");
         UserDetails ben = uds.loadUserByUsername("ben");
 
-        assertEquals(2, ben.getAuthorities().length);
+        Set authorities = AuthorityUtils.authorityArrayToSet(ben.getAuthorities());
+        assertEquals(2, authorities.size());
+        assertTrue(authorities.contains(new GrantedAuthorityImpl("ROLE_DEVELOPERS")));
     }
 
     @Test
@@ -48,6 +54,19 @@ public class LdapUserServiceBeanDefinitionParserTests {
         assertEquals("Joe Smeth", joe.getUsername());
     }
 
+    @Test
+    public void differentGroupRoleAttributeWorksAsExpected() throws Exception {
+        setContext("<ldap-user-service id='ldapUDS' user-search-filter='(uid={0})' group-role-attribute='ou' group-search-filter='member={0}' /><ldap-server />");
+
+        UserDetailsService uds = (UserDetailsService) appCtx.getBean("ldapUDS");
+        UserDetails ben = uds.loadUserByUsername("ben");
+
+        Set authorities = AuthorityUtils.authorityArrayToSet(ben.getAuthorities());
+        assertEquals(2, authorities.size());
+        assertTrue(authorities.contains(new GrantedAuthorityImpl("ROLE_DEVELOPER")));
+        
+    }
+        
     @Test
     public void isSupportedByAuthenticationProviderElement() {
         setContext(