Blob Blame Raw
From 6a741b3ef50babf2ac2479437a38829204ffd438 Mon Sep 17 00:00:00 2001
From: tbordaz <tbordaz@redhat.com>
Date: Thu, 17 Jun 2021 16:22:09 +0200
Subject: [PATCH] Issue 4788 - CLI should support Temporary Password Rules
 attributes (#4793)

Bug description:
    Since #4725, password policy support temporary password rules.
    CLI (dsconf) does not support this RFE and only direct ldap
    operation can configure global/local password policy

Fix description:
    Update dsconf to support this new RFE.
    To run successfully the testcase it relies on #4788

relates: #4788

Reviewed by: Simon Pichugin (thanks !!)

Platforms tested: F34
---
 .../password/pwdPolicy_attribute_test.py      | 172 ++++++++++++++++--
 src/lib389/lib389/cli_conf/pwpolicy.py        |   5 +-
 src/lib389/lib389/pwpolicy.py                 |   5 +-
 3 files changed, 165 insertions(+), 17 deletions(-)

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