21 changed files with 1194 additions and 22 deletions
@ -0,0 +1,103 @@
@@ -0,0 +1,103 @@
|
||||
dn: dc=springsource,dc=com |
||||
objectClass: dcObject |
||||
objectClass: domain |
||||
dc: springsource |
||||
|
||||
dn: ou=users,dc=springsource,dc=com |
||||
objectClass: organizationalUnit |
||||
objectClass: top |
||||
ou: users |
||||
|
||||
dn: uid=luke,ou=users,dc=springsource,dc=com |
||||
objectClass: person |
||||
objectClass: organizationalPerson |
||||
objectClass: inetOrgPerson |
||||
objectClass: top |
||||
cn: Luke |
||||
uid: luke |
||||
givenName: Luke |
||||
o: SpringSource |
||||
sn: Taylor |
||||
userPassword: password |
||||
|
||||
dn: ou=policies,dc=springsource,dc=com |
||||
objectClass: organizationalUnit |
||||
objectClass: top |
||||
ou: policies |
||||
|
||||
dn: cn=default,ou=policies,dc=springsource,dc=com |
||||
objectClass: device |
||||
objectClass: top |
||||
objectClass: pwdPolicy |
||||
cn: default |
||||
pwdAttribute: userPassword |
||||
pwdCheckQuality: 1 |
||||
pwdExpireWarning: 600000 |
||||
pwdFailureCountInterval: 0 |
||||
pwdGraceAuthNLimit: 100 |
||||
pwdInHistory: 50 |
||||
pwdLockout: FALSE |
||||
pwdLockoutDuration: 0 |
||||
pwdMaxAge: 5184000 |
||||
pwdMaxFailure: 3 |
||||
pwdMinAge: 0 |
||||
pwdMinLength: 8 |
||||
pwdMustChange: FALSE |
||||
|
||||
dn: cn=lockoutafter1,ou=policies,dc=springsource,dc=com |
||||
objectClass: device |
||||
objectClass: top |
||||
objectClass: pwdPolicy |
||||
cn: lockoutafter1 |
||||
pwdAttribute: userPassword |
||||
pwdCheckQuality: 1 |
||||
pwdFailureCountInterval: 0 |
||||
pwdGraceAuthNLimit: 2 |
||||
pwdInHistory: 3 |
||||
pwdLockout: TRUE |
||||
pwdLockoutDuration: 10 |
||||
pwdMaxFailure: 1 |
||||
pwdMinAge: 0 |
||||
pwdMinLength: 6 |
||||
pwdMustChange: TRUE |
||||
|
||||
dn: cn=expirein10,ou=policies,dc=springsource,dc=com |
||||
objectClass: device |
||||
objectClass: top |
||||
objectClass: pwdPolicy |
||||
cn: expirein10 |
||||
pwdAttribute: userPassword |
||||
pwdExpireWarning: 9999 |
||||
pwdGraceAuthNLimit: 5 |
||||
pwdMaxAge: 10000 |
||||
pwdInHistory: 3 |
||||
pwdLockout: FALSE |
||||
pwdMinLength: 6 |
||||
pwdMustChange: TRUE |
||||
|
||||
|
||||
dn: uid=expireme,ou=users,dc=springsource,dc=com |
||||
objectClass: person |
||||
objectClass: organizationalPerson |
||||
objectClass: inetOrgPerson |
||||
objectClass: top |
||||
uid: expireme |
||||
cn: Expired |
||||
givenName: Expired |
||||
o: SpringSource |
||||
sn: User |
||||
userPassword: password |
||||
pwdPolicySubentry: cn=expirein10,ou=policies,dc=springsource,dc=com |
||||
|
||||
dn: uid=lockme,ou=users,dc=springsource,dc=com |
||||
objectClass: person |
||||
objectClass: organizationalPerson |
||||
objectClass: inetOrgPerson |
||||
objectClass: top |
||||
uid: lockme |
||||
cn: Expired |
||||
givenName: Expired |
||||
o: SpringSource |
||||
sn: User |
||||
userPassword: password |
||||
pwdPolicySubentry: cn=lockoutafter1,ou=policies,dc=springsource,dc=com |
||||
@ -0,0 +1,7 @@
@@ -0,0 +1,7 @@
|
||||
#! /bin/sh |
||||
|
||||
rm -Rf target/openldap |
||||
mkdir -p target/openldap |
||||
/opt/local/libexec/slapd -h ldap://localhost:22389 -d -1 -f slapd.conf & |
||||
sleep 2 |
||||
ldapadd -h localhost -p 22389 -D cn=admin,dc=springsource,dc=com -w password -x -f openldaptest.ldif |
||||
@ -0,0 +1,53 @@
@@ -0,0 +1,53 @@
|
||||
include /opt/local/etc/openldap/schema/core.schema |
||||
include /opt/local/etc/openldap/schema/cosine.schema |
||||
include /opt/local/etc/openldap/schema/inetorgperson.schema |
||||
include /opt/local/etc/openldap/schema/ppolicy.schema |
||||
|
||||
|
||||
pidfile ./target/slapd.pid |
||||
argsfile ./target/slapd.args |
||||
|
||||
# Load dynamic backend modules: |
||||
modulepath /usr/lib/openldap/modules |
||||
# moduleload back_ldap.la |
||||
# moduleload back_meta.la |
||||
# moduleload back_monitor.la |
||||
# moduleload back_perl.la |
||||
|
||||
disallow bind_anon |
||||
require authc |
||||
|
||||
access to dn.base="" |
||||
by * read |
||||
|
||||
database bdb |
||||
suffix "dc=springsource,dc=com" |
||||
checkpoint 1024 5 |
||||
cachesize 10000 |
||||
rootdn "cn=admin,dc=springsource,dc=com" |
||||
|
||||
rootpw password |
||||
|
||||
directory ./target/openldap |
||||
|
||||
index uid eq |
||||
index cn eq |
||||
index objectClass eq |
||||
|
||||
access to attrs=userpassword |
||||
by self =wx |
||||
by anonymous =x |
||||
by * none |
||||
|
||||
access to dn.subtree="ou=users,dc=qbe,dc=com" |
||||
by self write |
||||
by * read |
||||
|
||||
|
||||
overlay ppolicy |
||||
ppolicy_default "cn=default,ou=policies,dc=springsource,dc=com" |
||||
ppolicy_use_lockout |
||||
ppolicy_hash_cleartext |
||||
|
||||
|
||||
|
||||
@ -0,0 +1,87 @@
@@ -0,0 +1,87 @@
|
||||
package org.springframework.security.ldap.ppolicy; |
||||
|
||||
import java.util.Hashtable; |
||||
|
||||
import javax.naming.Context; |
||||
import javax.naming.directory.DirContext; |
||||
import javax.naming.ldap.Control; |
||||
import javax.naming.ldap.LdapContext; |
||||
|
||||
import org.springframework.ldap.support.LdapUtils; |
||||
import org.springframework.security.ldap.DefaultSpringSecurityContextSource; |
||||
|
||||
|
||||
/** |
||||
* Extended version of the <tt>DefaultSpringSecurityContextSource</tt> which adds support for |
||||
* the use of {@link PasswordPolicyControl} to make use of user account data stored in the directory. |
||||
* <p> |
||||
* When binding with specific username (not the <tt>userDn</tt>) property it will connect |
||||
* first as the userDn, then reconnect as the user in order to retrieve any password-policy control |
||||
* sent with the response, even if an exception occurs. |
||||
* |
||||
* @author Luke Taylor |
||||
* @version $Id$ |
||||
* @since 3.0 |
||||
*/ |
||||
public class PasswordPolicyAwareContextSource extends DefaultSpringSecurityContextSource { |
||||
|
||||
public PasswordPolicyAwareContextSource(String providerUrl) { |
||||
super(providerUrl); |
||||
} |
||||
|
||||
@Override |
||||
public DirContext getContext(String principal, String credentials) throws PasswordPolicyException { |
||||
if (principal.equals(userDn)) { |
||||
return super.getContext(principal, credentials); |
||||
} |
||||
|
||||
final boolean debug = logger.isDebugEnabled(); |
||||
|
||||
if (debug) { |
||||
logger.debug("Binding as '" + userDn + "', prior to reconnect as user '" + principal + "'" ); |
||||
} |
||||
|
||||
// First bind as manager user before rebinding as the specific principal.
|
||||
LdapContext ctx = (LdapContext) super.getContext(userDn, password); |
||||
|
||||
Control[] rctls = { new PasswordPolicyControl(false) }; |
||||
|
||||
try { |
||||
ctx.addToEnvironment(Context.SECURITY_PRINCIPAL, principal ); |
||||
ctx.addToEnvironment(Context.SECURITY_CREDENTIALS, credentials); |
||||
ctx.reconnect(rctls); |
||||
} catch(javax.naming.NamingException ne) { |
||||
PasswordPolicyResponseControl ctrl = PasswordPolicyControlExtractor.extractControl(ctx); |
||||
if (debug) { |
||||
logger.debug("Failed to obtain context", ne); |
||||
logger.debug("Pasword policy response: " + ctrl); |
||||
} |
||||
|
||||
LdapUtils.closeContext(ctx); |
||||
|
||||
if (ctrl != null) { |
||||
if (ctrl.isLocked()) { |
||||
throw new PasswordPolicyException(ctrl.getErrorStatus()); |
||||
} |
||||
} |
||||
|
||||
throw LdapUtils.convertLdapException(ne); |
||||
} |
||||
|
||||
if (debug) { |
||||
logger.debug("PPolicy control returned: " + PasswordPolicyControlExtractor.extractControl(ctx)); |
||||
} |
||||
|
||||
return ctx; |
||||
} |
||||
|
||||
@Override |
||||
@SuppressWarnings("unchecked") |
||||
protected Hashtable getAuthenticatedEnv(String principal, String credentials) { |
||||
Hashtable env = super.getAuthenticatedEnv(principal, credentials); |
||||
|
||||
env.put(LdapContext.CONTROL_FACTORIES, PasswordPolicyControlFactory.class.getName()); |
||||
|
||||
return env; |
||||
} |
||||
} |
||||
@ -0,0 +1,88 @@
@@ -0,0 +1,88 @@
|
||||
/* 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.springframework.security.ldap.ppolicy; |
||||
|
||||
import javax.naming.ldap.Control; |
||||
|
||||
|
||||
/** |
||||
* |
||||
* A Password Policy request control. |
||||
* <p> |
||||
* Based on the information in the corresponding internet draft on LDAP password policy. |
||||
* |
||||
* @author Stefan Zoerner |
||||
* @author Luke Taylor |
||||
* @version $Id$ |
||||
* |
||||
* @see PasswordPolicyResponseControl |
||||
* @see <a href="http://www.ietf.org/internet-drafts/draft-behera-ldap-password-policy-09.txt">Password Policy for LDAP |
||||
* Directories</a> |
||||
*/ |
||||
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 ("1.3.6.1.4.1.42.2.27.8.5.1"). |
||||
*/ |
||||
public String getID() { |
||||
return OID; |
||||
} |
||||
|
||||
/** |
||||
* Returns whether the control is critical for the client. |
||||
*/ |
||||
public boolean isCritical() { |
||||
return critical; |
||||
} |
||||
} |
||||
@ -0,0 +1,38 @@
@@ -0,0 +1,38 @@
|
||||
package org.springframework.security.ldap.ppolicy; |
||||
|
||||
import javax.naming.directory.DirContext; |
||||
import javax.naming.ldap.Control; |
||||
import javax.naming.ldap.LdapContext; |
||||
|
||||
import org.apache.commons.logging.Log; |
||||
import org.apache.commons.logging.LogFactory; |
||||
|
||||
/** |
||||
* Obtains the <tt>PasswordPolicyControl</tt> from a context for use by other classes. |
||||
* |
||||
* @author Luke Taylor |
||||
* @version $Id$ |
||||
* @since 3.0 |
||||
*/ |
||||
public class PasswordPolicyControlExtractor { |
||||
private static final Log logger = LogFactory.getLog(PasswordPolicyControlExtractor.class); |
||||
|
||||
public static PasswordPolicyResponseControl extractControl(DirContext dirCtx) { |
||||
LdapContext ctx = (LdapContext) dirCtx; |
||||
Control[] ctrls = null; |
||||
try { |
||||
ctrls = ctx.getResponseControls(); |
||||
} catch (javax.naming.NamingException e) { |
||||
logger.error("Failed to obtain response controls", e); |
||||
} |
||||
|
||||
for (int i = 0; ctrls != null && i < ctrls.length; i++) { |
||||
if (ctrls[i] instanceof PasswordPolicyResponseControl) { |
||||
return (PasswordPolicyResponseControl) ctrls[i]; |
||||
} |
||||
} |
||||
|
||||
return null; |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,47 @@
@@ -0,0 +1,47 @@
|
||||
/* 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.springframework.security.ldap.ppolicy; |
||||
|
||||
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 { |
||||
//~ 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; |
||||
} |
||||
} |
||||
@ -0,0 +1,12 @@
@@ -0,0 +1,12 @@
|
||||
package org.springframework.security.ldap.ppolicy; |
||||
|
||||
/** |
||||
* @author Luke Taylor |
||||
* @version $Id$ |
||||
* @since 3.0 |
||||
*/ |
||||
public interface PasswordPolicyData { |
||||
int getTimeBeforeExpiration(); |
||||
|
||||
int getGraceLoginsRemaining(); |
||||
} |
||||
@ -0,0 +1,53 @@
@@ -0,0 +1,53 @@
|
||||
package org.springframework.security.ldap.ppolicy; |
||||
|
||||
|
||||
/** |
||||
* Defines status codes for use with <tt>PasswordPolicyException</tt>, with error codes (for message source lookup) and default |
||||
* messages. |
||||
* |
||||
* <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 Luke Taylor |
||||
* @since 3.0 |
||||
*/ |
||||
public enum PasswordPolicyErrorStatus { |
||||
PASSWORD_EXPIRED ("ppolicy.expired", "Your password has expired"), |
||||
ACCOUNT_LOCKED ("ppolicy.locked", "Account is locked"), |
||||
CHANGE_AFTER_RESET ("ppolicy.change.after.reset", "Your password must be changed after being reset"), |
||||
PASSWORD_MOD_NOT_ALLOWED ("ppolicy.mod.not.allowed", "Password cannot be changed"), |
||||
MUST_SUPPLY_OLD_PASSWORD ("ppolicy.must.supply.old.password", "The old password must be supplied"), |
||||
INSUFFICIENT_PASSWORD_QUALITY ("ppolicy.insufficient.password.quality", "The supplied password is of insufficient quality"), |
||||
PASSWORD_TOO_SHORT ("ppolicy.password.too.short", "The supplied password is too short"), |
||||
PASSWORD_TOO_YOUNG ("ppolicy.password.too.young", "Your password was changed too recently to be changed again"), |
||||
PASSWORD_IN_HISTORY ("ppolicy.password.in.history", "The supplied password has already been used"); |
||||
|
||||
private String errorCode; |
||||
private String defaultMessage; |
||||
|
||||
private PasswordPolicyErrorStatus(String errorCode, String defaultMessage) { |
||||
this.errorCode = errorCode; |
||||
this.defaultMessage = defaultMessage; |
||||
} |
||||
|
||||
public String getErrorCode() { |
||||
return errorCode; |
||||
} |
||||
|
||||
public String getDefaultMessage() { |
||||
return defaultMessage; |
||||
} |
||||
} |
||||
@ -0,0 +1,22 @@
@@ -0,0 +1,22 @@
|
||||
package org.springframework.security.ldap.ppolicy; |
||||
|
||||
/** |
||||
* Generic exception raised by the ppolicy package. |
||||
* <p> |
||||
* The <tt>status</tt> property should be checked for more detail on the cause of the exception. |
||||
* |
||||
* @author Luke Taylor |
||||
* @since 3.0 |
||||
*/ |
||||
public class PasswordPolicyException extends RuntimeException { |
||||
private PasswordPolicyErrorStatus status; |
||||
|
||||
public PasswordPolicyException(PasswordPolicyErrorStatus status) { |
||||
super(status.getDefaultMessage()); |
||||
this.status = status; |
||||
} |
||||
|
||||
public PasswordPolicyErrorStatus getStatus() { |
||||
return status; |
||||
} |
||||
} |
||||
@ -0,0 +1,349 @@
@@ -0,0 +1,349 @@
|
||||
/* 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.springframework.security.ldap.ppolicy; |
||||
|
||||
import java.io.ByteArrayInputStream; |
||||
import java.io.IOException; |
||||
import java.io.InputStream; |
||||
|
||||
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 netscape.ldap.ber.stream.BERIntegral; |
||||
import netscape.ldap.ber.stream.BERSequence; |
||||
import netscape.ldap.ber.stream.BERTag; |
||||
import netscape.ldap.ber.stream.BERTagDecoder; |
||||
|
||||
import org.apache.commons.logging.Log; |
||||
import org.apache.commons.logging.LogFactory; |
||||
import org.springframework.dao.DataRetrievalFailureException; |
||||
|
||||
|
||||
/** |
||||
* Represents 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 <tt>timeBeforeExpiration</tt>, |
||||
* <tt>graceLoginsRemaining</tt>. |
||||
* <p> |
||||
* |
||||
* |
||||
* @author Stefan Zoerner |
||||
* @author Luke Taylor |
||||
* @version $Id: PasswordPolicyResponseControl.java,v 1.4 2009/03/04 07:25:07 itslxt Exp $ |
||||
* |
||||
* @see org.springframework.security.ldap.ppolicy.PasswordPolicyControl |
||||
* @see <a href="http://www.ibm.com/developerworks/tivoli/library/t-ldap-controls/">Stefan Zoerner's IBM developerworks |
||||
* article on LDAP controls.</a> |
||||
*/ |
||||
public class PasswordPolicyResponseControl extends PasswordPolicyControl { |
||||
//~ Static fields/initializers =====================================================================================
|
||||
|
||||
private static final Log logger = LogFactory.getLog(PasswordPolicyResponseControl.class); |
||||
|
||||
//~ Instance fields ================================================================================================
|
||||
|
||||
private byte[] encodedValue; |
||||
|
||||
private PasswordPolicyErrorStatus errorStatus; |
||||
|
||||
private int graceLoginsRemaining = Integer.MAX_VALUE; |
||||
private int timeBeforeExpiration = Integer.MAX_VALUE; |
||||
|
||||
//~ Constructors ===================================================================================================
|
||||
|
||||
/** |
||||
* 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> |
||||
* |
||||
*/ |
||||
public PasswordPolicyResponseControl(byte[] encodedValue) { |
||||
this.encodedValue = encodedValue; |
||||
|
||||
//PPolicyDecoder decoder = new JLdapDecoder();
|
||||
PPolicyDecoder decoder = new NetscapeDecoder(); |
||||
|
||||
try { |
||||
decoder.decode(); |
||||
} catch (IOException e) { |
||||
throw new DataRetrievalFailureException("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. |
||||
*/ |
||||
public byte[] getEncodedValue() { |
||||
return encodedValue; |
||||
} |
||||
|
||||
public PasswordPolicyErrorStatus getErrorStatus() { |
||||
return errorStatus; |
||||
} |
||||
|
||||
/** |
||||
* 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 errorStatus != null; |
||||
} |
||||
|
||||
/** |
||||
* Checks whether a warning is present. |
||||
* |
||||
* @return true, if a warning is present |
||||
*/ |
||||
public boolean hasWarning() { |
||||
return (graceLoginsRemaining != Integer.MAX_VALUE) || (timeBeforeExpiration != Integer.MAX_VALUE); |
||||
} |
||||
|
||||
public boolean isExpired() { |
||||
return errorStatus == PasswordPolicyErrorStatus.PASSWORD_EXPIRED; |
||||
} |
||||
|
||||
public boolean isChangeAfterReset() { |
||||
return errorStatus == PasswordPolicyErrorStatus.CHANGE_AFTER_RESET; |
||||
} |
||||
|
||||
public boolean isUsingGraceLogins() { |
||||
return graceLoginsRemaining < Integer.MAX_VALUE; |
||||
} |
||||
|
||||
/** |
||||
* Determines whether an account locked error has been returned. |
||||
* |
||||
* @return true if the account is locked. |
||||
*/ |
||||
public boolean isLocked() { |
||||
return errorStatus == PasswordPolicyErrorStatus.ACCOUNT_LOCKED; |
||||
} |
||||
|
||||
/** |
||||
* Create a textual representation containing error and warning messages, if any are present. |
||||
* |
||||
* @return error and warning messages |
||||
*/ |
||||
public String toString() { |
||||
StringBuilder sb = new StringBuilder("PasswordPolicyResponseControl"); |
||||
|
||||
if (hasError()) { |
||||
sb.append(", error: ").append(errorStatus.getDefaultMessage()); |
||||
} |
||||
|
||||
if (graceLoginsRemaining != Integer.MAX_VALUE) { |
||||
sb.append(", warning: ").append(graceLoginsRemaining).append(" grace logins remain"); |
||||
} |
||||
|
||||
if (timeBeforeExpiration != Integer.MAX_VALUE) { |
||||
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) { |
||||
BERIntegral error = (BERIntegral) elt.getValue(); |
||||
errorStatus = PasswordPolicyErrorStatus.values()[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 DataRetrievalFailureException("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,67 @@
@@ -0,0 +1,67 @@
|
||||
package org.springframework.security.ldap.ppolicy; |
||||
|
||||
import static org.junit.Assert.*; |
||||
|
||||
import org.junit.Before; |
||||
import org.junit.Test; |
||||
import org.springframework.security.authentication.BadCredentialsException; |
||||
import org.springframework.security.authentication.LockedException; |
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; |
||||
import org.springframework.security.core.Authentication; |
||||
import org.springframework.security.ldap.authentication.BindAuthenticator; |
||||
import org.springframework.security.ldap.authentication.LdapAuthenticationProvider; |
||||
import org.springframework.security.ldap.userdetails.LdapUserDetailsImpl; |
||||
|
||||
/** |
||||
* Test cases which run against an OpenLDAP server. |
||||
* <p> |
||||
* Run the script in the module root to start the server and import the data before running. |
||||
* @author Luke Taylor |
||||
* @version $Id$ |
||||
* @since 3.0 |
||||
*/ |
||||
public class OpenLDAPIntegrationTestSuite { |
||||
PasswordPolicyAwareContextSource cs; |
||||
|
||||
@Before |
||||
public void createContextSource() throws Exception { |
||||
cs = new PasswordPolicyAwareContextSource("ldap://localhost:22389/dc=springsource,dc=com"); |
||||
cs.setUserDn("cn=admin,dc=springsource,dc=com"); |
||||
cs.setPassword("password"); |
||||
cs.afterPropertiesSet(); |
||||
} |
||||
|
||||
@Test |
||||
public void simpleBindSucceeds() throws Exception { |
||||
BindAuthenticator authenticator = new BindAuthenticator(cs); |
||||
authenticator.setUserDnPatterns(new String[] {"uid={0},ou=users"}); |
||||
LdapAuthenticationProvider provider = new LdapAuthenticationProvider(authenticator); |
||||
provider.authenticate(new UsernamePasswordAuthenticationToken("luke","password")); |
||||
} |
||||
|
||||
@Test(expected=LockedException.class) |
||||
public void repeatedBindWithWrongPasswordLocksAccount() throws Exception { |
||||
BindAuthenticator authenticator = new BindAuthenticator(cs); |
||||
authenticator.setUserDnPatterns(new String[] {"uid={0},ou=users"}); |
||||
LdapAuthenticationProvider provider = new LdapAuthenticationProvider(authenticator); |
||||
for (int count=1; count < 4; count++) { |
||||
try { |
||||
Authentication a = provider.authenticate(new UsernamePasswordAuthenticationToken("lockme","wrong")); |
||||
LdapUserDetailsImpl ud = (LdapUserDetailsImpl) a.getPrincipal(); |
||||
assertTrue(ud.getTimeBeforeExpiration() < Integer.MAX_VALUE && ud.getTimeBeforeExpiration() > 0); |
||||
} catch (BadCredentialsException expected) { |
||||
} |
||||
} |
||||
} |
||||
|
||||
@Test |
||||
public void passwordExpiryTimeIsDetectedCorrectly() throws Exception { |
||||
BindAuthenticator authenticator = new BindAuthenticator(cs); |
||||
authenticator.setUserDnPatterns(new String[] {"uid={0},ou=users"}); |
||||
LdapAuthenticationProvider provider = new LdapAuthenticationProvider(authenticator); |
||||
Authentication a = provider.authenticate(new UsernamePasswordAuthenticationToken("expireme","password")); |
||||
PasswordPolicyData ud = (LdapUserDetailsImpl) a.getPrincipal(); |
||||
assertTrue(ud.getTimeBeforeExpiration() < Integer.MAX_VALUE && ud.getTimeBeforeExpiration() > 0); |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,67 @@
@@ -0,0 +1,67 @@
|
||||
package org.springframework.security.ldap.ppolicy; |
||||
|
||||
import static org.junit.Assert.*; |
||||
|
||||
import org.junit.Before; |
||||
import org.junit.Test; |
||||
import org.springframework.security.authentication.BadCredentialsException; |
||||
import org.springframework.security.authentication.LockedException; |
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; |
||||
import org.springframework.security.core.Authentication; |
||||
import org.springframework.security.ldap.authentication.BindAuthenticator; |
||||
import org.springframework.security.ldap.authentication.LdapAuthenticationProvider; |
||||
import org.springframework.security.ldap.userdetails.LdapUserDetailsImpl; |
||||
|
||||
/** |
||||
* Test cases which run against an OpenLDAP server. |
||||
* <p> |
||||
* Run the script in the module root to start the server and import the data before running. |
||||
* @author Luke Taylor |
||||
* @version $Id$ |
||||
* @since 3.0 |
||||
*/ |
||||
public class OpenLDAPIntegrationTestSuite { |
||||
PasswordPolicyAwareContextSource cs; |
||||
|
||||
@Before |
||||
public void createContextSource() throws Exception { |
||||
cs = new PasswordPolicyAwareContextSource("ldap://localhost:22389/dc=springsource,dc=com"); |
||||
cs.setUserDn("cn=admin,dc=springsource,dc=com"); |
||||
cs.setPassword("password"); |
||||
cs.afterPropertiesSet(); |
||||
} |
||||
|
||||
@Test |
||||
public void simpleBindSucceeds() throws Exception { |
||||
BindAuthenticator authenticator = new BindAuthenticator(cs); |
||||
authenticator.setUserDnPatterns(new String[] {"uid={0},ou=users"}); |
||||
LdapAuthenticationProvider provider = new LdapAuthenticationProvider(authenticator); |
||||
provider.authenticate(new UsernamePasswordAuthenticationToken("luke","password")); |
||||
} |
||||
|
||||
@Test(expected=LockedException.class) |
||||
public void repeatedBindWithWrongPasswordLocksAccount() throws Exception { |
||||
BindAuthenticator authenticator = new BindAuthenticator(cs); |
||||
authenticator.setUserDnPatterns(new String[] {"uid={0},ou=users"}); |
||||
LdapAuthenticationProvider provider = new LdapAuthenticationProvider(authenticator); |
||||
for (int count=1; count < 4; count++) { |
||||
try { |
||||
Authentication a = provider.authenticate(new UsernamePasswordAuthenticationToken("lockme","wrong")); |
||||
LdapUserDetailsImpl ud = (LdapUserDetailsImpl) a.getPrincipal(); |
||||
assertTrue(ud.getTimeBeforeExpiration() < Integer.MAX_VALUE && ud.getTimeBeforeExpiration() > 0); |
||||
} catch (BadCredentialsException expected) { |
||||
} |
||||
} |
||||
} |
||||
|
||||
@Test |
||||
public void passwordExpiryTimeIsDetectedCorrectly() throws Exception { |
||||
BindAuthenticator authenticator = new BindAuthenticator(cs); |
||||
authenticator.setUserDnPatterns(new String[] {"uid={0},ou=users"}); |
||||
LdapAuthenticationProvider provider = new LdapAuthenticationProvider(authenticator); |
||||
Authentication a = provider.authenticate(new UsernamePasswordAuthenticationToken("expireme","password")); |
||||
PasswordPolicyData ud = (LdapUserDetailsImpl) a.getPrincipal(); |
||||
assertTrue(ud.getTimeBeforeExpiration() < Integer.MAX_VALUE && ud.getTimeBeforeExpiration() > 0); |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,124 @@
@@ -0,0 +1,124 @@
|
||||
/* 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.springframework.security.ldap.ppolicy; |
||||
|
||||
import junit.framework.TestCase; |
||||
|
||||
/** |
||||
* Tests for <tt>PasswordPolicyResponse</tt>. |
||||
* |
||||
* @author Luke Taylor |
||||
* @version $Id: PasswordPolicyResponseControlTests.java 2217 2007-10-27 00:45:30Z luke_t $ |
||||
*/ |
||||
public class PasswordPolicyResponseControlTests extends TestCase { |
||||
//~ Methods ========================================================================================================
|
||||
|
||||
/** |
||||
* 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=security,dc=org");
|
||||
// env.put(Context.SECURITY_CREDENTIALS, "security");
|
||||
// env.put(LdapContext.CONTROL_FACTORIES, PasswordPolicyControlFactory.class.getName());
|
||||
//
|
||||
// InitialLdapContext ctx = new InitialLdapContext(env, null);
|
||||
//
|
||||
// Control[] rctls = { new PasswordPolicyControl(false) };
|
||||
//
|
||||
// ctx.setRequestControls(rctls);
|
||||
//
|
||||
// try {
|
||||
// ctx.addToEnvironment(Context.SECURITY_PRINCIPAL, "uid=bob,ou=people,dc=security,dc=org" );
|
||||
// ctx.addToEnvironment(Context.SECURITY_CREDENTIALS, "bobspassword");
|
||||
// Object o = ctx.lookup("");
|
||||
//
|
||||
// System.out.println(o);
|
||||
//
|
||||
// } catch(NamingException ne) {
|
||||
// // Ok.
|
||||
// System.err.println(ne);
|
||||
// }
|
||||
//
|
||||
// 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 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()); |
||||
} |
||||
|
||||
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 testOpenLDAPAccountLockedCtrlIsParsedCorrectly() { |
||||
byte[] ctrlBytes = {0x30, 0x03, (byte) 0xA1, 0x01, 0x01}; |
||||
|
||||
PasswordPolicyResponseControl ctrl = new PasswordPolicyResponseControl(ctrlBytes); |
||||
|
||||
assertTrue(ctrl.hasError() && ctrl.isLocked()); |
||||
assertFalse(ctrl.hasWarning()); |
||||
} |
||||
|
||||
public void testOpenLDAPPasswordExpiredCtrlIsParsedCorrectly() { |
||||
byte[] ctrlBytes = {0x30, 0x03, (byte) 0xA1, 0x01, 0x00}; |
||||
|
||||
PasswordPolicyResponseControl ctrl = new PasswordPolicyResponseControl(ctrlBytes); |
||||
|
||||
assertTrue(ctrl.hasError() && ctrl.isExpired()); |
||||
assertFalse(ctrl.hasWarning()); |
||||
} |
||||
} |
||||
Loading…
Reference in new issue