Skip to content

Commit

Permalink
Fix comments and improve processing for each plugin
Browse files Browse the repository at this point in the history
Add tests to storage_test.py
It has debug logs for now
  • Loading branch information
droideck committed Dec 17, 2024
1 parent 4823efe commit 7451322
Show file tree
Hide file tree
Showing 11 changed files with 335 additions and 106 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 0 additions & 1 deletion dirsrvtests/tests/suites/openldap_2_389/migrate_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
#
import pytest
from lib389.topologies import topology_st
from lib389.password_plugins import PBKDF2Plugin
from lib389.password_plugins import PBKDF2SHA512Plugin
from lib389.utils import ds_is_older

pytestmark = pytest.mark.tier1
Expand All @@ -35,18 +35,18 @@ def test_pbkdf2_upgrade(topology_st):
"""
# Remove the pbkdf2 plugin config
p1 = PBKDF2Plugin(topology_st.standalone)
p1 = PBKDF2SHA512Plugin(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 = PBKDF2SHA512Plugin(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 = PBKDF2SHA512Plugin(topology_st.standalone)
assert(p3.exists())


162 changes: 153 additions & 9 deletions dirsrvtests/tests/suites/pwp_storage/storage_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,49 @@

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


@pytest.fixture(scope="function")
def test_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'test_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):
"""
Will set storage schema and create user.
Expand Down Expand Up @@ -106,27 +141,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.
: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') == 'PBKDF2-SHA512'
assert topo.standalone.status()
user.delete()

Expand Down Expand Up @@ -160,6 +195,115 @@ def test_check_ssha512(topo):
user.delete()


@pytest.mark.parametrize('plugin_class,plugin_name', [
(PBKDF2SHA1Plugin, 'PBKDF2-SHA1'),
(PBKDF2SHA256Plugin, 'PBKDF2-SHA256'),
(PBKDF2SHA512Plugin, 'PBKDF2-SHA512')
])
def test_pbkdf2_rounds_configuration(topo, test_user, plugin_class, plugin_name):
"""Test PBKDF2 rounds configuration for different variants"""
try:
# Enable plugin logging
topo.standalone.config.loglevel((ErrorLog.DEFAULT, ErrorLog.PLUGIN))

# Configure plugin
plugin = plugin_class(topo.standalone)
plugin.enable()

# Test rounds configuration
test_rounds = 20000
plugin.set_rounds(test_rounds)
# Restart after changing rounds
topo.standalone.restart()
assert plugin.get_rounds() == test_rounds

# Verify invalid rounds are rejected
with pytest.raises(ValueError):
plugin.set_rounds(5000) # Too low
with pytest.raises(ValueError):
plugin.set_rounds(2000000) # Too high

# Configure as password storage scheme
topo.standalone.config.replace('passwordStorageScheme', plugin_name)
topo.standalone.deleteErrorLogs()

TEST_PASSWORD = 'Secret123'
test_user.set('userPassword', TEST_PASSWORD)

# Verify password hash format
pwd_hash = test_user.get_attr_val_utf8('userPassword')
assert pwd_hash.startswith('{' + plugin_name.upper() + '}')

# Test authentication
topo.standalone.simple_bind_s(test_user.dn, TEST_PASSWORD)
topo.standalone.simple_bind_s(DN_DM, PASSWORD)

# Restart to flush logs
topo.standalone.restart()

# Verify logs
assert topo.standalone.searchErrorsLog(f'PBKDF2 rounds for {plugin_name.split("-")[1]} is {test_rounds}')

finally:
# Always rebind as Directory Manager
topo.standalone.simple_bind_s(DN_DM, PASSWORD)


@pytest.mark.parametrize('plugin_class,plugin_name', [
(PBKDF2SHA1Plugin, 'PBKDF2-SHA1'),
(PBKDF2SHA256Plugin, 'PBKDF2-SHA256'),
(PBKDF2SHA512Plugin, 'PBKDF2-SHA512')
])
def test_pbkdf2_rounds_modification(topo, test_user, plugin_class, plugin_name):
"""Test PBKDF2 rounds modification behavior"""
try:
# Enable plugin logging
topo.standalone.config.loglevel((ErrorLog.DEFAULT, ErrorLog.PLUGIN))

plugin = plugin_class(topo.standalone)
plugin.enable()

# Set initial rounds and restart
initial_rounds = 15000
plugin.set_rounds(initial_rounds)
topo.standalone.restart()

# Configure as password storage scheme
topo.standalone.config.replace('passwordStorageScheme', plugin_name)
topo.standalone.deleteErrorLogs()

INITIAL_PASSWORD = 'Initial123'
NEW_PASSWORD = 'New123'

test_user.set('userPassword', INITIAL_PASSWORD)

# Modify rounds and restart
new_rounds = 25000
plugin.set_rounds(new_rounds)
topo.standalone.restart()

