4 changed files with 574 additions and 0 deletions
@ -0,0 +1,68 @@
@@ -0,0 +1,68 @@
|
||||
package org.acegisecurity.providers.ldap.authenticator.controls; |
||||
|
||||
import javax.naming.ldap.Control; |
||||
|
||||
/** |
||||
* A Password Policy request control. |
||||
* <p> |
||||
* Based on the information in the corresponding internet draft on |
||||
* LDAP password policy. |
||||
* </p> |
||||
* |
||||
* @see PasswordPolicyResponseControl |
||||
* @see <a href="http://www.ietf.org/internet-drafts/draft-behera-ldap-password-policy-09.txt">Password Policy for LDAP Directories</a> |
||||
* |
||||
* @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; |
||||
} |
||||
} |
||||
@ -0,0 +1,35 @@
@@ -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; |
||||
} |
||||
} |
||||
@ -0,0 +1,358 @@
@@ -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 <tt>PasswordPolicyControl</tt> |
||||
* 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 <a href="http://www.ibm.com/developerworks/tivoli/library/t-ldap-controls/">Stefan Zoerner's IBM developerworks article on LDAP controls.</a> |
||||
* |
||||
* @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: |
||||
* |
||||
* <pre> |
||||
* 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 } |
||||
* </pre> |
||||
* |
||||
*/ |
||||
|
||||
|
||||
/** |
||||
* 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;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
} |
||||
@ -0,0 +1,113 @@
@@ -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 <tt>PasswordPolicyResponse</tt>. |
||||
* |
||||
* @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()); |
||||
} |
||||
|
||||
} |
||||
Loading…
Reference in new issue