From 658700d6424e852917b62c190dd23cbb3026b67d Mon Sep 17 00:00:00 2001 From: Iker Pedrosa Date: Mon, 5 Aug 2024 15:15:44 +0200 Subject: [PATCH 08/40] pam_userdb: migrate backend database 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 --- packaging/leapp-repository.spec | 6 +++ .../actors/pamuserdb/checkpamuserdb/actor.py | 18 ++++++++ .../libraries/checkpamuserdb.py | 28 ++++++++++++ .../tests/test_checkpamuserdb.py | 43 +++++++++++++++++++ .../pamuserdb/convertpamuserdb/actor.py | 18 ++++++++ .../libraries/convertpamuserdb.py | 27 ++++++++++++ .../tests/test_convertpamuserdb.py | 39 +++++++++++++++++ .../pamuserdb/removeoldpamuserdb/actor.py | 18 ++++++++ .../libraries/removeoldpamuserdb.py | 25 +++++++++++ .../tests/test_removeoldpamuserdb.py | 38 ++++++++++++++++ .../actors/pamuserdb/scanpamuserdb/actor.py | 18 ++++++++ .../scanpamuserdb/libraries/scanpamuserdb.py | 29 +++++++++++++ .../tests/files/pam_userdb_basic | 1 + .../tests/files/pam_userdb_complete | 9 ++++ .../tests/files/pam_userdb_missing | 1 + .../scanpamuserdb/tests/test_scanpamuserdb.py | 27 ++++++++++++ .../el9toel10/models/pamuserdblocation.py | 14 ++++++ 17 files changed, 359 insertions(+) create mode 100644 repos/system_upgrade/el9toel10/actors/pamuserdb/checkpamuserdb/actor.py create mode 100644 repos/system_upgrade/el9toel10/actors/pamuserdb/checkpamuserdb/libraries/checkpamuserdb.py create mode 100644 repos/system_upgrade/el9toel10/actors/pamuserdb/checkpamuserdb/tests/test_checkpamuserdb.py create mode 100644 repos/system_upgrade/el9toel10/actors/pamuserdb/convertpamuserdb/actor.py create mode 100644 repos/system_upgrade/el9toel10/actors/pamuserdb/convertpamuserdb/libraries/convertpamuserdb.py create mode 100644 repos/system_upgrade/el9toel10/actors/pamuserdb/convertpamuserdb/tests/test_convertpamuserdb.py create mode 100644 repos/system_upgrade/el9toel10/actors/pamuserdb/removeoldpamuserdb/actor.py create mode 100644 repos/system_upgrade/el9toel10/actors/pamuserdb/removeoldpamuserdb/libraries/removeoldpamuserdb.py create mode 100644 repos/system_upgrade/el9toel10/actors/pamuserdb/removeoldpamuserdb/tests/test_removeoldpamuserdb.py create mode 100644 repos/system_upgrade/el9toel10/actors/pamuserdb/scanpamuserdb/actor.py create mode 100644 repos/system_upgrade/el9toel10/actors/pamuserdb/scanpamuserdb/libraries/scanpamuserdb.py create mode 100644 repos/system_upgrade/el9toel10/actors/pamuserdb/scanpamuserdb/tests/files/pam_userdb_basic create mode 100644 repos/system_upgrade/el9toel10/actors/pamuserdb/scanpamuserdb/tests/files/pam_userdb_complete create mode 100644 repos/system_upgrade/el9toel10/actors/pamuserdb/scanpamuserdb/tests/files/pam_userdb_missing create mode 100644 repos/system_upgrade/el9toel10/actors/pamuserdb/scanpamuserdb/tests/test_scanpamuserdb.py create mode 100644 repos/system_upgrade/el9toel10/models/pamuserdblocation.py diff --git a/packaging/leapp-repository.spec b/packaging/leapp-repository.spec index 146afc45..0d63ba02 100644 --- a/packaging/leapp-repository.spec +++ b/packaging/leapp-repository.spec @@ -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 diff --git a/repos/system_upgrade/el9toel10/actors/pamuserdb/checkpamuserdb/actor.py b/repos/system_upgrade/el9toel10/actors/pamuserdb/checkpamuserdb/actor.py new file mode 100644 index 00000000..8fada645 --- /dev/null +++ b/repos/system_upgrade/el9toel10/actors/pamuserdb/checkpamuserdb/actor.py @@ -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() diff --git a/repos/system_upgrade/el9toel10/actors/pamuserdb/checkpamuserdb/libraries/checkpamuserdb.py b/repos/system_upgrade/el9toel10/actors/pamuserdb/checkpamuserdb/libraries/checkpamuserdb.py new file mode 100644 index 00000000..05cc71a9 --- /dev/null +++ b/repos/system_upgrade/el9toel10/actors/pamuserdb/checkpamuserdb/libraries/checkpamuserdb.py @@ -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' + ) diff --git a/repos/system_upgrade/el9toel10/actors/pamuserdb/checkpamuserdb/tests/test_checkpamuserdb.py b/repos/system_upgrade/el9toel10/actors/pamuserdb/checkpamuserdb/tests/test_checkpamuserdb.py new file mode 100644 index 00000000..2e11106b --- /dev/null +++ b/repos/system_upgrade/el9toel10/actors/pamuserdb/checkpamuserdb/tests/test_checkpamuserdb.py @@ -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 diff --git a/repos/system_upgrade/el9toel10/actors/pamuserdb/convertpamuserdb/actor.py b/repos/system_upgrade/el9toel10/actors/pamuserdb/convertpamuserdb/actor.py new file mode 100644 index 00000000..5f8525b6 --- /dev/null +++ b/repos/system_upgrade/el9toel10/actors/pamuserdb/convertpamuserdb/actor.py @@ -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() diff --git a/repos/system_upgrade/el9toel10/actors/pamuserdb/convertpamuserdb/libraries/convertpamuserdb.py b/repos/system_upgrade/el9toel10/actors/pamuserdb/convertpamuserdb/libraries/convertpamuserdb.py new file mode 100644 index 00000000..e55b4102 --- /dev/null +++ b/repos/system_upgrade/el9toel10/actors/pamuserdb/convertpamuserdb/libraries/convertpamuserdb.py @@ -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) diff --git a/repos/system_upgrade/el9toel10/actors/pamuserdb/convertpamuserdb/tests/test_convertpamuserdb.py b/repos/system_upgrade/el9toel10/actors/pamuserdb/convertpamuserdb/tests/test_convertpamuserdb.py new file mode 100644 index 00000000..46505492 --- /dev/null +++ b/repos/system_upgrade/el9toel10/actors/pamuserdb/convertpamuserdb/tests/test_convertpamuserdb.py @@ -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.' diff --git a/repos/system_upgrade/el9toel10/actors/pamuserdb/removeoldpamuserdb/actor.py b/repos/system_upgrade/el9toel10/actors/pamuserdb/removeoldpamuserdb/actor.py new file mode 100644 index 00000000..39a00855 --- /dev/null +++ b/repos/system_upgrade/el9toel10/actors/pamuserdb/removeoldpamuserdb/actor.py @@ -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() diff --git a/repos/system_upgrade/el9toel10/actors/pamuserdb/removeoldpamuserdb/libraries/removeoldpamuserdb.py b/repos/system_upgrade/el9toel10/actors/pamuserdb/removeoldpamuserdb/libraries/removeoldpamuserdb.py new file mode 100644 index 00000000..5fc4cb4d --- /dev/null +++ b/repos/system_upgrade/el9toel10/actors/pamuserdb/removeoldpamuserdb/libraries/removeoldpamuserdb.py @@ -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) diff --git a/repos/system_upgrade/el9toel10/actors/pamuserdb/removeoldpamuserdb/tests/test_removeoldpamuserdb.py b/repos/system_upgrade/el9toel10/actors/pamuserdb/removeoldpamuserdb/tests/test_removeoldpamuserdb.py new file mode 100644 index 00000000..2c1d5c75 --- /dev/null +++ b/repos/system_upgrade/el9toel10/actors/pamuserdb/removeoldpamuserdb/tests/test_removeoldpamuserdb.py @@ -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 + ) diff --git a/repos/system_upgrade/el9toel10/actors/pamuserdb/scanpamuserdb/actor.py b/repos/system_upgrade/el9toel10/actors/pamuserdb/scanpamuserdb/actor.py new file mode 100644 index 00000000..b6b35f1a --- /dev/null +++ b/repos/system_upgrade/el9toel10/actors/pamuserdb/scanpamuserdb/actor.py @@ -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/')) diff --git a/repos/system_upgrade/el9toel10/actors/pamuserdb/scanpamuserdb/libraries/scanpamuserdb.py b/repos/system_upgrade/el9toel10/actors/pamuserdb/scanpamuserdb/libraries/scanpamuserdb.py new file mode 100644 index 00000000..0f668c02 --- /dev/null +++ b/repos/system_upgrade/el9toel10/actors/pamuserdb/scanpamuserdb/libraries/scanpamuserdb.py @@ -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)) diff --git a/repos/system_upgrade/el9toel10/actors/pamuserdb/scanpamuserdb/tests/files/pam_userdb_basic b/repos/system_upgrade/el9toel10/actors/pamuserdb/scanpamuserdb/tests/files/pam_userdb_basic new file mode 100644 index 00000000..f115147b --- /dev/null +++ b/repos/system_upgrade/el9toel10/actors/pamuserdb/scanpamuserdb/tests/files/pam_userdb_basic @@ -0,0 +1 @@ +auth required pam_userdb.so db=/tmp/db1 diff --git a/repos/system_upgrade/el9toel10/actors/pamuserdb/scanpamuserdb/tests/files/pam_userdb_complete b/repos/system_upgrade/el9toel10/actors/pamuserdb/scanpamuserdb/tests/files/pam_userdb_complete new file mode 100644 index 00000000..84e40b48 --- /dev/null +++ b/repos/system_upgrade/el9toel10/actors/pamuserdb/scanpamuserdb/tests/files/pam_userdb_complete @@ -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 diff --git a/repos/system_upgrade/el9toel10/actors/pamuserdb/scanpamuserdb/tests/files/pam_userdb_missing b/repos/system_upgrade/el9toel10/actors/pamuserdb/scanpamuserdb/tests/files/pam_userdb_missing new file mode 100644 index 00000000..764947fc --- /dev/null +++ b/repos/system_upgrade/el9toel10/actors/pamuserdb/scanpamuserdb/tests/files/pam_userdb_missing @@ -0,0 +1 @@ +auth sufficient pam_unix.so nullok diff --git a/repos/system_upgrade/el9toel10/actors/pamuserdb/scanpamuserdb/tests/test_scanpamuserdb.py b/repos/system_upgrade/el9toel10/actors/pamuserdb/scanpamuserdb/tests/test_scanpamuserdb.py new file mode 100644 index 00000000..3b752d87 --- /dev/null +++ b/repos/system_upgrade/el9toel10/actors/pamuserdb/scanpamuserdb/tests/test_scanpamuserdb.py @@ -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 diff --git a/repos/system_upgrade/el9toel10/models/pamuserdblocation.py b/repos/system_upgrade/el9toel10/models/pamuserdblocation.py new file mode 100644 index 00000000..d15b2041 --- /dev/null +++ b/repos/system_upgrade/el9toel10/models/pamuserdblocation.py @@ -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. + """ -- 2.47.0