Skip to content

Commit

Permalink
pam_userdb: migrate backend database
Browse files Browse the repository at this point in the history
pam_userdb module changed its backend database technology from lidb to
gdbm for RHEL10. This requires a set of leapp actors to perform the
database migration automatically when upgrading to RHEL10:

* ScanPamUserDB takes care of scanning the PAM service folder to detect
  whether pam_userdb is used and the location of the database in use.
  This information is stored in a model.

* CheckPamUserDB checks the databases reported by ScanPamUserDB and
  prints a report about them.

* ConvertPamUserDB checks the databases reported by ScanPamUserDB and
  converts them to GDBM format.

* RemoveOldPamUserDB checks the databases reported by ScanPamUserDB and
  removes them.

All these actors include unit-tests.

Finally, there's also a spec file change to add `libdb-utils` dependency
as it is required to convert pam_userdb databases from BerkeleyDB to
GDBM.

Signed-off-by: Iker Pedrosa <[email protected]>
  • Loading branch information
ikerexxe authored and matejmatuska committed Oct 1, 2024
1 parent 88e13fb commit 658700d
Show file tree
Hide file tree
Showing 17 changed files with 359 additions and 0 deletions.
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.
"""

0 comments on commit 658700d

Please sign in to comment.