diff --git a/ldap/openldaptest.ldif b/ldap/openldaptest.ldif new file mode 100755 index 0000000000..78f0650c16 --- /dev/null +++ b/ldap/openldaptest.ldif @@ -0,0 +1,103 @@ +dn: dc=springsource,dc=com +objectClass: dcObject +objectClass: domain +dc: springsource + +dn: ou=users,dc=springsource,dc=com +objectClass: organizationalUnit +objectClass: top +ou: users + +dn: uid=luke,ou=users,dc=springsource,dc=com +objectClass: person +objectClass: organizationalPerson +objectClass: inetOrgPerson +objectClass: top +cn: Luke +uid: luke +givenName: Luke +o: SpringSource +sn: Taylor +userPassword: password + +dn: ou=policies,dc=springsource,dc=com +objectClass: organizationalUnit +objectClass: top +ou: policies + +dn: cn=default,ou=policies,dc=springsource,dc=com +objectClass: device +objectClass: top +objectClass: pwdPolicy +cn: default +pwdAttribute: userPassword +pwdCheckQuality: 1 +pwdExpireWarning: 600000 +pwdFailureCountInterval: 0 +pwdGraceAuthNLimit: 100 +pwdInHistory: 50 +pwdLockout: FALSE +pwdLockoutDuration: 0 +pwdMaxAge: 5184000 +pwdMaxFailure: 3 +pwdMinAge: 0 +pwdMinLength: 8 +pwdMustChange: FALSE + +dn: cn=lockoutafter1,ou=policies,dc=springsource,dc=com +objectClass: device +objectClass: top +objectClass: pwdPolicy +cn: lockoutafter1 +pwdAttribute: userPassword +pwdCheckQuality: 1 +pwdFailureCountInterval: 0 +pwdGraceAuthNLimit: 2 +pwdInHistory: 3 +pwdLockout: TRUE +pwdLockoutDuration: 10 +pwdMaxFailure: 1 +pwdMinAge: 0 +pwdMinLength: 6 +pwdMustChange: TRUE + +dn: cn=expirein10,ou=policies,dc=springsource,dc=com +objectClass: device +objectClass: top +objectClass: pwdPolicy +cn: expirein10 +pwdAttribute: userPassword +pwdExpireWarning: 9999 +pwdGraceAuthNLimit: 5 +pwdMaxAge: 10000 +pwdInHistory: 3 +pwdLockout: FALSE +pwdMinLength: 6 +pwdMustChange: TRUE + + +dn: uid=expireme,ou=users,dc=springsource,dc=com +objectClass: person +objectClass: organizationalPerson +objectClass: inetOrgPerson +objectClass: top +uid: expireme +cn: Expired +givenName: Expired +o: SpringSource +sn: User +userPassword: password +pwdPolicySubentry: cn=expirein10,ou=policies,dc=springsource,dc=com + +dn: uid=lockme,ou=users,dc=springsource,dc=com +objectClass: person +objectClass: organizationalPerson +objectClass: inetOrgPerson +objectClass: top +uid: lockme +cn: Expired +givenName: Expired +o: SpringSource +sn: User +userPassword: password +pwdPolicySubentry: cn=lockoutafter1,ou=policies,dc=springsource,dc=com \ No newline at end of file diff --git a/ldap/pom.xml b/ldap/pom.xml index 22a9253175..095939e923 100644 --- a/ldap/pom.xml +++ b/ldap/pom.xml @@ -23,6 +23,12 @@ commons-logging commons-logging + + ldapsdk + ldapsdk + 4.1 + true + org.apache.directory.server apacheds-core diff --git a/ldap/run_slapd.sh b/ldap/run_slapd.sh new file mode 100755 index 0000000000..5ea13b9ff7 --- /dev/null +++ b/ldap/run_slapd.sh @@ -0,0 +1,7 @@ +#! /bin/sh + +rm -Rf target/openldap +mkdir -p target/openldap +/opt/local/libexec/slapd -h ldap://localhost:22389 -d -1 -f slapd.conf & +sleep 2 +ldapadd -h localhost -p 22389 -D cn=admin,dc=springsource,dc=com -w password -x -f openldaptest.ldif \ No newline at end of file diff --git a/ldap/slapd.conf b/ldap/slapd.conf new file mode 100755 index 0000000000..e78aff339b --- /dev/null +++ b/ldap/slapd.conf @@ -0,0 +1,53 @@ +include /opt/local/etc/openldap/schema/core.schema +include /opt/local/etc/openldap/schema/cosine.schema +include /opt/local/etc/openldap/schema/inetorgperson.schema +include /opt/local/etc/openldap/schema/ppolicy.schema + + +pidfile ./target/slapd.pid +argsfile ./target/slapd.args + +# Load dynamic backend modules: +modulepath /usr/lib/openldap/modules +# moduleload back_ldap.la +# moduleload back_meta.la +# moduleload back_monitor.la +# moduleload back_perl.la + +disallow bind_anon +require authc + +access to dn.base="" + by * read + +database bdb +suffix "dc=springsource,dc=com" +checkpoint 1024 5 +cachesize 10000 +rootdn "cn=admin,dc=springsource,dc=com" + +rootpw password + +directory ./target/openldap + +index uid eq +index cn eq +index objectClass eq + +access to attrs=userpassword + by self =wx + by anonymous =x + by * none + +access to dn.subtree="ou=users,dc=qbe,dc=com" + by self write + by * read + + +overlay ppolicy +ppolicy_default "cn=default,ou=policies,dc=springsource,dc=com" +ppolicy_use_lockout +ppolicy_hash_cleartext + + + diff --git a/ldap/src/main/java/org/springframework/security/ldap/DefaultSpringSecurityContextSource.java b/ldap/src/main/java/org/springframework/security/ldap/DefaultSpringSecurityContextSource.java index ca70de7f6a..b9a6f7d329 100644 --- a/ldap/src/main/java/org/springframework/security/ldap/DefaultSpringSecurityContextSource.java +++ b/ldap/src/main/java/org/springframework/security/ldap/DefaultSpringSecurityContextSource.java @@ -29,8 +29,8 @@ import org.springframework.util.Assert; * @since 2.0 */ public class DefaultSpringSecurityContextSource extends LdapContextSource { + protected final Log logger = LogFactory.getLog(getClass()); - private static final Log logger = LogFactory.getLog(DefaultSpringSecurityContextSource.class); private String rootDn; /** diff --git a/ldap/src/main/java/org/springframework/security/ldap/authentication/BindAuthenticator.java b/ldap/src/main/java/org/springframework/security/ldap/authentication/BindAuthenticator.java index 277b363113..5e6079531c 100644 --- a/ldap/src/main/java/org/springframework/security/ldap/authentication/BindAuthenticator.java +++ b/ldap/src/main/java/org/springframework/security/ldap/authentication/BindAuthenticator.java @@ -29,6 +29,8 @@ import org.springframework.ldap.support.LdapUtils; import org.springframework.security.authentication.BadCredentialsException; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; +import org.springframework.security.ldap.ppolicy.PasswordPolicyControl; +import org.springframework.security.ldap.ppolicy.PasswordPolicyControlExtractor; import org.springframework.util.Assert; import org.springframework.util.StringUtils; @@ -101,11 +103,22 @@ public class BindAuthenticator extends AbstractLdapAuthenticator { logger.debug("Attempting to bind as " + fullDn); + DirContext ctx = null; try { - DirContext ctx = getContextSource().getContext(fullDn.toString(), password); + ctx = getContextSource().getContext(fullDn.toString(), password); + // Check for password policy control + PasswordPolicyControl ppolicy = PasswordPolicyControlExtractor.extractControl(ctx); + Attributes attrs = ctx.getAttributes(userDn, getUserAttributes()); - return new DirContextAdapter(attrs, new DistinguishedName(userDn), ctxSource.getBaseLdapPath()); + DirContextAdapter result = new DirContextAdapter(attrs, new DistinguishedName(userDn), + ctxSource.getBaseLdapPath()); + + if (ppolicy != null) { + result.setAttributeValue(ppolicy.getID(), ppolicy); + } + + return result; } catch (NamingException e) { // This will be thrown if an invalid user name is used and the method may // be called multiple times to try different names, so we trap the exception @@ -118,6 +131,8 @@ public class BindAuthenticator extends AbstractLdapAuthenticator { } } catch (javax.naming.NamingException e) { throw LdapUtils.convertLdapException(e); + } finally { + LdapUtils.closeContext(ctx); } return null; @@ -125,7 +140,7 @@ public class BindAuthenticator extends AbstractLdapAuthenticator { /** * Allows subclasses to inspect the exception thrown by an attempt to bind with a particular DN. - * The default implementation just reports the failure to the debug log. + * The default implementation just reports the failure to the debug logger. */ protected void handleBindException(String userDn, String username, Throwable cause) { if (logger.isDebugEnabled()) { diff --git a/ldap/src/main/java/org/springframework/security/ldap/authentication/LdapAuthenticationProvider.java b/ldap/src/main/java/org/springframework/security/ldap/authentication/LdapAuthenticationProvider.java index 1e0a04fcc2..4ea6310d6f 100644 --- a/ldap/src/main/java/org/springframework/security/ldap/authentication/LdapAuthenticationProvider.java +++ b/ldap/src/main/java/org/springframework/security/ldap/authentication/LdapAuthenticationProvider.java @@ -17,9 +17,17 @@ package org.springframework.security.ldap.authentication; import java.util.List; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.context.MessageSource; +import org.springframework.context.MessageSourceAware; +import org.springframework.context.support.MessageSourceAccessor; +import org.springframework.ldap.NamingException; +import org.springframework.ldap.core.DirContextOperations; import org.springframework.security.authentication.AuthenticationProvider; import org.springframework.security.authentication.AuthenticationServiceException; import org.springframework.security.authentication.BadCredentialsException; +import org.springframework.security.authentication.LockedException; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; @@ -28,21 +36,14 @@ import org.springframework.security.core.SpringSecurityMessageSource; import org.springframework.security.core.authority.AuthorityUtils; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.security.ldap.ppolicy.PasswordPolicyException; import org.springframework.security.ldap.userdetails.DefaultLdapAuthoritiesPopulator; import org.springframework.security.ldap.userdetails.LdapAuthoritiesPopulator; import org.springframework.security.ldap.userdetails.LdapUserDetailsMapper; import org.springframework.security.ldap.userdetails.UserDetailsContextMapper; -import org.springframework.context.MessageSource; -import org.springframework.context.MessageSourceAware; -import org.springframework.context.support.MessageSourceAccessor; -import org.springframework.ldap.NamingException; -import org.springframework.ldap.core.DirContextOperations; import org.springframework.util.Assert; import org.springframework.util.StringUtils; -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; - /** * An {@link org.springframework.security.authentication.AuthenticationProvider} implementation that authenticates @@ -124,7 +125,7 @@ import org.apache.commons.logging.LogFactory; * @author Luke Taylor * @version $Id$ * - * @see org.springframework.security.ldap.authentication.BindAuthenticator + * @see BindAuthenticator * @see DefaultLdapAuthoritiesPopulator */ public class LdapAuthenticationProvider implements AuthenticationProvider, MessageSourceAware { @@ -257,6 +258,10 @@ public class LdapAuthenticationProvider implements AuthenticationProvider, Messa UserDetails user = userDetailsContextMapper.mapUserFromContext(userData, username, extraAuthorities); return createSuccessfulAuthentication(userToken, user); + } catch (PasswordPolicyException ppe) { + // The only reason a ppolicy exception can occur during a bind is that the account is locked. + throw new LockedException(messages.getMessage(ppe.getStatus().getErrorCode(), + ppe.getStatus().getDefaultMessage())); } catch (UsernameNotFoundException notFound) { if (hideUserNotFoundExceptions) { throw new BadCredentialsException(messages.getMessage( diff --git a/ldap/src/main/java/org/springframework/security/ldap/ppolicy/PasswordPolicyAwareContextSource.java b/ldap/src/main/java/org/springframework/security/ldap/ppolicy/PasswordPolicyAwareContextSource.java new file mode 100755 index 0000000000..e2ffa7bba3 --- /dev/null +++ b/ldap/src/main/java/org/springframework/security/ldap/ppolicy/PasswordPolicyAwareContextSource.java @@ -0,0 +1,87 @@ +package org.springframework.security.ldap.ppolicy; + +import java.util.Hashtable; + +import javax.naming.Context; +import javax.naming.directory.DirContext; +import javax.naming.ldap.Control; +import javax.naming.ldap.LdapContext; + +import org.springframework.ldap.support.LdapUtils; +import org.springframework.security.ldap.DefaultSpringSecurityContextSource; + + +/** + * Extended version of the DefaultSpringSecurityContextSource which adds support for + * the use of {@link PasswordPolicyControl} to make use of user account data stored in the directory. + *

