andykimpe / rpms / 389-ds-base

Forked from rpms/389-ds-base 5 months ago
Clone
dc8c34
From c7f195fdf33ae995c12ff43f5b9c03da3d25b8da Mon Sep 17 00:00:00 2001
dc8c34
From: Mark Reynolds <mreynolds@redhat.com>
dc8c34
Date: Tue, 3 May 2016 09:57:36 -0400
dc8c34
Subject: [PATCH 381/382] Ticket 48813 - password history is not updated when
dc8c34
 an admin resets the password
dc8c34
dc8c34
Bug Description:  When an admin resets a password the current password is not
dc8c34
                  stored in the password history. This incorrectly allows the
dc8c34
                  user to reuse the previous password after the reset.
dc8c34
dc8c34
Fix Description:  When a password is being reset by an "admin", still grab the
dc8c34
                  old password so we can correctly update the password history.
dc8c34
dc8c34
https://fedorahosted.org/389/ticket/48813
dc8c34
dc8c34
Reviewed by: nhosoi(Thanks!)
dc8c34
dc8c34
(cherry picked from commit 9c310b09c481a32b1a012371c688c65156b33472)
dc8c34
(cherry picked from commit b357e443d06f32bcad2f410868299d43153dca62)
dc8c34
(cherry picked from commit 0a5047043051701d5b361500f605d782a2befa1d)
dc8c34
---
dc8c34
 .../tests/suites/password/pwp_history_test.py      | 264 +++++++++++++++++++++
dc8c34
 ldap/servers/slapd/modify.c                        |   4 +
dc8c34
 ldap/servers/slapd/proto-slap.h                    |   1 +
dc8c34
 ldap/servers/slapd/pw.c                            |  29 +++
dc8c34
 4 files changed, 298 insertions(+)
dc8c34
 create mode 100644 dirsrvtests/tests/suites/password/pwp_history_test.py
