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