diff --git a/config/alfresco/subsystems/Authentication/common-ldap-context.xml b/config/alfresco/subsystems/Authentication/common-ldap-context.xml new file mode 100644 index 0000000000..0075fd01b0 --- /dev/null +++ b/config/alfresco/subsystems/Authentication/common-ldap-context.xml @@ -0,0 +1,354 @@ + + + + + + + + + + + + + + + + + + + + + ${ldap.authentication.active} + + + + + + + ${ldap.authentication.userNameFormat} + + + + + + + + + + + + ${ldap.authentication.escapeCommasInBind} + + + ${ldap.authentication.escapeCommasInUid} + + + ${ldap.authentication.allowGuestLogin} + + + ${ldap.authentication.defaultAdministratorUserNames} + + + + + + + org.alfresco.repo.security.authentication.AuthenticationComponent + + + + + + + + + + ${server.transaction.mode.default} + + + + + + + + + + + + + + + + + + + + + + + + + + + + ${ldap.authentication.java.naming.factory.initial} + + + + + + + ${ldap.authentication.java.naming.provider.url} + + + + + + + + ${ldap.authentication.java.naming.security.authentication} + + + + + + ${ldap.synchronization.java.naming.security.principal} + + + + + ${ldap.synchronization.java.naming.security.credentials} + + + + + + + + + + ${ldap.synchronization.active} + + + + + ${ldap.synchronization.queryBatchSize} + + + + + ${ldap.synchronization.groupQuery} + + + + + ${ldap.synchronization.groupDifferentialQuery} + + + + + ${ldap.synchronization.personQuery} + + + + + ${ldap.synchronization.personDifferentialQuery} + + + + + ${ldap.synchronization.groupSearchBase} + + + + + ${ldap.synchronization.userSearchBase} + + + + + ${ldap.synchronization.userIdAttributeName} + + + + + ${ldap.synchronization.modifyTimestampAttributeName} + + + + + ${ldap.synchronization.timestampFormat} + + + + + ${ldap.synchronization.groupIdAttributeName} + + + + + ${ldap.synchronization.groupType} + + + + + ${ldap.synchronization.personType} + + + + + ${ldap.synchronization.groupMemberAttributeName} + + + + + + + + ${ldap.synchronization.userIdAttributeName} + + + + + ${ldap.synchronization.userFirstNameAttributeName} + + + + + ${ldap.synchronization.userLastNameAttributeName} + + + + + ${ldap.synchronization.userEmailAttributeName} + + + + + ${ldap.synchronization.userOrganizationalIdAttributeName} + + + + + + + + + + + + + ${ldap.synchronization.defaultHomeFolderProvider} + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/config/alfresco/subsystems/Authentication/ldap-ad/ldap-ad-authentication-context.xml b/config/alfresco/subsystems/Authentication/ldap-ad/ldap-ad-authentication-context.xml new file mode 100644 index 0000000000..05442f1f66 --- /dev/null +++ b/config/alfresco/subsystems/Authentication/ldap-ad/ldap-ad-authentication-context.xml @@ -0,0 +1,10 @@ + + + + + + + \ No newline at end of file diff --git a/config/alfresco/subsystems/Authentication/ldap-ad/ldap-ad-authentication.properties b/config/alfresco/subsystems/Authentication/ldap-ad/ldap-ad-authentication.properties new file mode 100644 index 0000000000..c6b59b0201 --- /dev/null +++ b/config/alfresco/subsystems/Authentication/ldap-ad/ldap-ad-authentication.properties @@ -0,0 +1,109 @@ +# This flag enables use of this LDAP subsystem for authentication. It may be +# that this subsytem should only be used for synchronization, in which case +# this flag should be set to false. +ldap.authentication.active=true + +# +# This properties file brings together the common options for LDAP authentication rather than editing the bean definitions +# +ldap.authentication.allowGuestLogin=true +# How to map the user id entered by the user to taht passed through to LDAP +# - simple +# - this must be a DN and would be something like +# uid=%s,ou=People,dc=company,dc=com +# - digest +# - usually pass through what is entered +# %s +ldap.authentication.userNameFormat=%s + +# The LDAP context factory to use +ldap.authentication.java.naming.factory.initial=com.sun.jndi.ldap.LdapCtxFactory + +# The URL to connect to the LDAP server +ldap.authentication.java.naming.provider.url=ldap://domaincontroller.company.com:389 + +# The authentication mechanism to use +ldap.authentication.java.naming.security.authentication=DIGEST-MD5 + +# Escape commas entered by the user at bind time +# Useful when using simple authentication and the CN is part of the DN and contains commas +ldap.authentication.escapeCommasInBind=false + +# Escape commas entered by the user when setting the authenticated user +# Useful when using simple authentication and the CN is part of the DN and contains commas, and the escaped \, is +# pulled in as part of an LDAP sync +# If this option is set to true it will break the default home folder provider as space names can not contain \ +ldap.authentication.escapeCommasInUid=false + +# Comma separated list of user names who should be considered administrators by default +ldap.authentication.defaultAdministratorUserNames=Administrator + +# This flag enables use of this LDAP subsystem for user and group +# synchronization. It may be that this subsytem should only be used for +# authentication, in which case this flag should be set to false. +ldap.synchronization.active=true + +# The default principal to use (only used for LDAP sync) +ldap.synchronization.java.naming.security.principal=alfresco + +# The password for the default principal (only used for LDAP sync) +ldap.synchronization.java.naming.security.credentials=secret + +# If positive, this property indicates that RFC 2696 paged results should be +# used to split query results into batches of the specified size. This +# overcomes any size limits imposed by the LDAP server. +ldap.synchronization.queryBatchSize=1000 + +# The query to select all objects that represent the groups to import. +ldap.synchronization.groupQuery=(objectclass\=group) + +# The query to select objects that represent the groups to import that have changed since a certain time. +ldap.synchronization.groupDifferentialQuery=(&(objectclass\=group)(!(modifyTimestamp<\={0}))) + +# The query to select all objects that represent the users to import. +ldap.synchronization.personQuery=(&(objectclass\=user)(userAccountControl\:1.2.840.113556.1.4.803\:\=512)) + +# The query to select objects that represent the users to import that have changed since a certain time. +ldap.synchronization.personDifferentialQuery=(&(objectclass\=user)(userAccountControl\:1.2.840.113556.1.4.803\:\=512)(!(modifyTimestamp<\={0}))) + +# The group search base restricts the LDAP group query to a sub section of tree on the LDAP server. +ldap.synchronization.groupSearchBase=ou\=Security Groups,ou\=Alfresco,dc=domain + +# The user search base restricts the LDAP user query to a sub section of tree on the LDAP server. +ldap.synchronization.userSearchBase=ou\=User Accounts,ou=\Alfresco,dc=domain + +# The name of the operational attribute recording the last update time for a group or user. +ldap.synchronization.modifyTimestampAttributeName=modifyTimestamp + +# The timestamp format. Unfortunately, this varies between directory servers. +ldap.synchronization.timestampFormat=yyyyMMddHHmmss'.0Z' + +# The attribute name on people objects found in LDAP to use as the uid in Alfresco +ldap.synchronization.userIdAttributeName=sAMAccountName + +# The attribute on person objects in LDAP to map to the first name property in Alfresco +ldap.synchronization.userFirstNameAttributeName=givenName + +# The attribute on person objects in LDAP to map to the last name property in Alfresco +ldap.synchronization.userLastNameAttributeName=sn + +# The attribute on person objects in LDAP to map to the email property in Alfresco +ldap.synchronization.userEmailAttributeName=mail + +# The attribute on person objects in LDAP to map to the organizational id property in Alfresco +ldap.synchronization.userOrganizationalIdAttributeName=company + +# The default home folder provider to use for people created via LDAP import +ldap.synchronization.defaultHomeFolderProvider=personalHomeFolderProvider + +# The attribute on LDAP group objects to map to the gid property in Alfrecso +ldap.synchronization.groupIdAttributeName=cn + +# The group type in LDAP +ldap.synchronization.groupType=group + +# The person type in LDAP +ldap.synchronization.personType=user + +# The attribute in LDAP on group objects that defines the DN for its members +ldap.synchronization.groupMemberAttributeName=member diff --git a/config/alfresco/subsystems/Authentication/ldap/ldap-authentication-context.xml b/config/alfresco/subsystems/Authentication/ldap/ldap-authentication-context.xml index 74f82bb910..05442f1f66 100644 --- a/config/alfresco/subsystems/Authentication/ldap/ldap-authentication-context.xml +++ b/config/alfresco/subsystems/Authentication/ldap/ldap-authentication-context.xml @@ -3,342 +3,8 @@ - - - - - - - - - - - - - - - ${ldap.authentication.active} - - - - - - - ${ldap.authentication.userNameFormat} - - - - - - - - - - - - ${ldap.authentication.escapeCommasInBind} - - - ${ldap.authentication.escapeCommasInUid} - - - ${ldap.authentication.allowGuestLogin} - - - ${ldap.authentication.defaultAdministratorUserNames} - - - - - - - org.alfresco.repo.security.authentication.AuthenticationComponent - - - - - - - - - - ${server.transaction.mode.default} - - - - - - - - - - - - - - - - - - - - - - - - - - - - ${ldap.authentication.java.naming.factory.initial} - - - - - - - ${ldap.authentication.java.naming.provider.url} - - - - - - - - ${ldap.authentication.java.naming.security.authentication} - - - - - - ${ldap.authentication.java.naming.security.principal} - - - - - ${ldap.authentication.java.naming.security.credentials} - - - - - - - - - - ${ldap.synchronization.active} - - - - - ${ldap.synchronization.queryBatchSize} - - - - - ${ldap.synchronization.groupQuery} - - - - - ${ldap.synchronization.groupDifferentialQuery} - - - - - ${ldap.synchronization.personQuery} - - - - - ${ldap.synchronization.personDifferentialQuery} - - - - - ${ldap.synchronization.groupSearchBase} - - - - - ${ldap.synchronization.userSearchBase} - - - - - ${ldap.synchronization.userIdAttributeName} - - - - - ${ldap.synchronization.modifyTimestampAttributeName} - - - - - ${ldap.synchronization.groupIdAttributeName} - - - - - ${ldap.synchronization.groupType} - - - - - ${ldap.synchronization.personType} - - - - - ${ldap.synchronization.groupMemberAttributeName} - - - - - - - - ${ldap.synchronization.userIdAttributeName} - - - - - ${ldap.synchronization.userFirstNameAttributeName} - - - - - ${ldap.synchronization.userLastNameAttributeName} - - - - - ${ldap.synchronization.userEmailAttributeName} - - - - - ${ldap.synchronization.userOrganizationalIdAttributeName} - - - - - - - - - - - - - ${ldap.synchronization.defaultHomeFolderProvider} - - - - - - - - - - - - - - + \ No newline at end of file diff --git a/config/alfresco/subsystems/Authentication/ldap/ldap-authentication.properties b/config/alfresco/subsystems/Authentication/ldap/ldap-authentication.properties index 8d1a2429ed..c17dfa1574 100644 --- a/config/alfresco/subsystems/Authentication/ldap/ldap-authentication.properties +++ b/config/alfresco/subsystems/Authentication/ldap/ldap-authentication.properties @@ -25,12 +25,6 @@ ldap.authentication.java.naming.provider.url=ldap://openldap.domain.com:389 # The authentication mechanism to use ldap.authentication.java.naming.security.authentication=simple -# The default principal to use (only used for LDAP sync) -ldap.authentication.java.naming.security.principal=cn\=Manager,dc\=company,dc\=com - -# The password for the default principal (only used for LDAP sync) -ldap.authentication.java.naming.security.credentials=secret - # Escape commas entered by the user at bind time # Useful when using simple authentication and the CN is part of the DN and contains commas ldap.authentication.escapeCommasInBind=false @@ -49,6 +43,12 @@ ldap.authentication.defaultAdministratorUserNames= # authentication, in which case this flag should be set to false. ldap.synchronization.active=true +# The default principal to use (only used for LDAP sync) +ldap.synchronization.java.naming.security.principal=cn\=Manager,dc\=company,dc\=com + +# The password for the default principal (only used for LDAP sync) +ldap.synchronization.java.naming.security.credentials=secret + # If positive, this property indicates that RFC 2696 paged results should be # used to split query results into batches of the specified size. This # overcomes any size limits imposed by the LDAP server. @@ -75,6 +75,9 @@ ldap.synchronization.userSearchBase=ou\=People,dc\=company,dc\=com # The name of the operational attribute recording the last update time for a group or user. ldap.synchronization.modifyTimestampAttributeName=modifyTimestamp +# The timestamp format. Unfortunately, this varies between directory servers. +ldap.synchronization.timestampFormat=yyyyMMddHHmmss'Z' + # The attribute name on people objects found in LDAP to use as the uid in Alfresco ldap.synchronization.userIdAttributeName=uid diff --git a/source/java/org/alfresco/repo/security/sync/ldap/LDAPUserRegistry.java b/source/java/org/alfresco/repo/security/sync/ldap/LDAPUserRegistry.java index cc680320a7..fbe14419b1 100644 --- a/source/java/org/alfresco/repo/security/sync/ldap/LDAPUserRegistry.java +++ b/source/java/org/alfresco/repo/security/sync/ldap/LDAPUserRegistry.java @@ -145,11 +145,12 @@ public class LDAPUserRegistry implements UserRegistry, InitializingBean, Activat private String[] groupAttributeNames; /** The LDAP generalized time format. */ - private static DateFormat LDAP_GENERALIZED_TIME_FORMAT; - static + private DateFormat timestampFormat; + + public LDAPUserRegistry() { - LDAPUserRegistry.LDAP_GENERALIZED_TIME_FORMAT = new SimpleDateFormat("yyyyMMddHHmmss'Z'"); - LDAPUserRegistry.LDAP_GENERALIZED_TIME_FORMAT.setTimeZone(TimeZone.getTimeZone("GMT")); + // Default to official LDAP generalized time format (unfortunately not used by Active Directory) + setTimestampFormat("yyyyMMddHHmmss'Z'"); } /** @@ -295,6 +296,22 @@ public class LDAPUserRegistry implements UserRegistry, InitializingBean, Activat this.modifyTimestampAttributeName = modifyTimestampAttributeName; } + /** + * Sets the timestamp format. Unfortunately, this varies between directory servers. + * + * @param timestampFormat + * the timestamp format + * + */ + public void setTimestampFormat(String timestampFormat) + { + this.timestampFormat = new SimpleDateFormat(timestampFormat); + this.timestampFormat.setTimeZone(TimeZone.getTimeZone("GMT")); + } + /** * Decides whether to error on missing group members. * @@ -464,12 +481,17 @@ public class LDAPUserRegistry implements UserRegistry, InitializingBean, Activat { searchResults = ctx.search(this.groupSearchBase, this.groupDifferentialQuery, new Object[] { - LDAPUserRegistry.LDAP_GENERALIZED_TIME_FORMAT.format(modifiedSince) + this.timestampFormat.format(modifiedSince) }, userSearchCtls); } LdapName groupDistinguishedNamePrefix = new LdapName(this.groupSearchBase); LdapName userDistinguishedNamePrefix = new LdapName(this.userSearchBase); + + // Work out whether the user and group trees are disjoint. This may allow us to optimize reverse DN + // resolution. + boolean disjoint = !groupDistinguishedNamePrefix.startsWith(userDistinguishedNamePrefix) + && !userDistinguishedNamePrefix.startsWith(groupDistinguishedNamePrefix); while (searchResults.hasMoreElements()) { @@ -511,8 +533,7 @@ public class LDAPUserRegistry implements UserRegistry, InitializingBean, Activat Attribute modifyTimestamp = attributes.get(this.modifyTimestampAttributeName); if (modifyTimestamp != null) { - group.setLastModified(LDAPUserRegistry.LDAP_GENERALIZED_TIME_FORMAT.parse(modifyTimestamp.get() - .toString())); + group.setLastModified(this.timestampFormat.parse(modifyTimestamp.get().toString())); } Set childAssocs = group.getChildAssociations(); @@ -530,7 +551,7 @@ public class LDAPUserRegistry implements UserRegistry, InitializingBean, Activat // If the user and group search bases are different we may be able to recognise user and // group DNs without a secondary lookup - if (!this.userSearchBase.equalsIgnoreCase(this.groupSearchBase)) + if (disjoint) { Attributes nameAttributes = distinguishedName.getRdn(distinguishedName.size() - 1) .toAttributes(); @@ -563,8 +584,8 @@ public class LDAPUserRegistry implements UserRegistry, InitializingBean, Activat { "objectclass", this.groupIdAttributeName, this.userIdAttributeName }); - String objectclass = (String) childAttributes.get("objectclass").get(); - if (objectclass.equalsIgnoreCase(this.personType)) + Attribute objectClass = childAttributes.get("objectclass"); + if (hasAttributeValue(objectClass, this.personType)) { nameAttribute = childAttributes.get(this.userIdAttributeName); if (nameAttribute == null) @@ -586,9 +607,8 @@ public class LDAPUserRegistry implements UserRegistry, InitializingBean, Activat childAssocs.add((String) nameAttribute.get()); continue; } - else if (objectclass.equalsIgnoreCase(this.groupType)) + else if (hasAttributeValue(objectClass, this.groupType)) { - nameAttribute = childAttributes.get(this.groupIdAttributeName); if (nameAttribute == null) { @@ -656,6 +676,37 @@ public class LDAPUserRegistry implements UserRegistry, InitializingBean, Activat } } + /** + * Does a case-insensitive search for the given value in an attribute + * + * @param attribute + * the attribute + * @param value + * the value to search for + * @return true, if the value was found + * @throws NamingException + * if there is a problem accessing the attribute values + */ + private boolean hasAttributeValue(Attribute attribute, String value) throws NamingException + { + NamingEnumeration values = attribute.getAll(); + while (values.hasMore()) + { + try + { + if (value.equalsIgnoreCase((String) values.next())) + { + return true; + } + } + catch (ClassCastException e) + { + // Not a string value. ignore and continue + } + } + return false; + } + /** * Wraps the LDAP user query as an {@link Iterator}. */ @@ -811,8 +862,8 @@ public class LDAPUserRegistry implements UserRegistry, InitializingBean, Activat { try { - person.setLastModified(LDAPUserRegistry.LDAP_GENERALIZED_TIME_FORMAT.parse(modifyTimestamp - .get().toString())); + person.setLastModified(LDAPUserRegistry.this.timestampFormat.parse(modifyTimestamp.get() + .toString())); } catch (ParseException e) { @@ -879,7 +930,7 @@ public class LDAPUserRegistry implements UserRegistry, InitializingBean, Activat this.searchResults = this.ctx.search(LDAPUserRegistry.this.userSearchBase, LDAPUserRegistry.this.personDifferentialQuery, new Object[] { - LDAPUserRegistry.LDAP_GENERALIZED_TIME_FORMAT.format(this.modifiedSince) + LDAPUserRegistry.this.timestampFormat.format(this.modifiedSince) }, this.userSearchCtls); } }