+ * When binding with specific username (not the userDn) property it will connect + * first as the userDn, then reconnect as the user in order to retrieve any password-policy control + * sent with the response, even if an exception occurs. + * + * @author Luke Taylor + * @version $Id$ + * @since 3.0 + */ +public class PasswordPolicyAwareContextSource extends DefaultSpringSecurityContextSource { + + public PasswordPolicyAwareContextSource(String providerUrl) { + super(providerUrl); + } + + @Override + public DirContext getContext(String principal, String credentials) throws PasswordPolicyException { + if (principal.equals(userDn)) { + return super.getContext(principal, credentials); + } + + final boolean debug = logger.isDebugEnabled(); + + if (debug) { + logger.debug("Binding as '" + userDn + "', prior to reconnect as user '" + principal + "'" ); + } + + // First bind as manager user before rebinding as the specific principal. + LdapContext ctx = (LdapContext) super.getContext(userDn, password); + + Control[] rctls = { new PasswordPolicyControl(false) }; + + try { + ctx.addToEnvironment(Context.SECURITY_PRINCIPAL, principal ); + ctx.addToEnvironment(Context.SECURITY_CREDENTIALS, credentials); + ctx.reconnect(rctls); + } catch(javax.naming.NamingException ne) { + PasswordPolicyResponseControl ctrl = PasswordPolicyControlExtractor.extractControl(ctx); + if (debug) { + logger.debug("Failed to obtain context", ne); + logger.debug("Pasword policy response: " + ctrl); + } + + LdapUtils.closeContext(ctx); + + if (ctrl != null) { + if (ctrl.isLocked()) { + throw new PasswordPolicyException(ctrl.getErrorStatus()); + } + } + + throw LdapUtils.convertLdapException(ne); + } + + if (debug) { + logger.debug("PPolicy control returned: " + PasswordPolicyControlExtractor.extractControl(ctx)); + } + + return ctx; + } + + @Override + @SuppressWarnings("unchecked") + protected Hashtable getAuthenticatedEnv(String principal, String credentials) { + Hashtable env = super.getAuthenticatedEnv(principal, credentials); + + env.put(LdapContext.CONTROL_FACTORIES, PasswordPolicyControlFactory.class.getName()); + + return env; + } +} diff --git a/ldap/src/main/java/org/springframework/security/ldap/ppolicy/PasswordPolicyControl.java b/ldap/src/main/java/org/springframework/security/ldap/ppolicy/PasswordPolicyControl.java new file mode 100755 index 0000000000..f1d524eab3 --- /dev/null +++ b/ldap/src/main/java/org/springframework/security/ldap/ppolicy/PasswordPolicyControl.java @@ -0,0 +1,88 @@ +/* Copyright 2004, 2005, 2006 Acegi Technology Pty Limited + * + * 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.security.ldap.ppolicy; + +import javax.naming.ldap.Control; + + +/** + * + * A Password Policy request control. + *

+ * Based on the information in the corresponding internet draft on LDAP password policy. + * + * @author Stefan Zoerner + * @author Luke Taylor + * @version $Id$ + * + * @see PasswordPolicyResponseControl + * @see Password Policy for LDAP + * Directories + */ +public class PasswordPolicyControl implements Control { + //~ Static fields/initializers ===================================================================================== + + /** OID of the Password Policy Control */ + public static final String OID = "1.3.6.1.4.1.42.2.27.8.5.1"; + + //~ Instance fields ================================================================================================ + + private boolean critical; + + //~ Constructors =================================================================================================== + + /** + * Creates a non-critical (request) control. + */ + public PasswordPolicyControl() { + this(Control.NONCRITICAL); + } + + /** + * Creates a (request) control. + * + * @param critical indicates whether the control is critical for the client + */ + public PasswordPolicyControl(boolean critical) { + this.critical = critical; + } + + //~ Methods ======================================================================================================== + + /** + * Retrieves the ASN.1 BER encoded value of the LDAP control. The request value for this control is always + * empty. + * + * @return always null + */ + public byte[] getEncodedValue() { + return null; + } + + /** + * Returns the OID of the Password Policy Control ("1.3.6.1.4.1.42.2.27.8.5.1"). + */ + public String getID() { + return OID; + } + + /** + * Returns whether the control is critical for the client. + */ + public boolean isCritical() { + return critical; + } +} diff --git a/ldap/src/main/java/org/springframework/security/ldap/ppolicy/PasswordPolicyControlExtractor.java b/ldap/src/main/java/org/springframework/security/ldap/ppolicy/PasswordPolicyControlExtractor.java new file mode 100644 index 0000000000..5eddfd2c31 --- /dev/null +++ b/ldap/src/main/java/org/springframework/security/ldap/ppolicy/PasswordPolicyControlExtractor.java @@ -0,0 +1,38 @@ +package org.springframework.security.ldap.ppolicy; + +import javax.naming.directory.DirContext; +import javax.naming.ldap.Control; +import javax.naming.ldap.LdapContext; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * Obtains the PasswordPolicyControl from a context for use by other classes. + * + * @author Luke Taylor + * @version $Id$ + * @since 3.0 + */ +public class PasswordPolicyControlExtractor { + private static final Log logger = LogFactory.getLog(PasswordPolicyControlExtractor.class); + + public static PasswordPolicyResponseControl extractControl(DirContext dirCtx) { + LdapContext ctx = (LdapContext) dirCtx; + Control[] ctrls = null; + try { + ctrls = ctx.getResponseControls(); + } catch (javax.naming.NamingException e) { + logger.error("Failed to obtain response controls", e); + } + + for (int i = 0; ctrls != null && i < ctrls.length; i++) { + if (ctrls[i] instanceof PasswordPolicyResponseControl) { + return (PasswordPolicyResponseControl) ctrls[i]; + } + } + + return null; + } + +} diff --git a/ldap/src/main/java/org/springframework/security/ldap/ppolicy/PasswordPolicyControlFactory.java b/ldap/src/main/java/org/springframework/security/ldap/ppolicy/PasswordPolicyControlFactory.java new file mode 100755 index 0000000000..2435a68673 --- /dev/null +++ b/ldap/src/main/java/org/springframework/security/ldap/ppolicy/PasswordPolicyControlFactory.java @@ -0,0 +1,47 @@ +/* Copyright 2004, 2005, 2006 Acegi Technology Pty Limited + * + * 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.security.ldap.ppolicy; + +import javax.naming.ldap.Control; +import javax.naming.ldap.ControlFactory; + + +/** + * Transforms a control object to a PasswordPolicyResponseControl object, if appropriate. + * + * @author Stefan Zoerner + * @author Luke Taylor + * @version $Id$ + */ +public class PasswordPolicyControlFactory extends ControlFactory { + //~ Methods ======================================================================================================== + + /** + * Creates an instance of PasswordPolicyResponseControl if the passed control is a response control of this + * type. Attributes of the result are filled with the correct values (e.g. error code). + * + * @param ctl the control the check + * + * @return a response control of type PasswordPolicyResponseControl, or null + */ + public Control getControlInstance(Control ctl) { + if (ctl.getID().equals(PasswordPolicyControl.OID)) { + return new PasswordPolicyResponseControl(ctl.getEncodedValue()); + } + + return null; + } +} diff --git a/ldap/src/main/java/org/springframework/security/ldap/ppolicy/PasswordPolicyData.java b/ldap/src/main/java/org/springframework/security/ldap/ppolicy/PasswordPolicyData.java new file mode 100644 index 0000000000..887b65a970 --- /dev/null +++ b/ldap/src/main/java/org/springframework/security/ldap/ppolicy/PasswordPolicyData.java @@ -0,0 +1,12 @@ +package org.springframework.security.ldap.ppolicy; + +/** + * @author Luke Taylor + * @version $Id$ + * @since 3.0 + */ +public interface PasswordPolicyData { + int getTimeBeforeExpiration(); + + int getGraceLoginsRemaining(); +} diff --git a/ldap/src/main/java/org/springframework/security/ldap/ppolicy/PasswordPolicyErrorStatus.java b/ldap/src/main/java/org/springframework/security/ldap/ppolicy/PasswordPolicyErrorStatus.java new file mode 100644 index 0000000000..a727270d5e --- /dev/null +++ b/ldap/src/main/java/org/springframework/security/ldap/ppolicy/PasswordPolicyErrorStatus.java @@ -0,0 +1,53 @@ +package org.springframework.security.ldap.ppolicy; + + +/** + * Defines status codes for use with PasswordPolicyException, with error codes (for message source lookup) and default + * messages. + * + *

+ *    PasswordPolicyResponseValue ::= SEQUENCE {
+ *        warning [0] CHOICE {
+ *           timeBeforeExpiration [0] INTEGER (0 .. maxInt),
+ *           graceAuthNsRemaining [1] INTEGER (0 .. maxInt)
+ *        } OPTIONAL,
+ *        error   [1] ENUMERATED {
+ *           passwordExpired             (0),     accountLocked               (1),
+ *           changeAfterReset            (2),     passwordModNotAllowed       (3),
+ *           mustSupplyOldPassword       (4),     insufficientPasswordQuality (5),
+ *           passwordTooShort            (6),     passwordTooYoung            (7),
+ *           passwordInHistory           (8)
+ *        } OPTIONAL
+ *    }
+ *
+ * + * @author Luke Taylor + * @since 3.0 + */ +public enum PasswordPolicyErrorStatus { + PASSWORD_EXPIRED ("ppolicy.expired", "Your password has expired"), + ACCOUNT_LOCKED ("ppolicy.locked", "Account is locked"), + CHANGE_AFTER_RESET ("ppolicy.change.after.reset", "Your password must be changed after being reset"), + PASSWORD_MOD_NOT_ALLOWED ("ppolicy.mod.not.allowed", "Password cannot be changed"), + MUST_SUPPLY_OLD_PASSWORD ("ppolicy.must.supply.old.password", "The old password must be supplied"), + INSUFFICIENT_PASSWORD_QUALITY ("ppolicy.insufficient.password.quality", "The supplied password is of insufficient quality"), + PASSWORD_TOO_SHORT ("ppolicy.password.too.short", "The supplied password is too short"), + PASSWORD_TOO_YOUNG ("ppolicy.password.too.young", "Your password was changed too recently to be changed again"), + PASSWORD_IN_HISTORY ("ppolicy.password.in.history", "The supplied password has already been used"); + + private String errorCode; + private String defaultMessage; + + private PasswordPolicyErrorStatus(String errorCode, String defaultMessage) { + this.errorCode = errorCode; + this.defaultMessage = defaultMessage; + } + + public String getErrorCode() { + return errorCode; + } + + public String getDefaultMessage() { + return defaultMessage; + } +} diff --git a/ldap/src/main/java/org/springframework/security/ldap/ppolicy/PasswordPolicyException.java b/ldap/src/main/java/org/springframework/security/ldap/ppolicy/PasswordPolicyException.java new file mode 100644 index 0000000000..3964232314 --- /dev/null +++ b/ldap/src/main/java/org/springframework/security/ldap/ppolicy/PasswordPolicyException.java @@ -0,0 +1,22 @@ +package org.springframework.security.ldap.ppolicy; + +/** + * Generic exception raised by the ppolicy package. + *

+ * The status property should be checked for more detail on the cause of the exception. + * + * @author Luke Taylor + * @since 3.0 + */ +public class PasswordPolicyException extends RuntimeException { + private PasswordPolicyErrorStatus status; + + public PasswordPolicyException(PasswordPolicyErrorStatus status) { + super(status.getDefaultMessage()); + this.status = status; + } + + public PasswordPolicyErrorStatus getStatus() { + return status; + } +} diff --git a/ldap/src/main/java/org/springframework/security/ldap/ppolicy/PasswordPolicyResponseControl.java b/ldap/src/main/java/org/springframework/security/ldap/ppolicy/PasswordPolicyResponseControl.java new file mode 100755 index 0000000000..4bb7de0aa8 --- /dev/null +++ b/ldap/src/main/java/org/springframework/security/ldap/ppolicy/PasswordPolicyResponseControl.java @@ -0,0 +1,349 @@ +/* Copyright 2004, 2005, 2006 Acegi Technology Pty Limited + * + * 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.security.ldap.ppolicy; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; + +import netscape.ldap.ber.stream.BERChoice; +import netscape.ldap.ber.stream.BERElement; +import netscape.ldap.ber.stream.BEREnumerated; +import netscape.ldap.ber.stream.BERInteger; +import netscape.ldap.ber.stream.BERIntegral; +import netscape.ldap.ber.stream.BERSequence; +import netscape.ldap.ber.stream.BERTag; +import netscape.ldap.ber.stream.BERTagDecoder; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.dao.DataRetrievalFailureException; + + +/** + * Represents the response control received when a PasswordPolicyControl is used when binding to a + * directory. Currently tested with the OpenLDAP 2.3.19 implementation of the LDAP Password Policy Draft. It extends + * the request control with the control specific data. This is accomplished by the properties timeBeforeExpiration, + * graceLoginsRemaining. + *

+ * + * + * @author Stefan Zoerner + * @author Luke Taylor + * @version $Id: PasswordPolicyResponseControl.java,v 1.4 2009/03/04 07:25:07 itslxt Exp $ + * + * @see org.springframework.security.ldap.ppolicy.PasswordPolicyControl + * @see Stefan Zoerner's IBM developerworks + * article on LDAP controls. + */ +public class PasswordPolicyResponseControl extends PasswordPolicyControl { + //~ Static fields/initializers ===================================================================================== + + private static final Log logger = LogFactory.getLog(PasswordPolicyResponseControl.class); + + //~ Instance fields ================================================================================================ + + private byte[] encodedValue; + + private PasswordPolicyErrorStatus errorStatus; + + private int graceLoginsRemaining = Integer.MAX_VALUE; + private int timeBeforeExpiration = Integer.MAX_VALUE; + + //~ Constructors =================================================================================================== + + /** + * Decodes the Ber encoded control data. The ASN.1 value of the control data is:

+     *    PasswordPolicyResponseValue ::= SEQUENCE {       warning [0] CHOICE {
+     *           timeBeforeExpiration [0] INTEGER (0 .. maxInt),
+     *           graceAuthNsRemaining [1] INTEGER (0 .. maxInt) } OPTIONAL,       error   [1] ENUMERATED {
+     *           passwordExpired             (0),          accountLocked               (1),
+     *           changeAfterReset            (2),          passwordModNotAllowed       (3),
+     *           mustSupplyOldPassword       (4),          insufficientPasswordQuality (5),
+     *           passwordTooShort            (6),          passwordTooYoung            (7),
+     *           passwordInHistory           (8) } OPTIONAL }
+ * + */ + public PasswordPolicyResponseControl(byte[] encodedValue) { + this.encodedValue = encodedValue; + + //PPolicyDecoder decoder = new JLdapDecoder(); + PPolicyDecoder decoder = new NetscapeDecoder(); + + try { + decoder.decode(); + } catch (IOException e) { + throw new DataRetrievalFailureException("Failed to parse control value", e); + } + } + + //~ Methods ======================================================================================================== + + /** + * Returns the unchanged value of the response control. Returns the unchanged value of the response + * control as byte array. + */ + public byte[] getEncodedValue() { + return encodedValue; + } + + public PasswordPolicyErrorStatus getErrorStatus() { + return errorStatus; + } + + /** + * Returns the graceLoginsRemaining. + * + * @return Returns the graceLoginsRemaining. + */ + public int getGraceLoginsRemaining() { + return graceLoginsRemaining; + } + + /** + * Returns the timeBeforeExpiration. + * + * @return Returns the time before expiration in seconds + */ + public int getTimeBeforeExpiration() { + return timeBeforeExpiration; + } + + /** + * Checks whether an error is present. + * + * @return true, if an error is present + */ + public boolean hasError() { + return errorStatus != null; + } + + /** + * Checks whether a warning is present. + * + * @return true, if a warning is present + */ + public boolean hasWarning() { + return (graceLoginsRemaining != Integer.MAX_VALUE) || (timeBeforeExpiration != Integer.MAX_VALUE); + } + + public boolean isExpired() { + return errorStatus == PasswordPolicyErrorStatus.PASSWORD_EXPIRED; + } + + public boolean isChangeAfterReset() { + return errorStatus == PasswordPolicyErrorStatus.CHANGE_AFTER_RESET; + } + + public boolean isUsingGraceLogins() { + return graceLoginsRemaining < Integer.MAX_VALUE; + } + + /** + * Determines whether an account locked error has been returned. + * + * @return true if the account is locked. + */ + public boolean isLocked() { + return errorStatus == PasswordPolicyErrorStatus.ACCOUNT_LOCKED; + } + + /** + * Create a textual representation containing error and warning messages, if any are present. + * + * @return error and warning messages + */ + public String toString() { + StringBuilder sb = new StringBuilder("PasswordPolicyResponseControl"); + + if (hasError()) { + sb.append(", error: ").append(errorStatus.getDefaultMessage()); + } + + if (graceLoginsRemaining != Integer.MAX_VALUE) { + sb.append(", warning: ").append(graceLoginsRemaining).append(" grace logins remain"); + } + + if (timeBeforeExpiration != Integer.MAX_VALUE) { + sb.append(", warning: time before expiration is ").append(timeBeforeExpiration); + } + + if (!hasError() && !hasWarning()) { + sb.append(" (no error, no warning)"); + } + + return sb.toString(); + } + + //~ Inner Interfaces =============================================================================================== + + private interface PPolicyDecoder { + void decode() throws IOException; + } + + //~ Inner Classes ================================================================================================== + + /** + * Decoder based on Netscape ldapsdk library + */ + private class NetscapeDecoder implements PPolicyDecoder { + public void decode() throws IOException { + int[] bread = {0}; + BERSequence seq = (BERSequence) BERElement.getElement(new SpecificTagDecoder(), + new ByteArrayInputStream(encodedValue), bread); + + int size = seq.size(); + + if (logger.isDebugEnabled()) { + logger.debug("PasswordPolicyResponse, ASN.1 sequence has " + size + " elements"); + } + + for (int i = 0; i < seq.size(); i++) { + BERTag elt = (BERTag) seq.elementAt(i); + + int tag = elt.getTag() & 0x1F; + + if (tag == 0) { + BERChoice warning = (BERChoice) elt.getValue(); + + BERTag content = (BERTag) warning.getValue(); + int value = ((BERInteger) content.getValue()).getValue(); + + if ((content.getTag() & 0x1F) == 0) { + timeBeforeExpiration = value; + } else { + graceLoginsRemaining = value; + } + } else if (tag == 1) { + BERIntegral error = (BERIntegral) elt.getValue(); + errorStatus = PasswordPolicyErrorStatus.values()[error.getValue()]; + } + } + } + + class SpecificTagDecoder extends BERTagDecoder { + /** Allows us to remember which of the two options we're decoding */ + private Boolean inChoice = null; + + public BERElement getElement(BERTagDecoder decoder, int tag, InputStream stream, int[] bytesRead, + boolean[] implicit) throws IOException { + tag &= 0x1F; + implicit[0] = false; + + if (tag == 0) { + // Either the choice or the time before expiry within it + if (inChoice == null) { + setInChoice(true); + + // Read the choice length from the stream (ignored) + BERElement.readLengthOctets(stream, bytesRead); + + int[] componentLength = new int[1]; + BERElement choice = new BERChoice(decoder, stream, componentLength); + bytesRead[0] += componentLength[0]; + + // inChoice = null; + return choice; + } else { + // Must be time before expiry + return new BERInteger(stream, bytesRead); + } + } else if (tag == 1) { + // Either the graceLogins or the error enumeration. + if (inChoice == null) { + // The enumeration + setInChoice(false); + + return new BEREnumerated(stream, bytesRead); + } else { + if (inChoice.booleanValue()) { + // graceLogins + return new BERInteger(stream, bytesRead); + } + } + } + + throw new DataRetrievalFailureException("Unexpected tag " + tag); + } + + private void setInChoice(boolean inChoice) { + this.inChoice = new Boolean(inChoice); + } + } + } + +/** Decoder based on the OpenLDAP/Novell JLDAP library */ + +// private class JLdapDecoder implements PPolicyDecoder { +// +// public void decode() throws IOException { +// +// LBERDecoder decoder = new LBERDecoder(); +// +// ASN1Sequence seq = (ASN1Sequence)decoder.decode(encodedValue); +// +// if(seq == null) { +// +// } +// +// int size = seq.size(); +// +// if(logger.isDebugEnabled()) { +// logger.debug("PasswordPolicyResponse, ASN.1 sequence has " + +// size + " elements"); +// } +// +// for(int i=0; i < size; i++) { +// +// ASN1Tagged taggedObject = (ASN1Tagged)seq.get(i); +// +// int tag = taggedObject.getIdentifier().getTag(); +// +// ASN1OctetString value = (ASN1OctetString)taggedObject.taggedValue(); +// byte[] content = value.byteValue(); +// +// if(tag == 0) { +// parseWarning(content, decoder); +// +// } else if(tag == 1) { +// // Error: set the code to the value +// errorCode = content[0]; +// } +// } +// } +// +// private void parseWarning(byte[] content, LBERDecoder decoder) { +// // It's the warning (choice). Parse the number and set either the +// // expiry time or number of logins remaining. +// ASN1Tagged taggedObject = (ASN1Tagged)decoder.decode(content); +// int contentTag = taggedObject.getIdentifier().getTag(); +// content = ((ASN1OctetString)taggedObject.taggedValue()).byteValue(); +// int number; +// +// try { +// number = ((Long)decoder.decodeNumeric(new ByteArrayInputStream(content), content.length)).intValue(); +// } catch(IOException e) { +// throw new LdapDataAccessException("Failed to parse number ", e); +// } +// +// if(contentTag == 0) { +// timeBeforeExpiration = number; +// } else if (contentTag == 1) { +// graceLoginsRemaining = number; +// } +// } +// } +} diff --git a/ldap/src/main/java/org/springframework/security/ldap/userdetails/LdapUserDetailsImpl.java b/ldap/src/main/java/org/springframework/security/ldap/userdetails/LdapUserDetailsImpl.java index 806b7a031e..923ebc7a02 100644 --- a/ldap/src/main/java/org/springframework/security/ldap/userdetails/LdapUserDetailsImpl.java +++ b/ldap/src/main/java/org/springframework/security/ldap/userdetails/LdapUserDetailsImpl.java @@ -23,6 +23,7 @@ import javax.naming.Name; import org.springframework.ldap.core.DirContextOperations; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.AuthorityUtils; +import org.springframework.security.ldap.ppolicy.PasswordPolicyData; import org.springframework.util.Assert; @@ -40,7 +41,7 @@ import org.springframework.util.Assert; * @author Luke Taylor * @version $Id$ */ -public class LdapUserDetailsImpl implements LdapUserDetails { +public class LdapUserDetailsImpl implements LdapUserDetails, PasswordPolicyData { //~ Instance fields ================================================================================================ @@ -52,6 +53,9 @@ public class LdapUserDetailsImpl implements LdapUserDetails { private boolean accountNonLocked = true; private boolean credentialsNonExpired = true; private boolean enabled = true; + // PPolicy data + private int timeBeforeExpiration = Integer.MAX_VALUE; + private int graceLoginsRemaining = Integer.MAX_VALUE; //~ Constructors =================================================================================================== @@ -91,6 +95,14 @@ public class LdapUserDetailsImpl implements LdapUserDetails { return enabled; } + public int getTimeBeforeExpiration() { + return timeBeforeExpiration; + } + + public int getGraceLoginsRemaining() { + return graceLoginsRemaining; + } + public String toString() { StringBuffer sb = new StringBuffer(); sb.append(super.toString()).append(": "); @@ -217,5 +229,13 @@ public class LdapUserDetailsImpl implements LdapUserDetails { public void setUsername(String username) { instance.username = username; } + + public void setTimeBeforeExpiration(int timeBeforeExpiration) { + instance.timeBeforeExpiration = timeBeforeExpiration; + } + + public void setGraceLoginsRemaining(int graceLoginsRemaining) { + instance.graceLoginsRemaining = graceLoginsRemaining; + } } } diff --git a/ldap/src/main/java/org/springframework/security/ldap/userdetails/LdapUserDetailsMapper.java b/ldap/src/main/java/org/springframework/security/ldap/userdetails/LdapUserDetailsMapper.java index 36b8cf7b4e..932e6f0921 100644 --- a/ldap/src/main/java/org/springframework/security/ldap/userdetails/LdapUserDetailsMapper.java +++ b/ldap/src/main/java/org/springframework/security/ldap/userdetails/LdapUserDetailsMapper.java @@ -17,16 +17,16 @@ package org.springframework.security.ldap.userdetails; import java.util.List; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.ldap.core.DirContextAdapter; +import org.springframework.ldap.core.DirContextOperations; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.GrantedAuthorityImpl; import org.springframework.security.core.userdetails.UserDetails; - -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; - +import org.springframework.security.ldap.ppolicy.PasswordPolicyControl; +import org.springframework.security.ldap.ppolicy.PasswordPolicyResponseControl; import org.springframework.util.Assert; -import org.springframework.ldap.core.DirContextAdapter; -import org.springframework.ldap.core.DirContextOperations; /** @@ -86,6 +86,15 @@ public class LdapUserDetailsMapper implements UserDetailsContextMapper { essence.addAuthority(authorities.get(i)); } + // Check for PPolicy data + + PasswordPolicyResponseControl ppolicy = (PasswordPolicyResponseControl) ctx.getObjectAttribute(PasswordPolicyControl.OID); + + if (ppolicy != null) { + essence.setTimeBeforeExpiration(ppolicy.getTimeBeforeExpiration()); + essence.setGraceLoginsRemaining(ppolicy.getGraceLoginsRemaining()); + } + return essence.createUserDetails(); } diff --git a/ldap/src/test/java/org/springframework/security/ldap/ppolicy/OpenLDAPIntegrationTestSuite.java b/ldap/src/test/java/org/springframework/security/ldap/ppolicy/OpenLDAPIntegrationTestSuite.java new file mode 100644 index 0000000000..2b78c21e1e --- /dev/null +++ b/ldap/src/test/java/org/springframework/security/ldap/ppolicy/OpenLDAPIntegrationTestSuite.java @@ -0,0 +1,67 @@ +package org.springframework.security.ldap.ppolicy; + +import static org.junit.Assert.*; + +import org.junit.Before; +import org.junit.Test; +import org.springframework.security.authentication.BadCredentialsException; +import org.springframework.security.authentication.LockedException; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.ldap.authentication.BindAuthenticator; +import org.springframework.security.ldap.authentication.LdapAuthenticationProvider; +import org.springframework.security.ldap.userdetails.LdapUserDetailsImpl; + +/** + * Test cases which run against an OpenLDAP server. + *

+ * Run the script in the module root to start the server and import the data before running. + * @author Luke Taylor + * @version $Id$ + * @since 3.0 + */ +public class OpenLDAPIntegrationTestSuite { + PasswordPolicyAwareContextSource cs; + + @Before + public void createContextSource() throws Exception { + cs = new PasswordPolicyAwareContextSource("ldap://localhost:22389/dc=springsource,dc=com"); + cs.setUserDn("cn=admin,dc=springsource,dc=com"); + cs.setPassword("password"); + cs.afterPropertiesSet(); + } + + @Test + public void simpleBindSucceeds() throws Exception { + BindAuthenticator authenticator = new BindAuthenticator(cs); + authenticator.setUserDnPatterns(new String[] {"uid={0},ou=users"}); + LdapAuthenticationProvider provider = new LdapAuthenticationProvider(authenticator); + provider.authenticate(new UsernamePasswordAuthenticationToken("luke","password")); + } + + @Test(expected=LockedException.class) + public void repeatedBindWithWrongPasswordLocksAccount() throws Exception { + BindAuthenticator authenticator = new BindAuthenticator(cs); + authenticator.setUserDnPatterns(new String[] {"uid={0},ou=users"}); + LdapAuthenticationProvider provider = new LdapAuthenticationProvider(authenticator); + for (int count=1; count < 4; count++) { + try { + Authentication a = provider.authenticate(new UsernamePasswordAuthenticationToken("lockme","wrong")); + LdapUserDetailsImpl ud = (LdapUserDetailsImpl) a.getPrincipal(); + assertTrue(ud.getTimeBeforeExpiration() < Integer.MAX_VALUE && ud.getTimeBeforeExpiration() > 0); + } catch (BadCredentialsException expected) { + } + } + } + + @Test + public void passwordExpiryTimeIsDetectedCorrectly() throws Exception { + BindAuthenticator authenticator = new BindAuthenticator(cs); + authenticator.setUserDnPatterns(new String[] {"uid={0},ou=users"}); + LdapAuthenticationProvider provider = new LdapAuthenticationProvider(authenticator); + Authentication a = provider.authenticate(new UsernamePasswordAuthenticationToken("expireme","password")); + PasswordPolicyData ud = (LdapUserDetailsImpl) a.getPrincipal(); + assertTrue(ud.getTimeBeforeExpiration() < Integer.MAX_VALUE && ud.getTimeBeforeExpiration() > 0); + } + +} diff --git a/ldap/src/test/java/org/springframework/security/ldap/ppolicy/OpenLDAPIntegrationTestsuite.java b/ldap/src/test/java/org/springframework/security/ldap/ppolicy/OpenLDAPIntegrationTestsuite.java new file mode 100644 index 0000000000..2b78c21e1e --- /dev/null +++ b/ldap/src/test/java/org/springframework/security/ldap/ppolicy/OpenLDAPIntegrationTestsuite.java @@ -0,0 +1,67 @@ +package org.springframework.security.ldap.ppolicy; + +import static org.junit.Assert.*; + +import org.junit.Before; +import org.junit.Test; +import org.springframework.security.authentication.BadCredentialsException; +import org.springframework.security.authentication.LockedException; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.ldap.authentication.BindAuthenticator; +import org.springframework.security.ldap.authentication.LdapAuthenticationProvider; +import org.springframework.security.ldap.userdetails.LdapUserDetailsImpl; + +/** + * Test cases which run against an OpenLDAP server. + *

+ * Run the script in the module root to start the server and import the data before running. + * @author Luke Taylor + * @version $Id$ + * @since 3.0 + */ +public class OpenLDAPIntegrationTestSuite { + PasswordPolicyAwareContextSource cs; + + @Before + public void createContextSource() throws Exception { + cs = new PasswordPolicyAwareContextSource("ldap://localhost:22389/dc=springsource,dc=com"); + cs.setUserDn("cn=admin,dc=springsource,dc=com"); + cs.setPassword("password"); + cs.afterPropertiesSet(); + } + + @Test + public void simpleBindSucceeds() throws Exception { + BindAuthenticator authenticator = new BindAuthenticator(cs); + authenticator.setUserDnPatterns(new String[] {"uid={0},ou=users"}); + LdapAuthenticationProvider provider = new LdapAuthenticationProvider(authenticator); + provider.authenticate(new UsernamePasswordAuthenticationToken("luke","password")); + } + + @Test(expected=LockedException.class) + public void repeatedBindWithWrongPasswordLocksAccount() throws Exception { + BindAuthenticator authenticator = new BindAuthenticator(cs); + authenticator.setUserDnPatterns(new String[] {"uid={0},ou=users"}); + LdapAuthenticationProvider provider = new LdapAuthenticationProvider(authenticator); + for (int count=1; count < 4; count++) { + try { + Authentication a = provider.authenticate(new UsernamePasswordAuthenticationToken("lockme","wrong")); + LdapUserDetailsImpl ud = (LdapUserDetailsImpl) a.getPrincipal(); + assertTrue(ud.getTimeBeforeExpiration() < Integer.MAX_VALUE && ud.getTimeBeforeExpiration() > 0); + } catch (BadCredentialsException expected) { + } + } + } + + @Test + public void passwordExpiryTimeIsDetectedCorrectly() throws Exception { + BindAuthenticator authenticator = new BindAuthenticator(cs); + authenticator.setUserDnPatterns(new String[] {"uid={0},ou=users"}); + LdapAuthenticationProvider provider = new LdapAuthenticationProvider(authenticator); + Authentication a = provider.authenticate(new UsernamePasswordAuthenticationToken("expireme","password")); + PasswordPolicyData ud = (LdapUserDetailsImpl) a.getPrincipal(); + assertTrue(ud.getTimeBeforeExpiration() < Integer.MAX_VALUE && ud.getTimeBeforeExpiration() > 0); + } + +} diff --git a/ldap/src/test/java/org/springframework/security/ldap/ppolicy/PasswordPolicyResponseControlTests.java b/ldap/src/test/java/org/springframework/security/ldap/ppolicy/PasswordPolicyResponseControlTests.java new file mode 100644 index 0000000000..bcb27c0362 --- /dev/null +++ b/ldap/src/test/java/org/springframework/security/ldap/ppolicy/PasswordPolicyResponseControlTests.java @@ -0,0 +1,124 @@ +/* Copyright 2004, 2005, 2006 Acegi Technology Pty Limited + * + * 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.security.ldap.ppolicy; + +import junit.framework.TestCase; + +/** + * Tests for PasswordPolicyResponse. + * + * @author Luke Taylor + * @version $Id: PasswordPolicyResponseControlTests.java 2217 2007-10-27 00:45:30Z luke_t $ + */ +public class PasswordPolicyResponseControlTests extends TestCase { + //~ Methods ======================================================================================================== + + /** + * Useful method for obtaining data from a server for use in tests + */ +// public void testAgainstServer() throws Exception { +// Hashtable env = new Hashtable(); +// env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory"); +// env.put(Context.PROVIDER_URL, "ldap://gorille:389/"); +// env.put(Context.SECURITY_AUTHENTICATION, "simple"); +// env.put(Context.SECURITY_PRINCIPAL, "cn=manager,dc=security,dc=org"); +// env.put(Context.SECURITY_CREDENTIALS, "security"); +// env.put(LdapContext.CONTROL_FACTORIES, PasswordPolicyControlFactory.class.getName()); +// +// InitialLdapContext ctx = new InitialLdapContext(env, null); +// +// Control[] rctls = { new PasswordPolicyControl(false) }; +// +// ctx.setRequestControls(rctls); +// +// try { +// ctx.addToEnvironment(Context.SECURITY_PRINCIPAL, "uid=bob,ou=people,dc=security,dc=org" ); +// ctx.addToEnvironment(Context.SECURITY_CREDENTIALS, "bobspassword"); +// Object o = ctx.lookup(""); +// +// System.out.println(o); +// +// } catch(NamingException ne) { +// // Ok. +// System.err.println(ne); +// } +// +// PasswordPolicyResponseControl ctrl = getPPolicyResponseCtl(ctx); +// System.out.println(ctrl); +// +// assertNotNull(ctrl); +// +// //com.sun.jndi.ldap.LdapPoolManager.showStats(System.out); +// } + + +// private PasswordPolicyResponseControl getPPolicyResponseCtl(InitialLdapContext ctx) throws NamingException { +// Control[] ctrls = ctx.getResponseControls(); +// +// for (int i = 0; ctrls != null && i < ctrls.length; i++) { +// if (ctrls[i] instanceof PasswordPolicyResponseControl) { +// return (PasswordPolicyResponseControl) ctrls[i]; +// } +// } +// +// return null; +// } + + public void testOpenLDAP33SecondsTillPasswordExpiryCtrlIsParsedCorrectly() { + byte[] ctrlBytes = {0x30, 0x05, (byte) 0xA0, 0x03, (byte) 0xA0, 0x1, 0x21}; + + PasswordPolicyResponseControl ctrl = new PasswordPolicyResponseControl(ctrlBytes); + + assertTrue(ctrl.hasWarning()); + assertEquals(33, ctrl.getTimeBeforeExpiration()); + } + + public void testOpenLDAP496GraceLoginsRemainingCtrlIsParsedCorrectly() { + byte[] ctrlBytes = {0x30, 0x06, (byte) 0xA0, 0x04, (byte) 0xA1, 0x02, 0x01, (byte) 0xF0}; + + PasswordPolicyResponseControl ctrl = new PasswordPolicyResponseControl(ctrlBytes); + + assertTrue(ctrl.hasWarning()); + assertEquals(496, ctrl.getGraceLoginsRemaining()); + } + + public void testOpenLDAP5GraceLoginsRemainingCtrlIsParsedCorrectly() { + byte[] ctrlBytes = {0x30, 0x05, (byte) 0xA0, 0x03, (byte) 0xA1, 0x01, 0x05}; + + PasswordPolicyResponseControl ctrl = new PasswordPolicyResponseControl(ctrlBytes); + + assertTrue(ctrl.hasWarning()); + assertEquals(5, ctrl.getGraceLoginsRemaining()); + } + + public void testOpenLDAPAccountLockedCtrlIsParsedCorrectly() { + byte[] ctrlBytes = {0x30, 0x03, (byte) 0xA1, 0x01, 0x01}; + + PasswordPolicyResponseControl ctrl = new PasswordPolicyResponseControl(ctrlBytes); + + assertTrue(ctrl.hasError() && ctrl.isLocked()); + assertFalse(ctrl.hasWarning()); + } + + public void testOpenLDAPPasswordExpiredCtrlIsParsedCorrectly() { + byte[] ctrlBytes = {0x30, 0x03, (byte) 0xA1, 0x01, 0x00}; + + PasswordPolicyResponseControl ctrl = new PasswordPolicyResponseControl(ctrlBytes); + + assertTrue(ctrl.hasError() && ctrl.isExpired()); + assertFalse(ctrl.hasWarning()); + } +} diff --git a/ldap/template.mf b/ldap/template.mf index 64a2d80fe6..7bca39354e 100644 --- a/ldap/template.mf +++ b/ldap/template.mf @@ -19,5 +19,5 @@ Import-Template: org.springframework.core.io.*;version="[3.0.0, 3.1.0)", org.springframework.dao.*;version="[3.0.0, 3.1.0)";resolution:=optional, org.springframework.util.*;version="[3.0.0, 3.1.0)", - javax.naming.*;version="0";resolution:=optional - \ No newline at end of file + javax.naming.*;version="0";resolution:=optional, + netscape.ldap.ber.stream;version="[4.1, 5.0)";resolution:=optional \ No newline at end of file