dc8c34
dc8c34
diff --git a/dirsrvtests/tests/suites/password/pwp_history_test.py b/dirsrvtests/tests/suites/password/pwp_history_test.py
dc8c34
new file mode 100644
dc8c34
index 0000000..3f66efd
dc8c34
--- /dev/null
dc8c34
+++ b/dirsrvtests/tests/suites/password/pwp_history_test.py
dc8c34
@@ -0,0 +1,264 @@
dc8c34
+import os
dc8c34
+import ldap
dc8c34
+import logging
dc8c34
+import pytest
dc8c34
+from lib389 import DirSrv, Entry
dc8c34
+from lib389._constants import *
dc8c34
+from lib389.properties import *
dc8c34
+from lib389.tasks import *
dc8c34
+from lib389.utils import *
dc8c34
+
dc8c34
+logging.getLogger(__name__).setLevel(logging.DEBUG)
dc8c34
+log = logging.getLogger(__name__)
dc8c34
+
dc8c34
+
dc8c34
+class TopologyStandalone(object):
dc8c34
+    """ Topology class """
dc8c34
+    def __init__(self, standalone):
dc8c34
+        """ init """
dc8c34
+        standalone.open()
dc8c34
+        self.standalone = standalone
dc8c34
+
dc8c34
+
dc8c34
+@pytest.fixture(scope="module")
dc8c34
+def topology(request):
dc8c34
+    """
dc8c34
+    Creating standalone instance ...
dc8c34
+    """
dc8c34
+    standalone = DirSrv(verbose=False)
dc8c34
+    args_instance[SER_HOST] = HOST_STANDALONE
dc8c34
+    args_instance[SER_PORT] = PORT_STANDALONE
dc8c34
+    args_instance[SER_SERVERID_PROP] = SERVERID_STANDALONE
dc8c34
+    args_instance[SER_CREATION_SUFFIX] = DEFAULT_SUFFIX
dc8c34
+    args_standalone = args_instance.copy()
dc8c34
+    standalone.allocate(args_standalone)
dc8c34
+    instance_standalone = standalone.exists()
dc8c34
+    if instance_standalone:
dc8c34
+        standalone.delete()
dc8c34
+    standalone.create()
dc8c34
+    standalone.open()
dc8c34
+
dc8c34
+    # Delete each instance in the end
dc8c34
+    def fin():
dc8c34
+        """ Clean up instance """
dc8c34
+        standalone.delete()
dc8c34
+    request.addfinalizer(fin)
dc8c34
+
dc8c34
+    # Clear out the tmp dir
dc8c34
+    standalone.clearTmpDir(__file__)
dc8c34
+
dc8c34
+    return TopologyStandalone(standalone)
dc8c34
+
dc8c34
+
dc8c34
+def test_pwp_history_test(topology):
dc8c34
+    """
dc8c34
+    Test password policy history feature:
dc8c34
+        - Test password history is enforced
dc8c34
+        - Test password history works after an Admin resets the password
dc8c34
+        - Test that the correct number of passwords are stored in history
dc8c34
+    """
dc8c34
+
dc8c34
+    USER_DN = 'uid=testuser,' + DEFAULT_SUFFIX
dc8c34
+
dc8c34
+    #
dc8c34
+    # Configure password history policy and add a test user
dc8c34
+    #
dc8c34
+    try:
dc8c34
+        topology.standalone.modify_s("cn=config",
dc8c34
+                                     [(ldap.MOD_REPLACE,
dc8c34
+                                       'passwordHistory', 'on'),
dc8c34
+                                      (ldap.MOD_REPLACE,
dc8c34
+                                       'passwordInHistory', '3'),
dc8c34
+                                      (ldap.MOD_REPLACE,
dc8c34
+                                       'passwordChange', 'on'),
dc8c34
+                                      (ldap.MOD_REPLACE,
dc8c34
+                                       'passwordStorageScheme', 'CLEAR')])
dc8c34
+        log.info('Configured password policy.')
dc8c34
+    except ldap.LDAPError as e:
dc8c34
+        log.fatal('Failed to configure password policy: ' + str(e))
dc8c34
+        assert False
dc8c34
+
dc8c34
+    try:
dc8c34
+        topology.standalone.add_s(Entry((USER_DN, {
dc8c34
+                                  'objectclass': ['top', 'extensibleObject'],
dc8c34
+                                  'sn': 'user',
dc8c34
+                                  'cn': 'test user',
dc8c34
+                                  'uid': 'testuser',
dc8c34
+                                  'userpassword': 'password'})))
dc8c34
+    except ldap.LDAPError as e:
dc8c34
+        log.fatal('Failed to add test user' + USER_DN + ': error ' + str(e))
dc8c34
+        assert False
dc8c34
+
dc8c34
+    #
dc8c34
+    # Test that password history is enforced.
dc8c34
+    #
dc8c34
+    try:
dc8c34
+        topology.standalone.simple_bind_s(USER_DN, 'password')
dc8c34
+    except ldap.LDAPError as e:
dc8c34
+        log.fatal('Failed to bind as user: ' + str(e))
dc8c34
+        assert False
dc8c34
+
dc8c34
+    # Attempt to change password to the same password
dc8c34
+    try:
dc8c34
+        topology.standalone.modify_s(USER_DN, [(ldap.MOD_REPLACE,
dc8c34
+                                                'userpassword', 'password')])
dc8c34
+        log.info('Incorrectly able to to set password to existing password.')
dc8c34
+        assert False
dc8c34
+    except ldap.CONSTRAINT_VIOLATION:
dc8c34
+        log.info('Password change correctly rejected')
dc8c34
+    except ldap.LDAPError as e:
dc8c34
+        log.fatal('Failed to attempt to change password: ' + str(e))
dc8c34
+        assert False
dc8c34
+
dc8c34
+    #
dc8c34
+    # Keep changing password until we fill the password history (3)
dc8c34
+    #
dc8c34
+
dc8c34
+    # password1
dc8c34
+    try:
dc8c34
+        topology.standalone.modify_s(USER_DN, [(ldap.MOD_REPLACE,
dc8c34
+                                                'userpassword', 'password1')])
dc8c34
+    except ldap.LDAPError as e:
dc8c34
+        log.fatal('Failed to change password: ' + str(e))
dc8c34
+        assert False
dc8c34
+    try:
dc8c34
+        topology.standalone.simple_bind_s(USER_DN, 'password1')
dc8c34
+    except ldap.LDAPError as e:
dc8c34
+        log.fatal('Failed to bind as user using "password1": ' + str(e))
dc8c34
+        assert False
dc8c34
+
dc8c34
+    # password2
dc8c34
+    try:
dc8c34
+        topology.standalone.modify_s(USER_DN, [(ldap.MOD_REPLACE,
dc8c34
+                                                'userpassword', 'password2')])
dc8c34
+    except ldap.LDAPError as e:
dc8c34
+        log.fatal('Failed to change password: ' + str(e))
dc8c34
+        assert False
dc8c34
+    try:
dc8c34
+        topology.standalone.simple_bind_s(USER_DN, 'password2')
dc8c34
+    except ldap.LDAPError as e:
dc8c34
+        log.fatal('Failed to bind as user using "password2": ' + str(e))
dc8c34
+        assert False
dc8c34
+
dc8c34
+    # password3
dc8c34
+    try:
dc8c34
+        topology.standalone.modify_s(USER_DN, [(ldap.MOD_REPLACE,
dc8c34
+                                                'userpassword', 'password3')])
dc8c34
+    except ldap.LDAPError as e:
dc8c34
+        log.fatal('Failed to change password: ' + str(e))
dc8c34
+        assert False
dc8c34
+    try:
dc8c34
+        topology.standalone.simple_bind_s(USER_DN, 'password3')
dc8c34
+    except ldap.LDAPError as e:
dc8c34
+        log.fatal('Failed to bind as user using "password3": ' + str(e))
dc8c34
+        assert False
dc8c34
+
dc8c34
+    # password4
dc8c34
+    try:
dc8c34
+        topology.standalone.modify_s(USER_DN, [(ldap.MOD_REPLACE,
dc8c34
+                                                'userpassword', 'password4')])
dc8c34
+    except ldap.LDAPError as e:
dc8c34
+        log.fatal('Failed to change password: ' + str(e))
dc8c34
+        assert False
dc8c34
+    try:
dc8c34
+        topology.standalone.simple_bind_s(USER_DN, 'password4')
dc8c34
+    except ldap.LDAPError as e:
dc8c34
+        log.fatal('Failed to bind as user using "password4": ' + str(e))
dc8c34
+        assert False
dc8c34
+
dc8c34
+    #
dc8c34
+    # Check that we only have 3 passwords stored in history\
dc8c34
+    #
dc8c34
+    try:
dc8c34
+        entry = topology.standalone.search_s(USER_DN, ldap.SCOPE_BASE,
dc8c34
+                                             'objectclass=*',
dc8c34
+                                             ['passwordHistory'])
dc8c34
+        pwds = entry[0].getValues('passwordHistory')
dc8c34
+        if len(pwds) != 3:
dc8c34
+            log.fatal('Incorrect number of passwords stored in histry: %d' %
dc8c34
+                      len(pwds))
dc8c34
+            assert False
dc8c34
+        else:
dc8c34
+            log.info('Correct number of passwords found in history.')
dc8c34
+    except ldap.LDAPError as e:
dc8c34
+        log.fatal('Failed to get user entry: ' + str(e))
dc8c34
+        assert False
dc8c34
+
dc8c34
+    #
dc8c34
+    # Attempt to change the password to previous passwords
dc8c34
+    #
dc8c34
+    try:
dc8c34
+        topology.standalone.modify_s(USER_DN, [(ldap.MOD_REPLACE,
dc8c34
+                                                'userpassword', 'password1')])
dc8c34
+        log.info('Incorrectly able to to set password to previous password1.')
dc8c34
+        assert False
dc8c34
+    except ldap.CONSTRAINT_VIOLATION:
dc8c34
+        log.info('Password change correctly rejected')
dc8c34
+    except ldap.LDAPError as e:
dc8c34
+        log.fatal('Failed to attempt to change password: ' + str(e))
dc8c34
+        assert False
dc8c34
+
dc8c34
+    try:
dc8c34
+        topology.standalone.modify_s(USER_DN, [(ldap.MOD_REPLACE,
dc8c34
+                                                'userpassword', 'password2')])
dc8c34
+        log.info('Incorrectly able to to set password to previous password2.')
dc8c34
+        assert False
dc8c34
+    except ldap.CONSTRAINT_VIOLATION:
dc8c34
+        log.info('Password change correctly rejected')
dc8c34
+    except ldap.LDAPError as e:
dc8c34
+        log.fatal('Failed to attempt to change password: ' + str(e))
dc8c34
+        assert False
dc8c34
+    try:
dc8c34
+        topology.standalone.modify_s(USER_DN, [(ldap.MOD_REPLACE,
dc8c34
+                                                'userpassword', 'password3')])
dc8c34
+        log.info('Incorrectly able to to set password to previous password3.')
dc8c34
+        assert False
dc8c34
+    except ldap.CONSTRAINT_VIOLATION:
dc8c34
+        log.info('Password change correctly rejected')
dc8c34
+    except ldap.LDAPError as e:
dc8c34
+        log.fatal('Failed to attempt to change password: ' + str(e))
dc8c34
+        assert False
dc8c34
+
dc8c34
+    #
dc8c34
+    # Reset password by Directory Manager(admin reset)
dc8c34
+    #
dc8c34
+    try:
dc8c34
+        topology.standalone.simple_bind_s(DN_DM, PASSWORD)
dc8c34
+    except ldap.LDAPError as e:
dc8c34
+        log.fatal('Failed to bind as rootDN: ' + str(e))
dc8c34
+        assert False
dc8c34
+
dc8c34
+    try:
dc8c34
+        topology.standalone.modify_s(USER_DN, [(ldap.MOD_REPLACE,
dc8c34
+                                                'userpassword',
dc8c34
+                                                'password-reset')])
dc8c34
+    except ldap.LDAPError as e:
dc8c34
+        log.fatal('Failed to attempt to reset password: ' + str(e))
dc8c34
+        assert False
dc8c34
+
dc8c34
+    # Try and change the password to the previous password before the reset
dc8c34
+    try:
dc8c34
+        topology.standalone.simple_bind_s(USER_DN, 'password-reset')
dc8c34
+    except ldap.LDAPError as e:
dc8c34
+        log.fatal('Failed to bind as user: ' + str(e))
dc8c34
+        assert False
dc8c34
+
dc8c34
+    try:
dc8c34
+        topology.standalone.modify_s(USER_DN, [(ldap.MOD_REPLACE,
dc8c34
+                                                'userpassword', 'password4')])
dc8c34
+        log.info('Incorrectly able to to set password to previous password4.')
dc8c34
+        assert False
dc8c34
+    except ldap.CONSTRAINT_VIOLATION:
dc8c34
+        log.info('Password change correctly rejected')
dc8c34
+    except ldap.LDAPError as e:
dc8c34
+        log.fatal('Failed to attempt to change password: ' + str(e))
dc8c34
+        assert False
dc8c34
+
dc8c34
+    log.info('Test suite PASSED.')
dc8c34
+
dc8c34
+
dc8c34
+if __name__ == '__main__':
dc8c34
+    # Run isolated
dc8c34
+    # -s for DEBUG mode
dc8c34
+    CURRENT_FILE = os.path.realpath(__file__)
dc8c34
+    pytest.main("-s %s" % CURRENT_FILE)
dc8c34
diff --git a/ldap/servers/slapd/modify.c b/ldap/servers/slapd/modify.c
dc8c34
index c67ef14..4bcc827 100644
dc8c34
--- a/ldap/servers/slapd/modify.c
dc8c34
+++ b/ldap/servers/slapd/modify.c
dc8c34
@@ -1259,6 +1259,10 @@ static int op_shared_allow_pw_change (Slapi_PBlock *pb, LDAPMod *mod, char **old
dc8c34
 		 * just return success.
dc8c34
 		 */
dc8c34
 		if(pw_is_pwp_admin(pb, pwpolicy)){
dc8c34
+			if (!SLAPI_IS_MOD_DELETE(mod->mod_op) && pwpolicy->pw_history){
dc8c34
+				/* Updating pw history, get the old password */
dc8c34
+				get_old_pw(pb, &sdn, old_pw);
dc8c34
+			}
dc8c34
 			rc = 1;
dc8c34
 			goto done;
dc8c34
 		}
dc8c34
diff --git a/ldap/servers/slapd/proto-slap.h b/ldap/servers/slapd/proto-slap.h
dc8c34
index decc29e..3b00c80 100644
dc8c34
--- a/ldap/servers/slapd/proto-slap.h
dc8c34
+++ b/ldap/servers/slapd/proto-slap.h
dc8c34
@@ -906,6 +906,7 @@ int check_pw_syntax( Slapi_PBlock *pb, const Slapi_DN *sdn, Slapi_Value **vals,
dc8c34
 	char **old_pw, Slapi_Entry *e, int mod_op );
dc8c34
 int check_pw_syntax_ext( Slapi_PBlock *pb, const Slapi_DN *sdn, Slapi_Value **vals,
dc8c34
 	char **old_pw, Slapi_Entry *e, int mod_op, Slapi_Mods *smods );
dc8c34
+void get_old_pw( Slapi_PBlock *pb, const Slapi_DN *sdn, char **old_pw);
dc8c34
 int check_account_lock( Slapi_PBlock *pb, Slapi_Entry * bind_target_entry, int pwresponse_req, int account_inactivation_only /*no wire/no pw policy*/);
dc8c34
 int check_pw_minage( Slapi_PBlock *pb, const Slapi_DN *sdn, struct berval **vals) ;
dc8c34
 void add_password_attrs( Slapi_PBlock *pb, Operation *op, Slapi_Entry *e );
dc8c34
diff --git a/ldap/servers/slapd/pw.c b/ldap/servers/slapd/pw.c
dc8c34
index dea949c..ab624e7 100644
dc8c34
--- a/ldap/servers/slapd/pw.c
dc8c34
+++ b/ldap/servers/slapd/pw.c
dc8c34
@@ -1084,6 +1084,35 @@ retry:
dc8c34
 }
