diff --git a/dirsrvtests/tests/suites/basic/basic_test.py b/dirsrvtests/tests/suites/basic/basic_test.py index 57a57a98dc..241ff60b6b 100644 --- a/dirsrvtests/tests/suites/basic/basic_test.py +++ b/dirsrvtests/tests/suites/basic/basic_test.py @@ -19,7 +19,7 @@ from lib389._constants import DN_DM, PASSWORD, PW_DM, ReplicaRole from lib389.paths import Paths from lib389.idm.directorymanager import DirectoryManager -from lib389.config import LDBMConfig +from lib389.config import LDBMConfig, CertmapLegacy from lib389.dseldif import DSEldif from lib389.rootdse import RootDSE from ....conftest import get_rpm_version @@ -27,6 +27,7 @@ from lib389.replica import Replicas, Changelog from lib389.backend import Backends from lib389.idm.domain import Domain +from lib389.nss_ssl import NssSsl pytestmark = pytest.mark.tier0 @@ -50,6 +51,24 @@ 'vendorVersion') +@pytest.fixture(scope="function") +def _reset_attr(request, topology_st): + """ Reset nsslapd-close-on-failed-bind attr to the default (off) """ + + def fin(): + dm = DirectoryManager(topology_st.standalone) + try: + dm_conn = dm.bind() + dm_conn.config.replace('nsslapd-close-on-failed-bind', 'off') + assert (dm_conn.config.get_attr_val_utf8('nsslapd-close-on-failed-bind')) == 'off' + except ldap.LDAPError as e: + log.error('Failure reseting attr') + assert False + topology_st.standalone.restart() + + request.addfinalizer(fin) + + @pytest.fixture(scope="module") def import_example_ldif(topology_st): """Import the Example LDIF for the tests in this suite""" @@ -1506,7 +1525,272 @@ def test_suffix_case(topology_st): domain = Domain(topology_st.standalone, TEST_SUFFIX) assert domain.dn == TEST_SUFFIX + +def test_bind_disconnect_invalid_entry(topology_st, _reset_attr): + """Test close connection on failed bind with invalid entry + + :id: b378543e-32dc-432a-9756-ce318d6d654b + :setup: Standalone instance + :steps: + 1. create/get user + 2. bind and search as user + 3. enable nsslapd-close-on-failed-bind attr + 4. bind as non existing entry to trigger connection closure + 5. verify connection has been closed and server is still running + 6. cleanup + :expectedresults: + 1. success + 2. success + 3. nsslapd-close-on-failed-bind attr set to on + 4. returns INVALID_CREDENTIALS, triggering connection closure + 5. success + 6. success + """ + + INVALID_ENTRY="cn=foooo,%s" % DEFAULT_SUFFIX + inst = topology_st.standalone + + dm = DirectoryManager(inst) + + # create/get user + users = UserAccounts(inst, DEFAULT_SUFFIX) + try: + user = users.create_test_user() + user.set("userPassword", PW_DM) + except ldap.ALREADY_EXISTS: + user = users.get('test_user_1000') + + # verify user can bind and search + try: + inst.simple_bind_s(user.dn, PW_DM) + except ldap.LDAPError as e: + log.error('Failed to bind {}'.format(user.dn)) + raise e + try: + inst.search_s(DEFAULT_SUFFIX, ldap.SCOPE_SUBTREE, '(objectclass=top)', ['dn']) + except ldap.LDAPError as e: + log.error('Search failed on {}'.format(DEFAULT_SUFFIX)) + raise e + + # enable and verify attr + try: + dm_conn = dm.bind() + dm_conn.config.replace('nsslapd-close-on-failed-bind', 'on') + assert (dm_conn.config.get_attr_val_utf8('nsslapd-close-on-failed-bind')) == 'on' + except ldap.LDAPError as e: + log.error('Failed to replace nsslapd-close-on-failed-bind attr') + raise e + + # bind as non existing entry which triggers connection close + with pytest.raises(ldap.INVALID_CREDENTIALS): + inst.simple_bind_s(INVALID_ENTRY, PW_DM) + + # verify the connection is closed but the server is still running + assert (inst.status()) + with pytest.raises(ldap.SERVER_DOWN): + inst.search_s(DEFAULT_SUFFIX, ldap.SCOPE_SUBTREE, '(objectclass=top)', ['dn']) + try: + dm_conn = dm.bind() + except ldap.LDAPError as e: + log.error('DM bind failed') + raise e + + +def test_bind_disconnect_cert_map_failed(topology_st, _reset_attr): + """Test close connection on failed bind with a failed cert mapping + + :id: 0ac60f76-1fd9-4080-a82b-21807e6bc292 + :setup: Standalone Instance + :steps: + 1. enable TLS + 2. create/get a user + 3. get details of ssca key and cert + 4. create 2 user certificates, one good, one bad + 5. configure certmap + 6. check that EXTERNAL is listed in supported mechns. + 7. bind with good cert + 8. bind with bad cert + 9. enable nsslapd-close-on-failed-bind attr + 10. bind with bad cert + 11. verify connection has been closed and server is still running + 12. cleanup + :expectedresults: + 1. success + 2. success + 3. success + 4. success + 5. success + 6. success + 7. success + 8. generates INVALID_CREDENTIALS exception + 9. success + 10. generates INVALID_CREDENTIALS exception, triggering connection closure + 11. success + 12. success + """ + + RDN_TEST_USER = 'test_user_1000' + RDN_TEST_USER_WRONG = 'test_user_wrong' + inst = topology_st.standalone + + inst.enable_tls() + dm = DirectoryManager(inst) + + # create/get user + users = UserAccounts(inst, DEFAULT_SUFFIX) + try: + user = users.create_test_user() + user.set("userPassword", PW_DM) + except ldap.ALREADY_EXISTS: + user = users.get(RDN_TEST_USER) + + ssca_dir = inst.get_ssca_dir() + ssca = NssSsl(dbpath=ssca_dir) + + ssca.create_rsa_user(RDN_TEST_USER) + ssca.create_rsa_user(RDN_TEST_USER_WRONG) + + # Get the details of where the key and crt are. + tls_locs = ssca.get_rsa_user(RDN_TEST_USER) + tls_locs_wrong = ssca.get_rsa_user(RDN_TEST_USER_WRONG) + + user.enroll_certificate(tls_locs['crt_der_path']) + + # Turn on the certmap. + cm = CertmapLegacy(inst) + certmaps = cm.list() + certmaps['default']['DNComps'] = '' + certmaps['default']['FilterComps'] = ['cn'] + certmaps['default']['VerifyCert'] = 'off' + cm.set(certmaps) + + # Check that EXTERNAL is listed in supported mechns. + assert(inst.rootdse.supports_sasl_external()) + + # Restart to allow certmaps to be re-read: Note, we CAN NOT use post_open + # here, it breaks on auth. see lib389/__init__.py + inst.restart(post_open=False) + + # bind with good cert + try: + inst.open(saslmethod='EXTERNAL', connOnly=True, certdir=ssca_dir, userkey=tls_locs['key'], usercert=tls_locs['crt']) + except ldap.LDAPError as e: + log.error('Bind with good cert failed') + raise e + + inst.restart() + + # bind with bad cert + with pytest.raises(ldap.INVALID_CREDENTIALS): + inst.open(saslmethod='EXTERNAL', connOnly=True, certdir=ssca_dir, userkey=tls_locs_wrong['key'], usercert=tls_locs_wrong['crt']) + + # enable and verify attr + try: + dm_conn = dm.bind() + dm_conn.config.replace('nsslapd-close-on-failed-bind', 'on') + assert (dm_conn.config.get_attr_val_utf8('nsslapd-close-on-failed-bind')) == 'on' + except ldap.LDAPError as e: + log.error('Failed to replace nsslapd-close-on-failed-bind attr') + raise e + + # bind with bad cert + with pytest.raises(ldap.INVALID_CREDENTIALS): + inst.open(saslmethod='EXTERNAL', connOnly=True, certdir=ssca_dir, userkey=tls_locs_wrong['key'], usercert=tls_locs_wrong['crt']) + + # check the connection is closed but the server is still running + assert (inst.status()) + with pytest.raises(ldap.SERVER_DOWN): + inst.search_s(DEFAULT_SUFFIX, ldap.SCOPE_SUBTREE, '(objectclass=top)', ['dn']) + try: + dm_conn = dm.bind() + except ldap.LDAPError as e: + log.error('DM bind failed') + raise e + + +def test_bind_disconnect_account_lockout(topology_st, _reset_attr): + """Test close connection on failed bind with user account lockout + + :id: 12e56d79-ce57-4574-a80a-d3b6d1d74d8f + :setup: Standalone Instance + :steps: + 1. configure account lockout + 2. create/get a user + 3. bind and search as user + 4. force account lock out + 5. enable nsslapd-close-on-failed-bind attr + 6. attempt user bind + 7. verify connection has been closed and server is still running + 8. cleanup + :expectedresults: + 1. success + 2. success + 3. success + 4. generates CONSTRAINT_VIOLATION exception + 5. success + 6. generates CONSTRAINT_VIOLATION exception, triggering connection closure + 7. success + 8. success + """ + + inst = topology_st.standalone + dm = DirectoryManager(inst) + inst.config.set('passwordlockout', 'on') + inst.config.set('passwordMaxFailure', '2') + + # create/get user + users = UserAccounts(inst, DEFAULT_SUFFIX) + try: + user = users.create_test_user() + user.set("userPassword", PW_DM) + except ldap.ALREADY_EXISTS: + user = users.get('test_user_1000') + + # verify user bind and search + try: + inst.simple_bind_s(user.dn, PW_DM) + except ldap.LDAPError as e: + log.error('Failed to bind {}'.format(user.dn)) + raise e + try: + inst.search_s(DEFAULT_SUFFIX, ldap.SCOPE_SUBTREE, '(objectclass=top)', ['dn']) + except ldap.LDAPError as e: + log.error('Search failed on {}'.format(DEFAULT_SUFFIX)) + raise e + # Force entry to get locked out + with pytest.raises(ldap.INVALID_CREDENTIALS): + inst.simple_bind_s(user.dn, 'whateverlike') + with pytest.raises(ldap.INVALID_CREDENTIALS): + inst.simple_bind_s(user.dn, 'whateverlike') + with pytest.raises(ldap.CONSTRAINT_VIOLATION): + # Should fail with good or bad password + inst.simple_bind_s(user.dn, PW_DM) + + # enable and verify attr + try: + dm_conn = dm.bind() + dm_conn.config.replace('nsslapd-close-on-failed-bind', 'on') + assert (dm_conn.config.get_attr_val_utf8('nsslapd-close-on-failed-bind')) == 'on' + except ldap.LDAPError as e: + log.error('Failed to replace nsslapd-close-on-failed-bind attr') + raise e + + # Should fail with good or bad password + with pytest.raises(ldap.CONSTRAINT_VIOLATION): + inst.simple_bind_s(user.dn, PW_DM) + + # check the connection is closed but the server is still running + assert (inst.status()) + with pytest.raises(ldap.SERVER_DOWN): + inst.search_s(DEFAULT_SUFFIX, ldap.SCOPE_SUBTREE, '(objectclass=top)', ['dn']) + try: + dm_conn = dm.bind() + except ldap.LDAPError as e: + log.error('DM bind failed') + raise e + + def test_dscreate(request): """Test that dscreate works diff --git a/ldap/servers/slapd/bind.c b/ldap/servers/slapd/bind.c index 1aa0784422..5365ab8c02 100644 --- a/ldap/servers/slapd/bind.c +++ b/ldap/servers/slapd/bind.c @@ -418,6 +418,9 @@ do_bind(Slapi_PBlock *pb) slapi_log_security(pb, SECURITY_BIND_FAILED, SECURITY_MSG_CERT_MAP_FAILED); /* call postop plugins */ plugin_call_plugins(pb, SLAPI_PLUGIN_POST_BIND_FN); + if (config_get_close_on_failed_bind()) { + disconnect_server_nomutex(pb_conn, pb_op->o_connid, pb_op->o_opid, SLAPD_DISCONNECT_UNBIND, 0); + } goto free_and_return; } @@ -608,6 +611,9 @@ do_bind(Slapi_PBlock *pb) /* increment BindSecurityErrorcount */ slapi_counter_increment(g_get_per_thread_snmp_vars()->ops_tbl.dsBindSecurityErrors); value_done(&cv); + if (config_get_close_on_failed_bind()) { + disconnect_server_nomutex(pb_conn, pb_op->o_connid, pb_op->o_opid, SLAPD_DISCONNECT_UNBIND, 0); + } goto free_and_return; } @@ -746,6 +752,9 @@ do_bind(Slapi_PBlock *pb) if (1 == myrc) { /* account is locked */ rc = myrc; slapi_log_security(pb, SECURITY_BIND_FAILED, SECURITY_MSG_ACCOUNT_LOCKED); + if (config_get_close_on_failed_bind()) { + disconnect_server_nomutex(pb_conn, pb_op->o_connid, pb_op->o_opid, SLAPD_DISCONNECT_UNBIND, 0); + } goto account_locked; } myrc = 0; @@ -795,6 +804,9 @@ do_bind(Slapi_PBlock *pb) slapi_pblock_set(pb, SLAPI_PB_RESULT_TEXT, "No such entry"); send_ldap_result(pb, LDAP_INVALID_CREDENTIALS, NULL, "", 0, NULL); slapi_log_security(pb, SECURITY_BIND_FAILED, SECURITY_MSG_NO_ENTRY); + if (config_get_close_on_failed_bind()) { + disconnect_server_nomutex(pb_conn, pb_op->o_connid, pb_op->o_opid, SLAPD_DISCONNECT_UNBIND, 0); + } goto free_and_return; } } @@ -849,6 +861,9 @@ do_bind(Slapi_PBlock *pb) } else { slapi_log_security(pb, SECURITY_BIND_FAILED, SECURITY_MSG_NO_ENTRY); } + if (config_get_close_on_failed_bind()) { + disconnect_server_nomutex(pb_conn, pb_op->o_connid, pb_op->o_opid, SLAPD_DISCONNECT_UNBIND, 0); + } } account_locked: if (cred.bv_len == 0) { diff --git a/ldap/servers/slapd/libglobs.c b/ldap/servers/slapd/libglobs.c index 34c9642833..f1c1f60e07 100644 --- a/ldap/servers/slapd/libglobs.c +++ b/ldap/servers/slapd/libglobs.c @@ -239,6 +239,7 @@ slapi_onoff_t init_ldapi_bind_switch; slapi_onoff_t init_ldapi_map_entries; slapi_onoff_t init_allow_unauth_binds; slapi_onoff_t init_require_secure_binds; +slapi_onoff_t init_close_on_failed_bind; slapi_onoff_t init_minssf_exclude_rootdse; slapi_onoff_t init_force_sasl_external; slapi_onoff_t init_slapi_counters; @@ -1067,6 +1068,11 @@ static struct config_get_and_set (void **)&global_slapdFrontendConfig.require_secure_binds, CONFIG_ON_OFF, (ConfigGetFunc)config_get_require_secure_binds, &init_require_secure_binds, NULL}, + {CONFIG_CLOSE_ON_FAILED_BIND, config_set_close_on_failed_bind, + NULL, 0, + (void **)&global_slapdFrontendConfig.close_on_failed_bind, + CONFIG_ON_OFF, (ConfigGetFunc)config_get_close_on_failed_bind, + &init_close_on_failed_bind, NULL}, {CONFIG_ANON_ACCESS_ATTRIBUTE, config_set_anon_access_switch, NULL, 0, (void **)&global_slapdFrontendConfig.allow_anon_access, @@ -1755,6 +1761,7 @@ FrontendConfig_init(void) #endif init_allow_unauth_binds = cfg->allow_unauth_binds = LDAP_OFF; init_require_secure_binds = cfg->require_secure_binds = LDAP_OFF; + init_close_on_failed_bind = cfg->close_on_failed_bind = LDAP_OFF; cfg->allow_anon_access = SLAPD_DEFAULT_ALLOW_ANON_ACCESS; init_slapi_counters = cfg->slapi_counters = LDAP_ON; cfg->threadnumber = util_get_hardware_threads(); @@ -6900,6 +6907,13 @@ config_get_require_secure_binds(void) return slapi_atomic_load_32(&(slapdFrontendConfig->require_secure_binds), __ATOMIC_ACQUIRE); } +int32_t +config_get_close_on_failed_bind(void) +{ + slapdFrontendConfig_t *slapdFrontendConfig = getFrontendConfig(); + return slapi_atomic_load_32(&(slapdFrontendConfig->close_on_failed_bind), __ATOMIC_ACQUIRE); +} + int32_t config_get_anon_access_switch(void) { @@ -7851,6 +7865,21 @@ config_set_require_secure_binds(const char *attrname, char *value, char *errorbu return retVal; } +int32_t +config_set_close_on_failed_bind(const char *attrname, char *value, char *errorbuf, int apply) +{ + int32_t retVal = LDAP_SUCCESS; + slapdFrontendConfig_t *slapdFrontendConfig = getFrontendConfig(); + + retVal = config_set_onoff(attrname, + value, + &(slapdFrontendConfig->close_on_failed_bind), + errorbuf, + apply); + + return retVal; +} + int config_set_anon_access_switch(const char *attrname, char *value, char *errorbuf, int apply) { diff --git a/ldap/servers/slapd/proto-slap.h b/ldap/servers/slapd/proto-slap.h index ed43479ce6..9c28f21358 100644 --- a/ldap/servers/slapd/proto-slap.h +++ b/ldap/servers/slapd/proto-slap.h @@ -375,6 +375,7 @@ int config_set_rewrite_rfc1274(const char *attrname, char *value, char *errorbuf int config_set_outbound_ldap_io_timeout(const char *attrname, char *value, char *errorbuf, int apply); int config_set_unauth_binds_switch(const char *attrname, char *value, char *errorbuf, int apply); int config_set_require_secure_binds(const char *attrname, char *value, char *errorbuf, int apply); +int config_set_close_on_failed_bind(const char *attrname, char *value, char *errorbuf, int apply); int config_set_anon_access_switch(const char *attrname, char *value, char *errorbuf, int apply); int config_set_localssf(const char *attrname, char *value, char *errorbuf, int apply); int config_set_minssf(const char *attrname, char *value, char *errorbuf, int apply); @@ -549,6 +550,7 @@ int config_get_rewrite_rfc1274(void); int config_get_outbound_ldap_io_timeout(void); int config_get_unauth_binds_switch(void); int config_get_require_secure_binds(void); +int config_get_close_on_failed_bind(void); int config_get_anon_access_switch(void); int config_get_localssf(void); int config_get_minssf(void); diff --git a/ldap/servers/slapd/slap.h b/ldap/servers/slapd/slap.h index a966e9dc84..b3b885bd0b 100644 --- a/ldap/servers/slapd/slap.h +++ b/ldap/servers/slapd/slap.h @@ -2201,6 +2201,7 @@ typedef struct _slapdEntryPoints #define CONFIG_SVRTAB_ATTRIBUTE "nsslapd-svrtab" #define CONFIG_UNAUTH_BINDS_ATTRIBUTE "nsslapd-allow-unauthenticated-binds" #define CONFIG_REQUIRE_SECURE_BINDS_ATTRIBUTE "nsslapd-require-secure-binds" +#define CONFIG_CLOSE_ON_FAILED_BIND "nsslapd-close-on-failed-bind" #define CONFIG_ANON_ACCESS_ATTRIBUTE "nsslapd-allow-anonymous-access" #define CONFIG_LOCALSSF_ATTRIBUTE "nsslapd-localssf" #define CONFIG_MINSSF_ATTRIBUTE "nsslapd-minssf" @@ -2654,6 +2655,7 @@ typedef struct _slapdFrontendConfig char *allowed_sasl_mechs; /* comma/space separated list of allowed sasl mechs */ char **allowed_sasl_mechs_array; /* Array of allow sasl mechs */ int sasl_max_bufsize; /* The max receive buffer size for SASL */ + slapi_onoff_t close_on_failed_bind; /* Close connection following a failed bind */ /* disk monitoring */ slapi_onoff_t disk_monitoring;