Added methods for 'simple' bind authentication in LdapTemplate (LdapOperations, SimpleLdapOperations and SimpleLdapTemplate as well).

Introduced new callback interface for use with certain authentication methods.
Added LdapEntryIdentification to wrap the full identification of an LDAP entry (absolute and relative DNs)
Added LdapEntryIdentificationContextMapper.
This commit is contained in:
Mattias Arthursson
2008-11-18 14:10:27 +00:00
parent 0819e34ab6
commit e910acfbba
12 changed files with 3300 additions and 2867 deletions

View File

@@ -22,6 +22,7 @@ import javax.naming.directory.Attributes;
import javax.naming.directory.SearchControls;
import org.springframework.ldap.NamingException;
import org.springframework.ldap.core.ContextSource;
import org.springframework.ldap.core.DirContextOperations;
import org.springframework.ldap.core.DirContextProcessor;
import org.springframework.ldap.core.LdapOperations;
@@ -131,6 +132,36 @@ public interface SimpleLdapOperations {
*/
<T> T lookup(Name dn, ParameterizedContextMapper<T> mapper);
/**
* Perform a search for a unique entry matching the specified search
* criteria and return the found object. If no entry is found or if there
* are more than one matching entry, an
* {@link IncorrectResultSizeDataAccessException} is thrown.
* @param base the DN to use as the base of the search.
* @param filter the search filter.
* @param mapper the mapper to use for the search.
* @return the single object returned by the mapper that matches the search
* criteria.
* @throws IncorrectResultSizeDataAccessException if the result is not one unique entry
* @since 1.3
*/
<T> T searchForObject(String base, String filter, ParameterizedContextMapper<T> mapper);
/**
* Perform a search for a unique entry matching the specified search
* criteria and return the found object. If no entry is found or if there
* are more than one matching entry, an
* {@link IncorrectResultSizeDataAccessException} is thrown.
* @param base the DN to use as the base of the search.
* @param filter the search filter.
* @param mapper the mapper to use for the search.
* @return the single object returned by the mapper that matches the search
* criteria.
* @throws IncorrectResultSizeDataAccessException if the result is not one unique entry
* @since 1.3
*/
<T> T searchForObject(Name base, String filter, ParameterizedContextMapper<T> mapper);
/**
* Look up the specified DN, and automatically cast it to a
* {@link DirContextOperations} instance.
@@ -232,4 +263,52 @@ public interface SimpleLdapOperations {
* @throws NamingException if any error occurs.
*/
void modifyAttributes(DirContextOperations ctx);
/**
* Utility method to perform a simple LDAP 'bind' authentication. Search for
* the LDAP entry to authenticate using the supplied base DN and filter; use
* the DN of the found entry together with the password as input to
* {@link ContextSource#getContext(String, String)}, thus authenticating the
* entry.
* <p>
* Example:<br/>
*
* <pre>
* AndFilter filter = new AndFilter();
* filter.and(&quot;objectclass&quot;, &quot;person&quot;).and(&quot;uid&quot;, userId);
* boolean authenticated = ldapTemplate.authenticate(DistinguishedName.EMPTY_PATH, filter.toString(), password);
* </pre>
*
* @param base the DN to use as the base of the search.
* @param filter the search filter - must result in a unique result.
* @param password the password to use for authentication.
* @return <code>true</code> if the authentication was successful,
* <code>false</code> otherwise.
* @since 1.3
*/
boolean authenticate(String base, String filter, String password);
/**
* Utility method to perform a simple LDAP 'bind' authentication. Search for
* the LDAP entry to authenticate using the supplied base DN and filter; use
* the DN of the found entry together with the password as input to
* {@link ContextSource#getContext(String, String)}, thus authenticating the
* entry.
* <p>
* Example:<br/>
*
* <pre>
* AndFilter filter = new AndFilter();
* filter.and(&quot;objectclass&quot;, &quot;person&quot;).and(&quot;uid&quot;, userId);
* boolean authenticated = ldapTemplate.authenticate(DistinguishedName.EMPTY_PATH, filter.toString(), password);
* </pre>
*
* @param base the DN to use as the base of the search.
* @param filter the search filter - must result in a unique result.
* @param password the password to use for authentication.
* @return <code>true</code> if the authentication was successful,
* <code>false</code> otherwise.
* @since 1.3
*/
boolean authenticate(Name base, String filter, String password);
}

