6d0b66
From 6a741b3ef50babf2ac2479437a38829204ffd438 Mon Sep 17 00:00:00 2001
6d0b66
From: tbordaz <tbordaz@redhat.com>
6d0b66
Date: Thu, 17 Jun 2021 16:22:09 +0200
6d0b66
Subject: [PATCH] Issue 4788 - CLI should support Temporary Password Rules
6d0b66
 attributes (#4793)
6d0b66
6d0b66
Bug description:
6d0b66
    Since #4725, password policy support temporary password rules.
6d0b66
    CLI (dsconf) does not support this RFE and only direct ldap
6d0b66
    operation can configure global/local password policy
6d0b66
6d0b66
Fix description:
6d0b66
    Update dsconf to support this new RFE.
6d0b66
    To run successfully the testcase it relies on #4788
6d0b66
6d0b66
relates: #4788
6d0b66
6d0b66
Reviewed by: Simon Pichugin (thanks !!)
6d0b66
6d0b66
Platforms tested: F34
6d0b66
---
6d0b66
 .../password/pwdPolicy_attribute_test.py      | 172 ++++++++++++++++--
6d0b66
 src/lib389/lib389/cli_conf/pwpolicy.py        |   5 +-
6d0b66
 src/lib389/lib389/pwpolicy.py                 |   5 +-
6d0b66
 3 files changed, 165 insertions(+), 17 deletions(-)
6d0b66
6d0b66
diff --git a/dirsrvtests/tests/suites/password/pwdPolicy_attribute_test.py b/dirsrvtests/tests/suites/password/pwdPolicy_attribute_test.py
6d0b66
index aee3a91ad..085d0a373 100644
6d0b66
--- a/dirsrvtests/tests/suites/password/pwdPolicy_attribute_test.py
6d0b66
+++ b/dirsrvtests/tests/suites/password/pwdPolicy_attribute_test.py
6d0b66
@@ -34,7 +34,7 @@ log = logging.getLogger(__name__)
6d0b66
 
6d0b66
 
6d0b66
 @pytest.fixture(scope="module")
6d0b66
-def create_user(topology_st, request):
6d0b66
+def test_user(topology_st, request):
6d0b66
     """User for binding operation"""
6d0b66
     topology_st.standalone.config.set('nsslapd-auditlog-logging-enabled', 'on')
6d0b66
     log.info('Adding test user {}')
6d0b66
@@ -56,10 +56,11 @@ def create_user(topology_st, request):
6d0b66
         topology_st.standalone.simple_bind_s(DN_DM, PASSWORD)
6d0b66
 
6d0b66
     request.addfinalizer(fin)
6d0b66
+    return user
6d0b66
 
6d0b66
 
6d0b66
 @pytest.fixture(scope="module")
6d0b66
-def password_policy(topology_st, create_user):
6d0b66
+def password_policy(topology_st, test_user):
6d0b66
     """Set up password policy for subtree and user"""
6d0b66
 
6d0b66
     pwp = PwPolicyManager(topology_st.standalone)
6d0b66
@@ -71,7 +72,7 @@ def password_policy(topology_st, create_user):
6d0b66
     pwp.create_user_policy(TEST_USER_DN, policy_props)
6d0b66
 
6d0b66
 @pytest.mark.skipif(ds_is_older('1.4.3.3'), reason="Not implemented")
6d0b66
-def test_pwd_reset(topology_st, create_user):
6d0b66
+def test_pwd_reset(topology_st, test_user):
6d0b66
     """Test new password policy attribute "pwdReset"
6d0b66
 
6d0b66
     :id: 03db357b-4800-411e-a36e-28a534293004
6d0b66
@@ -124,7 +125,7 @@ def test_pwd_reset(topology_st, create_user):
6d0b66
                          [('on', 'off', ldap.UNWILLING_TO_PERFORM),
6d0b66
                           ('off', 'off', ldap.UNWILLING_TO_PERFORM),
6d0b66
                           ('off', 'on', False), ('on', 'on', False)])
6d0b66
-def test_change_pwd(topology_st, create_user, password_policy,
6d0b66
+def test_change_pwd(topology_st, test_user, password_policy,
6d0b66
                     subtree_pwchange, user_pwchange, exception):
6d0b66
     """Verify that 'passwordChange' attr works as expected
6d0b66
     User should have a priority over a subtree.
6d0b66
@@ -184,7 +185,7 @@ def test_change_pwd(topology_st, create_user, password_policy,
6d0b66
         user.reset_password(TEST_USER_PWD)
6d0b66
 
6d0b66
 
6d0b66
-def test_pwd_min_age(topology_st, create_user, password_policy):
6d0b66
+def test_pwd_min_age(topology_st, test_user, password_policy):
6d0b66
     """If we set passwordMinAge to some value, for example to 10, then it
6d0b66
     should not allow the user to change the password within 10 seconds after
6d0b66
     his previous change.
6d0b66
@@ -257,7 +258,7 @@ def test_pwd_min_age(topology_st, create_user, password_policy):
6d0b66
         topology_st.standalone.simple_bind_s(DN_DM, PASSWORD)
6d0b66
         user.reset_password(TEST_USER_PWD)
6d0b66
 
6d0b66
-def test_global_tpr_maxuse_1(topology_st, create_user, request):
6d0b66
+def test_global_tpr_maxuse_1(topology_st, test_user, request):
6d0b66
     """Test global TPR policy : passwordTPRMaxUse
6d0b66
     Test that after passwordTPRMaxUse failures to bind
6d0b66
     additional bind with valid password are failing with CONSTRAINT_VIOLATION
6d0b66
@@ -374,7 +375,7 @@ def test_global_tpr_maxuse_1(topology_st, create_user, request):
6d0b66
 
6d0b66
     request.addfinalizer(fin)
6d0b66
 
6d0b66
-def test_global_tpr_maxuse_2(topology_st, create_user, request):
6d0b66
+def test_global_tpr_maxuse_2(topology_st, test_user, request):
6d0b66
     """Test global TPR policy : passwordTPRMaxUse
6d0b66
     Test that after less than passwordTPRMaxUse failures to bind
6d0b66
     additional bind with valid password are successfull
6d0b66
@@ -474,7 +475,7 @@ def test_global_tpr_maxuse_2(topology_st, create_user, request):
6d0b66
 
6d0b66
     request.addfinalizer(fin)
6d0b66
 
6d0b66
-def test_global_tpr_maxuse_3(topology_st, create_user, request):
6d0b66
+def test_global_tpr_maxuse_3(topology_st, test_user, request):
6d0b66
     """Test global TPR policy : passwordTPRMaxUse
6d0b66
     Test that after less than passwordTPRMaxUse failures to bind
6d0b66
     A bind with valid password is successfull but passwordMustChange
6d0b66
@@ -587,7 +588,7 @@ def test_global_tpr_maxuse_3(topology_st, create_user, request):
6d0b66
 
6d0b66
     request.addfinalizer(fin)
6d0b66
 
6d0b66
-def test_global_tpr_maxuse_4(topology_st, create_user, request):
6d0b66
+def test_global_tpr_maxuse_4(topology_st, test_user, request):
6d0b66
     """Test global TPR policy : passwordTPRMaxUse
6d0b66
     Test that a TPR attribute passwordTPRMaxUse
6d0b66
     can be updated by DM but not the by user itself
6d0b66
@@ -701,7 +702,148 @@ def test_global_tpr_maxuse_4(topology_st, create_user, request):
6d0b66
 
6d0b66
     request.addfinalizer(fin)
6d0b66
 
6d0b66
-def test_global_tpr_delayValidFrom_1(topology_st, create_user, request):
6d0b66
+def test_local_tpr_maxuse_5(topology_st, test_user, request):
6d0b66
+    """Test TPR local policy overpass global one: passwordTPRMaxUse
6d0b66
+    Test that after passwordTPRMaxUse failures to bind
6d0b66
+    additional bind with valid password are failing with CONSTRAINT_VIOLATION
6d0b66
+
6d0b66
+    :id: c3919707-d804-445a-8754-8385b1072c42
6d0b66
+    :customerscenario: False
6d0b66
+    :setup: Standalone instance
6d0b66
+    :steps:
6d0b66
+        1. Global password policy Enable passwordMustChange
6d0b66
+        2. Global password policy Set passwordTPRMaxUse=5
6d0b66
+        3. Global password policy Set passwordMaxFailure to a higher value to not disturb the test
6d0b66
+        4. Local password policy Enable passwordMustChange
6d0b66
+        5. Local password policy Set passwordTPRMaxUse=10 (higher than global)
6d0b66
+        6. Bind with a wrong password 10 times and check INVALID_CREDENTIALS
6d0b66
+        7. Check that passwordTPRUseCount got to the limit (5)
6d0b66
+        8. Bind with a wrong password (CONSTRAINT_VIOLATION)
6d0b66
+           and check passwordTPRUseCount overpass the limit by 1 (11)
6d0b66
+        9. Bind with a valid password 10 times and check CONSTRAINT_VIOLATION
6d0b66
+           and check passwordTPRUseCount increases
6d0b66
+        10. Reset password policy configuration and remove local password from user
6d0b66
+    :expected results:
6d0b66
+        1. Success
6d0b66
+        2. Success
6d0b66
+        3. Success
6d0b66
+        4. Success
6d0b66
+        5. Success
6d0b66
+        6. Success
6d0b66
+        7. Success
6d0b66
+        8. Success
6d0b66
+        9. Success
6d0b66
+        10. Success
6d0b66
+    """
6d0b66
+
6d0b66
+    global_tpr_maxuse = 5
6d0b66
+    # Set global password policy config, passwordMaxFailure being higher than
6d0b66
+    # passwordTPRMaxUse so that TPR is enforced first
6d0b66
+    topology_st.standalone.config.replace('passwordMustChange', 'on')
6d0b66
+    topology_st.standalone.config.replace('passwordMaxFailure', str(global_tpr_maxuse + 20))
6d0b66
+    topology_st.standalone.config.replace('passwordTPRMaxUse', str(global_tpr_maxuse))
6d0b66
+    time.sleep(.5)
6d0b66
+
6d0b66
+    local_tpr_maxuse = global_tpr_maxuse + 5
6d0b66
+    # Reset user's password with a local password policy
6d0b66
+    # that has passwordTPRMaxUse higher than global
6d0b66
+    #our_user = UserAccount(topology_st.standalone, TEST_USER_DN)
6d0b66
+    subprocess.call(['%s/dsconf' % topology_st.standalone.get_sbin_dir(),
6d0b66
+                     'slapd-standalone1',
6d0b66
+                     'localpwp',
6d0b66
+                     'adduser',
6d0b66
+                     test_user.dn])
6d0b66
+    subprocess.call(['%s/dsconf' % topology_st.standalone.get_sbin_dir(),
6d0b66
+                     'slapd-standalone1',
6d0b66
+                     'localpwp',
6d0b66
+                     'set',
6d0b66
+                     '--pwptprmaxuse',
6d0b66
+                     str(local_tpr_maxuse),
6d0b66
+                     '--pwdmustchange',
6d0b66
+                     'on',
6d0b66
+                     test_user.dn])
6d0b66
+    test_user.replace('userpassword', PASSWORD)
6d0b66
+    time.sleep(.5)
6d0b66
+
6d0b66
+    # look up to passwordTPRMaxUse with failing
6d0b66
+    # bind to check that the limits of TPR are enforced
6d0b66
+    for i in range(local_tpr_maxuse):
6d0b66
+        # Bind as user with a wrong password
6d0b66
+        with pytest.raises(ldap.INVALID_CREDENTIALS):
6d0b66
+            test_user.rebind('wrong password')
6d0b66
+        time.sleep(.5)
6d0b66
+
6d0b66
+        # Check that pwdReset is TRUE
6d0b66
+        topology_st.standalone.simple_bind_s(DN_DM, PASSWORD)
6d0b66
+        #assert test_user.get_attr_val_utf8('pwdReset') == 'TRUE'
6d0b66
+
6d0b66
+        # Check that pwdTPRReset is TRUE
6d0b66
+        assert test_user.get_attr_val_utf8('pwdTPRReset') == 'TRUE'
6d0b66
+        assert test_user.get_attr_val_utf8('pwdTPRUseCount') == str(i+1)
6d0b66
+        log.info("%dth failing bind (INVALID_CREDENTIALS) => pwdTPRUseCount = %d" % (i+1, i+1))
6d0b66
+
6d0b66
+
6d0b66
+    # Now the #failures reached passwordTPRMaxUse
6d0b66
+    # Check that pwdReset is TRUE
6d0b66
+    topology_st.standalone.simple_bind_s(DN_DM, PASSWORD)
6d0b66
+
6d0b66
+    # Check that pwdTPRReset is TRUE
6d0b66
+    assert test_user.get_attr_val_utf8('pwdTPRReset') == 'TRUE'
6d0b66
+    assert test_user.get_attr_val_utf8('pwdTPRUseCount') == str(local_tpr_maxuse)
6d0b66
+    log.info("last failing bind (INVALID_CREDENTIALS) => pwdTPRUseCount = %d" % (local_tpr_maxuse))
6d0b66
+
6d0b66
+    # Bind as user with wrong password --> ldap.CONSTRAINT_VIOLATION
6d0b66
+    with pytest.raises(ldap.CONSTRAINT_VIOLATION):
6d0b66
+        test_user.rebind("wrong password")
6d0b66
+    time.sleep(.5)
6d0b66
+
6d0b66
+    # Check that pwdReset is TRUE
6d0b66
+    topology_st.standalone.simple_bind_s(DN_DM, PASSWORD)
6d0b66
+
6d0b66
+    # Check that pwdTPRReset is TRUE
6d0b66
+    assert test_user.get_attr_val_utf8('pwdTPRReset') == 'TRUE'
6d0b66
+    assert test_user.get_attr_val_utf8('pwdTPRUseCount') == str(local_tpr_maxuse + 1)
6d0b66
+    log.info("failing bind (CONSTRAINT_VIOLATION) => pwdTPRUseCount = %d" % (local_tpr_maxuse + i))
6d0b66
+
6d0b66
+    # Now check that all next attempts with correct password are all in LDAP_CONSTRAINT_VIOLATION
6d0b66
+    # and passwordTPRRetryCount remains unchanged
6d0b66
+    # account is now similar to locked
6d0b66
+    for i in range(10):
6d0b66
+        # Bind as user with valid password
6d0b66
+        with pytest.raises(ldap.CONSTRAINT_VIOLATION):
6d0b66
+            test_user.rebind(PASSWORD)
6d0b66
+        time.sleep(.5)
6d0b66
+
6d0b66
+        # Check that pwdReset is TRUE
6d0b66
+        topology_st.standalone.simple_bind_s(DN_DM, PASSWORD)
6d0b66
+
6d0b66
+        # Check that pwdTPRReset is TRUE
6d0b66
+        # pwdTPRUseCount keeps increasing
6d0b66
+        assert test_user.get_attr_val_utf8('pwdTPRReset') == 'TRUE'
6d0b66
+        assert test_user.get_attr_val_utf8('pwdTPRUseCount') == str(local_tpr_maxuse + i + 2)
6d0b66
+        log.info("Rejected bind (CONSTRAINT_VIOLATION) => pwdTPRUseCount = %d" % (local_tpr_maxuse + i + 2))
6d0b66
+
6d0b66
+
6d0b66
+    def fin():
6d0b66
+        topology_st.standalone.restart()
6d0b66
+        # Reset password policy config
6d0b66
+        topology_st.standalone.simple_bind_s(DN_DM, PASSWORD)
6d0b66
+        topology_st.standalone.config.replace('passwordMustChange', 'off')
6d0b66
+
6d0b66
+        # Remove local password policy from that entry
6d0b66
+        subprocess.call(['%s/dsconf' % topology_st.standalone.get_sbin_dir(),
6d0b66
+                        'slapd-standalone1',
6d0b66
+                        'localpwp',
6d0b66
+                        'remove',
6d0b66
+                        test_user.dn])
6d0b66
+
6d0b66
+        # Reset user's password
6d0b66
+        test_user.replace('userpassword', TEST_USER_PWD)
6d0b66
+
6d0b66
+
6d0b66
+    request.addfinalizer(fin)
6d0b66
+
6d0b66
+def test_global_tpr_delayValidFrom_1(topology_st, test_user, request):
6d0b66
     """Test global TPR policy : passwordTPRDelayValidFrom
6d0b66
     Test that a TPR password is not valid before reset time +
6d0b66
     passwordTPRDelayValidFrom
6d0b66
@@ -766,7 +908,7 @@ def test_global_tpr_delayValidFrom_1(topology_st, create_user, request):
6d0b66
 
6d0b66
     request.addfinalizer(fin)
6d0b66
 
6d0b66
-def test_global_tpr_delayValidFrom_2(topology_st, create_user, request):
6d0b66
+def test_global_tpr_delayValidFrom_2(topology_st, test_user, request):
6d0b66
     """Test global TPR policy : passwordTPRDelayValidFrom
6d0b66
     Test that a TPR password is valid after reset time +
6d0b66
     passwordTPRDelayValidFrom
6d0b66
@@ -838,7 +980,7 @@ def test_global_tpr_delayValidFrom_2(topology_st, create_user, request):
6d0b66
 
6d0b66
     request.addfinalizer(fin)
6d0b66
 
6d0b66
-def test_global_tpr_delayValidFrom_3(topology_st, create_user, request):
6d0b66
+def test_global_tpr_delayValidFrom_3(topology_st, test_user, request):
6d0b66
     """Test global TPR policy : passwordTPRDelayValidFrom
6d0b66
     Test that a TPR attribute passwordTPRDelayValidFrom
6d0b66
     can be updated by DM but not the by user itself
6d0b66
@@ -940,7 +1082,7 @@ def test_global_tpr_delayValidFrom_3(topology_st, create_user, request):
6d0b66
 
6d0b66
     request.addfinalizer(fin)
6d0b66
 
6d0b66
-def test_global_tpr_delayExpireAt_1(topology_st, create_user, request):
6d0b66
+def test_global_tpr_delayExpireAt_1(topology_st, test_user, request):
6d0b66
     """Test global TPR policy : passwordTPRDelayExpireAt
6d0b66
     Test that a TPR password is not valid after reset time +
6d0b66
     passwordTPRDelayExpireAt
6d0b66
@@ -1010,7 +1152,7 @@ def test_global_tpr_delayExpireAt_1(topology_st, create_user, request):
6d0b66
 
6d0b66
     request.addfinalizer(fin)
6d0b66
 
6d0b66
-def test_global_tpr_delayExpireAt_2(topology_st, create_user, request):
6d0b66
+def test_global_tpr_delayExpireAt_2(topology_st, test_user, request):
6d0b66
     """Test global TPR policy : passwordTPRDelayExpireAt
6d0b66
     Test that a TPR password is valid before reset time +
6d0b66
     passwordTPRDelayExpireAt
6d0b66
@@ -1082,7 +1224,7 @@ def test_global_tpr_delayExpireAt_2(topology_st, create_user, request):
6d0b66
 
6d0b66
     request.addfinalizer(fin)
6d0b66
 
6d0b66
-def test_global_tpr_delayExpireAt_3(topology_st, create_user, request):
6d0b66
+def test_global_tpr_delayExpireAt_3(topology_st, test_user, request):
6d0b66
     """Test global TPR policy : passwordTPRDelayExpireAt
6d0b66
     Test that a TPR attribute passwordTPRDelayExpireAt
6d0b66
     can be updated by DM but not the by user itself
6d0b66
diff --git a/src/lib389/lib389/cli_conf/pwpolicy.py b/src/lib389/lib389/cli_conf/pwpolicy.py
6d0b66
index 2838afcb8..26af6e7ec 100644
6d0b66
--- a/src/lib389/lib389/cli_conf/pwpolicy.py
6d0b66
+++ b/src/lib389/lib389/cli_conf/pwpolicy.py
6d0b66
@@ -255,6 +255,9 @@ def create_parser(subparsers):
6d0b66
     set_parser.add_argument('--pwpinheritglobal', help="Set to \"on\" to allow local policies to inherit the global policy")
6d0b66
     set_parser.add_argument('--pwddictcheck', help="Set to \"on\" to enforce CrackLib dictionary checking")
6d0b66
     set_parser.add_argument('--pwddictpath', help="Filesystem path to specific/custom CrackLib dictionary files")
6d0b66
+    set_parser.add_argument('--pwptprmaxuse', help="Number of times a reset password can be used for authentication")
6d0b66
+    set_parser.add_argument('--pwptprdelayexpireat', help="Number of seconds after which a reset password expires")
6d0b66
+    set_parser.add_argument('--pwptprdelayvalidfrom', help="Number of seconds to wait before using a reset password to authenticated")
6d0b66
     # delete local password policy
6d0b66
     del_parser = local_subcommands.add_parser('remove', help='Remove a local password policy')
6d0b66
     del_parser.set_defaults(func=del_local_policy)
6d0b66
@@ -291,4 +294,4 @@ def create_parser(subparsers):
6d0b66
     #############################################
6d0b66
     set_parser.add_argument('DN', nargs=1, help='Set the local policy for this entry DN')
6d0b66
     add_subtree_parser.add_argument('DN', nargs=1, help='Add/replace the subtree policy for this entry DN')
6d0b66
-    add_user_parser.add_argument('DN', nargs=1, help='Add/replace the local password policy for this entry DN')
6d0b66
\ No newline at end of file
6d0b66
+    add_user_parser.add_argument('DN', nargs=1, help='Add/replace the local password policy for this entry DN')
6d0b66
diff --git a/src/lib389/lib389/pwpolicy.py b/src/lib389/lib389/pwpolicy.py
6d0b66
index 8653cb195..d2427933b 100644
6d0b66
--- a/src/lib389/lib389/pwpolicy.py
6d0b66
+++ b/src/lib389/lib389/pwpolicy.py
6d0b66
@@ -65,7 +65,10 @@ class PwPolicyManager(object):
6d0b66
             'pwddictcheck': 'passworddictcheck',
6d0b66
             'pwddictpath': 'passworddictpath',
6d0b66
             'pwdallowhash': 'nsslapd-allow-hashed-passwords',
6d0b66
-            'pwpinheritglobal': 'nsslapd-pwpolicy-inherit-global'
6d0b66
+            'pwpinheritglobal': 'nsslapd-pwpolicy-inherit-global',
6d0b66
+            'pwptprmaxuse': 'passwordTPRMaxUse',
6d0b66
+            'pwptprdelayexpireat': 'passwordTPRDelayExpireAt',
6d0b66
+            'pwptprdelayvalidfrom': 'passwordTPRDelayValidFrom'
6d0b66
         }
6d0b66
 
6d0b66
     def is_subtree_policy(self, dn):
6d0b66
-- 
6d0b66
2.31.1
6d0b66