dc8c34
 
dc8c34
 /*
dc8c34
+ * Get the old password -used by password admin so we properly
dc8c34
+ * update pw history when reseting a password.
dc8c34
+ */
dc8c34
+void
dc8c34
+get_old_pw( Slapi_PBlock *pb, const Slapi_DN *sdn, char **old_pw )
dc8c34
+{
dc8c34
+    Slapi_Entry *e = NULL;
dc8c34
+    Slapi_Value **va = NULL;
dc8c34
+    Slapi_Attr *attr = NULL;
dc8c34
+    char *dn = (char*)slapi_sdn_get_ndn(sdn);
dc8c34
+
dc8c34
+    e = get_entry ( pb, dn );
dc8c34
+    if ( e == NULL ) {
dc8c34
+        return;
dc8c34
+    }
dc8c34
+
dc8c34
+    /* get current password, and remember it  */
dc8c34
+    attr = attrlist_find(e->e_attrs, "userpassword");
dc8c34
+    if ( attr && !valueset_isempty(&attr->a_present_values) ) {
dc8c34
+        va = valueset_get_valuearray(&attr->a_present_values);
dc8c34
+        *old_pw = slapi_ch_strdup(slapi_value_get_string(va[0]));
dc8c34
+    } else {
dc8c34
+        *old_pw = NULL;
dc8c34
+    }
dc8c34
+
dc8c34
+    slapi_entry_free(e);
dc8c34
+}
dc8c34
+
dc8c34
+/*
dc8c34
  * Basically, h0 and h1 must be longer than GENERALIZED_TIME_LENGTH.
dc8c34
  */
dc8c34
 static int
dc8c34
-- 
dc8c34
2.4.11
dc8c34