[Date Prev][Date Next] [Chronological] [Thread] [Top]

ppolicy: pardon password history



Hi list,

I have made a tiny modification to the ppolicy-module. The aim is to go easy on people who forgot their password, or forgot to deploy their recently changed password to all devices (think of laptops, smartphones, etc.). Whenever a login fails due to a invalid password, the ppolicy-module will count this as a failure. After a configurable number of password failures in a given time, ppolicy will take action and - for example - lock the acount. I have tried to tweak this behaviour: When the password is found in the password history, the ppolicy-module will not count this as a password failure. If anyone is interested in this, please find the attached patch which also includes a working example configuration/testcase.

Best regards,
Florian


--
Florian Hercher | Hochschulrechenzentrum | Philipps-Universität Marburg
Hans-Meerwein-Str. 6, 35032 Marburg | fon +49-(0)6421-28-21036
diff --git a/servers/slapd/overlays/ppolicy.c b/servers/slapd/overlays/ppolicy.c
index b92f352..3ffbe21 100644
--- a/servers/slapd/overlays/ppolicy.c
+++ b/servers/slapd/overlays/ppolicy.c
@@ -81,6 +81,10 @@ typedef struct pass_policy {
 	int pwdMaxFailure; /* number of failed binds allowed before lockout */
 	int pwdFailureCountInterval; /* number of seconds before failure
 									counts are zeroed */
+	int pwdPardonHistory; /* 0 = count every failed bind for password failure count,
+							1 = ignore failed bind for failure count
+							if password is found in the history of old passwords */
+
 	int pwdMustChange; /* 0 = users can use admin set password
 							1 = users must change password after admin set */
 	int pwdAllowUserChange; /* 0 = users cannot change their passwords
@@ -175,10 +179,10 @@ static struct schema_info {
 
 /* User attributes */
 static AttributeDescription *ad_pwdMinAge, *ad_pwdMaxAge, *ad_pwdInHistory,
-	*ad_pwdCheckQuality, *ad_pwdMinLength, *ad_pwdMaxFailure, 
+	*ad_pwdCheckQuality, *ad_pwdMinLength, *ad_pwdMaxFailure,
 	*ad_pwdGraceAuthNLimit, *ad_pwdExpireWarning, *ad_pwdLockoutDuration,
-	*ad_pwdFailureCountInterval, *ad_pwdCheckModule, *ad_pwdLockout,
-	*ad_pwdMustChange, *ad_pwdAllowUserChange, *ad_pwdSafeModify,
+	*ad_pwdFailureCountInterval, *ad_pwdPardonHistory, *ad_pwdCheckModule,
+	*ad_pwdLockout,	*ad_pwdMustChange, *ad_pwdAllowUserChange, *ad_pwdSafeModify,
 	*ad_pwdAttribute;
 
 #define TAB(name)	{ #name, &ad_##name }
@@ -196,6 +200,7 @@ static struct schema_info pwd_UsSchema[] = {
 	TAB(pwdLockout),
 	TAB(pwdLockoutDuration),
 	TAB(pwdFailureCountInterval),
+	TAB(pwdPardonHistory),
 	TAB(pwdCheckModule),
 	TAB(pwdMustChange),
 	TAB(pwdAllowUserChange),
@@ -554,7 +559,9 @@ ppolicy_get( Operation *op, Entry *e, PassPolicy *pp )
 	    	pp->pwdAllowUserChange = bvmatch( &a->a_nvals[0], &slap_true_bv );
 	if ((a = attr_find( pe->e_attrs, ad_pwdSafeModify )))
 	    	pp->pwdSafeModify = bvmatch( &a->a_nvals[0], &slap_true_bv );
-    
+        if ((a = attr_find( pe->e_attrs, ad_pwdPardonHistory )))
+                pp->pwdPardonHistory = bvmatch( &a->a_nvals[0], &slap_true_bv );
+
 	op->o_bd->bd_info = (BackendInfo *)on->on_info;
 	be_entry_release_r( op, pe );
 	op->o_bd->bd_info = (BackendInfo *)on;
@@ -912,6 +919,38 @@ ppolicy_ctrls_cleanup( Operation *op, SlapReply *rs )
 }
 
 static int
+ppolicy_check_list( Operation *op, Entry *e ) {
+	ppbind 		*ppb = op->o_callback->sc_private;
+	int		i, rc;
+	Attribute	*ha;
+
+	if ( ppb->pp.pwdInHistory > 0 && (ha = attr_find( e->e_attrs, ad_pwdHistory ))) {
+		struct berval oldpw;
+		time_t oldtime;
+		const char *txt;
+
+		for(i=0; ha->a_nvals[i].bv_val; i++) {
+			oldpw.bv_val = NULL;
+			oldpw.bv_len = 0;
+
+			rc = parse_pwdhistory( &(ha->a_nvals[i]), NULL,
+				&oldtime, &oldpw );
+
+			if (rc != LDAP_SUCCESS) continue; /* invalid history entry */
+
+			rc = lutil_passwd( &oldpw, &op->orb_cred,
+				NULL, &txt );
+
+			if ( rc == LDAP_SUCCESS) {
+				return LDAP_SUCCESS;
+			}
+		}
+	}
+
+	return LDAP_UNWILLING_TO_PERFORM;
+}
+
+static int
 ppolicy_bind_response( Operation *op, SlapReply *rs )
 {
 	ppbind *ppb = op->o_callback->sc_private;
@@ -959,6 +998,19 @@ ppolicy_bind_response( Operation *op, SlapReply *rs )
 	if ( rs->sr_err == LDAP_INVALID_CREDENTIALS ) {
 		int i = 0, fc = 0;
 
+		/*
+		 * if the given password is found in the
+		 * password history, don't increase the
+		 * password failure count
+		 */
+		if ( ppb->pp.pwdPardonHistory ) {
+			rc = ppolicy_check_list( op, e );
+			if ( rc == LDAP_SUCCESS ) {
+				Debug(LDAP_DEBUG_STATS, "ppolicy: ignore failed login attempt with old password\n", 0, 0, 0);
+				goto done;
+			}
+		}
+
 		m = ch_calloc( sizeof(Modifications), 1 );
 		m->sml_op = LDAP_MOD_ADD;
 		m->sml_flags = 0;
@@ -1021,6 +1073,7 @@ ppolicy_bind_response( Operation *op, SlapReply *rs )
 			ber_dupbv( &m->sml_nvalues[0], &timestamp );
 			m->sml_next = mod;
 			mod = m;
+			Statslog( LDAP_DEBUG_STATS, "ppolicy: account[%s] locked for [%d]\n", op->o_req_dn.bv_val, ppb->pp.pwdLockoutDuration, 0, 0, 0);
 		}
 	} else if ( rs->sr_err == LDAP_SUCCESS ) {
 		if ((a = attr_find( e->e_attrs, ad_pwdChangedTime )) != NULL)
diff --git a/servers/slapd/schema/ppolicy.schema b/servers/slapd/schema/ppolicy.schema
index 18bb1e6..301a04b 100644
--- a/servers/slapd/schema/ppolicy.schema
+++ b/servers/slapd/schema/ppolicy.schema
@@ -325,6 +325,18 @@ attributetype ( 1.3.6.1.4.1.42.2.27.8.1.15
       SYNTAX 1.3.6.1.4.1.1466.115.121.1.7
       SINGLE-VALUE )
 
+# Pardon History Exstension
+#   This attribute specifies whether or not a failed login attempt 
+#   should be counted when the password is found in the password 
+#   history. If this attribute is not present, a "FALSE" value is assumed.
+
+attributetype ( 1.3.6.1.4.1.42.2.27.8.1.77
+      NAME 'pwdPardonHistory'
+      EQUALITY booleanMatch
+      SYNTAX 1.3.6.1.4.1.1466.115.121.1.7
+      SINGLE-VALUE )
+
+
 # HP extensions
 #
 # pwdCheckModule
@@ -366,7 +378,7 @@ objectclass ( 1.3.6.1.4.1.42.2.27.8.2.1
       MAY ( pwdMinAge $ pwdMaxAge $ pwdInHistory $ pwdCheckQuality $
       pwdMinLength $ pwdExpireWarning $ pwdGraceAuthNLimit $ pwdLockout
       $ pwdLockoutDuration $ pwdMaxFailure $ pwdFailureCountInterval $
-      pwdMustChange $ pwdAllowUserChange $ pwdSafeModify ) )
+      pwdMustChange $ pwdAllowUserChange $ pwdSafeModify $ pwdPardonHistory ) )
 
 #5.3  Attribute Types for Password Policy State Information
 #
