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