Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update pam_userdb database backend for RHEL10 #1289

Merged
merged 1 commit into from
Oct 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions packaging/leapp-repository.spec
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,12 @@ Requires: dracut
Requires: NetworkManager-libnm
Requires: python3-gobject-base

%endif

%if 0%{?rhel} && 0%{?rhel} == 9
############# RHEL 9 dependencies (when the source system is RHEL 9) ##########
# Required to convert pam_userdb database from BerkeleyDB to GDBM
Requires: libdb-utils
%endif
##################################################
# end requirement
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
from leapp.actors import Actor
from leapp.libraries.actor import checkpamuserdb
from leapp.models import PamUserDbLocation, Report
from leapp.tags import ChecksPhaseTag, IPUWorkflowTag


class CheckPamUserDb(Actor):
"""
Create report with the location of pam_userdb databases
"""

name = 'check_pam_user_db'
consumes = (PamUserDbLocation,)
produces = (Report,)
tags = (ChecksPhaseTag, IPUWorkflowTag)

def process(self):
checkpamuserdb.process()
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
from leapp import reporting
from leapp.exceptions import StopActorExecutionError
from leapp.libraries.stdlib import api
from leapp.models import PamUserDbLocation

FMT_LIST_SEPARATOR = "\n - "


def process():
msg = next(api.consume(PamUserDbLocation), None)
if not msg:
raise StopActorExecutionError('Expected PamUserDbLocation, but got None')

