/* See LICENSE for licensing and NOTICE for copyright. */
package org.ldaptive.control;
import java.nio.ByteBuffer;
import javax.security.auth.login.AccountException;
import javax.security.auth.login.AccountLockedException;
import javax.security.auth.login.CredentialException;
import javax.security.auth.login.CredentialExpiredException;
import javax.security.auth.login.LoginException;
import org.ldaptive.LdapUtils;
import org.ldaptive.asn1.AbstractParseHandler;
import org.ldaptive.asn1.DERParser;
import org.ldaptive.asn1.DERPath;
import org.ldaptive.asn1.IntegerType;
import org.ldaptive.auth.AccountState;
/**
* Request/response control for password policy. See http://tools.ietf.org/html/draft-behera-ldap-password-policy-10.
* Control is defined as:
*
* <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>
*
* @author Middleware Services
*/
public class PasswordPolicyControl extends AbstractControl implements RequestControl, ResponseControl
{
/** OID of this control. */
public static final String OID = "1.3.6.1.4.1.42.2.27.8.5.1";
/** hash code seed. */
private static final int HASH_CODE_SEED = 719;
/** Enum for ppolicy errors. */
public enum Error implements AccountState.Error {
/** password expired. */
PASSWORD_EXPIRED(0),
/** account locked. */
ACCOUNT_LOCKED(1),
/** change after reset. */
CHANGE_AFTER_RESET(2),
/** password modification not allowed. */
PASSWORD_MOD_NOT_ALLOWED(3),
/** must supply old password. */
MUST_SUPPLY_OLD_PASSWORD(4),
/** insufficient password quality. */
INSUFFICIENT_PASSWORD_QUALITY(5),
/** password too short. */
PASSWORD_TOO_SHORT(6),
/** password too young. */
PASSWORD_TOO_YOUNG(7),
/** password in history. */
PASSWORD_IN_HISTORY(8);
/** underlying error code. */
private final int code;
/**
* Creates a new error.
*
* @param i error code
*/
Error(final int i)
{
code = i;
}
@Override
public int getCode()
{
return code;
}
@Override
public String getMessage()
{
return name();
}
@Override
public void throwSecurityException()
throws LoginException
{
switch (this) {
case PASSWORD_EXPIRED:
throw new CredentialExpiredException(name());
case ACCOUNT_LOCKED:
throw new AccountLockedException(name());
case CHANGE_AFTER_RESET:
throw new CredentialExpiredException(name());
case PASSWORD_MOD_NOT_ALLOWED:
throw new AccountException(name());
case MUST_SUPPLY_OLD_PASSWORD:
throw new AccountException(name());
case INSUFFICIENT_PASSWORD_QUALITY:
throw new CredentialException(name());
case PASSWORD_TOO_SHORT:
throw new CredentialException(name());
case PASSWORD_TOO_YOUNG:
throw new CredentialException(name());
case PASSWORD_IN_HISTORY:
throw new CredentialException(name());
default:
throw new IllegalStateException("Unknown password policy error: " + this);
}
}
/**
* Returns the error for the supplied integer constant.
*
* @param code to find error for
*
* @return error
*/
public static Error valueOf(final int code)
{
for (Error e : Error.values()) {
if (e.getCode() == code) {
return e;
}
}
return null;
}
}
/** Ppolicy warning. */
private int timeBeforeExpiration;
/** Ppolicy warning. */
private int graceAuthNsRemaining;
/** Ppolicy error. */
private Error error;
/** Default constructor. */
public PasswordPolicyControl()
{
super(OID);
}
/**
* Creates a new password policy control.
*
* @param critical whether this control is critical
*/
public PasswordPolicyControl(final boolean critical)
{
super(OID, critical);
}
/**
* Returns the time before expiration in seconds.
*
* @return time before expiration
*/
public int getTimeBeforeExpiration()
{
return timeBeforeExpiration;
}
/**
* Sets the time before expiration in seconds.
*
* @param time before expiration
*/
public void setTimeBeforeExpiration(final int time)
{
timeBeforeExpiration = time;
}
/**
* Returns the number of grace authentications remaining.
*
* @return number of grace authentications remaining
*/
public int getGraceAuthNsRemaining()
{
return graceAuthNsRemaining;
}
/**
* Sets the number of grace authentications remaining.
*
* @param count number of grace authentications remaining
*/
public void setGraceAuthNsRemaining(final int count)
{
graceAuthNsRemaining = count;
}
/**
* Returns the password policy error.
*
* @return password policy error
*/
public Error getError()
{
return error;
}
/**
* Sets the password policy error.
*
* @param e password policy error
*/
public void setError(final Error e)
{
error = e;
}
@Override
public boolean equals(final Object o)
{
if (o == this) {
return true;
}
if (o instanceof PasswordPolicyControl && super.equals(o)) {
final PasswordPolicyControl v = (PasswordPolicyControl) o;
return LdapUtils.areEqual(timeBeforeExpiration, v.timeBeforeExpiration) &&
LdapUtils.areEqual(graceAuthNsRemaining, v.graceAuthNsRemaining) &&
LdapUtils.areEqual(error, v.error);
}
return false;
}
@Override
public int hashCode()
{
return
LdapUtils.computeHashCode(
HASH_CODE_SEED,
getOID(),
getCriticality(),
timeBeforeExpiration,
graceAuthNsRemaining,
error);
}
@Override
public String toString()
{
return
String.format(
"[%s@%d::criticality=%s, timeBeforeExpiration=%s, " +
"graceAuthNsRemaining=%s, error=%s]",
getClass().getName(),
hashCode(),
getCriticality(),
timeBeforeExpiration,
graceAuthNsRemaining,
error);
}
@Override
public byte[] encode()
{
return null;
}
@Override
public void decode(final byte[] berValue)
{
logger.trace("decoding control: {}", LdapUtils.base64Encode(berValue));
final DERParser parser = new DERParser();
parser.registerHandler(TimeBeforeExpirationHandler.PATH, new TimeBeforeExpirationHandler(this));
parser.registerHandler(GraceAuthnsRemainingHandler.PATH, new GraceAuthnsRemainingHandler(this));
parser.registerHandler(ErrorHandler.PATH, new ErrorHandler(this));
parser.parse(ByteBuffer.wrap(berValue));
}
/** Parse handler implementation for the time before expiration. */
private static class TimeBeforeExpirationHandler extends AbstractParseHandler<PasswordPolicyControl>
{
/** DER path to warning. */
public static final DERPath PATH = new DERPath("/SEQ/CTX(0)/CTX(0)");
/**
* Creates a new time before expiration handler.
*
* @param control to configure
*/
TimeBeforeExpirationHandler(final PasswordPolicyControl control)
{
super(control);
}
@Override
public void handle(final DERParser parser, final ByteBuffer encoded)
{
getObject().setTimeBeforeExpiration(IntegerType.decode(encoded).intValue());
}
}
/** Parse handler implementation for the grace authns remaining. */
private static class GraceAuthnsRemainingHandler extends AbstractParseHandler<PasswordPolicyControl>
{
/** DER path to warning. */
public static final DERPath PATH = new DERPath("/SEQ/CTX(0)/CTX(1)");
/**
* Creates a new grace authns remaining handler.
*
* @param control to configure
*/
GraceAuthnsRemainingHandler(final PasswordPolicyControl control)
{
super(control);
}
@Override
public void handle(final DERParser parser, final ByteBuffer encoded)
{
getObject().setGraceAuthNsRemaining(IntegerType.decode(encoded).intValue());
}
}
/** Parse handler implementation for the error. */
private static class ErrorHandler extends AbstractParseHandler<PasswordPolicyControl>
{
/** DER path to error. */
public static final DERPath PATH = new DERPath("/SEQ/CTX(1)");
/**
* Creates a new error handler.
*
* @param control to configure
*/
ErrorHandler(final PasswordPolicyControl control)
{
super(control);
}
@Override
public void handle(final DERParser parser, final ByteBuffer encoded)
{
final int errValue = IntegerType.decode(encoded).intValue();
final PasswordPolicyControl.Error e = PasswordPolicyControl.Error.valueOf(errValue);
if (e == null) {
throw new IllegalArgumentException("Unknown error code " + errValue);
}
getObject().setError(e);
}
}
}