View File

@@ -239,4 +239,52 @@ public class SimpleLdapTemplate implements SimpleLdapOperations {
public void bind(DirContextOperations ctx) {
ldapOperations.bind(ctx);
}
/*
* (non-Javadoc)
*
* @see
* org.springframework.ldap.core.simple.SimpleLdapOperations#searchForObject
* (java.lang.String, java.lang.String,
* org.springframework.ldap.core.simple.ParameterizedContextMapper)
*/
@SuppressWarnings("unchecked")
public <T> T searchForObject(String base, String filter, ParameterizedContextMapper<T> mapper) {
return (T) ldapOperations.searchForObject(base, filter, mapper);
}
/*
* (non-Javadoc)
*
* @see
* org.springframework.ldap.core.simple.SimpleLdapOperations#searchForObject
* (javax.naming.Name, java.lang.String,
* org.springframework.ldap.core.simple.ParameterizedContextMapper)
*/
@SuppressWarnings("unchecked")
public <T> T searchForObject(Name base, String filter, ParameterizedContextMapper<T> mapper) {
return (T) ldapOperations.searchForObject(base, filter, mapper);
}
/*
* (non-Javadoc)
*
* @see
* org.springframework.ldap.core.simple.SimpleLdapOperations#authenticate
* (java.lang.String, java.lang.String, java.lang.String)
*/
public boolean authenticate(String base, String filter, String password) {
return ldapOperations.authenticate(base, filter, password);
}
/*
* (non-Javadoc)
*
* @see
* org.springframework.ldap.core.simple.SimpleLdapOperations#authenticate
* (javax.naming.Name, java.lang.String, java.lang.String)
*/
public boolean authenticate(Name base, String filter, String password) {
return ldapOperations.authenticate(base, filter, password);
}
}

View File

@@ -0,0 +1,40 @@
/*
* Copyright 2005-2008 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.ldap.core;
import javax.naming.directory.DirContext;
/**
* Callback interface to be used in the authentication methods in
* {@link LdapOperations} for performing operations on individually
* authenticated contexts.
*
* @author Mattias Hellborg Arthursson
* @since 1.3
*/
public interface AuthenticatedLdapEntryContextCallback {
/**
* Perform some LDAP operation on the supplied authenticated
* <code>DirContext</code> instance. The target context will be
* automatically closed.
*
* @param ctx the <code>DirContext</code> instance to perform an operation
* on.
* @param ldapEntryIdentification the identification of the LDAP entry used
* to authenticate the supplied <code>DirContext</code>.
*/
void executeWithContext(DirContext ctx, LdapEntryIdentification ldapEntryIdentification);
}

View File

@@ -0,0 +1,87 @@
/*
* Copyright 2005-2008 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.ldap.core;
import javax.naming.directory.DirContext;
import org.springframework.util.Assert;
/**
* Wrapper class to handle the full identification of an LDAP entry. An LDAP
* entry is identified by its Distinguished Name, in Spring LDAP represented by
* the {@link DistinguishedName} class. A Distinguished Name can be absolute -
* i.e. complete including the very root (base) of the LDAP tree - or relative -
* i.e relative to the base LDAP path of the current LDAP connection (specified
* as <code>base</code> to the {@link ContextSource}).
* <p>
* The different representations are needed on different occasions, e.g. the
* relative DN is typically what is needed to perform lookups and searches in
* the LDAP tree, whereas the absolute DN is needed when authenticating and when
* an LDAP entry is referred to in e.g. a group. This wrapper class contains
* both of these representations.
*
* @author Mattias Hellborg Arthursson
*/
public class LdapEntryIdentification {
private final DistinguishedName relativeDn;
private final DistinguishedName absoluteDn;
/**
* Construct an LdapEntryIdentification instance.
* @param absoluteDn the absolute DN of the identified entry, e.g. as
* returned by {@link DirContext#getNameInNamespace()}.
* @param relativeDn the DN of the identified entry relative to the base
* LDAP path, e.g. as returned by {@link DirContextOperations#getDn()}.
*/
public LdapEntryIdentification(DistinguishedName absoluteDn, DistinguishedName relativeDn) {
Assert.notNull(absoluteDn, "Absolute DN must not be null");
Assert.notNull(relativeDn, "Relative DN must not be null");
this.absoluteDn = absoluteDn.immutableDistinguishedName();
this.relativeDn = relativeDn.immutableDistinguishedName();
}
/**
* Get the DN of the identified entry relative to the base LDAP path, e.g.
* as returned by {@link DirContextOperations#getDn()}.
* @return the relative DN.
*/
public DistinguishedName getRelativeDn() {
return relativeDn;
}
/**
* Get the absolute DN of the identified entry, e.g. as returned by
* {@link DirContext#getNameInNamespace()}.
* @return the absolute DN.
*/
public DistinguishedName getAbsoluteDn() {
return absoluteDn;
}
public boolean equals(Object obj) {
if (obj != null && obj.getClass().equals(this.getClass())) {
LdapEntryIdentification that = (LdapEntryIdentification) obj;
return this.absoluteDn.equals(that.absoluteDn) && this.relativeDn.equals(that.relativeDn);
}
return false;
}
public int hashCode() {
return absoluteDn.hashCode() ^ relativeDn.hashCode();
}
}