if msg.locations:
reporting.create_report([
reporting.Title('pam_userdb databases will be converted to GDBM'),
reporting.Summary(
'On RHEL 10, GDMB is used by pam_userdb as it\'s backend database,'
' replacing BerkeleyDB. Existing pam_userdb databases will be'
' converted to GDBM. The following databases will be converted:'
'{sep}{locations}'.format(sep=FMT_LIST_SEPARATOR, locations=FMT_LIST_SEPARATOR.join(msg.locations))),
reporting.Severity(reporting.Severity.INFO),
reporting.Groups([reporting.Groups.SECURITY, reporting.Groups.AUTHENTICATION])
])
else:
api.current_logger().debug(
'No pam_userdb databases were located, thus nothing will be converted'
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import pytest

from leapp import reporting
from leapp.exceptions import StopActorExecutionError
from leapp.libraries.actor import checkpamuserdb
from leapp.libraries.common.testutils import create_report_mocked, logger_mocked
from leapp.libraries.stdlib import api
from leapp.models import PamUserDbLocation


def test_process_no_msg(monkeypatch):
def consume_mocked(*args, **kwargs):
yield None

monkeypatch.setattr(api, 'consume', consume_mocked)

with pytest.raises(StopActorExecutionError):
checkpamuserdb.process()


def test_process_no_location(monkeypatch):
def consume_mocked(*args, **kwargs):
yield PamUserDbLocation(locations=[])

monkeypatch.setattr(api, 'current_logger', logger_mocked())
monkeypatch.setattr(api, 'consume', consume_mocked)

checkpamuserdb.process()
assert (
'No pam_userdb databases were located, thus nothing will be converted'
in api.current_logger.dbgmsg
)


def test_process_locations(monkeypatch):
def consume_mocked(*args, **kwargs):
yield PamUserDbLocation(locations=['/tmp/db1', '/tmp/db2'])

monkeypatch.setattr(reporting, "create_report", create_report_mocked())
monkeypatch.setattr(api, 'consume', consume_mocked)

checkpamuserdb.process()
assert reporting.create_report.called == 1
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
from leapp.actors import Actor
from leapp.libraries.actor import convertpamuserdb
from leapp.models import PamUserDbLocation
from leapp.tags import IPUWorkflowTag, PreparationPhaseTag


class ConvertPamUserDb(Actor):
"""
Convert the pam_userdb databases to GDBM
"""

name = 'convert_pam_user_db'
consumes = (PamUserDbLocation,)
produces = ()
tags = (PreparationPhaseTag, IPUWorkflowTag)

def process(self):
convertpamuserdb.process()
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
from leapp.exceptions import StopActorExecutionError
from leapp.libraries.stdlib import api, CalledProcessError, run
from leapp.models import PamUserDbLocation


def _convert_db(db_path):
cmd = ['db_converter', '--src', f'{db_path}.db', '--dest', f'{db_path}.gdbm']
try:
run(cmd)
except (CalledProcessError, OSError) as e:
# As the db_converter does not remove the original DB after conversion or upon failure,
# interrupt the upgrade, keeping the original DBs.
# If all DBs are successfully converted, the leftover DBs are removed in the removeoldpamuserdb actor.
raise StopActorExecutionError(
'Cannot convert pam_userdb database.',
details={'details': '{}: {}'.format(str(e), e.stderr)}
)


def process():
msg = next(api.consume(PamUserDbLocation), None)
if not msg:
raise StopActorExecutionError('Expected PamUserDbLocation, but got None')

if msg.locations:
for location in msg.locations:
_convert_db(location)
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import os

import pytest

from leapp.exceptions import StopActorExecutionError
from leapp.libraries.actor import convertpamuserdb
from leapp.libraries.common.testutils import logger_mocked
from leapp.libraries.stdlib import api, CalledProcessError

CUR_DIR = os.path.dirname(os.path.abspath(__file__))


def test_convert_db_success(monkeypatch):
location = os.path.join(CUR_DIR, '/files/db1')

def run_mocked(cmd, **kwargs):
assert cmd == ['db_converter', '--src', f'{location}.db', '--dest', f'{location}.gdbm']

monkeypatch.setattr(api, 'current_logger', logger_mocked())
monkeypatch.setattr(convertpamuserdb, 'run', run_mocked)
convertpamuserdb._convert_db(location)
assert len(api.current_logger.errmsg) == 0


def test_convert_db_failure(monkeypatch):
location = os.path.join(CUR_DIR, '/files/db1')

def run_mocked(cmd, **kwargs):
raise CalledProcessError(
message='A Leapp Command Error occurred.',
command=cmd,
result={'exit_code': 1}
)

monkeypatch.setattr(api, 'current_logger', logger_mocked())
monkeypatch.setattr(convertpamuserdb, 'run', run_mocked)
with pytest.raises(StopActorExecutionError) as err:
convertpamuserdb._convert_db(location)
assert str(err.value) == 'Cannot convert pam_userdb database.'
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
from leapp.actors import Actor
from leapp.libraries.actor import removeoldpamuserdb
from leapp.models import PamUserDbLocation
from leapp.tags import ApplicationsPhaseTag, IPUWorkflowTag


class RemoveOldPamUserDb(Actor):
"""
Remove old pam_userdb databases
"""

name = 'remove_old_pam_user_db'
consumes = (PamUserDbLocation,)
produces = ()
tags = (ApplicationsPhaseTag, IPUWorkflowTag)

def process(self):
removeoldpamuserdb.process()
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
from leapp.exceptions import StopActorExecutionError
from leapp.libraries.stdlib import api, CalledProcessError, run
from leapp.models import PamUserDbLocation


def _remove_db(db_path):
cmd = ['rm', '-f', f'{db_path}.db']
try:
run(cmd)
except (CalledProcessError, OSError) as e:
api.current_logger().error(
'Failed to remove {}.db: {}'.format(
db_path, e
)
)


def process():
msg = next(api.consume(PamUserDbLocation), None)
if not msg:
raise StopActorExecutionError('Expected PamUserDbLocation, but got None')

if msg.locations:
for location in msg.locations:
_remove_db(location)
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import os

from leapp.libraries.actor import removeoldpamuserdb
from leapp.libraries.common.testutils import logger_mocked
from leapp.libraries.stdlib import api, CalledProcessError

CUR_DIR = os.path.dirname(os.path.abspath(__file__))


def test_remove_db_success(monkeypatch):
location = os.path.join(CUR_DIR, '/files/db1')

def run_mocked(cmd, **kwargs):
assert cmd == ['rm', '-f', f'{location}.db']

monkeypatch.setattr(api, 'current_logger', logger_mocked())
monkeypatch.setattr(removeoldpamuserdb, 'run', run_mocked)
removeoldpamuserdb._remove_db(location)
assert len(api.current_logger.errmsg) == 0


def test_remove_db_failure(monkeypatch):
location = os.path.join(CUR_DIR, '/files/db1')

def run_mocked(cmd, **kwargs):
raise CalledProcessError(
message='A Leapp Command Error occurred.',
command=cmd,
result={'exit_code': 1}
)

monkeypatch.setattr(api, 'current_logger', logger_mocked())
monkeypatch.setattr(removeoldpamuserdb, 'run', run_mocked)
removeoldpamuserdb._remove_db(location)
assert (
'Failed to remove /files/db1.db'
not in api.current_logger.errmsg
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
from leapp.actors import Actor
from leapp.libraries.actor import scanpamuserdb
from leapp.models import PamUserDbLocation
from leapp.tags import FactsPhaseTag, IPUWorkflowTag


class ScanPamUserDb(Actor):
"""
Scan the PAM service folder for the location of pam_userdb databases
"""

name = 'scan_pam_user_db'
consumes = ()
produces = (PamUserDbLocation,)
tags = (FactsPhaseTag, IPUWorkflowTag)

def process(self):
self.produce(scanpamuserdb.parse_pam_config_folder('/etc/pam.d/'))
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import os
import re

from leapp.models import PamUserDbLocation


def _parse_pam_config_file(conf_file):
with open(conf_file, 'r') as file:
for line in file:
if 'pam_userdb' in line:
match = re.search(r'db=(\S+)', line)
if match:
return match.group(1)

return None


def parse_pam_config_folder(conf_folder):
locations = set()

for file_name in os.listdir(conf_folder):
file_path = os.path.join(conf_folder, file_name)

if os.path.isfile(file_path):
location = _parse_pam_config_file(file_path)
if location is not None:
locations.add(location)

return PamUserDbLocation(locations=list(locations))
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
auth required pam_userdb.so db=/tmp/db1
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
auth required pam_env.so
auth required pam_faildelay.so delay=2000000
auth sufficient pam_fprintd.so
auth [default=1 ignore=ignore success=ok] pam_usertype.so isregular
auth [default=1 ignore=ignore success=ok] pam_localuser.so
auth required pam_userdb.so db=/tmp/db2
auth [default=1 ignore=ignore success=ok] pam_usertype.so isregular
auth sufficient pam_sss.so forward_pass
auth required pam_deny.so
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
auth sufficient pam_unix.so nullok
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import os

import pytest

from leapp.libraries.actor import scanpamuserdb

CUR_DIR = os.path.dirname(os.path.abspath(__file__))


@pytest.mark.parametrize(
"inp,exp_out",
[
("files/pam_userdb_missing", None),
("files/pam_userdb_basic", "/tmp/db1"),
("files/pam_userdb_complete", "/tmp/db2"),
],
)
def test_parse_pam_config_file(inp, exp_out):
file = scanpamuserdb._parse_pam_config_file(os.path.join(CUR_DIR, inp))
assert file == exp_out


def test_parse_pam_config_folder():
msg = scanpamuserdb.parse_pam_config_folder(os.path.join(CUR_DIR, "files/"))
assert len(msg.locations) == 2
assert "/tmp/db1" in msg.locations
assert "/tmp/db2" in msg.locations
14 changes: 14 additions & 0 deletions repos/system_upgrade/el9toel10/models/pamuserdblocation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
from leapp.models import fields, Model
from leapp.topics import SystemInfoTopic


class PamUserDbLocation(Model):
"""
Provides a list of all database files for pam_userdb
"""
topic = SystemInfoTopic

locations = fields.List(fields.String(), default=[])
"""
The list with the full path to the database files.
"""