diff --git a/sandbox/src/main/java/org/acegisecurity/ldap/ContextSourceInitialDirContextFactory.java b/sandbox/src/main/java/org/acegisecurity/ldap/ContextSourceInitialDirContextFactory.java new file mode 100644 index 0000000000..190f97f30a --- /dev/null +++ b/sandbox/src/main/java/org/acegisecurity/ldap/ContextSourceInitialDirContextFactory.java @@ -0,0 +1,30 @@ +package org.acegisecurity.ldap; + +import net.sf.ldaptemplate.ContextSource; + +import javax.naming.directory.DirContext; + +import org.springframework.dao.DataAccessException; + +/** + * A version of InitialDirContextFactory that implements the ldaptemplate ContextSource interface. + * + * DefaultInitialDirContextFactory should be modified to implement this when it is possible to + * introduce a dependency on ldaptemplate in the main code. + * + * @author Luke + * @version $Id$ + */ +public class ContextSourceInitialDirContextFactory extends DefaultInitialDirContextFactory implements ContextSource { + public ContextSourceInitialDirContextFactory(String providerUrl) { + super(providerUrl); + } + + public DirContext getReadOnlyContext() throws DataAccessException { + return newInitialDirContext(); + } + + public DirContext getReadWriteContext() throws DataAccessException { + return newInitialDirContext(); + } +} diff --git a/sandbox/src/main/java/org/acegisecurity/ldap/ppolicy/AccountLockedException.java b/sandbox/src/main/java/org/acegisecurity/ldap/ppolicy/AccountLockedException.java new file mode 100644 index 0000000000..6aa12f351f --- /dev/null +++ b/sandbox/src/main/java/org/acegisecurity/ldap/ppolicy/AccountLockedException.java @@ -0,0 +1,15 @@ +package org.acegisecurity.ldap.ppolicy; + +/** + * @author Luke + * @version $Id$ + */ +public class AccountLockedException extends PasswordPolicyException { + public AccountLockedException(String msg) { + super(msg); + } + + public AccountLockedException(String msg, Throwable t) { + super(msg, t); + } +} diff --git a/sandbox/src/main/java/org/acegisecurity/ldap/ppolicy/OracleIDPasswordPolicyControl.java b/sandbox/src/main/java/org/acegisecurity/ldap/ppolicy/OracleIDPasswordPolicyControl.java new file mode 100644 index 0000000000..9789994f4e --- /dev/null +++ b/sandbox/src/main/java/org/acegisecurity/ldap/ppolicy/OracleIDPasswordPolicyControl.java @@ -0,0 +1,11 @@ +package org.acegisecurity.ldap.ppolicy; + +/** + * @author Luke + * @version $Id$ + */ +public class OracleIDPasswordPolicyControl extends PasswordPolicyControl { + public String getID() { + return "2.16.840.1.113894.1.8.6"; + } +} diff --git a/sandbox/src/main/java/org/acegisecurity/ldap/ppolicy/PasswordExpiredException.java b/sandbox/src/main/java/org/acegisecurity/ldap/ppolicy/PasswordExpiredException.java new file mode 100644 index 0000000000..55c68bb590 --- /dev/null +++ b/sandbox/src/main/java/org/acegisecurity/ldap/ppolicy/PasswordExpiredException.java @@ -0,0 +1,15 @@ +package org.acegisecurity.ldap.ppolicy; + +/** + * @author Luke + * @version $Id$ + */ +public class PasswordExpiredException extends PasswordPolicyException { + public PasswordExpiredException(String msg) { + super(msg); + } + + public PasswordExpiredException(String msg, Throwable t) { + super(msg, t); + } +} diff --git a/sandbox/src/main/java/org/acegisecurity/ldap/ppolicy/PasswordInHistoryException.java b/sandbox/src/main/java/org/acegisecurity/ldap/ppolicy/PasswordInHistoryException.java new file mode 100644 index 0000000000..e00cf51f53 --- /dev/null +++ b/sandbox/src/main/java/org/acegisecurity/ldap/ppolicy/PasswordInHistoryException.java @@ -0,0 +1,16 @@ +package org.acegisecurity.ldap.ppolicy; + +/** + * @author Luke + * @version $Id$ + */ +public class PasswordInHistoryException extends PasswordPolicyException { + + public PasswordInHistoryException(String msg) { + super(msg); + } + + public PasswordInHistoryException(String msg, Throwable t) { + super(msg, t); + } +} diff --git a/sandbox/src/main/java/org/acegisecurity/ldap/ppolicy/PasswordPolicyControl.java b/sandbox/src/main/java/org/acegisecurity/ldap/ppolicy/PasswordPolicyControl.java new file mode 100644 index 0000000000..4eccd6cc79 --- /dev/null +++ b/sandbox/src/main/java/org/acegisecurity/ldap/ppolicy/PasswordPolicyControl.java @@ -0,0 +1,91 @@ +/* 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.acegisecurity.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: PasswordPolicyControl.java 1496 2006-05-23 13:38:33Z benalex $ + * + * @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. + * + * @return DOCUMENT ME! + */ + public String getID() { + return OID; + } + + /** + * Returns whether the control is critical for the client. + * + * @return DOCUMENT ME! + */ + public boolean isCritical() { + return critical; + } +} diff --git a/sandbox/src/main/java/org/acegisecurity/ldap/ppolicy/PasswordPolicyControlFactory.java b/sandbox/src/main/java/org/acegisecurity/ldap/ppolicy/PasswordPolicyControlFactory.java new file mode 100644 index 0000000000..c3a38553b9 --- /dev/null +++ b/sandbox/src/main/java/org/acegisecurity/ldap/ppolicy/PasswordPolicyControlFactory.java @@ -0,0 +1,49 @@ +/* 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.acegisecurity.ldap.ppolicy; + +import org.acegisecurity.ldap.ppolicy.PasswordPolicyControl; + +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: PasswordPolicyControlFactory.java 1496 2006-05-23 13:38:33Z benalex $ + */ +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/sandbox/src/main/java/org/acegisecurity/ldap/ppolicy/PasswordPolicyException.java b/sandbox/src/main/java/org/acegisecurity/ldap/ppolicy/PasswordPolicyException.java new file mode 100644 index 0000000000..e80e74be4d --- /dev/null +++ b/sandbox/src/main/java/org/acegisecurity/ldap/ppolicy/PasswordPolicyException.java @@ -0,0 +1,17 @@ +package org.acegisecurity.ldap.ppolicy; + +import org.acegisecurity.AuthenticationException; + +/** + * @author Luke + * @version $Id$ + */ +public class PasswordPolicyException extends AuthenticationException { + public PasswordPolicyException(String msg) { + super(msg); + } + + public PasswordPolicyException(String msg, Throwable t) { + super(msg, t); + } +} diff --git a/sandbox/src/main/java/org/acegisecurity/ldap/ppolicy/PasswordPolicyResponseControl.java b/sandbox/src/main/java/org/acegisecurity/ldap/ppolicy/PasswordPolicyResponseControl.java new file mode 100644 index 0000000000..1fe019fcd8 --- /dev/null +++ b/sandbox/src/main/java/org/acegisecurity/ldap/ppolicy/PasswordPolicyResponseControl.java @@ -0,0 +1,361 @@ +/* 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.acegisecurity.ldap.ppolicy; + +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 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.BERTag; +import netscape.ldap.ber.stream.BERTagDecoder; + +import org.acegisecurity.ldap.LdapDataAccessException; +import org.acegisecurity.ldap.ppolicy.PasswordPolicyControl; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; + + +/** + * 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. + * + * @author Stefan Zoerner + * @author Luke Taylor + * @version $Id: PasswordPolicyResponseControl.java 1496 2006-05-23 13:38:33Z benalex $ + * + * @see org.acegisecurity.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); + 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 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" + }; + + //~ Instance fields ================================================================================================ + + private byte[] encodedValue; + private int errorCode = ERROR_NONE; + private int graceLoginsRemaining = WARNINGS_DEFAULT; + private int timeBeforeExpiration = WARNINGS_DEFAULT; + + //~ Constructors =================================================================================================== + + 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); + } + } + + //~ Methods ======================================================================================================== + + /** + * Returns the unchanged value of the response control. Returns the unchanged value of the response + * control as byte array. + * + * @return DOCUMENT ME! + */ + 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; + } + + /** + * 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 }
+ * + * @return DOCUMENT ME! + */ + /** + * 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 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 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) { + 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; +// } +// } +// } +}