JLDAPDirectoryProvider
=======================
This module contains an implementation of
org.sakaiproject.user.api.UserDirectoryProvider which delegates user
authentication and attribute loading to an LDAP service provider. The Novell
JLDAP library (http://www.openldap.org/jldap/) acts as a driver layer between
the UserDirectoryProvider and the LDAP host.
In its current form, this codebase is a refactored version of the code
originally contributed by David Ross and Rishi Pande, as well as patches
contributed by Erik Froese.
To include and enable this provider in your Sakai deployment, uncomment the
"sakai-jldap-provider" and "ldap" artifact dependencies in
../component/pom.xml and uncomment the "jldap-beans.xml" import in
../component/src/webapp/WEB-INF/components.xml. Then rebuild and redeploy
with maven: 'mvn clean install sakai:deploy'.
Architecture
=======================
JLDAPDirectorProvider
------------------------
JLDAPDirectoryProvider is a concrete class responsible for:
1) Collecting "standard" configuration options
2) Ensuring initialization of LdapConnectionManager and LdapAttributeMapper
collaborators
3) Preparing and executing LDAP searches
4) Caching LDAP search results
The current implementation makes every effort to preserve backward compatibility
with existing Spring bean definitions. Thus JLDAPDirectoryProvider implements
the LdapConnectionManagerConfig interface as a mixin, which allows most
"standard" behavior options to be specified directly on the
JLDAPDirectoryProvider bean, as has been the case in previous releases. This
should be considered a concession rather than a feature. Future revisions
may push configuration design to a more modular model. As a small step in
that direction, as well as an attempt to un-clutter
sakai-provider-pack/WEB-INF/components.xml in general,
JLDAPDirectoryProvider-related bean definitions have been relocated into
a sibling file named jldap-beans.xml.
Internally, JLDAPDirectoryProvider executes searches much like a Spring
"template" class, where searchDirectory() is similar to
org.springframework.jdbc.core.JdbcOperations.execute(). This method applies
search constraints, executes the search, passes the results to a
LdapEntryMapper which acts very much like a
org.springframework.jdbc.core.RowMapper, and performs connection cleanup.
JLDAPDirectoryProvider's cache implementation is simplistic, synchronized Map
of User eid's to LdapDataObjects, the latter being a DTO
represention of a User's mapped LDAPEntry. Empty search results are not cached.
Caching behaviors could be significantly improved in future revisions.
Performance of bulk lookup operations (getUsers(Collection)) could also be
radically improved, although
http://jira.sakaiproject.org/jira/browse/SAK-10830 suggests that the relative
performance gains might not be significant.
Certain portions of JLDAPDirectoryProvider's work are delegated to two
collaborator interfaces: LdapConnectionManager and LdapAttributeMapper.
LdapConnectionManager
------------------------
LdapConnectionManager encapsulates LDAP connection resource management. Two
concrete implementations are available OOTB: SimpleLdapConnectionManager and
PoolingLdapConnectionManager.
PoolingLdapConnectionManager implements a superset of
SimpleLdapConnectionManager features. As indicated by its name,
PoolingLdapConnectionManager implements (optional) LDAPConnection pooling
features, delegating the actual pooling mechanics to an instance of
org.apache.commons.pool.ObjectPool. Both LdapConnectionManager implementations
support optional "auto-binding" and SSL/TLS connectivity. "Auto-binding" is
a significant distiguishing feature for this implementation: previous
implementations were not deployable where the LDAP host disallowed
anonymous bindings.
LdapAttributeMapper
------------------------
LdapAttributeMapper encapsulates search filter building and user attribute
mapping rules. One concrete implementation is available OOTB:
SimpleLdapAttributeMapper.
SimpleLdapAttributeMapper expects the administrator to configure a simple map
of logical attribute names to physical LDAP attribute names. This map is
used to specify the set of attributes to return from _any_ LDAP search. Certain
"well-known" attributes (defined by AttributeMappingConstants) will be
mapped to first-class org.sakaiproject.user.api.User
instance members. Any additional attributes will be mapped to the User's
ResourceProperties instance.
SimpleLdapAttributeMapper collaborates with an injected UserTypeMapper
implementation to calculate Sakai User "type". Four concrete UserTypeMapper
implementations are available OOTB:
EmptyStringUserTypeMapper -- The default. Mimics historical
JLDAPDirectoryProvider behavior. Always assigns an empty String to
UserEdit.type.
StringUserTypeMapper -- A generalization of EmptyStringUserTypeMapper
which allows a given String to be assigned to UserEdit.type in
all cases.
EntryAttributeToUserTypeMapper -- Assigns a mapped attribute value, typically
an attribute representing group membership(s), to UserEdit.type. This is a
somewhat simplified version of the user type mapping code commented out in
the original JLDAPDirectoryProvider implementation.
EntryContainerRdnToUserTypeMapper -- Maps the user's LDAPEntry's container's
RDN value to a Sakai user type (will use most local RDN value by default,
recurseRdnIfNoMapping enabled will recurse through available RDNs). For
example, if the user's DN is cn=user1,ou=faculty,ou=users,dc=university,dc=edu,
the ou=faculty RDN can be mapped to a Sakai user type String.
SimpleLdapAttributeMapper mapping behaviors can also be extended by
overriding mapLdapEntryOntoUserData(LDAPEntry ldapEntry, LdapUserData userData)
and/or mapLdapAttributeOntoUserData(LDAPAttribute attribute,
LdapUserData userData, String logicalAttrName). Override the former to
localize mapping behaviors at the LDAPEntry scope. Override the latter
to localize mapping behaviors at the LDAPAttribute scope.
As of this writing, SimpleLdapAttributeMapper treats all LDAP attributes
as single-valued.
Configuration
=======================
Until work is complete on fully externalizing UDP bean configuration
into [sakai|local|security].properties, localization of
JLDAPDirectoryProvider configuration usually requires modifications to
../component/WEB-INF/components.xml and
../component/WEB-INF/jldap-beans.xml, relative to this readme.
../component/WEB-INF/jldap-beans.xml is merged into
../component/WEB-INF/components.xml by by import. If you believe you've
made all the correct changes to jldap-beans.xml, but Sakai still does not
seem to behave as expected, be sure that you have uncommented the
jldap-beans.xml import in components.xml.
OOTB, jldap-beans.xml describes nearly all available configuration
options. Several options are worthy of special note:
Non-Anonymous Binding (aka "auto-binding")
------------------------
Set the following properties on the JLDAPDirectoryProvider bean definition:
1) autoBind = true
2) ldapUser = {some-DN}
3) ldapPassword = {ldapUser-password}
Please note that as of this writing, ldapUser and ldapPassword will be
ignored if autoBind is set to false.
Because bean definitions are checked into source control,
ldapPassword is typically defined in one of the three standard Sakai
property-override configuration files, e.g. ${sakai.home}/security.properties.
For example:
ldapPassword@org.sakaiproject.user.api.UserDirectoryProvider=#######
ldaps:// and StartTLS
------------------------
For ldaps:// connectivity, set the following properties on the
JLDAPDirectoryProvider bean definition:
1) ldapPort = 636
2) secureConnection = true
Check with your LDAP administrator for the correct port for ldaps://
connections. 636 is just a commonly configured value.
For StartTLS support set the following:
1) secureConnection = true
2) secureSocketFactory=com.novell.ldap.LDAPJSSEStartTLSFactory
Most likely, you will leave ldapPort set to the default value (389) for
StartTLS-secured connections.
If the secureConnection property is true, the current implementation will
force the deployer to configure keystoreLocation and keystorePassword properties
on the JLDAPDirectoryProvider, unless the javax.net.ssl.trustStore and
javax.net.ssl.trustStorePassword system properties have been set. Future
implementations may be more flexible in this regard.
EID Blacklisting
-----------------------
In some cases it may be appropriate to short-circuit searches on certain EID values.
For example, perhaps your directory loads placeholder entries with stock login
values like "guest" while user accounts are provisioned. Directory ACLs may be such
that Sakai can read attributes from these entries, but doing so would in fact be
inappropriate since the user record has not yet been properly initialized. In other
cases, you may be able to predict that certain EIDs will never resolve to directory
entries, in which cases skipping a network hop to the directory may be appealing. Such
EID interception policies can be configured by injecting an
edu.amc.sakai.user.EidValidator instance into the JLDAPDirectoryProvider.
jldap-beans.xml contains a commented-out example of configuring a
RegexpBlacklistEidValidator implementation of that interface. That class accepts a
list of Java Pattern strings and refuses to "validate" any EID matching any pattern
in that list.
Please note that email address blacklisting is not supported in any way at this time.
For example, if a client invokes JLDAPDirectoryProvider.findUserByEmail(), the result
is _not_ suppressed even if the current EIDValidator would refuse to validate the
resulting user's EID.
Typically, EID blacklists are configured at startup and require a restart to pick up
new configuration. However, it is technically possible to adjust the blacklist
configuration at runtime by any number of means. Please be aware, though, that EID
blacklisting may not have an immediate effect if a blacklisted user has already been
cached. That is, the cache entry must timeout or otherwise flush the user record before
blacklisting policies will go into effect for that user.
Derived Email Addresses
------------------------
At some institutions, not all user attributes are stored in a single directory. In
the absence of being able to easily configure attribute merging from multiple directories,
though, it is at least possible able to calculate email addresses from user EID values.
This behavior is implemented by EmailAddressDerivingLdapAttributeMapper, which takes two
new properties in addition to the standard properties supported by
SimpleLdapAttributeMapper:
1) addressPattern -- Regexp describing the addresses which the provider will assume
are not known to the LDAP host. This will cause the provider to attempt to search for
a user using an EID derived from the email address.
2) defaultAddressDomain -- This domain will be used to calculate an email address for
users entries returned from the LDAP host which do not contain email attributes.
Specifically, the address will be created by concatenating the users EID and this
domain.
In most cases, these two properties are set to nearly identical values. For example:
However, these configuration properties are asymmetric in order to allow for situations
where a school may wish to configure this feature for performance reasons rather than
limited data stores. In these cases, addressPattern would be configured to match several
email domains such that most findUserByEmail() operations are converted to getUserByEid()
operations in order to leverage the provider's EID-keyed cache. In this situation
defaultAddressDomain could be set to null if the LDAP host in fact supplies email
attributes. Otherwise a single domain will need to be specified for any user entry which
does not report an email attribute. More complicated domain selection strategies could
be implemented via EmailAddressDerivingLdapAttributeMapper extension.
Connection Pooling
------------------------
Currently, unless a custom LdapConnectionManager is injected into
LDAPDirectoryProvider, configuration options are limited to enabling/disabling
pooling and setting the pool size. Enable pooling by setting
JLDAPDirectoryProvider.pooling property to true. This will cause the
JLDAPDirectoryProvider to instantiate a PoolingLdapConnectionManager at
initialization time. PoolingLdapConnectionManager.init() will create and
cache the pool itself, sizing it based on the value configured in
JLDAPDirectoryProvider.poolMaxConns.
During testing at Georgia Tech against Fedora LDAP, it was discovered that the
LDAPConnection.isConnectionAlive() method is unreliable for non-OpenLDAP
service providers. Because of this, PooledLDAPConnectionFactory can be
injected with a LdapConnectionLivenessValidator implementation in which
arbitrary connection validity checks can be executed. The default
implementation simply checks the return value of
LDAPConnection.isConnectionAlive(). A generalized version of
the Georgia Tech connection validator is also available out of the box.
The following sample configuration in the JLDAPDirectoryProvider bean
definition illustrates deployment of this validator:
cn=admin,dc=nodomain
cn
true
10
Note that customized connection liveness testing is only available
when explicitly defining an LdapConnectionManager bean.
Expect pooling configuration to be refactored in future revisions to avoid
splitting configuration options between the LdapConnectionManager and the
LDAPDirectoryProvider.
User Attribute Mapping
------------------------
SimpleLdapAttributeMapper has two configurable properties:
1) attributeMappings -- a Map of logical attribute names to physical LDAP
attribute names. AttributeMappingConstants.DEFAULT_ATTR_MAPPINGS
defines the set of default attribute names. attributeMappings need
not be specified if DEFAULT_ATTR_MAPPINGS meets your needs.
The attributeMappings specified in your bean definition completely
override DEFAULT_ATTR_MAPPINGS. The two Maps are not merged in
any way. Keep in mind, though, that the keys in DEFAULT_ATTR_MAPPINGS
are considered "well known" values and may be used elsewhere, even
if you do not define mappings for those names. For example,
getFindUserByEmailFilter() implicitly requires the presence of a
mapping for AttributeMappingConstants.EMAIL_ATTR_MAPPING_KEY.
By default, any mapped attribute which does not have a corresponding
key in DEFAULT_ATTR_MAPPINGS will be mapped onto a User
ResourceProperties name-value pair.
AttributeMappingConstants.GROUP_MEMBERSHIP_ATTR_MAPPING_KEY is the
exception to this rule. If the mapped attribute is present in an
LDAPEntry, it too will be mapped to ResourceProperties name-value pair.
SimpleLDAPAttributMapper will also map user DNs into User
ResourceProperties.
2) userTypeMapper -- a strategy for calculating Sakai user "type". This
mechanism, and the three OOTB implementations, were discussed in the
"Architecture" section above. By default, SimpleLdapAttributeMapper
will delegate user type calclulation to an instance of
EmptyStringUserTypeMapper. This preserves backward-compatible
behavior.
See the ../component/WEB-XML/jldap-beans.xml, relative to this README, for
an example of a SimpleLdapAttributeMapper bean definition.
Testing
=======================
JLDAPDirectoryProvider source code includes JUnit and JMock-implemented unit-
and integration tests. Unit tests are located in ./src/test, relative to this
README. Integration tests are located in a separate Maven project:
../jldap-integration-test/, relative to this README. (Several unit tests exist
in ../jldap-integration-test as well, mainly to verify that utility classes
in that project function as expected.) Unit tests are relatively white-box
tests intended to verify certain JLDAPDirectoryProvider implementation details
during each build. The integration tests, which leverage Josh Holtzman's
"test-harness" framework, are relatively black-box tests intended to verify
your localized configuration against a "real" LDAP service provider.
No configuration is necessary to enable execution of unit tests.
Integration tests require that Sakai has already been built and deployed to
a Tomcat instance on the local file system. These tests will reuse the bean
definitions and property overrides which have deployed to that Tomcat.
In my experience, executing integration tests from Eclipse is generally more
reliable, faster and more useful than executing integration tests from Maven,
although both approaches will work.
To execute integration tests from Eclipse, you may find that you need to
create a new run configuration for JLDAPDirectoryProviderIntegrationTestSuite,
for example to adjust heap size or specify a value for test.tomcat.home
as a JVM system property. Otherwise, just right click on
JLDAPDirectoryProviderIntegrationTestSuite in the Package Explorer and select
Run As -> JUnit Test. You may experience an extended pause as the Sakai
ComponentManager initializes during TestSuite setup.
To execute integration tests from Maven, you will need to define and activate
a profile which overrides the value of the maven.test.skip property. For
example, in ~/.m2/settings.xml:
sakai
/opt/tomcat/
sakai-force-tests
/opt/tomcat/
false
sakai
Then, from the command line within ../jldap-directory-test/
(after building and deploying Sakai, of course):
%> mvn -P sakai-force-tests test
Integration Test Configuration
------------------------
For each test method in JLDAPDirectoryProviderTest, a new ApplicationContext
will be created and attached to the Sakai ApplicationContext as a child.
Beans in this child ApplicationContext are defined by
../jldap-integration-test/src/test/resources/jldap-test-context.xml. You can
override, extend and otherwise localize those bean definitions by creating a
jldap-test-context-local.xml in the same directory.
Many individual bean properties are externalized into
../jldap-integration-test/src/testresources/jldap-test.properties. You can
override, extend and otherwise localize properties in jldap-test.properties by
defining a jldap-test-local.properties file in the same directory.
Comments in jldap-test-context.xml document most configuration options. The key
abstraction to be aware of when localizing your tests is the UserEditStub.
Instance of that class define test inputs and expectations. For example,
JLDAPDirectoryProviderTest.
testMapsLdapAttributesOntoSakaiUserEditInstanceWhenSearchingByEid()
essentially compares the UserEdit populated by
JLDAPDirectoryProvider.getUser() to a UserEditStub defined in your test
application context, using the latter's eid field to "seed" the UserEdit
to be populated.
At a minimum, you will need to provide definitions for one valid UserEditStub
for positive tests, and one invalid UserEditStub for negative tests. To be
most useful, though, especially if you are deploying a UserTypeMapper, you
should provide at least one additional UserEditStub definition to verify
that your UserTypeMapper can discriminate between user types.
For example, in my current test configuration, I have the following
beans defined in jldap-test-context-local.xml, which reflects the fact that I
intend to map a groupMembership attribute into my valid Sakai User's
ResourceProperties:
${user-1-type}
false
${user-2-type}
false
Then, in jldap-test-local.properties, I override several properties in
jldap-test.properties to reflect the actual state of my LDAP tree:
user-1-login=student1
user-1-type=student
user-1-property-udp.dn=cn=student1,ou=users,dc=nodomain
user-2-login=faculty1
user-2-type=faculty
user-2-property-udp.dn=cn=faculty1,ou=users,dc=nodomain
Issues Addressed
=======================
This implementation addresses the following open (at least as of Aug 18, 2007)
Jira issues:
http://bugs.sakaiproject.org/jira/browse/SAK-4184 -- AD support
http://bugs.sakaiproject.org/jira/browse/SAK-4190 -- AD support
http://bugs.sakaiproject.org/jira/browse/SAK-4530 -- Connection leak
http://bugs.sakaiproject.org/jira/browse/SAK-7338 -- User.eid mapping
Questions/Comments
=======================
Please contact Dan Mccallum (dmccallum@unicon.net).