View File

@@ -32,7 +32,10 @@ import org.apache.commons.lang.Validate;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.dao.EmptyResultDataAccessException;
import org.springframework.dao.IncorrectResultSizeDataAccessException;
import org.springframework.ldap.NamingException;
import org.springframework.ldap.support.LdapEntryIdentificationContextMapper;
import org.springframework.ldap.support.LdapUtils;
/**
@@ -1347,4 +1350,112 @@ public class LdapTemplate implements LdapOperations, InitializingBean {
throw new IllegalStateException("The DirContextOperations instance needs to be properly initialized.");
}
}
/*
* (non-Javadoc)
*
* @see
* org.springframework.ldap.core.LdapOperations#authenticate(javax.naming
* .Name, java.lang.String, java.lang.String)
*/
public boolean authenticate(Name base, String filter, String password) {
return authenticate(base, filter, password, new NullAuthenticatedLdapEntryContextCallback());
}
/*
* (non-Javadoc)
*
* @see
* org.springframework.ldap.core.LdapOperations#authenticate(java.lang.String
* , java.lang.String, java.lang.String)
*/
public boolean authenticate(String base, String filter, String password) {
return authenticate(base, filter, password, new NullAuthenticatedLdapEntryContextCallback());
}
/*
* (non-Javadoc)
*
* @see
* org.springframework.ldap.core.LdapOperations#authenticate(java.lang.String
* , java.lang.String, java.lang.String,
* org.springframework.ldap.core.AuthenticatedLdapEntryContextCallback)
*/
public boolean authenticate(String base, String filter, String password,
AuthenticatedLdapEntryContextCallback callback) {
return authenticate(new DistinguishedName(base), filter, password, callback);
}
/*
* (non-Javadoc)
*
* @see
* org.springframework.ldap.core.LdapOperations#authenticate(javax.naming
* .Name, java.lang.String, java.lang.String,
* org.springframework.ldap.core.AuthenticatedLdapEntryContextCallback)
*/
public boolean authenticate(Name base, String filter, String password,
final AuthenticatedLdapEntryContextCallback callback) {
List result = search(base, filter, new LdapEntryIdentificationContextMapper());
if (result.size() != 1) {
log.error("Unable to find unique entry matching in authentication; base: '" + base + "'; filter: '"
+ filter + "'. Found " + result.size() + " mathching entries");
return false;
}
final LdapEntryIdentification entryIdentification = (LdapEntryIdentification) result.get(0);
try {
DirContext ctx = contextSource.getContext(entryIdentification.getAbsoluteDn().toString(), password);
executeWithContext(new ContextExecutor() {
public Object executeWithContext(DirContext ctx) throws javax.naming.NamingException {
callback.executeWithContext(ctx, entryIdentification);
return null;
}
}, ctx);
return true;
}
catch (Exception e) {
log.error("Authentication failed for entry with DN '" + entryIdentification.getAbsoluteDn() + "'", e);
return false;
}
}
/*
* (non-Javadoc)
*
* @see
* org.springframework.ldap.core.LdapOperations#searchForObject(javax.naming
* .Name, java.lang.String, org.springframework.ldap.core.ContextMapper)
*/
public Object searchForObject(Name base, String filter, ContextMapper mapper) {
List result = search(base, filter, mapper);
if (result.size() == 0) {
throw new EmptyResultDataAccessException(1);
}
else if (result.size() != 1) {
throw new IncorrectResultSizeDataAccessException(1, result.size());
}
return result.get(0);
}
/*
* (non-Javadoc)
*
* @see
* org.springframework.ldap.core.LdapOperations#searchForObject(java.lang
* .String, java.lang.String, org.springframework.ldap.core.ContextMapper)
*/
public Object searchForObject(String base, String filter, ContextMapper mapper) {
return searchForObject(new DistinguishedName(base), filter, mapper);
}
private static final class NullAuthenticatedLdapEntryContextCallback implements
AuthenticatedLdapEntryContextCallback {
public void executeWithContext(DirContext ctx, LdapEntryIdentification ldapEntryIdentification) {
// Do nothing
}
}
}

