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 5812ebb
Show file tree
Hide file tree
Showing 7 changed files with 335 additions and 102 deletions.
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 5812ebb

Please sign in to comment.