# Verify old password still works
topo.standalone.simple_bind_s(test_user.dn, INITIAL_PASSWORD)
topo.standalone.simple_bind_s(DN_DM, PASSWORD)

# Set new password
test_user.set('userPassword', NEW_PASSWORD)

# Verify new password works
topo.standalone.simple_bind_s(test_user.dn, NEW_PASSWORD)
topo.standalone.simple_bind_s(DN_DM, PASSWORD)

# Restart to flush logs
topo.standalone.restart()

# Verify logs show new rounds value
assert topo.standalone.searchErrorsLog(f'PBKDF2 rounds for {plugin_name.split("-")[1]} is {new_rounds}')

finally:
# Always rebind as Directory Manager
topo.standalone.simple_bind_s(DN_DM, PASSWORD)


if __name__ == "__main__":
CURRENT_FILE = os.path.realpath(__file__)
pytest.main("-s -v %s" % CURRENT_FILE)
1 change: 1 addition & 0 deletions src/cockpit/389-console/src/lib/server/settings.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,7 @@ export class ServerSettings extends React.Component {
};

this.options = [
{ value: 'PBKDF2-SHA512', label: 'PBKDF2-SHA512', disabled: false },
{ value: 'PBKDF2_SHA256', label: 'PBKDF2_SHA256', disabled: false },
{ value: 'SSHA512', label: 'SSHA512', disabled: false },
{ value: 'SSHA384', label: 'SSHA384', disabled: false },
Expand Down
58 changes: 54 additions & 4 deletions src/lib389/lib389/password_plugins.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,6 @@ def __init__(self, instance, dn=None):
# We'll mark this protected, and people can just disable the plugins.
self._protected = True

class PBKDF2Plugin(PasswordPlugin):
def __init__(self, instance, dn="cn=PBKDF2-SHA256,cn=Password Storage Schemes,cn=plugins,cn=config"):
super(PBKDF2Plugin, self).__init__(instance, dn)


class SSHA512Plugin(PasswordPlugin):
def __init__(self, instance, dn=f'cn=SSHA512,{DN_PWDSTORAGE_SCHEMES}'):
Expand All @@ -56,6 +52,60 @@ def __init__(self, instance, dn=f'cn=SSHA,{DN_PWDSTORAGE_SCHEMES}'):
super(SSHAPlugin, self).__init__(instance, dn)


class PBKDF2BasePlugin(PasswordPlugin):
"""Base class for all PBKDF2 variants"""
def __init__(self, instance, dn):
super(PBKDF2BasePlugin, self).__init__(instance, dn)
self._create_objectclasses.append('pwdPBKDF2PluginConfig')

def set_rounds(self, rounds):
"""Set the number of rounds for PBKDF2 hashing (requires restart)
:param rounds: Number of rounds (10000-1000000)
:type rounds: int
"""
rounds = int(rounds)
if rounds < 10000 or rounds > 1000000:
raise ValueError("PBKDF2 rounds must be between 10000 and 1000000")
self.replace('nsslapd-pwdPBKDF2Rounds', str(rounds))

def get_rounds(self):
"""Get the current number of rounds
:returns: Current rounds setting or 10000 if not set
:rtype: int
"""
rounds = self.get_attr_val_utf8('nsslapd-pwdPBKDF2Rounds')
return int(rounds) if rounds else 10000


class PBKDF2SHA1Plugin(PBKDF2BasePlugin):
"""PBKDF2-SHA1 password storage scheme"""
def __init__(self, instance, dn=f'cn=PBKDF2-SHA1,{DN_PWDSTORAGE_SCHEMES}'):
super(PBKDF2SHA1Plugin, self).__init__(instance, dn)
self._plugin_properties.update({
'nsslapd-pluginInitfunc': 'pwdchan_pbkdf2_sha1_init'
})


class PBKDF2SHA256Plugin(PBKDF2BasePlugin):
"""PBKDF2-SHA256 password storage scheme"""
def __init__(self, instance, dn=f'cn=PBKDF2-SHA256,{DN_PWDSTORAGE_SCHEMES}'):
super(PBKDF2SHA256Plugin, self).__init__(instance, dn)
self._plugin_properties.update({
'nsslapd-pluginInitfunc': 'pwdchan_pbkdf2_sha256_init'
})


class PBKDF2SHA512Plugin(PBKDF2BasePlugin):
"""PBKDF2-SHA512 password storage scheme"""
def __init__(self, instance, dn=f'cn=PBKDF2-SHA512,{DN_PWDSTORAGE_SCHEMES}'):
super(PBKDF2SHA512Plugin, self).__init__(instance, dn)
self._plugin_properties.update({
'nsslapd-pluginInitfunc': 'pwdchan_pbkdf2_sha512_init'
})


class PasswordPlugins(Plugins):
def __init__(self, instance):
super(PasswordPlugins, self).__init__(instance=instance)
Expand Down
Loading

0 comments on commit 7451322

Please sign in to comment.