diff --git a/sandbox/src/main/java/org/acegisecurity/providers/ldap/authenticator/controls/PasswordPolicyControl.java b/sandbox/src/main/java/org/acegisecurity/providers/ldap/authenticator/controls/PasswordPolicyControl.java new file mode 100644 index 0000000000..9799775a9b --- /dev/null +++ b/sandbox/src/main/java/org/acegisecurity/providers/ldap/authenticator/controls/PasswordPolicyControl.java @@ -0,0 +1,68 @@ +package org.acegisecurity.providers.ldap.authenticator.controls; + +import javax.naming.ldap.Control; + +/** + * A Password Policy request control. + *

+ * Based on the information in the corresponding internet draft on + * LDAP password policy. + *

+ * + * @see PasswordPolicyResponseControl + * @see Password Policy for LDAP Directories + * + * @author Stefan Zoerner + * @author Luke Taylor + * + * @version $Id$ + * + */ +public class PasswordPolicyControl implements Control { + + /** OID of the Password Policy Control */ + public static final String OID = "1.3.6.1.4.1.42.2.27.8.5.1"; + + private boolean critical; + + /** + * 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; + } + + /** + * Returns the OID of the Password Policy Control. + */ + public String getID() { + return OID; + } + + /** + * Returns whether the control is critical for the client. + */ + public boolean isCritical() { + return critical; + } + + /** + * 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; + } +} \ No newline at end of file diff --git a/sandbox/src/main/java/org/acegisecurity/providers/ldap/authenticator/controls/PasswordPolicyControlFactory.java b/sandbox/src/main/java/org/acegisecurity/providers/ldap/authenticator/controls/PasswordPolicyControlFactory.java new file mode 100644 index 0000000000..752280e7b5 --- /dev/null +++ b/sandbox/src/main/java/org/acegisecurity/providers/ldap/authenticator/controls/PasswordPolicyControlFactory.java @@ -0,0 +1,35 @@ +package org.acegisecurity.providers.ldap.authenticator.controls; + +import java.io.ByteArrayInputStream; +import java.io.IOException; + +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 { + + /** + * 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; + } +} \ No newline at end of file diff --git a/sandbox/src/main/java/org/acegisecurity/providers/ldap/authenticator/controls/PasswordPolicyResponseControl.java b/sandbox/src/main/java/org/acegisecurity/providers/ldap/authenticator/controls/PasswordPolicyResponseControl.java new file mode 100644 index 0000000000..8aade7df78 --- /dev/null +++ b/sandbox/src/main/java/org/acegisecurity/providers/ldap/authenticator/controls/PasswordPolicyResponseControl.java @@ -0,0 +1,358 @@ +package org.acegisecurity.providers.ldap.authenticator.controls; + + +import java.io.IOException; +import java.io.ByteArrayInputStream; +import java.io.InputStream; + +import org.acegisecurity.providers.ldap.LdapDataAccessException; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +//import com.novell.ldap.asn1.LBERDecoder; +//import com.novell.ldap.asn1.ASN1Sequence; +//import com.novell.ldap.asn1.ASN1Tagged; +//import com.novell.ldap.asn1.ASN1OctetString; +import netscape.ldap.ber.stream.BERSequence; +import netscape.ldap.ber.stream.BERElement; +import netscape.ldap.ber.stream.BERTagDecoder; +import netscape.ldap.ber.stream.BERTag; +import netscape.ldap.ber.stream.BERChoice; +import netscape.ldap.ber.stream.BERInteger; +import netscape.ldap.ber.stream.BEREnumerated; + +/** + * Represent 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 and + * errorCodes. getEncodedValue returns the + * unchanged value of the response control as a byte array. + * + * @see PasswordPolicyControl + * @see Stefan Zoerner's IBM developerworks article on LDAP controls. + * + * @author Stefan Zoerner + * @author Luke Taylor + * @version $Id$ + */ +public class PasswordPolicyResponseControl extends PasswordPolicyControl { + + private static final Log logger = LogFactory.getLog(PasswordPolicyResponseControl.class); + + public static final int ERROR_NONE = -1; + + public static final int ERROR_PASSWORD_EXPIRED = 0; + public static final int ERROR_ACCOUNT_LOCKED = 1; + + public static final int WARNINGS_DEFAULT = -1; + + private byte[] encodedValue; + + private int errorCode = ERROR_NONE; + + private int timeBeforeExpiration = WARNINGS_DEFAULT; + + private int graceLoginsRemaining = WARNINGS_DEFAULT; + + private static final String[] errorText = { "password expired", "account locked", + "change after reset", "password mod not allowed", "must supply old password", + "invalid password syntax", "password too short", "password too young", + "password in history" }; + + public PasswordPolicyResponseControl(byte[] encodedValue) { + this.encodedValue = encodedValue; + + //PPolicyDecoder decoder = new JLdapDecoder(); + PPolicyDecoder decoder = new NetscapeDecoder(); + + try { + decoder.decode(); + } catch (IOException e) { + throw new LdapDataAccessException("Failed to parse control value", e); + } + } + + /** + * 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 }
+     * 
+ * + */ + + + /** + * 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; + } + + /** + * Returns the unchanged value of the response control. + * + * Returns the unchanged value of the response control as byte array. + */ + public byte[] getEncodedValue() { + return encodedValue; + } + + /** + * Returns the error code, or ERROR_NONE, if no error is present. + * + * @return the error code (0-8), or ERROR_NONE + */ + public int getErrorCode() { + return errorCode; + } + + /** + * Checks whether an error is present. + * + * @return true, if an error is present + */ + public boolean hasError() { + return this.getErrorCode() != ERROR_NONE; + } + + /** + * Checks whether a warning is present. + * + * @return true, if a warning is present + */ + public boolean hasWarning() { + return graceLoginsRemaining != WARNINGS_DEFAULT + || timeBeforeExpiration != WARNINGS_DEFAULT; + } + + public boolean isExpired() { + return errorCode == ERROR_PASSWORD_EXPIRED; + } + + /** + * Determines whether an account locked error has + * been returned. + * + * @return true if the account is locked. + */ + public boolean isLocked() { + return errorCode == ERROR_ACCOUNT_LOCKED; + } + + /** + * Create a textual representation containing error and warning messages, if + * any are present. + * + * @return error and warning messages + */ + public String toString() { + + StringBuffer sb = new StringBuffer("PasswordPolicyResponseControl"); + + if (hasError()) { + sb.append(", error: ").append(errorText[errorCode]); + } + if (graceLoginsRemaining != WARNINGS_DEFAULT) { + sb.append(", warning: ").append(graceLoginsRemaining).append(" grace logins remain"); + } + if (timeBeforeExpiration != WARNINGS_DEFAULT) { + sb.append(", warning: time before expiration is ").append(timeBeforeExpiration); + } + if (!hasError() && !hasWarning()) { + sb.append(" (no error, no warning)"); + } + + return sb.toString(); + } + + //~ Inner Classes ========================================================= + + private interface PPolicyDecoder { + void decode() throws IOException; + } + + /** 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) { + BEREnumerated error = (BEREnumerated)elt.getValue(); + errorCode = 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 LdapDataAccessException("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/sandbox/src/main/java/org/acegisecurity/providers/ldap/authenticator/controls/PasswordPolicyResponseControlTests.java b/sandbox/src/main/java/org/acegisecurity/providers/ldap/authenticator/controls/PasswordPolicyResponseControlTests.java new file mode 100644 index 0000000000..055564dca9 --- /dev/null +++ b/sandbox/src/main/java/org/acegisecurity/providers/ldap/authenticator/controls/PasswordPolicyResponseControlTests.java @@ -0,0 +1,113 @@ +package org.acegisecurity.providers.ldap.authenticator.controls; + +import junit.framework.TestCase; + +import javax.naming.Context; +import javax.naming.NamingException; +import javax.naming.ldap.Control; +import javax.naming.ldap.InitialLdapContext; +import javax.naming.ldap.LdapContext; +import java.util.Hashtable; + + +/** + * Tests for PasswordPolicyResponse. + * + * @author Luke Taylor + * @version $Id$ + */ +public class PasswordPolicyResponseControlTests extends TestCase { + + /** 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=acegisecurity,dc=org"); +// env.put(Context.SECURITY_CREDENTIALS, "acegisecurity"); +// +// InitialLdapContext ctx = new InitialLdapContext(env, null); +// +// Control[] rctls = { new PasswordPolicyControl(false) }; +// +// +// try { +// ctx.addToEnvironment(LdapContext.CONTROL_FACTORIES, PasswordPolicyControlFactory.class.getName()); +// ctx.addToEnvironment(Context.SECURITY_PRINCIPAL, "uid=bob,ou=people,dc=acegisecurity,dc=org" ); +// ctx.addToEnvironment(Context.SECURITY_CREDENTIALS, "bobspassword"); +// ctx.reconnect(rctls); +// } catch(NamingException ne) { +// // Ok. +// } +// +// 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 testOpenLDAPPasswordExpiredCtrlIsParsedCorrectly() { + byte[] ctrlBytes = {0x30, 0x03, (byte)0xA1, 0x01, 0x00}; + + PasswordPolicyResponseControl ctrl = new PasswordPolicyResponseControl(ctrlBytes); + + assertTrue(ctrl.hasError() && ctrl.isExpired()); + assertFalse(ctrl.hasWarning()); + + } + + 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 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 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()); + } + +} \ No newline at end of file