diff --git a/tests/scripts/conf.sh b/tests/scripts/conf.sh
old mode 100755
new mode 100644
diff --git a/tests/scripts/defines.sh b/tests/scripts/defines.sh
old mode 100755
new mode 100644
index 96e3d9b..c26454e
--- a/tests/scripts/defines.sh
+++ b/tests/scripts/defines.sh
@@ -224,6 +224,7 @@ LDIFPASSWD=$DATADIR/passwd.ldif
 LDIFWHOAMI=$DATADIR/test-whoami.ldif
 LDIFPASSWDOUT=$DATADIR/passwd-out.ldif
 LDIFPPOLICY=$DATADIR/ppolicy.ldif
+LDIFPPARDON=$DATADIR/ppardon.ldif
 LDIFLANG=$DATADIR/test-lang.ldif
 LDIFLANGOUT=$DATADIR/lang-out.ldif
 LDIFREF=$DATADIR/referrals.ldif
diff --git a/tests/data/ppardon.ldif b/tests/data/ppardon.ldif
index e69de29..886e026 100644
--- a/tests/data/ppardon.ldif
+++ b/tests/data/ppardon.ldif
@@ -0,0 +1,69 @@
+dn: dc=example, dc=com
+objectClass: top
+objectClass: organization
+objectClass: dcObject
+o: Example
+dc: example
+
+dn: ou=People, dc=example, dc=com
+objectClass: top
+objectClass: organizationalUnit
+ou: People
+
+dn: ou=Policies, dc=example, dc=com
+objectClass: top
+objectClass: organizationalUnit
+ou: Policies
+
+dn: cn=Standard Policy, ou=Policies, dc=example, dc=com
+objectClass: top
+objectClass: device
+objectClass: pwdPolicy
+cn: Standard Policy
+pwdAttribute: 2.5.4.35
+pwdLockoutDuration: 15
+pwdInHistory: 5
+pwdCheckQuality: 0
+pwdExpireWarning: 0
+pwdMaxAge: 0
+pwdMinLength: 5
+pwdGraceAuthnLimit: 0
+pwdAllowUserChange: TRUE
+pwdMustChange: FALSE
+pwdMaxFailure: 3
+pwdFailureCountInterval: 120
+pwdSafeModify: TRUE
+pwdLockout: TRUE
+pwdPardonHistory: TRUE
+
+dn: uid=nd, ou=People, dc=example, dc=com
+objectClass: top
+objectClass: person
+objectClass: inetOrgPerson
+cn: Neil Dunbar
+uid: nd
+sn: Dunbar
+givenName: Neil
+userPassword: testpassword
+
+dn: uid=ndadmin, ou=People, dc=example, dc=com
+objectClass: top
+objectClass: person
+objectClass: inetOrgPerson
+cn: Neil Dunbar (Admin)
+uid: ndadmin
+sn: Dunbar
+givenName: Neil
+userPassword: testpw
+
+dn: uid=test, ou=People, dc=example, dc=com
+objectClass: top
+objectClass: person
+objectClass: inetOrgPerson
+cn: test test
+uid: test
+sn: Test
+givenName:  Test
+userPassword: kfhgkjhfdgkfd
+pwdPolicySubEntry: cn=No Policy, ou=Policies, dc=example, dc=com
+
diff --git a/tests/scripts/test077-ppolicy-pardon b/tests/scripts/test077-ppolicy-pardon
index e69de29..734133b 100755
--- a/tests/scripts/test077-ppolicy-pardon
+++ b/tests/scripts/test077-ppolicy-pardon
@@ -0,0 +1,200 @@
+#! /bin/sh
+# $OpenLDAP$
+## This work is part of OpenLDAP Software <http://www.openldap.org/>.
+##
+## Copyright 1998-2012 The OpenLDAP Foundation.
+## All rights reserved.
+##
+## Redistribution and use in source and binary forms, with or without
+## modification, are permitted only as authorized by the OpenLDAP
+## Public License.
+##
+## A copy of this license is available in the file LICENSE in the
+## top-level directory of the distribution or, alternatively, at
+## <http://www.OpenLDAP.org/license.html>.
+
+echo "running defines.sh"
+. $SRCDIR/scripts/defines.sh
+
+if test $PPOLICY = ppolicyno; then 
+	echo "Password policy overlay not available, test skipped"
+	exit 0
+fi 
+
+mkdir -p $TESTDIR $DBDIR1
+
+$SLAPPASSWD -g -n >$CONFIGPWF
+echo "rootpw `$SLAPPASSWD -T $CONFIGPWF`" >$TESTDIR/configpw.conf
+
+echo "Starting slapd on TCP/IP port $PORT1..."
+. $CONFFILTER $BACKEND $MONITORDB < $PPOLICYCONF > $CONF1
+$SLAPD -f $CONF1 -h $URI1 -d $LVL $TIMING > $LOG1 2>&1 &
+PID=$!
+if test $WAIT != 0 ; then
+    echo PID $PID
+    read foo
+fi
+KILLPIDS="$PID"
+
+USER="uid=nd, ou=People, dc=example, dc=com"
+PASS=testpassword
+
+sleep 1
+
+echo "Using ldapsearch to check that slapd is running..."
+for i in 0 1 2 3 4 5; do
+	$LDAPSEARCH -s base -b "$MONITOR" -h $LOCALHOST -p $PORT1 \
+		'objectclass=*' > /dev/null 2>&1
+	RC=$?
+	if test $RC = 0 ; then
+		break
+	fi
+	echo "Waiting 5 seconds for slapd to start..."
+	sleep 5
+done
+if test $RC != 0 ; then
+	echo "ldapsearch failed ($RC)!"
+	test $KILLSERVERS != no && kill -HUP $KILLPIDS
+	exit $RC
+fi
+
+echo /dev/null > $TESTOUT
+
+echo "Using ldapadd to populate the database..."
+# may need "-e relax" for draft 09, but not yet.
+$LDAPADD -D "$MANAGERDN" -h $LOCALHOST -p $PORT1 -w $PASSWD < \
+	$LDIFPPARDON >> $TESTOUT 2>&1
+RC=$?
+if test $RC != 0 ; then
+	echo "ldapadd failed ($RC)!"
+	test $KILLSERVERS != no && kill -HUP $KILLPIDS
+	exit $RC
+fi
+
+echo "Filling password history..."
+$LDAPMODIFY -v -D "$USER" -h $LOCALHOST -p $PORT1 -w $PASS >> \
+	$TESTOUT 2>&1 << EOMODS
+dn: uid=nd, ou=People, dc=example, dc=com
+changetype: modify
+delete: userpassword
+userpassword: $PASS
+-
+replace: userpassword
+userpassword: 20urgle12-1
+
+dn: uid=nd, ou=People, dc=example, dc=com
+changetype: modify
+delete: userpassword
+userpassword: 20urgle12-1
+-
+replace: userpassword
+userpassword: 20urgle12-2
+
+dn: uid=nd, ou=People, dc=example, dc=com
+changetype: modify
+delete: userpassword
+userpassword: 20urgle12-2
+-
+replace: userpassword
+userpassword: 20urgle12-3
+
+dn: uid=nd, ou=People, dc=example, dc=com
+changetype: modify
+delete: userpassword
+userpassword: 20urgle12-3
+-
+replace: userpassword
+userpassword: 20urgle12-4
+
+dn: uid=nd, ou=People, dc=example, dc=com
+changetype: modify
+delete: userpassword
+userpassword: 20urgle12-4
+-
+replace: userpassword
+userpassword: 20urgle12-5
+
+dn: uid=nd, ou=People, dc=example, dc=com
+changetype: modify
+delete: userpassword
+userpassword: 20urgle12-5
+-
+replace: userpassword
+userpassword: 20urgle12-6
+
+dn: uid=nd, ou=People, dc=example, dc=com
+changetype: modify
+delete: userpassword
+userpassword: 20urgle12-6
+-
+replace: userpassword
+userpassword: $PASS
+EOMODS
+
+RC=$?
+if test $RC != 0 ; then
+	echo "ldapmodify failed ($RC)!"
+	test $KILLSERVERS != no && kill -HUP $KILLPIDS
+	exit $RC
+fi
+
+echo "check access..."
+
+$LDAPSEARCH -e ppolicy -h $LOCALHOST -p $PORT1 -D "$USER" -w $PASS \
+	-b "$BASEDN" -s base >> $SEARCHOUT 2>&1
+RC=$?
+if test $RC != 0 ; then
+	echo "ldapsearch failed ($RC)!"
+	test $KILLSERVERS != no && kill -HUP $KILLPIDS
+	exit $RC
+fi
+
+echo "test lockout with unseen password..."
+$LDAPSEARCH -h $LOCALHOST -p $PORT1 -D "$USER" -w wrongpw >$SEARCHOUT 2>&1
+sleep 2
+$LDAPSEARCH -h $LOCALHOST -p $PORT1 -D "$USER" -w wrongpw >>$SEARCHOUT 2>&1
+sleep 2
+$LDAPSEARCH -h $LOCALHOST -p $PORT1 -D "$USER" -w wrongpw >>$SEARCHOUT 2>&1
+sleep 2
+$LDAPSEARCH -e ppolicy -h $LOCALHOST -p $PORT1 -D "$USER" -w wrongpw >> $SEARCHOUT 2>&1
+sleep 2
+
+$LDAPSEARCH -e ppolicy -h $LOCALHOST -p $PORT1 -D "$USER" -w $PASS \
+	-b "$BASEDN" -s base >> $SEARCHOUT 2>&1
+RC=$?
+if test $RC != 49 ; then
+	echo "ldapsearch failed ($RC)!"
+	test $KILLSERVERS != no && kill -HUP $KILLPIDS
+	exit $RC
+fi
+
+echo "Waiting 20 seconds for lockout to reset..."
+sleep 20
+
+echo "test lockout with password from history..."
+
+$LDAPSEARCH -h $LOCALHOST -p $PORT1 -D "$USER" -w 20urgle12-3 >$SEARCHOUT 2>&1
+sleep 2
+$LDAPSEARCH -h $LOCALHOST -p $PORT1 -D "$USER" -w 20urgle12-3 >>$SEARCHOUT 2>&1
+sleep 2
+$LDAPSEARCH -h $LOCALHOST -p $PORT1 -D "$USER" -w 20urgle12-3 >>$SEARCHOUT 2>&1
+sleep 2
+$LDAPSEARCH -e ppolicy -h $LOCALHOST -p $PORT1 -D "$USER" -w 20urgle12-3 >> $SEARCHOUT 2>&1
+sleep 2
+
+$LDAPSEARCH -e ppolicy -h $LOCALHOST -p $PORT1 -D "$USER" -w $PASS \
+	-b "$BASEDN" -s base >> $SEARCHOUT 2>&1
+RC=$?
+if test $RC != 0 ; then
+	echo "ldapsearch failed ($RC)!"
+	test $KILLSERVERS != no && kill -HUP $KILLPIDS
+	exit $RC
+fi
+
+test $KILLSERVERS != no && kill -HUP $KILLPIDS
+
+echo ">>>>> Test succeeded"
+
+test $KILLSERVERS != no && wait
+
+exit 0