diff --git a/tests/system/data/test_grpconv/group_old_format b/tests/system/data/test_grpconv/group_old_format new file mode 100644 index 0000000000..0ee23302e5 --- /dev/null +++ b/tests/system/data/test_grpconv/group_old_format @@ -0,0 +1,3 @@ +root:*:0: +daemon:*:1: +tgroup1:$1$yQnIAZWV$gDAMB2IkqaONgrQiRdo4y.:1001: diff --git a/tests/system/data/test_grpunconv/group b/tests/system/data/test_grpunconv/group new file mode 100644 index 0000000000..c5cb28c9b0 --- /dev/null +++ b/tests/system/data/test_grpunconv/group @@ -0,0 +1,3 @@ +root:x:0: +daemon:x:1: +tgroup1:x:1001: diff --git a/tests/system/data/test_grpunconv/gshadow b/tests/system/data/test_grpunconv/gshadow new file mode 100644 index 0000000000..ebd6e1a559 --- /dev/null +++ b/tests/system/data/test_grpunconv/gshadow @@ -0,0 +1,3 @@ +root:*:root: +daemon:*:: +tgroup1:$1$yQnIAZWV$gDAMB2IkqaONgrQiRdo4y.:: diff --git a/tests/system/data/test_pwconv/passwd_old_format b/tests/system/data/test_pwconv/passwd_old_format new file mode 100644 index 0000000000..0b2f398039 --- /dev/null +++ b/tests/system/data/test_pwconv/passwd_old_format @@ -0,0 +1,3 @@ +root:$1$NBLBLIXb$WUgojj1bNuxWEADQGt1m9.:0:0:root:/root:/bin/bash +daemon:*:1:1:daemon:/usr/sbin:/bin/sh +tuser1:$1$yQnIAZWV$gDAMB2IkqaONgrQiRdo4y.:1001:1001::/home/tuser1:/bin/bash diff --git a/tests/system/data/test_pwunconv/passwd b/tests/system/data/test_pwunconv/passwd new file mode 100644 index 0000000000..0eb290e1f9 --- /dev/null +++ b/tests/system/data/test_pwunconv/passwd @@ -0,0 +1,3 @@ +root:x:0:0:root:/root:/bin/bash +daemon:x:1:1:daemon:/usr/sbin:/bin/sh +tuser1:x:1001:1001::/home/tuser1:/bin/bash diff --git a/tests/system/data/test_pwunconv/shadow b/tests/system/data/test_pwunconv/shadow new file mode 100644 index 0000000000..e5adac1e15 --- /dev/null +++ b/tests/system/data/test_pwunconv/shadow @@ -0,0 +1,3 @@ +root:$1$NBLBLIXb$WUgojj1bNuxWEADQGt1m9.:19999:0:99999:7::: +daemon:*:19999:0:99999:7::: +tuser1:$1$yQnIAZWV$gDAMB2IkqaONgrQiRdo4y.:19999:0:99999:7::: diff --git a/tests/system/framework/roles/shadow.py b/tests/system/framework/roles/shadow.py index 2317680292..1fce007c0c 100644 --- a/tests/system/framework/roles/shadow.py +++ b/tests/system/framework/roles/shadow.py @@ -542,3 +542,55 @@ def vipw(self, *args, editor_script: str | None = None) -> ProcessResult: self.host.discard_file(file_to_discard) return result + + def pwconv(self, *args) -> ProcessResult: + """ + Convert passwd file to shadow password format. + """ + cmd_args = " ".join(args) if args else "" + self.logger.info(f"Converting passwd file to shadow format on {self.host.hostname}") + cmd = self.host.conn.run(f"pwconv {cmd_args}", log_level=ProcessLogLevel.Error) + + self.host.discard_file("/etc/passwd") + self.host.discard_file("/etc/shadow") + + return cmd + + def pwunconv(self, *args) -> ProcessResult: + """ + Convert shadow password format back to passwd file. + """ + cmd_args = " ".join(args) if args else "" + self.logger.info(f"Converting shadow format back to passwd on {self.host.hostname}") + cmd = self.host.conn.run(f"pwunconv {cmd_args}", log_level=ProcessLogLevel.Error) + + self.host.discard_file("/etc/passwd") + self.host.discard_file("/etc/shadow") + + return cmd + + def grpconv(self, *args) -> ProcessResult: + """ + Convert group file to shadow group format. + """ + cmd_args = " ".join(args) if args else "" + self.logger.info(f"Converting group file to shadow format on {self.host.hostname}") + cmd = self.host.conn.run(f"grpconv {cmd_args}", log_level=ProcessLogLevel.Error) + + self.host.discard_file("/etc/group") + self.host.discard_file("/etc/gshadow") + + return cmd + + def grpunconv(self, *args) -> ProcessResult: + """ + Convert shadow group format back to group file. + """ + cmd_args = " ".join(args) if args else "" + self.logger.info(f"Converting shadow group format back to group on {self.host.hostname}") + cmd = self.host.conn.run(f"grpunconv {cmd_args}", log_level=ProcessLogLevel.Error) + + self.host.discard_file("/etc/group") + self.host.discard_file("/etc/gshadow") + + return cmd diff --git a/tests/system/requirements.txt b/tests/system/requirements.txt index a1d922d8ed..bfd4e03f65 100644 --- a/tests/system/requirements.txt +++ b/tests/system/requirements.txt @@ -2,5 +2,5 @@ flaky jc passlib pytest -git+https://github.com/next-actions/pytest-mh@1.0.21 +git+https://github.com/next-actions/pytest-mh@1.0.30 git+https://github.com/next-actions/pytest-ticket diff --git a/tests/system/tests/test_grpconv.py b/tests/system/tests/test_grpconv.py new file mode 100644 index 0000000000..cafd8efeba --- /dev/null +++ b/tests/system/tests/test_grpconv.py @@ -0,0 +1,70 @@ +""" +Test grpconv +""" + +from __future__ import annotations + +import re +from pathlib import Path + +import pytest + +from framework.misc import shadow_password_pattern +from framework.roles.shadow import Shadow +from framework.topology import KnownTopology + + +@pytest.mark.topology(KnownTopology.Shadow) +@pytest.mark.builtwith(shadow="gshadow") +def test_grpconv__basic_conversion(shadow: Shadow): + """ + :title: Basic group to shadow conversion + :setup: + 1. Remove existing group and gshadow files + 2. Copy group file with old format (passwords in group attribute) + :steps: + 1. Convert group file + 2. Check group entries + 3. Check gshadow file exists with correct permissions + 4. Check gshadow entries + :expectedresults: + 1. group file converted successfully + 2. group entriy exist with correct passwd field + 3. gshadow file exists with correct permissions + 4. gshadow entries exist with correct attributes + :customerscenario: False + """ + shadow.host.fs.rm("/etc/group") + shadow.host.fs.rm("/etc/gshadow") + + data_file = Path(__file__).parent.parent / "data" / "test_grpconv" / "group_old_format" + shadow.host.fs.upload(str(data_file.resolve()), "/etc/group") + + shadow.grpconv() + + group_entry = shadow.tools.getent.group("tgroup1") + assert group_entry is not None, "tgroup1 should exist in group" + assert group_entry.name == "tgroup1", "Incorrect groupname" + assert group_entry.password == "x", "tgroup1 group should have 'x' after grpconv" + assert group_entry.gid == 1001, "Incorrect GID" + + assert shadow.host.fs.exists("/etc/gshadow"), "/etc/gshadow should exist" + gshadow_stat = shadow.host.fs.stat("/etc/gshadow") + assert gshadow_stat.access == "0400", f"gshadow file should have 0400 permissions, got {gshadow_stat.access}" + assert gshadow_stat.user == "root", f"gshadow file should be owned by root, got {gshadow_stat.user}" + + gshadow_root = shadow.tools.getent.gshadow("root") + assert gshadow_root is not None, "root should exist in gshadow" + assert gshadow_root.name == "root", "Incorrect groupname" + assert gshadow_root.password == "*", "Incorrect password" + + gshadow_daemon = shadow.tools.getent.gshadow("daemon") + assert gshadow_daemon is not None, "daemon should exist in gshadow" + assert gshadow_daemon.name == "daemon", "Incorrect groupname" + assert gshadow_daemon.password == "*", "Incorrect password" + + gshadow_tgroup1 = shadow.tools.getent.gshadow("tgroup1") + assert gshadow_tgroup1 is not None, "tgroup1 should exist in gshadow" + assert gshadow_tgroup1.name == "tgroup1", "Incorrect groupname" + assert gshadow_tgroup1.password is not None, "Password should not be None" + assert re.match(shadow_password_pattern(), gshadow_tgroup1.password), "Incorrect password" diff --git a/tests/system/tests/test_grpunconv.py b/tests/system/tests/test_grpunconv.py new file mode 100644 index 0000000000..b00d4ff2e1 --- /dev/null +++ b/tests/system/tests/test_grpunconv.py @@ -0,0 +1,65 @@ +""" +Test grpunconv +""" + +from __future__ import annotations + +import re +from pathlib import Path + +import pytest + +from framework.misc import shadow_password_pattern +from framework.roles.shadow import Shadow +from framework.topology import KnownTopology + + +@pytest.mark.topology(KnownTopology.Shadow) +@pytest.mark.builtwith(shadow="gshadow") +def test_grpunconv__basic_conversion(shadow: Shadow): + """ + :title: Basic shadow to group conversion + :setup: + 1. Remove existing group and gshadow files + 2. Copy group and gshadow files + :steps: + 1. Convert shadow format back to group format + 2. Check gshadow file is removed + 3. Check group entries + :expectedresults: + 1. shadow format converted successfully + 2. gshadow file is removed + 3. group entries exist with correct attributes + :customerscenario: False + """ + shadow.host.fs.rm("/etc/group") + shadow.host.fs.rm("/etc/gshadow") + + test_dir = Path(__file__).parent.parent + group_file = test_dir / "data" / "test_grpunconv" / "group" + shadow.host.fs.upload(str(group_file.resolve()), "/etc/group") + gshadow_file = test_dir / "data" / "test_grpunconv" / "gshadow" + shadow.host.fs.upload(str(gshadow_file.resolve()), "/etc/gshadow") + + shadow.grpunconv() + + assert not shadow.host.fs.exists("/etc/gshadow"), "/etc/gshadow should not exist after grpunconv" + + group_root = shadow.tools.getent.group("root") + assert group_root is not None, "root should exist in group" + assert group_root.name == "root", "Incorrect groupname" + assert group_root.password == "*", "Incorrect password" + assert group_root.gid == 0, "Incorrect GID" + + group_daemon = shadow.tools.getent.group("daemon") + assert group_daemon is not None, "daemon should exist in group" + assert group_daemon.name == "daemon", "Incorrect groupname" + assert group_daemon.password == "*", "Incorrect password" + assert group_daemon.gid == 1, "Incorrect GID" + + group_tgroup1 = shadow.tools.getent.group("tgroup1") + assert group_tgroup1 is not None, "tgroup1 should exist in group" + assert group_tgroup1.name == "tgroup1", "Incorrect groupname" + assert group_tgroup1.password is not None, "Password should not be None" + assert re.match(shadow_password_pattern(), group_tgroup1.password), "Incorrect password" + assert group_tgroup1.gid == 1001, "Incorrect GID" diff --git a/tests/system/tests/test_pwconv.py b/tests/system/tests/test_pwconv.py new file mode 100644 index 0000000000..d83a1c3692 --- /dev/null +++ b/tests/system/tests/test_pwconv.py @@ -0,0 +1,77 @@ +""" +Test pwconv +""" + +from __future__ import annotations + +import re +from pathlib import Path + +import pytest + +from framework.misc import days_since_epoch, shadow_password_pattern +from framework.roles.shadow import Shadow +from framework.topology import KnownTopology + + +@pytest.mark.topology(KnownTopology.Shadow) +def test_pwconv__basic_conversion(shadow: Shadow): + """ + :title: Basic passwd to shadow conversion + :setup: + 1. Remove existing passwd and shadow files + 2. Copy passwd file with old format (passwords in passwd attribute) + :steps: + 1. Convert passwd file + 2. Check passwd entry + 3. Check shadow file exists with correct permissions + 4. Check shadow entries + :expectedresults: + 1. passwd file converted successfully + 2. passwd entry exists with correct passwd field + 3. shadow file exists with correct permissions + 4. shadow entries exist with correct attributes + :customerscenario: False + """ + shadow.host.fs.rm("/etc/passwd") + shadow.host.fs.rm("/etc/shadow") + + data_file = Path(__file__).parent.parent / "data" / "test_pwconv" / "passwd_old_format" + shadow.host.fs.upload(str(data_file.resolve()), "/etc/passwd") + + shadow.pwconv() + + passwd_entry = shadow.tools.getent.passwd("tuser1") + assert passwd_entry is not None, "tuser1 should still exist in passwd" + assert passwd_entry.password == "x", "tuser1 passwd should have 'x' after pwconv" + + assert shadow.host.fs.exists("/etc/shadow"), "/etc/shadow should exist" + shadow_stat = shadow.host.fs.stat("/etc/shadow") + assert shadow_stat.access == "0400", f"Shadow file should have 0400 permissions, got {shadow_stat.access}" + assert shadow_stat.user == "root", f"Shadow file should be owned by root, got {shadow_stat.user}" + + shadow_root = shadow.tools.getent.shadow("root") + assert shadow_root is not None, "User should be found" + assert shadow_root.name == "root", "Incorrect username" + assert shadow_root.password is not None, "Password should not be None" + assert re.match(shadow_password_pattern(), shadow_root.password), "Incorrect password" + assert shadow_root.last_changed == days_since_epoch(), "Incorrect last changed" + assert shadow_root.max_days == 99999, "Incorrect max days" + assert shadow_root.warn_days == 7, "Incorrect warn days" + + shadow_daemon = shadow.tools.getent.shadow("daemon") + assert shadow_daemon is not None, "User should be found" + assert shadow_daemon.name == "daemon", "Incorrect username" + assert shadow_daemon.password == "*", "Incorrect password" + assert shadow_daemon.last_changed == days_since_epoch(), "Incorrect last changed" + assert shadow_daemon.max_days == 99999, "Incorrect max days" + assert shadow_daemon.warn_days == 7, "Incorrect warn days" + + shadow_tuser1 = shadow.tools.getent.shadow("tuser1") + assert shadow_tuser1 is not None, "User should be found" + assert shadow_tuser1.name == "tuser1", "Incorrect username" + assert shadow_tuser1.password is not None, "Password should not be None" + assert re.match(shadow_password_pattern(), shadow_tuser1.password), "Incorrect password" + assert shadow_tuser1.last_changed == days_since_epoch(), "Incorrect last changed" + assert shadow_tuser1.max_days == 99999, "Incorrect max days" + assert shadow_tuser1.warn_days == 7, "Incorrect warn days" diff --git a/tests/system/tests/test_pwunconv.py b/tests/system/tests/test_pwunconv.py new file mode 100644 index 0000000000..586dfc44ef --- /dev/null +++ b/tests/system/tests/test_pwunconv.py @@ -0,0 +1,74 @@ +""" +Test pwunconv +""" + +from __future__ import annotations + +import re +from pathlib import Path + +import pytest + +from framework.misc import shadow_password_pattern +from framework.roles.shadow import Shadow +from framework.topology import KnownTopology + + +@pytest.mark.topology(KnownTopology.Shadow) +def test_pwunconv__basic_conversion(shadow: Shadow): + """ + :title: Basic shadow to passwd conversion + :setup: + 1. Remove existing passwd and shadow files + 2. Copy passwd and shadow files + :steps: + 1. Convert shadow format back to passwd format + 2. Check shadow file is removed + 3. Check passwd entries + :expectedresults: + 1. shadow format converted successfully + 2. shadow file is removed + 3. passwd entries exist with correct attributes + :customerscenario: False + """ + shadow.host.fs.rm("/etc/passwd") + shadow.host.fs.rm("/etc/shadow") + + test_dir = Path(__file__).parent.parent + passwd_file = test_dir / "data" / "test_pwunconv" / "passwd" + shadow.host.fs.upload(str(passwd_file.resolve()), "/etc/passwd") + shadow_file = test_dir / "data" / "test_pwunconv" / "shadow" + shadow.host.fs.upload(str(shadow_file.resolve()), "/etc/shadow") + + shadow.pwunconv() + + assert not shadow.host.fs.exists("/etc/shadow"), "/etc/shadow should not exist after pwunconv" + + passwd_root = shadow.tools.getent.passwd("root") + assert passwd_root is not None, "root should exist in passwd" + assert passwd_root.name == "root", "Incorrect username" + assert passwd_root.password is not None, "Password should not be None" + assert re.match(shadow_password_pattern(), passwd_root.password), "Incorrect password" + assert passwd_root.uid == 0, "Incorrect UID" + assert passwd_root.gid == 0, "Incorrect GID" + assert passwd_root.home == "/root", "Incorrect home" + assert passwd_root.shell == "/bin/bash", "Incorrect shell" + + passwd_daemon = shadow.tools.getent.passwd("daemon") + assert passwd_daemon is not None, "daemon should exist in passwd" + assert passwd_daemon.name == "daemon", "Incorrect username" + assert passwd_daemon.password == "*", "Incorrect password" + assert passwd_daemon.uid == 1, "Incorrect UID" + assert passwd_daemon.gid == 1, "Incorrect GID" + assert passwd_daemon.home == "/usr/sbin", "Incorrect home" + assert passwd_daemon.shell == "/bin/sh", "Incorrect shell" + + passwd_tuser1 = shadow.tools.getent.passwd("tuser1") + assert passwd_tuser1 is not None, "tuser1 should exist in passwd" + assert passwd_tuser1.name == "tuser1", "Incorrect username" + assert passwd_tuser1.password is not None, "Password should not be None" + assert re.match(shadow_password_pattern(), passwd_tuser1.password), "Incorrect password" + assert passwd_tuser1.uid == 1001, "Incorrect UID" + assert passwd_tuser1.gid == 1001, "Incorrect GID" + assert passwd_tuser1.home == "/home/tuser1", "Incorrect home" + assert passwd_tuser1.shell == "/bin/bash", "Incorrect shell"