View File

@@ -0,0 +1,37 @@
/*
* Copyright 2005-2008 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.ldap.support;
import org.springframework.ldap.core.DirContextOperations;
import org.springframework.ldap.core.DistinguishedName;
import org.springframework.ldap.core.LdapEntryIdentification;
import org.springframework.ldap.core.support.AbstractContextMapper;
/**
* <code>ContextMapper</code> implementation that maps the found entries to the
* {@link LdapEntryIdentification} of each respective entry.
*
* @author Mattias Hellborg Arthursson
* @since 1.3
*/
public class LdapEntryIdentificationContextMapper extends AbstractContextMapper {
protected Object doMapFromContext(DirContextOperations ctx) {
return new LdapEntryIdentification(new DistinguishedName(ctx.getNameInNamespace()), new DistinguishedName(ctx
.getDn()));
}
}

View File

@@ -49,7 +49,7 @@ public class AttributeCheckContextMapper implements ContextMapper {
Assert.assertNull(adapter.getStringAttribute(absentAttributes[i]));
}
return null;
return adapter;
}
public void setAbsentAttributes(String[] absentAttributes) {

View File

@@ -0,0 +1,81 @@
/*
* Copyright 2005-2008 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.ldap;
import static junit.framework.Assert.assertEquals;
import static junit.framework.Assert.assertFalse;
import static junit.framework.Assert.assertNull;
import static junit.framework.Assert.assertTrue;
import javax.naming.NamingException;
import javax.naming.directory.Attributes;
import org.junit.Ignore;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.ldap.core.AttributesMapper;
import org.springframework.ldap.core.ContextMapper;
import org.springframework.ldap.core.DirContextAdapter;
import org.springframework.ldap.core.DistinguishedName;
import org.springframework.ldap.core.LdapTemplate;
import org.springframework.ldap.core.support.AbstractContextSource;
import org.springframework.ldap.filter.AndFilter;
import org.springframework.ldap.filter.EqualsFilter;
import org.springframework.ldap.filter.WhitespaceWildcardsFilter;
import org.springframework.test.context.ContextConfiguration;
/**
* Tests the lookup methods of LdapTemplate.
*
* @author Mattias Hellborg Arthursson
* @author Ulrik Sandberg
*/
@ContextConfiguration(locations = { "/conf/ldapTemplateTestContext.xml" })
public class LdapTemplateAuthenticationITest extends AbstractLdapTemplateIntegrationTest {
@Autowired
private LdapTemplate tested;
@Test
public void testAuthenticate() {
AndFilter filter = new AndFilter();
filter.and(new EqualsFilter("objectclass", "person")).and(new EqualsFilter("uid", "some.person3"));
assertTrue(tested.authenticate("", filter.toString(), "password"));
}
@Test
public void testAuthenticateWithInvalidPassword() {
AndFilter filter = new AndFilter();
filter.and(new EqualsFilter("objectclass", "person")).and(new EqualsFilter("uid", "some.person3"));
assertFalse(tested.authenticate("", filter.toString(), "invalidpassword"));
}
@Test
public void testAuthenticateWithFilterThatDoesNotMatchAnything() {
AndFilter filter = new AndFilter();
filter.and(new EqualsFilter("objectclass", "person")).and(
new EqualsFilter("uid", "some.person.that.isnt.there"));
assertFalse(tested.authenticate("", filter.toString(), "password"));
}
@Test
public void testAuthenticateWithFilterThatMatchesSeveralEntries() {
AndFilter filter = new AndFilter();
filter.and(new EqualsFilter("objectclass", "person")).and(new WhitespaceWildcardsFilter("uid", "some.person"));
assertFalse(tested.authenticate("", filter.toString(), "password"));
}
}

View File

@@ -16,6 +16,7 @@
package org.springframework.ldap;
import static junit.framework.Assert.assertEquals;
import static junit.framework.Assert.assertNotNull;
import static junit.framework.Assert.assertTrue;
import static junit.framework.Assert.fail;
@@ -28,8 +29,13 @@ import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.EmptyResultDataAccessException;
import org.springframework.dao.IncorrectResultSizeDataAccessException;
import org.springframework.ldap.core.DirContextAdapter;
import org.springframework.ldap.core.DirContextOperations;
import org.springframework.ldap.core.DistinguishedName;
import org.springframework.ldap.core.LdapTemplate;
import org.springframework.ldap.core.support.AbstractContextMapper;
import org.springframework.ldap.test.AttributeCheckAttributesMapper;
import org.springframework.ldap.test.AttributeCheckContextMapper;
import org.springframework.test.context.ContextConfiguration;
@@ -140,6 +146,35 @@ public class LdapTemplateSearchResultITest extends AbstractLdapTemplateIntegrati
assertEquals(1, list.size());
}
@Test
public void testSearchForObject() {
contextMapper.setExpectedAttributes(ALL_ATTRIBUTES);
contextMapper.setExpectedValues(ALL_VALUES);
DirContextAdapter result = (DirContextAdapter) tested
.searchForObject(BASE_STRING, FILTER_STRING, contextMapper);
assertNotNull(result);
}
@Test(expected = IncorrectResultSizeDataAccessException.class)
public void testSearchForObjectWithMultipleHits() {
tested.searchForObject(BASE_STRING, "(&(objectclass=person)(sn=*))", new AbstractContextMapper() {
@Override
protected Object doMapFromContext(DirContextOperations ctx) {
return ctx;
}
});
}
@Test(expected = EmptyResultDataAccessException.class)
public void testSearchForObjectNoHits() {
tested.searchForObject(BASE_STRING, "(&(objectclass=person)(sn=Person does not exist))", new AbstractContextMapper() {
@Override
protected Object doMapFromContext(DirContextOperations ctx) {
return ctx;
}
});
}
@Test
public void testSearch_SearchScope_ContextMapper() {
contextMapper.setExpectedAttributes(ALL_ATTRIBUTES);

View File

@@ -33,6 +33,8 @@ import org.springframework.ldap.core.DirContextAdapter;
import org.springframework.ldap.core.DirContextOperations;
import org.springframework.ldap.core.DirContextProcessor;
import org.springframework.ldap.core.DistinguishedName;
import org.springframework.ldap.filter.AndFilter;
import org.springframework.ldap.filter.EqualsFilter;
import org.springframework.test.context.ContextConfiguration;
@ContextConfiguration(locations = { "/conf/simpleLdapTemplateTestContext.xml" })
@@ -65,6 +67,12 @@ public class SimpleLdapTemplateITest extends AbstractLdapTemplateIntegrationTest
assertEquals("Some Person3", cns.get(0));
}
@Test
public void testSearchForObject() {
String cn = ldapTemplate.searchForObject("", "(&(objectclass=person)(sn=Person3))", new CnContextMapper());
assertEquals("Some Person3", cn);
}
@Test
public void testSearchProcessor() {
SearchControls searchControls = new SearchControls();
@@ -184,6 +192,13 @@ public class SimpleLdapTemplateITest extends AbstractLdapTemplateIntegrationTest
verifyCleanup();
}
@Test
public void testAuthenticate() {
AndFilter filter = new AndFilter();
filter.and(new EqualsFilter("objectclass", "person")).and(new EqualsFilter("uid", "some.person3"));
assertTrue(ldapTemplate.authenticate("", filter.toString(), "password"));
}
private void verifyBoundCorrectData() {
DirContextOperations result = ldapTemplate.lookupContext(DN_STRING);
assertEquals("Some Person4", result.getStringAttribute("cn"));