From a929745d11e62b4a0235a2d60bf07935690f1b80 Mon Sep 17 00:00:00 2001 From: Mark Reynolds Date: Tue, 4 Oct 2022 17:09:59 -0400 Subject: [PATCH] Issue 3061 - RFE - Add password policy debug log level Description: Added new logging level (1048576) for password policy debugging where the failure message, entry dn, and pwd policy is recorded (it's either the global policy or the DN of the local policy). relates: https://github.com/389ds/389-ds-base/issues/3061 Reviewed by: firstyear & spichugi(Thanks!!) --- dirsrvtests/create_test.py | 104 ++++++------ .../suites/password/pwdPolicy_logging_test.py | 152 ++++++++++++++++++ ldap/servers/slapd/log.c | 2 +- ldap/servers/slapd/modify.c | 4 + ldap/servers/slapd/proto-slap.h | 2 +- ldap/servers/slapd/pw.c | 99 ++++++++---- ldap/servers/slapd/pw_mgmt.c | 9 +- ldap/servers/slapd/pw_retry.c | 24 +-- ldap/servers/slapd/result.c | 3 + ldap/servers/slapd/slap.h | 3 + ldap/servers/slapd/slapi-plugin.h | 2 +- .../389-console/src/lib/server/errorLog.jsx | 2 + src/cockpit/389-console/src/security.jsx | 10 +- 13 files changed, 324 insertions(+), 92 deletions(-) create mode 100644 dirsrvtests/tests/suites/password/pwdPolicy_logging_test.py diff --git a/dirsrvtests/create_test.py b/dirsrvtests/create_test.py index 8bae9aa050..a36f2a60bc 100755 --- a/dirsrvtests/create_test.py +++ b/dirsrvtests/create_test.py @@ -1,13 +1,17 @@ #!/usr/bin/python3 # # --- BEGIN COPYRIGHT BLOCK --- -# Copyright (C) 2020 Red Hat, Inc. +# Copyright (C) 2022 Red Hat, Inc. # All rights reserved. # # License: GPL (version 3 or any later version). # See LICENSE for details. # --- END COPYRIGHT BLOCK --- +# +# PYTHON_ARGCOMPLETE_OK +import argparse, argcomplete +import argcomplete import optparse import os import re @@ -23,27 +27,27 @@ """ -def displayUsage(): +def display_usage(): """Display the usage""" - print ('\nUsage:\ncreate_ticket.py -t|--ticket ' + - '-s|--suite ' + - '[ i|--instances ' + - '[ -m|--suppliers -h|--hubs ' + - '-c|--consumers ] -o|--outputfile ]\n') - print ('If only "-t" is provided then a single standalone instance is ' + - 'created. Or you can create a test suite script using ' + - '"-s|--suite" instead of using "-t|--ticket". The "-i" option ' + - 'can add multiple standalone instances (maximum 99). However, you' + - ' can not mix "-i" with the replication options (-m, -h , -c). ' + - 'There is a maximum of 99 suppliers, 99 hubs, and 99 consumers.') + print('\nUsage:\ncreate_ticket.py -t|--ticket ' + + '-s|--suite ' + + '[ i|--instances ' + + '[ -m|--suppliers -h|--hubs ' + + '-c|--consumers ] -o|--outputfile ]\n') + print('If only "-t" is provided then a single standalone instance is ' + + 'created. Or you can create a test suite script using ' + + '"-s|--suite" instead of using "-t|--ticket". The "-i" option ' + + 'can add multiple standalone instances (maximum 99). However, you' + + ' can not mix "-i" with the replication options (-m, -h , -c). ' + + 'There is a maximum of 99 suppliers, 99 hubs, and 99 consumers.') print('If "-s|--suite" option was chosen, then no topology would be added ' + 'to the test script. You can find predefined fixtures in the lib389/topologies.py ' + 'and use them or write a new one if you have a special case.') exit(1) -def writeFinalizer(): +def write_finalizer(): """Write the finalizer function - delete/stop each instance""" def writeInstanceOp(action): @@ -119,7 +123,6 @@ def check_id_uniqueness(id_value): """ tests_dir = os.path.join(os.getcwd(), 'tests') - for root, dirs, files in os.walk(tests_dir): for name in files: if name.endswith('.py'): @@ -131,74 +134,83 @@ def check_id_uniqueness(id_value): return True +def display_uuid(): + tc_uuid = '0' + while not check_id_uniqueness(tc_uuid): tc_uuid = uuid.uuid4() + print(str(tc_uuid)) + exit(0) + + desc = 'Script to generate an initial lib389 test script. ' + \ 'This generates the topology, test, final, and run-isolated functions.' if len(sys.argv) > 0: - parser = optparse.OptionParser(description=desc, add_help_option=False) - - # Script options - parser.add_option('-t', '--ticket', dest='ticket', default=None) - parser.add_option('-s', '--suite', dest='suite', default=None) - parser.add_option('-i', '--instances', dest='inst', default='0') - parser.add_option('-m', '--suppliers', dest='suppliers', default='0') - parser.add_option('-h', '--hubs', dest='hubs', default='0') - parser.add_option('-c', '--consumers', dest='consumers', default='0') - parser.add_option('-o', '--outputfile', dest='filename', default=None) - - # Validate the options - try: - (args, opts) = parser.parse_args() - except: - displayUsage() + parser = argparse.ArgumentParser() + parser.add_argument('-t', '--ticket', default=None, + help="The name of the ticket/issue to include in the script name: 'ticket__test.py'") + parser.add_argument('-s', '--suite', default=None, help="Name for the test: '_test.py'") + parser.add_argument('-i', '--instances', default='0', help="Number of instances needed in the test") + parser.add_argument('-m', '--suppliers', default='0', + help="Number of replication suppliers needed in the test") + parser.add_argument('-b', '--hubs', default='0', help="Number of replication hubs needed in the test") + parser.add_argument('-c', '--consumers', default='0', + help="Number of replication consumers needed in the test") + parser.add_argument('-o', '--filename', default=None, help="Custom test script file name") + parser.add_argument('-u', '--uuid', action='store_true', + help="Display a test case uuid to used for new test functions in script") + argcomplete.autocomplete(parser) + args = parser.parse_args() + + if args.uuid: + display_uuid() if args.ticket is None and args.suite is None: print('Missing required ticket number/suite name') - displayUsage() + display_usage() if args.ticket and args.suite: print('You must choose either "-t|--ticket" or "-s|--suite", ' + 'but not both.') - displayUsage() + display_usage() if int(args.suppliers) == 0: if int(args.hubs) > 0 or int(args.consumers) > 0: print('You must use "-m|--suppliers" if you want to have hubs ' + 'and/or consumers') - displayUsage() + display_usage() if not args.suppliers.isdigit() or \ - int(args.suppliers) > 99 or \ - int(args.suppliers) < 0: + int(args.suppliers) > 99 or \ + int(args.suppliers) < 0: print('Invalid value for "--suppliers", it must be a number and it can' + ' not be greater than 99') - displayUsage() + display_usage() if not args.hubs.isdigit() or int(args.hubs) > 99 or int(args.hubs) < 0: print('Invalid value for "--hubs", it must be a number and it can ' + 'not be greater than 99') - displayUsage() + display_usage() if not args.consumers.isdigit() or \ - int(args.consumers) > 99 or \ - int(args.consumers) < 0: + int(args.consumers) > 99 or \ + int(args.consumers) < 0: print('Invalid value for "--consumers", it must be a number and it ' + 'can not be greater than 99') - displayUsage() + display_usage() if args.inst: if not args.inst.isdigit() or \ - int(args.inst) > 99 or \ - int(args.inst) < 0: + int(args.inst) > 99 or \ + int(args.inst) < 0: print('Invalid value for "--instances", it must be a number ' + 'greater than 0 and not greater than 99') - displayUsage() + display_usage() if int(args.inst) > 0: if int(args.suppliers) > 0 or \ int(args.hubs) > 0 or \ int(args.consumers) > 0: print('You can not mix "--instances" with replication.') - displayUsage() + display_usage() # Extract usable values ticket = args.ticket @@ -273,7 +285,7 @@ def check_id_uniqueness(id_value): TEST.write(' # replicas = Replicas(topology.ms["supplier1"])\n') TEST.write(' # replicas.test(DEFAULT_SUFFIX, topology.cs["consumer1"])\n') - writeFinalizer() + write_finalizer() TEST.write(' return topology\n\n') tc_id = '0' diff --git a/dirsrvtests/tests/suites/password/pwdPolicy_logging_test.py b/dirsrvtests/tests/suites/password/pwdPolicy_logging_test.py new file mode 100644 index 0000000000..6754fc0f3c --- /dev/null +++ b/dirsrvtests/tests/suites/password/pwdPolicy_logging_test.py @@ -0,0 +1,152 @@ +import ldap +import logging +import pytest +import os +import time +from lib389._constants import DEFAULT_SUFFIX, PASSWORD +from lib389.topologies import topology_st as topo +from lib389.pwpolicy import PwPolicyManager +from lib389.idm.organizationalunit import OrganizationalUnits +from lib389.idm.user import UserAccount, UserAccounts, TEST_USER_PROPERTIES +from lib389.idm.domain import Domain + +log = logging.getLogger(__name__) + +LOCAL_RDN = 'ou=People' +GLOBAL_RDN = 'ou=Global' +LOCAL_BIND_DN ="uid=local_user,ou=people," + DEFAULT_SUFFIX +GLOBAL_BIND_DN ="uid=global_user,ou=global," + DEFAULT_SUFFIX + + +def create_entries(inst): + # Create local user + users = UserAccounts(inst, DEFAULT_SUFFIX, rdn=LOCAL_RDN) + user_props = TEST_USER_PROPERTIES.copy() + user_props.update({'uid': 'local_user', 'cn': 'local_user', 'userpassword': PASSWORD}) + users.create(properties=user_props) + + # Create new OU + ou_global = OrganizationalUnits(inst, DEFAULT_SUFFIX).create(properties={'ou': 'Global'}) + + # Create global user + users = UserAccounts(inst, DEFAULT_SUFFIX, rdn=GLOBAL_RDN) + user_props = TEST_USER_PROPERTIES.copy() + user_props.update({'uid': 'global_user', 'cn': 'global_user', 'userpassword': PASSWORD}) + users.create(properties=user_props) + + # Add aci + aci = '(targetattr="*")(version 3.0; acl "pwp test"; allow (all) userdn="ldap:///self";)' + suffix = Domain(inst, DEFAULT_SUFFIX) + suffix.add('aci', aci) + + +def create_policies(inst): + # Configure subtree policy + pwp = PwPolicyManager(inst) + subtree_policy_props = { + 'passwordCheckSyntax': 'on', + 'passwordMinLength': '6', + 'passwordChange': 'on', + 'passwordLockout': 'on', + 'passwordMaxFailure': '2', + } + pwp.create_subtree_policy(f'{LOCAL_RDN},{DEFAULT_SUFFIX}', subtree_policy_props) + + # Configure global policy + inst.config.replace('nsslapd-pwpolicy-local', 'on') + inst.config.replace('passwordCheckSyntax', 'on') + inst.config.replace('passwordMinLength', '8') + inst.config.replace('passwordChange', 'on') + inst.config.replace('passwordLockout', 'on') + inst.config.replace('passwordMaxFailure', '5') + time.sleep(1) + + +def test_debug_logging(topo): + """Enable password policy logging + + :id: cc152c65-94e0-4716-a77c-abdd2deec00d + :setup: Standalone Instance + :steps: + 1. Set password policy logging level + 2. Add database entries + 3. Configure local and global policies + 4. Test syntax checking on local policy + 5. Test syntax checking on global policy + 6. Test account lockout on local policy + 7. Test account lockout on global policy + + :expectedresults: + 1. Success + 2. Success + 3. Success + 4. Success + 5. Success + 6. Success + 7. Success + """ + + inst = topo.standalone + + # Enable password policy debug logging + inst.config.replace('nsslapd-errorlog-level', '1048576') + + # Create entries and password policies + create_entries(inst) + create_policies(inst) + + # Setup bind connections + + local_conn = UserAccounts(inst, DEFAULT_SUFFIX, rdn=LOCAL_RDN).get('local_user').bind(PASSWORD) + local_user = UserAccount(local_conn, LOCAL_BIND_DN) + global_conn = UserAccounts(inst, DEFAULT_SUFFIX, rdn=GLOBAL_RDN).get('global_user').bind(PASSWORD) + global_user = UserAccount(global_conn, GLOBAL_BIND_DN) + + # Test syntax checking on local policy + passwd_val = "passw" # len 5 which is less than configured 6 + with pytest.raises(ldap.CONSTRAINT_VIOLATION): + local_user.replace('userpassword', passwd_val) + time.sleep(1) + + err_msg = "PWDPOLICY_DEBUG - invalid password syntax - password must be at least 6 characters long: Entry " + \ + "\\(uid=local_user,ou=people,dc=example,dc=com\\) Policy \\(cn=" + assert inst.searchErrorsLog(err_msg) + + # Test syntax checking on global policy + passwd_val = "passwod" # len 7 which is less than configured 8 + with pytest.raises(ldap.CONSTRAINT_VIOLATION): + global_user.replace('userpassword', passwd_val) + time.sleep(1) + + err_msg = "PWDPOLICY_DEBUG - invalid password syntax - password must be at least 8 characters long: Entry " + \ + "\\(uid=global_user,ou=global,dc=example,dc=com\\) Policy \\(Global\\)" + assert inst.searchErrorsLog(err_msg) + + # Test account lock is logging for local policy + for i in range(2): + with pytest.raises(ldap.INVALID_CREDENTIALS): + local_user.bind("bad") + with pytest.raises(ldap.CONSTRAINT_VIOLATION): + local_user.bind("bad") + + err_msg = "PWDPOLICY_DEBUG - Account is locked: Entry " + \ + "\\(uid=local_user,ou=people,dc=example,dc=com\\) Policy \\(cn=" + assert inst.searchErrorsLog(err_msg) + + # Test account lock is logging for global policy + for i in range(5): + with pytest.raises(ldap.INVALID_CREDENTIALS): + global_user.bind("bad") + with pytest.raises(ldap.CONSTRAINT_VIOLATION): + global_user.bind("bad") + + err_msg = "PWDPOLICY_DEBUG - Account is locked: Entry " + \ + "\\(uid=global_user,ou=global,dc=example,dc=com\\) Policy \\(Global\\)" + assert inst.searchErrorsLog(err_msg) + + +if __name__ == '__main__': + # Run isolated + # -s for DEBUG mode + CURRENT_FILE = os.path.realpath(__file__) + pytest.main(["-s", CURRENT_FILE]) diff --git a/ldap/servers/slapd/log.c b/ldap/servers/slapd/log.c index b4bf330ae1..3bf6d2f055 100644 --- a/ldap/servers/slapd/log.c +++ b/ldap/servers/slapd/log.c @@ -71,7 +71,7 @@ static int slapi_log_map[] = { LDAP_DEBUG_TIMING, /* SLAPI_LOG_TIMING */ LDAP_DEBUG_BACKLDBM, /* SLAPI_LOG_BACKLDBM */ LDAP_DEBUG_ACLSUMMARY, /* SLAPI_LOG_ACLSUMMARY */ - LDAP_DEBUG_NUNCSTANS, /* SLAPI_LOG_NUNCSTANS */ + LDAP_DEBUG_PWDPOLICY, /* SLAPI_LOG_PWDPOLICY */ LDAP_DEBUG_EMERG, /* SLAPI_LOG_EMERG */ LDAP_DEBUG_ALERT, /* SLAPI_LOG_ALERT */ LDAP_DEBUG_CRIT, /* SLAPI_LOG_CRIT */ diff --git a/ldap/servers/slapd/modify.c b/ldap/servers/slapd/modify.c index 87c9201972..e017799f7c 100644 --- a/ldap/servers/slapd/modify.c +++ b/ldap/servers/slapd/modify.c @@ -1249,6 +1249,10 @@ op_shared_allow_pw_change(Slapi_PBlock *pb, LDAPMod *mod, char **old_pw, Slapi_M if (pwresponse_req == 1) { slapi_pwpolicy_make_response_control(pb, -1, -1, LDAP_PWPOLICY_PWDMODNOTALLOWED); } + slapi_log_err(SLAPI_LOG_PWDPOLICY, PWDPOLICY_DEBUG, + "user is not allowed to change password. Entry (%s) Policy (%s)\n", + slapi_sdn_get_dn(&sdn), + pwpolicy->pw_local_dn ? pwpolicy->pw_local_dn : "Global"); send_ldap_result(pb, LDAP_UNWILLING_TO_PERFORM, NULL, "user is not allowed to change password", 0, NULL); diff --git a/ldap/servers/slapd/proto-slap.h b/ldap/servers/slapd/proto-slap.h index f261d848df..5c79218e3a 100644 --- a/ldap/servers/slapd/proto-slap.h +++ b/ldap/servers/slapd/proto-slap.h @@ -801,7 +801,7 @@ int lock_fclose(FILE *fp, FILE *lfp); #define LDAP_DEBUG_TIMING 0x00020000 /* 131072 */ #define LDAP_DEBUG_ACLSUMMARY 0x00040000 /* 262144 */ #define LDAP_DEBUG_BACKLDBM 0x00080000 /* 524288 */ -#define LDAP_DEBUG_NUNCSTANS 0x00100000 /* 1048576 */ +#define LDAP_DEBUG_PWDPOLICY 0x00100000 /* 1048576 */ #define LDAP_DEBUG_EMERG 0x00200000 /* 2097152 */ #define LDAP_DEBUG_ALERT 0x00400000 /* 4194304 */ #define LDAP_DEBUG_CRIT 0x00800000 /* 8388608 */ diff --git a/ldap/servers/slapd/pw.c b/ldap/servers/slapd/pw.c index 3e310b400e..3fd341a6e7 100644 --- a/ldap/servers/slapd/pw.c +++ b/ldap/servers/slapd/pw.c @@ -226,6 +226,7 @@ slapi_encode_ext(Slapi_PBlock *pb, const Slapi_DN *sdn, char *value, char *alg) slapi_log_err(SLAPI_LOG_ERR, "slapi_encode_ext", "no encoding password storage scheme found for %s\n", pwpolicy->pw_storagescheme->pws_name); + /* new_passwdPolicy registers the policy in the pblock so there is no leak */ /* coverity[leaked_storage] */ return NULL; @@ -887,12 +888,15 @@ check_pw_minage(Slapi_PBlock *pb, const Slapi_DN *sdn, struct berval **vals __at /* check if allow to change the password */ cur_time_str = format_genTime(slapi_current_utc_time()); - if (difftime(pw_allowchange_date, - parse_genTime(cur_time_str)) > 0) { + if (difftime(pw_allowchange_date, parse_genTime(cur_time_str)) > 0) { if (pwresponse_req == 1) { slapi_pwpolicy_make_response_control(pb, -1, -1, LDAP_PWPOLICY_PWDTOOYOUNG); } + slapi_log_err(SLAPI_LOG_PWDPOLICY, PWDPOLICY_DEBUG, + "password within minimum age: Entry (%s) Policy (%s)\n", + dn, pwpolicy->pw_local_dn ? pwpolicy->pw_local_dn : "Global"); + pw_send_ldap_result(pb, LDAP_CONSTRAINT_VIOLATION, NULL, "within password minimum age", 0, NULL); slapi_entry_free(e); slapi_ch_free((void **)&cur_time_str); @@ -1041,10 +1045,11 @@ pw_max_class_repeats(const char *new, int32_t max_repeats) } static void -report_pw_violation(Slapi_PBlock *pb, int pwresponse_req, char *fmt, ...) +report_pw_violation(Slapi_PBlock *pb, char *dn, int pwresponse_req, char *fmt, ...) { char errormsg[SLAPI_DSE_RETURNTEXT_SIZE] = {0}; va_list msg; + passwdPolicy *pwpolicy = slapi_pblock_get_pwdpolicy(pb); va_start(msg, fmt); PR_vsnprintf(errormsg, SLAPI_DSE_RETURNTEXT_SIZE - 1, fmt, msg); @@ -1053,6 +1058,9 @@ report_pw_violation(Slapi_PBlock *pb, int pwresponse_req, char *fmt, ...) } pw_send_ldap_result(pb, LDAP_CONSTRAINT_VIOLATION, NULL, errormsg, 0, NULL); va_end(msg); + slapi_log_err(SLAPI_LOG_PWDPOLICY, PWDPOLICY_DEBUG, + "%s: Entry (%s) Policy (%s)\n", + errormsg, dn, pwpolicy->pw_local_dn ? pwpolicy->pw_local_dn : "Global"); } /* check_pw_syntax is called before add or modify operation on userpassword attribute*/ @@ -1123,7 +1131,7 @@ check_pw_syntax_ext(Slapi_PBlock *pb, const Slapi_DN *sdn, Slapi_Value **vals, c ((internal_op && pb_conn && !slapi_dn_isroot(pb_conn->c_dn)) || (!internal_op && !pw_is_pwp_admin(pb, pwpolicy)))) { - report_pw_violation(pb, pwresponse_req, "invalid password syntax - passwords with storage scheme are not allowed"); + report_pw_violation(pb, dn, pwresponse_req, "invalid password syntax - passwords with storage scheme are not allowed"); return (1); } else { /* We want to skip syntax checking since this is a pre-hashed password */ @@ -1152,7 +1160,7 @@ check_pw_syntax_ext(Slapi_PBlock *pb, const Slapi_DN *sdn, Slapi_Value **vals, c if (pwpolicy->pw_check_dict) { const char *crack_msg; if ((crack_msg = FascistCheck(pwd, pwpolicy->pw_dict_path))) { - report_pw_violation(pb, pwresponse_req, "Password failed dictionary check: %s", crack_msg); + report_pw_violation(pb, dn, pwresponse_req, "Password failed dictionary check: %s", crack_msg); return (1); } } @@ -1160,7 +1168,7 @@ check_pw_syntax_ext(Slapi_PBlock *pb, const Slapi_DN *sdn, Slapi_Value **vals, c /* check palindrome */ if (pwpolicy->pw_palindrome) { if (palindrome(pwd)) { - report_pw_violation(pb, pwresponse_req, "Password is a palindrome"); + report_pw_violation(pb, dn, pwresponse_req, "Password is a palindrome"); return (1); } } @@ -1170,7 +1178,7 @@ check_pw_syntax_ext(Slapi_PBlock *pb, const Slapi_DN *sdn, Slapi_Value **vals, c if (bad_words_array) { for (size_t b = 0; bad_words_array && bad_words_array[b]; b++) { if (strcasestr(pwd, bad_words_array[b])) { - report_pw_violation(pb, pwresponse_req, "Password contains a restricted word"); + report_pw_violation(pb, dn, pwresponse_req, "Password contains a restricted word"); charray_free(bad_words_array); return (1); } @@ -1181,9 +1189,7 @@ check_pw_syntax_ext(Slapi_PBlock *pb, const Slapi_DN *sdn, Slapi_Value **vals, c /* Check for sequences */ if (pwpolicy->pw_max_seq) { if (pw_sequence(pwd, pwpolicy->pw_max_seq)) { - report_pw_violation(pb, pwresponse_req, "Password contains a monotonic sequence"); - PR_snprintf(errormsg, sizeof(errormsg) - 1, "Password contains a monotonic sequence"); - + report_pw_violation(pb, dn, pwresponse_req, "Password contains a monotonic sequence"); return (1); } } @@ -1191,7 +1197,7 @@ check_pw_syntax_ext(Slapi_PBlock *pb, const Slapi_DN *sdn, Slapi_Value **vals, c /* Check for sets of sequences */ if (pwpolicy->pw_seq_char_sets) { if (pw_sequence_sets(pwd, pwpolicy->pw_seq_char_sets, 1)){ - report_pw_violation(pb, pwresponse_req, "Password contains repeated identical sequences"); + report_pw_violation(pb, dn, pwresponse_req, "Password contains repeated identical sequences"); return (1); } } @@ -1199,7 +1205,7 @@ check_pw_syntax_ext(Slapi_PBlock *pb, const Slapi_DN *sdn, Slapi_Value **vals, c /* Check for max repeated characters from the same class */ if (pwpolicy->pw_max_class_repeats) { if (pw_max_class_repeats(pwd, pwpolicy->pw_max_class_repeats)){ - report_pw_violation(pb, pwresponse_req, + report_pw_violation(pb, dn, pwresponse_req, "Password contains too many repeated characters from the same character class"); return (1); } @@ -1207,7 +1213,7 @@ check_pw_syntax_ext(Slapi_PBlock *pb, const Slapi_DN *sdn, Slapi_Value **vals, c /* check for the minimum password length */ if (pwpolicy->pw_minlength > (int)ldap_utf8characters((char *)pwd)) { - report_pw_violation(pb, pwresponse_req, + report_pw_violation(pb, dn, pwresponse_req, "invalid password syntax - password must be at least %d characters long", pwpolicy->pw_minlength); return (1); @@ -1320,6 +1326,9 @@ check_pw_syntax_ext(Slapi_PBlock *pb, const Slapi_DN *sdn, Slapi_Value **vals, c LDAP_PWPOLICY_INVALIDPWDSYNTAX); } pw_send_ldap_result(pb, LDAP_CONSTRAINT_VIOLATION, NULL, errormsg, 0, NULL); + slapi_log_err(SLAPI_LOG_PWDPOLICY, PWDPOLICY_DEBUG, + "%s: Entry (%s) Policy (%s)\n", + errormsg, dn, pwpolicy->pw_local_dn ? pwpolicy->pw_local_dn : "Global"); return (1); } } @@ -1351,6 +1360,9 @@ check_pw_syntax_ext(Slapi_PBlock *pb, const Slapi_DN *sdn, Slapi_Value **vals, c slapi_pwpolicy_make_response_control(pb, -1, -1, LDAP_PWPOLICY_PWDINHISTORY); } pw_send_ldap_result(pb, LDAP_CONSTRAINT_VIOLATION, NULL, "password in history", 0, NULL); + slapi_log_err(SLAPI_LOG_PWDPOLICY, PWDPOLICY_DEBUG, + "password in history: Entry (%s) Policy (%s)\n", + dn, pwpolicy->pw_local_dn ? pwpolicy->pw_local_dn : "Global"); slapi_entry_free(e); return (1); } @@ -1363,12 +1375,18 @@ check_pw_syntax_ext(Slapi_PBlock *pb, const Slapi_DN *sdn, Slapi_Value **vals, c if (slapi_is_encoded((char *)slapi_value_get_string(vals[0]))) { if (slapi_attr_value_find(attr, (struct berval *)slapi_value_get_berval(vals[0])) == 0) { pw_send_ldap_result(pb, LDAP_CONSTRAINT_VIOLATION, NULL, "password in history", 0, NULL); + slapi_log_err(SLAPI_LOG_PWDPOLICY, PWDPOLICY_DEBUG, + "password in history: Entry (%s) Policy (%s)\n", + dn, pwpolicy->pw_local_dn ? pwpolicy->pw_local_dn : "Global"); slapi_entry_free(e); return (1); } } else { if (slapi_pw_find_sv(va, vals[0]) == 0) { pw_send_ldap_result(pb, LDAP_CONSTRAINT_VIOLATION, NULL, "password in history", 0, NULL); + slapi_log_err(SLAPI_LOG_PWDPOLICY, PWDPOLICY_DEBUG, + "password in history: Entry (%s) Policy (%s)\n", + dn, pwpolicy->pw_local_dn ? pwpolicy->pw_local_dn : "Global"); slapi_entry_free(e); return (1); } @@ -1397,7 +1415,8 @@ check_pw_syntax_ext(Slapi_PBlock *pb, const Slapi_DN *sdn, Slapi_Value **vals, c check_trivial_words(pb, e, vals, "sn", pwpolicy->pw_mintokenlength, smods) == 1 || check_trivial_words(pb, e, vals, "givenname", pwpolicy->pw_mintokenlength, smods) == 1 || check_trivial_words(pb, e, vals, "ou", pwpolicy->pw_mintokenlength, smods) == 1 || - check_trivial_words(pb, e, vals, "mail", pwpolicy->pw_mintokenlength, smods) == 1) { + check_trivial_words(pb, e, vals, "mail", pwpolicy->pw_mintokenlength, smods) == 1) + { if (mod_op) { slapi_entry_free(e); } @@ -1786,6 +1805,7 @@ check_trivial_words(Slapi_PBlock *pb, Slapi_Entry *e, Slapi_Value **vals, char * Slapi_Mod *smodp = NULL, *smod = NULL; Slapi_ValueSet *vs = NULL; Slapi_Value *valp = NULL; + passwdPolicy *pwpolicy = slapi_pblock_get_pwdpolicy(pb); struct berval *bvp = NULL; int i, pwresponse_req = 0; @@ -1868,10 +1888,12 @@ check_trivial_words(Slapi_PBlock *pb, Slapi_Entry *e, Slapi_Value **vals, char * slapi_pwpolicy_make_response_control(pb, -1, -1, LDAP_PWPOLICY_INVALIDPWDSYNTAX); } - pw_send_ldap_result(pb, - LDAP_CONSTRAINT_VIOLATION, NULL, + pw_send_ldap_result(pb, LDAP_CONSTRAINT_VIOLATION, NULL, "invalid password syntax - password based off of user entry", 0, NULL); - + slapi_log_err(SLAPI_LOG_PWDPOLICY, PWDPOLICY_DEBUG, + "password based off of user entry (attr=%s token_len=%d): Entry (%s) Policy (%s)\n", + attrtype, toklen, slapi_entry_get_dn_const(e), + pwpolicy->pw_local_dn ? pwpolicy->pw_local_dn : "Global"); /* Free valueset */ slapi_valueset_free(vs); return (1); @@ -2148,6 +2170,9 @@ new_passwdPolicy(Slapi_PBlock *pb, const char *dn) goto done; } + /* policy is local, store the DN of the policy for logging */ + pwdpolicy->pw_local_dn = slapi_ch_strdup(slapi_entry_get_dn(pw_entry)); + /* Set the default values (from libglobs.c) */ pwpolicy_init_defaults(pwdpolicy); @@ -2446,6 +2471,7 @@ delete_passwdPolicy(passwdPolicy **pwpolicy) } slapi_ch_free((void **)&(*(*pwpolicy)).pw_admin_user); } + slapi_ch_free_string(&(*(*pwpolicy)).pw_local_dn); slapi_ch_free((void **)pwpolicy); } } @@ -2676,10 +2702,11 @@ slapi_check_tpr_limits(Slapi_PBlock *pb, Slapi_Entry *bind_target_entry, int sen use_count++; update_tpr_pw_usecount(pb, bind_target_entry, (int32_t) use_count); if (use_count > pwpolicy->pw_tpr_maxuse) { - slapi_log_err(SLAPI_LOG_TRACE, - "slapi_check_tpr_limits", - "LDAP_CONSTRAINT_VIOLATION, number of bind (%d) on entry (%s) is larger than TPR password max use (%d).\n", - use_count, dn, pwpolicy->pw_tpr_maxuse); + slapi_log_err(SLAPI_LOG_PWDPOLICY, PWDPOLICY_DEBUG, + "slapi_check_tpr_limits - " + "number of bind (%u) is larger than TPR password max use (%d): Entry (%s) Policy (%s)\n", + use_count, pwpolicy->pw_tpr_maxuse, + dn, pwpolicy->pw_local_dn ? pwpolicy->pw_local_dn : "Global"); if (send_result) { send_ldap_result(pb, LDAP_CONSTRAINT_VIOLATION, NULL, "TPR checking. Contact system administrator", 0, NULL); @@ -2708,10 +2735,11 @@ slapi_check_tpr_limits(Slapi_PBlock *pb, Slapi_Entry *bind_target_entry, int sen if (value) { /* max Use is enforced */ if (difftime(parse_genTime(cur_time_str), parse_genTime(value)) >= 0) { - slapi_log_err(SLAPI_LOG_TRACE, - "slapi_check_tpr_limits", - "LDAP_CONSTRAINT_VIOLATION, attempt to bind with an expired TPR password (current=%s, expiration=%s), entry %s\n", - cur_time_str, value, dn); + slapi_log_err(SLAPI_LOG_PWDPOLICY, PWDPOLICY_DEBUG, + "slapi_check_tpr_limits - " + "attempt to bind with an expired TPR password (current=%s, expiration=%s): Entry (%s) Policy (%s)\n", + cur_time_str, value, + dn, pwpolicy->pw_local_dn ? pwpolicy->pw_local_dn : "Global"); if (send_result) { send_ldap_result(pb, LDAP_CONSTRAINT_VIOLATION, NULL, "TPR checking. Contact system administrator", 0, NULL); @@ -2736,10 +2764,11 @@ slapi_check_tpr_limits(Slapi_PBlock *pb, Slapi_Entry *bind_target_entry, int sen if (value) { /* validity after a specific time is enforced */ if (difftime(parse_genTime(value), parse_genTime(cur_time_str)) >= 0) { - slapi_log_err(SLAPI_LOG_TRACE, - "slapi_check_tpr_limits", - "LDAP_CONSTRAINT_VIOLATION, attempt to bind with TPR password not yet valid (current=%s, validity=%s), entry %s\n", - cur_time_str, value, dn); + slapi_log_err(SLAPI_LOG_PWDPOLICY, PWDPOLICY_DEBUG, + "slapi_check_tpr_limits - " + "attempt to bind with TPR password not yet valid (current=%s, validity=%s): Entry (%s) Policy (%s)\n", + cur_time_str, value, + dn, pwpolicy->pw_local_dn ? pwpolicy->pw_local_dn : "Global"); if (send_result) { send_ldap_result(pb, LDAP_CONSTRAINT_VIOLATION, NULL, "TPR checking. Contact system administrator", 0, NULL); @@ -2858,10 +2887,14 @@ slapi_check_account_lock(Slapi_PBlock *pb, Slapi_Entry *bind_target_entry, int p slapi_pwpolicy_make_response_control(pb, -1, -1, LDAP_PWPOLICY_ACCTLOCKED); } - if (send_result) + if (send_result) { send_ldap_result(pb, LDAP_CONSTRAINT_VIOLATION, NULL, "Exceed password retry limit. Contact system administrator to reset.", 0, NULL); + slapi_log_err(SLAPI_LOG_PWDPOLICY, PWDPOLICY_DEBUG, + "Account is locked and requires administrator reset. Entry (%s) Policy (%s)\n", + dn, pwpolicy->pw_local_dn ? pwpolicy->pw_local_dn : "Global"); + } goto locked; } cur_time = slapi_current_utc_time(); @@ -2873,10 +2906,14 @@ slapi_check_account_lock(Slapi_PBlock *pb, Slapi_Entry *bind_target_entry, int p slapi_pwpolicy_make_response_control(pb, -1, -1, LDAP_PWPOLICY_ACCTLOCKED); } - if (send_result) + if (send_result) { send_ldap_result(pb, LDAP_CONSTRAINT_VIOLATION, NULL, "Exceed password retry limit. Please try later.", 0, NULL); + slapi_log_err(SLAPI_LOG_PWDPOLICY, PWDPOLICY_DEBUG, + "Account is locked: Entry (%s) Policy (%s)\n", + dn, pwpolicy->pw_local_dn ? pwpolicy->pw_local_dn : "Global"); + } slapi_ch_free((void **)&cur_time_str); goto locked; } diff --git a/ldap/servers/slapd/pw_mgmt.c b/ldap/servers/slapd/pw_mgmt.c index a47a443b08..eec967a264 100644 --- a/ldap/servers/slapd/pw_mgmt.c +++ b/ldap/servers/slapd/pw_mgmt.c @@ -203,13 +203,17 @@ need_new_pw(Slapi_PBlock *pb, Slapi_Entry *e, int pwresponse_req) -1); } } + slapi_log_err(SLAPI_LOG_PWDPOLICY, PWDPOLICY_DEBUG, + "password expired, but within grace limit (curr=%d limit=%d needpw=%d): Entry (%s) Policy (%s)\n", + pwpolicy->pw_gracelimit, pwdGraceUserTime, needpw, dn, + pwpolicy->pw_local_dn ? pwpolicy->pw_local_dn : "Global"); slapi_add_pwd_control(pb, LDAP_CONTROL_PWEXPIRED, 0); /* new_passwdPolicy registers the policy in the pblock so there is no leak */ /* coverity[leaked_storage] */ return (0); } - /* password expired and user exceeded limit of grace attemps. + /* password expired and user exceeded limit of grace attempts. * Send result and also the control */ if (pwresponse_req) { slapi_pwpolicy_make_response_control(pb, -1, -1, LDAP_PWPOLICY_PWDEXPIRED); @@ -220,6 +224,9 @@ need_new_pw(Slapi_PBlock *pb, Slapi_Entry *e, int pwresponse_req) } slapi_send_ldap_result(pb, LDAP_INVALID_CREDENTIALS, NULL, "password expired!", 0, NULL); + slapi_log_err(SLAPI_LOG_PWDPOLICY, PWDPOLICY_DEBUG, + "password expired: Entry (%s) Policy (%s)\n", + dn, pwpolicy->pw_local_dn ? pwpolicy->pw_local_dn : "Global"); /* abort bind */ /* pass pb to do_unbind(). pb->pb_op->o_opid and diff --git a/ldap/servers/slapd/pw_retry.c b/ldap/servers/slapd/pw_retry.c index df59281f8d..7b7c0307f2 100644 --- a/ldap/servers/slapd/pw_retry.c +++ b/ldap/servers/slapd/pw_retry.c @@ -115,10 +115,12 @@ update_tpr_pw_usecount(Slapi_PBlock *pb, Slapi_Entry *e, int32_t use_count) * set in the password policy */ if (use_count >= 0) { - slapi_log_err(SLAPI_LOG_TRACE, - "update_tpr_pw_usecount", - "update pwdTPRUseCount=%d on entry (%s).\n", - use_count, slapi_entry_get_ndn(e)); + passwdPolicy *pwpolicy = new_passwdPolicy(pb, slapi_entry_get_ndn(e)); + slapi_log_err(SLAPI_LOG_PWDPOLICY, + PWDPOLICY_DEBUG, + "update pwdTPRUseCount=%d on entry (%s) policy (%s).\n", + use_count, slapi_entry_get_ndn(e), + pwpolicy->pw_local_dn ? pwpolicy->pw_local_dn : "Global"); rc = set_tpr_usecount(pb, use_count); } } @@ -176,16 +178,20 @@ set_tpr_usecount_mods(Slapi_PBlock *pb, Slapi_Mods *smods, int count) if (smods) { sprintf(retry_cnt, "%d", count); slapi_mods_add_string(smods, LDAP_MOD_REPLACE, "pwdTPRUseCount", retry_cnt); - slapi_log_err(SLAPI_LOG_TRACE, - "set_tpr_retry_cnt_mods", - "Unsuccessfull bind, increase pwdTPRUseCount = %d.\n", count); + slapi_log_err(SLAPI_LOG_PWDPOLICY, + PWDPOLICY_DEBUG, + "Unsuccessful bind, increase pwdTPRUseCount = %d (max %d) - entry (%s) policy (%s)\n", + count, pwpolicy->pw_tpr_maxuse, + dn, pwpolicy->pw_local_dn ? pwpolicy->pw_local_dn : "Global"); /* return a failure if it reaches the retry limit */ if (count > pwpolicy->pw_tpr_maxuse) { slapi_log_err(SLAPI_LOG_INFO, "set_tpr_retry_cnt_mods", - "Unsuccessfull bind, LDAP_CONSTRAINT_VIOLATION pwdTPRUseCount %d > %d.\n", + "Unsuccessful bind, LDAP_CONSTRAINT_VIOLATION pwdTPRUseCount " + "%d > %d - entry (%s) policy (%s)\n", count, - pwpolicy->pw_tpr_maxuse); + pwpolicy->pw_tpr_maxuse, + dn, pwpolicy->pw_local_dn ? pwpolicy->pw_local_dn : "Global"); rc = LDAP_CONSTRAINT_VIOLATION; } } diff --git a/ldap/servers/slapd/result.c b/ldap/servers/slapd/result.c index e1d859115e..a4e43764f5 100644 --- a/ldap/servers/slapd/result.c +++ b/ldap/servers/slapd/result.c @@ -467,6 +467,9 @@ send_ldap_result_ext( */ err = LDAP_CONSTRAINT_VIOLATION; text = "Invalid credentials, you now have exceeded the password retry limit."; + slapi_log_err(SLAPI_LOG_PWDPOLICY, PWDPOLICY_DEBUG, + "password retry limit exceeded. Entry (%s) Policy (%s)\n", + dn, pwpolicy->pw_local_dn ? pwpolicy->pw_local_dn : "Global"); } } } diff --git a/ldap/servers/slapd/slap.h b/ldap/servers/slapd/slap.h index 5cd022007d..ad5c41ed18 100644 --- a/ldap/servers/slapd/slap.h +++ b/ldap/servers/slapd/slap.h @@ -1864,7 +1864,10 @@ typedef struct passwordpolicyarray struct pw_scheme *pw_storagescheme; Slapi_DN *pw_admin; Slapi_DN **pw_admin_user; + char *pw_local_dn; /* DN of the subtree/user policy */ + } passwdPolicy; +#define PWDPOLICY_DEBUG "PWDPOLICY_DEBUG" void pwpolicy_init_defaults (passwdPolicy *pw_policy); diff --git a/ldap/servers/slapd/slapi-plugin.h b/ldap/servers/slapd/slapi-plugin.h index b318066489..3f818b62ef 100644 --- a/ldap/servers/slapd/slapi-plugin.h +++ b/ldap/servers/slapd/slapi-plugin.h @@ -6050,7 +6050,7 @@ int slapi_log_error_ext(int loglevel, char *subsystem, char *fmt, va_list varg1, #define SLAPI_LOG_TIMING 15 #define SLAPI_LOG_BACKLDBM 16 #define SLAPI_LOG_ACLSUMMARY 17 -#define SLAPI_LOG_NUNCSTANS 18 +#define SLAPI_LOG_PWDPOLICY 18 /* Severity levels */ #define SLAPI_LOG_EMERG 19 #define SLAPI_LOG_ALERT 20 diff --git a/src/cockpit/389-console/src/lib/server/errorLog.jsx b/src/cockpit/389-console/src/lib/server/errorLog.jsx index aac3eb6209..e824e168a9 100644 --- a/src/cockpit/389-console/src/lib/server/errorLog.jsx +++ b/src/cockpit/389-console/src/lib/server/errorLog.jsx @@ -85,6 +85,7 @@ export class ServerErrorLog extends React.Component { this.pluginLevel = <>Plugin (level 65536); this.aclSummaryevel = <>Access Control Summary (level 262144); this.dbLevel = <>Backend Database (level 524288); + this.pwdpolicyLevel = <>Password Policy (level 1048576); this.state = { loading: true, @@ -112,6 +113,7 @@ export class ServerErrorLog extends React.Component { { cells: [{ title: this.pluginLevel }], level: 65536, selected: false }, { cells: [{ title: this.aclSummaryevel }], level: 262144, selected: false }, { cells: [{ title: this.dbLevel }], level: 524288, selected: false }, + { cells: [{ title: this.pwdpolicyLevel }], level: 1048576, selected: false }, ], columns: [ { title: 'Logging Level' }, diff --git a/src/cockpit/389-console/src/security.jsx b/src/cockpit/389-console/src/security.jsx index 9bee2387d7..cefe6eb0b9 100644 --- a/src/cockpit/389-console/src/security.jsx +++ b/src/cockpit/389-console/src/security.jsx @@ -427,6 +427,7 @@ export class Security extends React.Component { let cipherPref = "default"; let allowWeak = false; let renegot = true; + let checkHostname = false; if ('nstlsallowclientrenegotiation' in config.items) { if (config.items.nstlsallowclientrenegotiation === "off") { @@ -458,6 +459,11 @@ export class Security extends React.Component { allowWeak = true; } } + if ('nsslapd-ssl-check-hostname' in attrs) { + if (attrs['nsslapd-ssl-check-hostname'].toLowerCase() === "on") { + checkHostname = true; + } + } if ('nsssl3ciphers' in attrs) { if (attrs.nsssl3ciphers !== "") { cipherPref = attrs.nsssl3ciphers; @@ -470,7 +476,7 @@ export class Security extends React.Component { requireSecureBinds: secReqSecBinds, secureListenhost: attrs['nsslapd-securelistenhost'], clientAuth: clientAuth, - checkHostname: attrs['nsslapd-ssl-check-hostname'], + checkHostname: checkHostname, validateCert: validateCert, sslVersionMin: attrs.sslversionmin, sslVersionMax: attrs.sslversionmax, @@ -482,7 +488,7 @@ export class Security extends React.Component { _requireSecureBinds: secReqSecBinds, _secureListenhost: attrs['nsslapd-securelistenhost'], _clientAuth: clientAuth, - _checkHostname: attrs['nsslapd-ssl-check-hostname'], + _checkHostname: checkHostname, _validateCert: validateCert, _sslVersionMin: attrs.sslversionmin, _sslVersionMax: attrs.sslversionmax,