diff --git a/dirsrvtests/tests/suites/openldap_2_389/migrate_hdb_test.py b/dirsrvtests/tests/suites/openldap_2_389/migrate_hdb_test.py index 95597898e6..da67ed244c 100644 --- a/dirsrvtests/tests/suites/openldap_2_389/migrate_hdb_test.py +++ b/dirsrvtests/tests/suites/openldap_2_389/migrate_hdb_test.py @@ -9,7 +9,6 @@ import pytest import os from lib389.topologies import topology_st -from lib389.password_plugins import PBKDF2Plugin from lib389.utils import ds_is_older from lib389.migrate.openldap.config import olConfig from lib389.migrate.openldap.config import olOverlayType diff --git a/dirsrvtests/tests/suites/openldap_2_389/migrate_memberof_test.py b/dirsrvtests/tests/suites/openldap_2_389/migrate_memberof_test.py index 4092bb36df..e5f749c01a 100644 --- a/dirsrvtests/tests/suites/openldap_2_389/migrate_memberof_test.py +++ b/dirsrvtests/tests/suites/openldap_2_389/migrate_memberof_test.py @@ -9,7 +9,6 @@ import pytest import os from lib389.topologies import topology_st -from lib389.password_plugins import PBKDF2Plugin from lib389.utils import ds_is_older from lib389.migrate.openldap.config import olConfig from lib389.migrate.openldap.config import olOverlayType diff --git a/dirsrvtests/tests/suites/openldap_2_389/migrate_monitor_test.py b/dirsrvtests/tests/suites/openldap_2_389/migrate_monitor_test.py index bf056f0e05..6935900a55 100644 --- a/dirsrvtests/tests/suites/openldap_2_389/migrate_monitor_test.py +++ b/dirsrvtests/tests/suites/openldap_2_389/migrate_monitor_test.py @@ -9,7 +9,6 @@ import pytest import os from lib389.topologies import topology_st -from lib389.password_plugins import PBKDF2Plugin from lib389.utils import ds_is_older from lib389.migrate.openldap.config import olConfig from lib389.migrate.openldap.config import olOverlayType diff --git a/dirsrvtests/tests/suites/openldap_2_389/migrate_test.py b/dirsrvtests/tests/suites/openldap_2_389/migrate_test.py index 492c94fc8f..fa41a9daf3 100644 --- a/dirsrvtests/tests/suites/openldap_2_389/migrate_test.py +++ b/dirsrvtests/tests/suites/openldap_2_389/migrate_test.py @@ -9,7 +9,6 @@ import pytest import os from lib389.topologies import topology_st -from lib389.password_plugins import PBKDF2Plugin from lib389.utils import ds_is_older from lib389.migrate.openldap.config import olConfig from lib389.migrate.openldap.config import olOverlayType diff --git a/dirsrvtests/tests/suites/password/pbkdf2_upgrade_plugin_test.py b/dirsrvtests/tests/suites/password/pbkdf2_upgrade_plugin_test.py index 90dae36ec7..6652013b15 100644 --- a/dirsrvtests/tests/suites/password/pbkdf2_upgrade_plugin_test.py +++ b/dirsrvtests/tests/suites/password/pbkdf2_upgrade_plugin_test.py @@ -8,7 +8,7 @@ # import pytest from lib389.topologies import topology_st -from lib389.password_plugins import PBKDF2Plugin +from lib389.password_plugins import PBKDF2SHA256Plugin from lib389.utils import ds_is_older pytestmark = pytest.mark.tier1 @@ -35,18 +35,18 @@ def test_pbkdf2_upgrade(topology_st): """ # Remove the pbkdf2 plugin config - p1 = PBKDF2Plugin(topology_st.standalone) + p1 = PBKDF2SHA256Plugin(topology_st.standalone) assert(p1.exists()) p1._protected = False p1.delete() # Restart topology_st.standalone.restart() # check it's been readded. - p2 = PBKDF2Plugin(topology_st.standalone) + p2 = PBKDF2SHA256Plugin(topology_st.standalone) assert(p2.exists()) # Now restart to make sure we still work from the non-bootstrap form topology_st.standalone.restart() - p3 = PBKDF2Plugin(topology_st.standalone) + p3 = PBKDF2SHA256Plugin(topology_st.standalone) assert(p3.exists()) diff --git a/dirsrvtests/tests/suites/pwp_storage/storage_test.py b/dirsrvtests/tests/suites/pwp_storage/storage_test.py index ed0dd9e533..4ee7e896be 100644 --- a/dirsrvtests/tests/suites/pwp_storage/storage_test.py +++ b/dirsrvtests/tests/suites/pwp_storage/storage_test.py @@ -18,13 +18,54 @@ from lib389.topologies import topology_st as topo from lib389.idm.user import UserAccounts, UserAccount -from lib389._constants import DEFAULT_SUFFIX +from lib389._constants import DEFAULT_SUFFIX, DN_DM, PASSWORD, ErrorLog from lib389.config import Config -from lib389.password_plugins import PBKDF2Plugin, SSHA512Plugin +from lib389.password_plugins import ( + SSHA512Plugin, + PBKDF2SHA1Plugin, + PBKDF2SHA256Plugin, + PBKDF2SHA512Plugin +) from lib389.utils import ds_is_older pytestmark = pytest.mark.tier1 +PBKDF2_SCHEMES = [ + ('PBKDF2-SHA1', PBKDF2SHA1Plugin, 70000), + ('PBKDF2-SHA256', PBKDF2SHA256Plugin, 30000), + ('PBKDF2-SHA512', PBKDF2SHA512Plugin, 10000), +] + + +@pytest.fixture(scope="function") +def new_user(request, topo): + """Fixture to create and clean up a test user for each test""" + # Generate unique user ID based on test name + uid = f'new_user_{request.node.name[:20]}' + + # Create user + users = UserAccounts(topo.standalone, DEFAULT_SUFFIX) + user = users.create(properties={ + 'uid': uid, + 'cn': 'Test User', + 'sn': 'User', + 'uidNumber': '1000', + 'gidNumber': '2000', + 'homeDirectory': f'/home/{uid}' + }) + + def fin(): + try: + # Ensure we're bound as DM before cleanup + topo.standalone.simple_bind_s(DN_DM, PASSWORD) + if user.exists(): + user.delete() + except Exception as e: + log.error(f"Error during user cleanup: {e}") + + request.addfinalizer(fin) + return user + def user_config(topo, field_value): """ @@ -62,6 +103,248 @@ def test_check_password_scheme(topo, value): user.delete() +@pytest.mark.parametrize('scheme_name,plugin_class,default_rounds', PBKDF2_SCHEMES) +def test_pbkdf2_default_rounds(topo, new_user, scheme_name, plugin_class, default_rounds): + """Test PBKDF2 schemes with default iteration rounds. + + :id: bd58cd76-14f9-4d54-9793-ee7bba8e5369 + :parametrized: yes + :setup: Standalone + :steps: + 1. Remove any existing rounds configuration + 2. Verify default rounds are used + 3. Set password and verify hash format + 4. Test authentication + :expectedresults: + 1. Pass + 2. Pass + 3. Pass + 4. Pass + """ + try: + # Flush logs + topo.standalone.restart() + topo.standalone.config.loglevel((ErrorLog.DEFAULT, ErrorLog.PLUGIN)) + topo.standalone.deleteErrorLogs() + + plugin = plugin_class(topo.standalone) + plugin.remove_all('nsslapd-pwdpbkdf2numiterations') + topo.standalone.restart() + + topo.standalone.config.replace('passwordStorageScheme', scheme_name) + + current_rounds = plugin.get_rounds() + assert current_rounds == default_rounds, \ + f"Expected default {default_rounds} rounds, got {current_rounds}" + + new_user.set('userPassword', 'Secret123') + pwd_hash = new_user.get_attr_val_utf8('userPassword') + assert pwd_hash.startswith('{' + scheme_name.upper() + '}') + assert str(default_rounds) in pwd_hash + + topo.standalone.simple_bind_s(new_user.dn, 'Secret123') + + assert topo.standalone.searchErrorsLog( + f'Number of iterations for {scheme_name} password scheme set to {default_rounds} from default' + ) + finally: + topo.standalone.simple_bind_s(DN_DM, PASSWORD) + + +@pytest.mark.parametrize('scheme_name,plugin_class,default_rounds', PBKDF2_SCHEMES) +def test_pbkdf2_rounds_reset(topo, new_user, scheme_name, plugin_class, default_rounds): + """Test PBKDF2 schemes rounds reset to defaults. + + :id: 59bf95c5-6a07-4db1-81eb-d59b54436826 + :parametrized: yes + :setup: Standalone + :steps: + 1. Set custom rounds for PBKDF2 plugin + 2. Verify custom rounds are used + 3. Remove rounds configuration + 4. Verify defaults are restored + 5. Test password operations with default rounds + :expectedresults: + 1. Pass + 2. Pass + 3. Pass + 4. Pass + 5. Pass + """ + try: + # Flush logs + topo.standalone.restart() + topo.standalone.config.loglevel((ErrorLog.DEFAULT, ErrorLog.PLUGIN)) + topo.standalone.deleteErrorLogs() + + test_rounds = 25000 + plugin = plugin_class(topo.standalone) + plugin.set_rounds(test_rounds) + topo.standalone.restart() + + current_rounds = plugin.get_rounds() + assert current_rounds == test_rounds, \ + f"Expected {test_rounds} rounds, got {current_rounds}" + + plugin.remove_all('nsslapd-pwdpbkdf2numiterations') + topo.standalone.restart() + + current_rounds = plugin.get_rounds() + assert current_rounds == default_rounds, \ + f"Expected default {default_rounds} rounds after reset, got {current_rounds}" + + topo.standalone.config.replace('passwordStorageScheme', scheme_name) + + new_user.set('userPassword', 'Secret123') + pwd_hash = new_user.get_attr_val_utf8('userPassword') + assert pwd_hash.startswith('{' + scheme_name.upper() + '}') + assert str(default_rounds) in pwd_hash + + topo.standalone.simple_bind_s(new_user.dn, 'Secret123') + + assert topo.standalone.searchErrorsLog( + f'Number of iterations for {scheme_name} password scheme set to {default_rounds} from default' + ) + finally: + topo.standalone.simple_bind_s(DN_DM, PASSWORD) + + +@pytest.mark.parametrize('scheme_name,plugin_class,_', PBKDF2_SCHEMES) +@pytest.mark.parametrize('rounds', [10000, 20000, 50000]) +def test_pbkdf2_custom_rounds(topo, new_user, scheme_name, plugin_class, _, rounds): + """Test PBKDF2 schemes with custom iteration rounds. + + :id: 6bec6542-ed8d-4a0e-89d6-e047757767c2 + :parametrized: yes + :setup: Standalone + :steps: + 1. Set custom rounds for PBKDF2 plugin + 2. Verify rounds are set correctly + 3. Set password and verify hash format + 4. Test authentication + 5. Verify rounds in password hash + :expectedresults: + 1. Pass + 2. Pass + 3. Pass + 4. Pass + 5. Pass + """ + try: + # Flush logs + topo.standalone.restart() + topo.standalone.config.loglevel((ErrorLog.DEFAULT, ErrorLog.PLUGIN)) + topo.standalone.deleteErrorLogs() + + plugin = plugin_class(topo.standalone) + plugin.set_rounds(rounds) + topo.standalone.restart() + + current_rounds = plugin.get_rounds() + assert current_rounds == rounds, \ + f"Expected {rounds} rounds, got {current_rounds}" + + topo.standalone.config.replace('passwordStorageScheme', scheme_name) + + new_user.set('userPassword', 'Secret123') + pwd_hash = new_user.get_attr_val_utf8('userPassword') + assert pwd_hash.startswith('{' + scheme_name.upper() + '}') + assert str(rounds) in pwd_hash + + topo.standalone.simple_bind_s(new_user.dn, 'Secret123') + + assert topo.standalone.searchErrorsLog( + f'Number of iterations for {scheme_name}' + ) + finally: + topo.standalone.simple_bind_s(DN_DM, PASSWORD) + + +@pytest.mark.parametrize('scheme_name,plugin_class,_', PBKDF2_SCHEMES) +def test_pbkdf2_invalid_rounds(topo, scheme_name, plugin_class, _): + """Test PBKDF2 schemes with invalid iteration rounds. + + :id: 4e5b4f37-c97b-4f58-b5c5-726495d9fa4e + :parametrized: yes + :setup: Standalone + :steps: + 1. Try to set invalid rounds (too low and too high) + 2. Verify appropriate errors are raised + 3. Verify original rounds are maintained + :expectedresults: + 1. Pass + 2. Pass + 3. Pass + """ + # Flush logs + topo.standalone.restart() + topo.standalone.config.loglevel((ErrorLog.DEFAULT, ErrorLog.PLUGIN)) + topo.standalone.deleteErrorLogs() + + plugin = plugin_class(topo.standalone) + plugin.enable() + + original_rounds = plugin.get_rounds() + + with pytest.raises(ValueError) as excinfo: + plugin.set_rounds(5000) + assert "rounds must be between 10,000 and 10,000,000" in str(excinfo.value) + + with pytest.raises(ValueError) as excinfo: + plugin.set_rounds(20000000) + assert "rounds must be between 10,000 and 10,000,000" in str(excinfo.value) + + current_rounds = plugin.get_rounds() + assert current_rounds == original_rounds, \ + f"Rounds changed from {original_rounds} to {current_rounds}" + + +@pytest.mark.parametrize('scheme_name,plugin_class,_', PBKDF2_SCHEMES) +def test_pbkdf2_rounds_persistence(topo, new_user, scheme_name, plugin_class, _): + """Test PBKDF2 rounds persistence across server restarts. + + :id: b15de1ae-53ac-429f-991b-cea5e6a7b383 + :parametrized: yes + :setup: Standalone + :steps: + 1. Set custom rounds for PBKDF2 plugin + 2. Restart server + 3. Verify rounds are maintained + 4. Set password and verify hash + 5. Test authentication + :expectedresults: + 1. Pass + 2. Pass + 3. Pass + 4. Pass + 5. Pass + """ + try: + # Flush logs + topo.standalone.restart() + topo.standalone.config.loglevel((ErrorLog.DEFAULT, ErrorLog.PLUGIN)) + topo.standalone.deleteErrorLogs() + + test_rounds = 15000 + plugin = plugin_class(topo.standalone) + plugin.set_rounds(test_rounds) + topo.standalone.restart() + + current_rounds = plugin.get_rounds() + assert current_rounds == test_rounds, \ + f"Expected {test_rounds} rounds after restart, got {current_rounds}" + + topo.standalone.config.replace('passwordStorageScheme', scheme_name) + + new_user.set('userPassword', 'Secret123') + pwd_hash = new_user.get_attr_val_utf8('userPassword') + assert str(test_rounds) in pwd_hash + + topo.standalone.simple_bind_s(new_user.dn, 'Secret123') + finally: + topo.standalone.simple_bind_s(DN_DM, PASSWORD) + + def test_clear_scheme(topo): """Check clear password scheme. @@ -106,27 +389,27 @@ def test_check_two_scheme(topo): user.delete() @pytest.mark.skipif(ds_is_older('1.4'), reason="Not implemented") -def test_check_pbkdf2_sha256(topo): - """Check password scheme PBKDF2_SHA256. +def test_check_pbkdf2_sha512(topo): + """Check password scheme PBKDF2-SHA512 is restored after deletion :id: 31612e7e-33a6-11ea-a750-8c16451d917b :setup: Standalone :steps: - 1. Try to delete PBKDF2_SHA256. - 2. Should not deleted PBKDF2_SHA256 and server should up. + 1. Try to delete PBKDF2-SHA512. + 2. Should not deleted PBKDF2-SHA512 and server should up. :expectedresults: 1. Pass 2. Pass """ - value = 'PBKDF2_SHA256' + value = 'PBKDF2-SHA512' user = user_config(topo, value) assert '{' + f'{value.lower()}' + '}' in \ UserAccount(topo.standalone, user.dn).get_attr_val_utf8('userpassword').lower() - plg = PBKDF2Plugin(topo.standalone) + plg = PBKDF2SHA512Plugin(topo.standalone) plg._protected = False plg.delete() topo.standalone.restart() - assert Config(topo.standalone).get_attr_val_utf8('passwordStorageScheme') == 'PBKDF2_SHA256' + assert Config(topo.standalone).get_attr_val_utf8('passwordStorageScheme') == value assert topo.standalone.status() user.delete() diff --git a/ldap/ldif/template-dse-minimal.ldif.in b/ldap/ldif/template-dse-minimal.ldif.in index d2b02f8be9..264cc51e06 100644 --- a/ldap/ldif/template-dse-minimal.ldif.in +++ b/ldap/ldif/template-dse-minimal.ldif.in @@ -195,6 +195,7 @@ nsslapd-pluginenabled: on dn: cn=PBKDF2,cn=Password Storage Schemes,cn=plugins,cn=config objectclass: top objectclass: nsSlapdPlugin +objectClass: pwdPBKDF2PluginConfig cn: PBKDF2 nsslapd-pluginpath: libpwdchan-plugin nsslapd-plugininitfunc: pwdchan_pbkdf2_plugin_init @@ -208,6 +209,7 @@ nsslapd-pluginDescription: PBKDF2 dn: cn=PBKDF2-SHA1,cn=Password Storage Schemes,cn=plugins,cn=config objectclass: top objectclass: nsSlapdPlugin +objectClass: pwdPBKDF2PluginConfig cn: PBKDF2-SHA1 nsslapd-pluginpath: libpwdchan-plugin nsslapd-plugininitfunc: pwdchan_pbkdf2_sha1_plugin_init @@ -221,6 +223,7 @@ nsslapd-pluginDescription: PBKDF2-SHA1\ dn: cn=PBKDF2-SHA256,cn=Password Storage Schemes,cn=plugins,cn=config objectclass: top objectclass: nsSlapdPlugin +objectClass: pwdPBKDF2PluginConfig cn: PBKDF2-SHA256 nsslapd-pluginpath: libpwdchan-plugin nsslapd-plugininitfunc: pwdchan_pbkdf2_sha256_plugin_init @@ -234,6 +237,7 @@ nsslapd-pluginDescription: PBKDF2-SHA256\ dn: cn=PBKDF2-SHA512,cn=Password Storage Schemes,cn=plugins,cn=config objectclass: top objectclass: nsSlapdPlugin +objectClass: pwdPBKDF2PluginConfig cn: PBKDF2-SHA512 nsslapd-pluginpath: libpwdchan-plugin nsslapd-plugininitfunc: pwdchan_pbkdf2_sha512_plugin_init diff --git a/ldap/ldif/template-dse.ldif.in b/ldap/ldif/template-dse.ldif.in index ce44d75cd1..ccb736eb86 100644 --- a/ldap/ldif/template-dse.ldif.in +++ b/ldap/ldif/template-dse.ldif.in @@ -252,6 +252,7 @@ nsslapd-pluginenabled: on dn: cn=PBKDF2,cn=Password Storage Schemes,cn=plugins,cn=config objectclass: top objectclass: nsSlapdPlugin +objectClass: pwdPBKDF2PluginConfig cn: PBKDF2 nsslapd-pluginpath: libpwdchan-plugin nsslapd-plugininitfunc: pwdchan_pbkdf2_plugin_init @@ -265,6 +266,7 @@ nsslapd-pluginDescription: PBKDF2 dn: cn=PBKDF2-SHA1,cn=Password Storage Schemes,cn=plugins,cn=config objectclass: top objectclass: nsSlapdPlugin +objectClass: pwdPBKDF2PluginConfig cn: PBKDF2-SHA1 nsslapd-pluginpath: libpwdchan-plugin nsslapd-plugininitfunc: pwdchan_pbkdf2_sha1_plugin_init @@ -278,6 +280,7 @@ nsslapd-pluginDescription: PBKDF2-SHA1\ dn: cn=PBKDF2-SHA256,cn=Password Storage Schemes,cn=plugins,cn=config objectclass: top objectclass: nsSlapdPlugin +objectClass: pwdPBKDF2PluginConfig cn: PBKDF2-SHA256 nsslapd-pluginpath: libpwdchan-plugin nsslapd-plugininitfunc: pwdchan_pbkdf2_sha256_plugin_init @@ -291,6 +294,7 @@ nsslapd-pluginDescription: PBKDF2-SHA256\ dn: cn=PBKDF2-SHA512,cn=Password Storage Schemes,cn=plugins,cn=config objectclass: top objectclass: nsSlapdPlugin +objectClass: pwdPBKDF2PluginConfig cn: PBKDF2-SHA512 nsslapd-pluginpath: libpwdchan-plugin nsslapd-plugininitfunc: pwdchan_pbkdf2_sha512_plugin_init diff --git a/ldap/schema/01core389.ldif b/ldap/schema/01core389.ldif index c98e5b34b3..bfe8259f82 100644 --- a/ldap/schema/01core389.ldif +++ b/ldap/schema/01core389.ldif @@ -332,6 +332,7 @@ attributeTypes: ( 2.16.840.1.113730.3.1.2391 NAME 'dsEntryDN' DESC '389 Director attributeTypes: ( 2.16.840.1.113730.3.1.2392 NAME 'nsslapd-return-original-entrydn' DESC '389 Directory Server defined attribute type' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE X-ORIGIN '389 Directory Server' ) attributeTypes: ( 2.16.840.1.113730.3.1.2393 NAME 'nsslapd-auditlog-display-attrs' DESC '389 Directory Server defined attribute type' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE X-ORIGIN '389 Directory Server' ) attributeTypes: ( 2.16.840.1.113730.3.1.2398 NAME 'nsslapd-haproxy-trusted-ip' DESC '389 Directory Server defined attribute type' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 X-ORIGIN '389 Directory Server' ) +attributeTypes: ( 2.16.840.1.113730.3.1.2400 NAME 'nsslapd-pwdPBKDF2NumIterations' DESC '389 Directory Server defined attribute type' SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE X-ORIGIN 'Directory Server' ) # # objectclasses # @@ -353,3 +354,4 @@ objectClasses: ( 2.16.840.1.113730.3.2.327 NAME 'rootDNPluginConfig' DESC 'Netsc objectClasses: ( 2.16.840.1.113730.3.2.328 NAME 'nsSchemaPolicy' DESC 'Netscape defined objectclass' SUP top MAY ( cn $ schemaUpdateObjectclassAccept $ schemaUpdateObjectclassReject $ schemaUpdateAttributeAccept $ schemaUpdateAttributeReject) X-ORIGIN 'Netscape Directory Server' ) objectClasses: ( 2.16.840.1.113730.3.2.332 NAME 'nsChangelogConfig' DESC 'Configuration of the changelog5 object' SUP top MUST ( cn $ nsslapd-changelogdir ) MAY ( nsslapd-changelogmaxage $ nsslapd-changelogtrim-interval $ nsslapd-changelogmaxentries $ nsslapd-changelogsuffix $ nsslapd-changelogcompactdb-interval $ nsslapd-encryptionalgorithm $ nsSymmetricKey ) X-ORIGIN '389 Directory Server' ) objectClasses: ( 2.16.840.1.113730.3.2.337 NAME 'rewriterEntry' DESC '' SUP top MUST ( nsslapd-libPath ) MAY ( cn $ nsslapd-filterrewriter $ nsslapd-returnedAttrRewriter ) X-ORIGIN '389 Directory Server' ) +objectClasses: ( 2.16.840.1.113730.3.2.340 NAME 'pwdPBKDF2PluginConfig' DESC 'PBKDF2 Password Storage Plugin configuration' SUP top MAY ( nsslapd-pwdPBKDF2NumIterations ) X-ORIGIN '389 Directory Server' ) diff --git a/ldap/servers/slapd/config.c b/ldap/servers/slapd/config.c index eb8c9a4fee..3db7b7840c 100644 --- a/ldap/servers/slapd/config.c +++ b/ldap/servers/slapd/config.c @@ -43,6 +43,7 @@ static char *bootstrap_plugins[] = { "dn: cn=PBKDF2-SHA512,cn=Password Storage Schemes,cn=plugins,cn=config\n" "objectclass: top\n" "objectclass: nsSlapdPlugin\n" + "objectClass: pwdPBKDF2PluginConfig\n" "cn: PBKDF2-SHA512\n" "nsslapd-pluginpath: libpwdchan-plugin\n" "nsslapd-plugininitfunc: pwdchan_pbkdf2_sha512_plugin_init\n" diff --git a/ldap/servers/slapd/fedse.c b/ldap/servers/slapd/fedse.c index 74f7e6cf99..85bcf063c8 100644 --- a/ldap/servers/slapd/fedse.c +++ b/ldap/servers/slapd/fedse.c @@ -231,6 +231,7 @@ static const char *internal_entries[] = "dn: cn=PBKDF2,cn=Password Storage Schemes,cn=plugins,cn=config\n" "objectclass: top\n" "objectclass: nsSlapdPlugin\n" + "objectClass: pwdPBKDF2PluginConfig\n" "cn: PBKDF2\n" "nsslapd-pluginpath: libpwdchan-plugin\n" "nsslapd-plugininitfunc: pwdchan_pbkdf2_plugin_init\n" @@ -244,6 +245,7 @@ static const char *internal_entries[] = "dn: cn=PBKDF2-SHA1,cn=Password Storage Schemes,cn=plugins,cn=config\n" "objectclass: top\n" "objectclass: nsSlapdPlugin\n" + "objectClass: pwdPBKDF2PluginConfig\n" "cn: PBKDF2-SHA1\n" "nsslapd-pluginpath: libpwdchan-plugin\n" "nsslapd-plugininitfunc: pwdchan_pbkdf2_sha1_plugin_init\n" @@ -257,6 +259,7 @@ static const char *internal_entries[] = "dn: cn=PBKDF2-SHA256,cn=Password Storage Schemes,cn=plugins,cn=config\n" "objectclass: top\n" "objectclass: nsSlapdPlugin\n" + "objectClass: pwdPBKDF2PluginConfig\n" "cn: PBKDF2-SHA256\n" "nsslapd-pluginpath: libpwdchan-plugin\n" "nsslapd-plugininitfunc: pwdchan_pbkdf2_sha256_plugin_init\n" diff --git a/src/cockpit/389-console/src/lib/database/globalPwp.jsx b/src/cockpit/389-console/src/lib/database/globalPwp.jsx index a757361ec3..8681584677 100644 --- a/src/cockpit/389-console/src/lib/database/globalPwp.jsx +++ b/src/cockpit/389-console/src/lib/database/globalPwp.jsx @@ -89,6 +89,16 @@ const tpr_attrs = [ "passwordtprdelayvalidfrom", ]; +const password_storage_attrs = [ + "nsslapd-pwdpbkdf2numiterations" +]; + +const PBKDF2_SCHEMES = ['pbkdf2', 'pbkdf2-sha1', 'pbkdf2-sha256', 'pbkdf2-sha512']; + +const isPBKDF2Scheme = (scheme) => { + return PBKDF2_SCHEMES.includes(scheme.toLowerCase()); +}; + export class GlobalPwPolicy extends React.Component { constructor(props) { super(props); @@ -102,6 +112,7 @@ export class GlobalPwPolicy extends React.Component { // each field, so we can loop over them to efficently // check for changes, and updating/saving the config. saveGeneralDisabled: true, + savePasswordStorageDisabled: true, saveExpDisabled: true, saveLockoutDisabled: true, saveSyntaxDisabled: true, @@ -118,6 +129,8 @@ export class GlobalPwPolicy extends React.Component { this.handleGeneralChange = this.handleGeneralChange.bind(this); this.handleSaveGeneral = this.handleSaveGeneral.bind(this); + this.handlePasswordStorageChange = this.handlePasswordStorageChange.bind(this); + this.handleSavePasswordStorage = this.handleSavePasswordStorage.bind(this); this.handleExpChange = this.handleExpChange.bind(this); this.handleSaveExp = this.handleSaveExp.bind(this); this.handleLockoutChange = this.handleLockoutChange.bind(this); @@ -127,6 +140,7 @@ export class GlobalPwPolicy extends React.Component { this.handleTPRChange = this.handleTPRChange.bind(this); this.handleSaveTPR = this.handleSaveTPR.bind(this); this.handleLoadGlobal = this.handleLoadGlobal.bind(this); + this.handleLoadPasswordStorage = this.handleLoadPasswordStorage.bind(this); // Select Typeahead this.handleSelectToggle = this.handleSelectToggle.bind(this); this.handleSelectClear = this.handleSelectClear.bind(this); @@ -145,6 +159,68 @@ export class GlobalPwPolicy extends React.Component { this.setState({ activeKey: key }); } + handlePasswordStorageChange(e) { + const value = e.target.type === 'checkbox' ? e.target.checked : e.target.value; + const attr = e.target.id.toLowerCase(); + let disableSaveBtn = true; + + for (const password_storage_attr of password_storage_attrs) { + const storageAttr = password_storage_attr.toLowerCase(); + const oldValue = String(this.state['_' + storageAttr] || ''); + const newValue = String(value || ''); + + if (attr === storageAttr && oldValue !== newValue) { + disableSaveBtn = false; + break; + } + } + + this.setState({ + [attr]: value || '', + savePasswordStorageDisabled: disableSaveBtn, + }); + } + + handleSavePasswordStorage() { + if (!isPBKDF2Scheme(this.state.passwordstoragescheme)) { + return; + } + this.setState({ + saving: true + }); + + const cmd = [ + 'dsconf', '-j', "ldapi://%2fvar%2frun%2fslapd-" + this.props.serverId + ".socket", + 'plugin', 'pwstorage-scheme', this.state.passwordstoragescheme.toLowerCase(), + 'set-num-iterations', this.state[password_storage_attrs[0]] + ]; + + log_cmd("handleSavePasswordStorage", "Saving password storage settings", cmd); + cockpit + .spawn(cmd, { superuser: true, err: "message" }) + .done(content => { + this.handleLoadGlobal(); + this.setState({ + saving: false + }); + this.props.addNotification( + "success", + _("Successfully updated number of iterations for password storage scheme") + ); + }) + .fail(err => { + const errMsg = JSON.parse(err); + this.handleLoadGlobal(); + this.setState({ + saving: false + }); + this.props.addNotification( + "error", + cockpit.format(_("Error updating number of iterations for password storage scheme - $0"), errMsg.desc) + ); + }); + } + handleGeneralChange(e) { const value = e.target.type === 'checkbox' ? e.target.checked : e.target.value; const attr = e.target.id; @@ -166,9 +242,18 @@ export class GlobalPwPolicy extends React.Component { } } - this.setState({ + // Create state update object + const stateUpdate = { [attr]: value, saveGeneralDisabled: disableSaveBtn, + }; + + this.setState(stateUpdate, () => { + // If passwordstoragescheme was changed and it's a PBKDF2 scheme, + // load the iterations value + if (attr === 'passwordstoragescheme' && isPBKDF2Scheme(value)) { + this.handleLoadPasswordStorage(true); + } }); } @@ -176,6 +261,15 @@ export class GlobalPwPolicy extends React.Component { this.setState({ saving: true }); + if (!this.state.savePasswordStorageDisabled) { + this.handleSavePasswordStorage(); + } + if (this.state.saveGeneralDisabled) { + this.setState({ + saving: false + }); + return; + } const cmd = [ 'dsconf', '-j', "ldapi://%2fvar%2frun%2fslapd-" + this.props.serverId + ".socket", @@ -594,10 +688,74 @@ export class GlobalPwPolicy extends React.Component { }); } + handleLoadPasswordStorage(skipLoading = false) { + if (!skipLoading) { + this.setState({ + loading: true + }); + } + + if (!isPBKDF2Scheme(this.state.passwordstoragescheme)) { + this.setState({ + loading: false, + 'nsslapd-pwdpbkdf2numiterations': '', + '_nsslapd-pwdpbkdf2numiterations': '' + }); + return; + } + + const cmd = [ + 'dsconf', '-j', "ldapi://%2fvar%2frun%2fslapd-" + this.props.serverId + ".socket", + 'plugin', 'pwstorage-scheme', this.state.passwordstoragescheme.toLowerCase(), + 'get-num-iterations' + ]; + + log_cmd("handleLoadPasswordStorage", "Load password storage settings", cmd); + cockpit + .spawn(cmd, { superuser: true, err: "message" }) + .done(content => { + const config = JSON.parse(content); + const attrs = config.attrs; + + const stateUpdates = { + 'nsslapd-pwdpbkdf2numiterations': '', + '_nsslapd-pwdpbkdf2numiterations': '' + }; + + if (!skipLoading) { + stateUpdates["loading"] = false + } + password_storage_attrs.forEach(attr => { + const attrLower = attr.toLowerCase(); + const attrValue = attrs[attr] || attrs[attrLower]; + + if (attrValue && attrValue[0]) { + stateUpdates[attrLower] = attrValue[0]; + stateUpdates['_' + attrLower] = attrValue[0]; + } + }); + + this.setState(stateUpdates); + }) + .fail(err => { + const errMsg = JSON.parse(err); + this.setState({ + loading: false, + 'nsslapd-pwdpbkdf2numiterations': '', + '_nsslapd-pwdpbkdf2numiterations': '' + }); + this.props.addNotification( + "error", + cockpit.format(_("Error loading password storage settings - $0"), errMsg.desc) + ); + }); + } + handleLoadGlobal() { this.setState({ loading: true }); + const cmd = [ "dsconf", "-j", "ldapi://%2fvar%2frun%2fslapd-" + this.props.serverId + ".socket", "config", "get" @@ -700,6 +858,7 @@ export class GlobalPwPolicy extends React.Component { loaded: true, loading: false, saveGeneralDisabled: true, + savePasswordStorageDisabled: true, saveUserDisabled: true, saveExpDisabled: true, saveLockoutDisabled: true, @@ -795,8 +954,10 @@ export class GlobalPwPolicy extends React.Component { _passwordtprmaxuse: attrs.passwordtprmaxuse[0], _passwordtprdelayexpireat: attrs.passwordtprdelayexpireat[0], _passwordtprdelayvalidfrom: attrs.passwordtprdelayvalidfrom[0], - }), this.props.enableTree() - ); + }), () => { + this.props.enableTree(); + this.handleLoadPasswordStorage(); + }); }) .fail(err => { const errMsg = JSON.parse(err); @@ -1385,6 +1546,25 @@ export class GlobalPwPolicy extends React.Component { + {isPBKDF2Scheme(this.state.passwordstoragescheme) && ( + + + {_("PBKDF2 Iterations")} + + + { + this.handlePasswordStorageChange(e); + }} + /> + + + )} @@ -1439,7 +1619,7 @@ export class GlobalPwPolicy extends React.Component {