Migrate tests/unit/modules/test_linux_shadow.py to pytest

Signed-off-by: Pedro Algarvio <palgarvio@vmware.com>
This commit is contained in:
Pedro Algarvio 2023-03-15 17:50:16 +00:00 committed by Pedro Algarvio
parent 77ba11a7da
commit 26acc39662
2 changed files with 324 additions and 312 deletions

View file

@ -0,0 +1,324 @@
"""
:codeauthor: Erik Johnson <erik@saltstack.com>
"""
import types
import pytest
from tests.support.mock import DEFAULT, MagicMock, mock_open, patch
pytestmark = [
pytest.mark.skip_unless_on_linux,
]
shadow = pytest.importorskip(
"salt.modules.linux_shadow", reason="shadow module is not available"
)
spwd = pytest.importorskip(
"spwd", reason="Standard library spwd module is not available"
)
def _pw_hash_ids(value):
return value.algorithm
@pytest.fixture(
params=[
types.SimpleNamespace(
algorithm="md5",
clear="lamepassword",
pw_salt="TgIp9OTu",
pw_hash="$1$TgIp9OTu$.d0FFP6jVi5ANoQmk6GpM1",
pw_hash_passlib="$1$TgIp9OTu$.d0FFP6jVi5ANoQmk6GpM1",
),
types.SimpleNamespace(
algorithm="sha256",
clear="lamepassword",
pw_salt="3vINbSrC",
pw_hash="$5$3vINbSrC$hH8A04jAY3bG123yU4FQ0wvP678QDTvWBhHHFbz6j0D",
pw_hash_passlib="$5$rounds=535000$3vINbSrC$YUDOmjJNDLWhL2Z7aAdLJnGIAsbUgkHNEcdUUujHHy8",
),
types.SimpleNamespace(
algorithm="sha512",
clear="lamepassword",
pw_salt="PiGA3V2o",
pw_hash="$6$PiGA3V2o$/PrntRYufz49bRV/V5Eb1V6DdHaS65LB0fu73Tp/xxmDFr6HWJKptY2TvHRDViXZugWpnAcOnrbORpOgZUGTn.",
pw_hash_passlib="$6$rounds=656000$PiGA3V2o$eaAfTU0e1iUFcQycB94otS66/hTgVj94VIAaDp9IJHagSQ.gZascQYOE5.RO87kSY52lJ1LoYX8LNVa2OG8/U/",
),
],
ids=_pw_hash_ids,
)
def password(request):
# Not testing blowfish as it is not available on most Linux distros
return request.param
@pytest.fixture(params=["crypto", "passlib"])
def library(request):
with patch("salt.utils.pycrypto.HAS_CRYPT", request.param == "crypto"):
yield request.param
@pytest.fixture
def configure_loader_modules():
return {shadow: {}}
def test_gen_password(password, library):
"""
Test shadow.gen_password
"""
if library == "passlib":
pw_hash = password.pw_hash_passlib
else:
pw_hash = password.pw_hash
assert (
shadow.gen_password(
password.clear,
crypt_salt=password.pw_salt,
algorithm=password.algorithm,
)
== pw_hash
)
def test_set_password():
"""
Test the corner case in which shadow.set_password is called for a user
that has an entry in /etc/passwd but not /etc/shadow.
"""
original_lines = [
"foo:orighash:17955::::::\n",
"bar:somehash:17955::::::\n",
]
data = {
"/etc/shadow": "".join(original_lines),
"*": Exception("Attempted to open something other than /etc/shadow"),
}
isfile_mock = MagicMock(
side_effect=lambda x: True if x == "/etc/shadow" else DEFAULT
)
password = "newhash"
shadow_info_mock = MagicMock(return_value={"passwd": password})
#
# CASE 1: Normal password change
#
user = "bar"
user_exists_mock = MagicMock(
side_effect=lambda x, **y: 0 if x == ["id", user] else DEFAULT
)
with patch(
"salt.utils.files.fopen", mock_open(read_data=data)
) as shadow_mock, patch("os.path.isfile", isfile_mock), patch.object(
shadow, "info", shadow_info_mock
), patch.dict(
shadow.__salt__, {"cmd.retcode": user_exists_mock}
), patch.dict(
shadow.__grains__, {"os": "CentOS"}
):
assert shadow.set_password(user, password, use_usermod=False)
filehandles = shadow_mock.filehandles["/etc/shadow"]
# We should only have opened twice, once to read the contents and once
# to write.
assert len(filehandles) == 2
# We're rewriting the entire file
assert filehandles[1].mode == "w+"
# We should be calling writelines instead of write, to rewrite the
# entire file.
assert len(filehandles[1].writelines_calls) == 1
# Make sure we wrote the correct info
lines = filehandles[1].writelines_calls[0]
# Should only have the same two users in the file
assert len(lines) == 2
# The first line should be unchanged
assert lines[0] == original_lines[0]
# The second line should have the new password hash
assert lines[1].split(":")[:2] == [user, password]
#
# CASE 2: Corner case: no /etc/shadow entry for user
#
user = "baz"
user_exists_mock = MagicMock(
side_effect=lambda x, **y: 0 if x == ["id", user] else DEFAULT
)
with patch(
"salt.utils.files.fopen", mock_open(read_data=data)
) as shadow_mock, patch("os.path.isfile", isfile_mock), patch.object(
shadow, "info", shadow_info_mock
), patch.dict(
shadow.__salt__, {"cmd.retcode": user_exists_mock}
), patch.dict(
shadow.__grains__, {"os": "CentOS"}
):
assert shadow.set_password(user, password, use_usermod=False)
filehandles = shadow_mock.filehandles["/etc/shadow"]
# We should only have opened twice, once to read the contents and once
# to write.
assert len(filehandles) == 2
# We're just appending to the file, not rewriting
assert filehandles[1].mode == "a+"
# We should only have written to the file once
assert len(filehandles[1].write_calls) == 1
# Make sure we wrote the correct info
assert filehandles[1].write_calls[0].split(":")[:2] == [user, password]
def test_info(password):
"""
Test if info shows the correct user information
"""
# First test is with a succesful call
expected_result = [
("expire", -1),
("inact", -1),
("lstchg", 31337),
("max", 99999),
("min", 0),
("name", "foo"),
("passwd", password.pw_hash),
("warn", 7),
]
getspnam_return = spwd.struct_spwd(
["foo", password.pw_hash, 31337, 0, 99999, 7, -1, -1, -1]
)
with patch("spwd.getspnam", return_value=getspnam_return):
result = shadow.info("foo")
assert expected_result == sorted(result.items(), key=lambda x: x[0])
# The next two is for a non-existent user
expected_result = [
("expire", ""),
("inact", ""),
("lstchg", ""),
("max", ""),
("min", ""),
("name", ""),
("passwd", ""),
("warn", ""),
]
# We get KeyError exception for non-existent users in glibc based systems
getspnam_return = KeyError
with patch("spwd.getspnam", side_effect=getspnam_return):
result = shadow.info("foo")
assert expected_result == sorted(result.items(), key=lambda x: x[0])
# And FileNotFoundError in musl based systems
getspnam_return = FileNotFoundError
with patch("spwd.getspnam", side_effect=getspnam_return):
result = shadow.info("foo")
assert expected_result == sorted(result.items(), key=lambda x: x[0])
def test_set_password_malformed_shadow_entry():
"""
Test that Salt will repair a malformed shadow entry (that is, one that
doesn't have the correct number of fields).
"""
original_lines = [
"valid:s00persekr1thash:17955::::::\n",
"tooshort:orighash:17955:::::\n",
"toolong:orighash:17955:::::::\n",
]
data = {
"/etc/shadow": "".join(original_lines),
"*": Exception("Attempted to open something other than /etc/shadow"),
}
isfile_mock = MagicMock(
side_effect=lambda x: True if x == "/etc/shadow" else DEFAULT
)
password = "newhash"
shadow_info_mock = MagicMock(return_value={"passwd": password})
#
# CASE 1: Fix an entry with too few fields
#
user = "tooshort"
user_exists_mock = MagicMock(
side_effect=lambda x, **y: 0 if x == ["id", user] else DEFAULT
)
with patch(
"salt.utils.files.fopen", mock_open(read_data=data)
) as shadow_mock, patch("os.path.isfile", isfile_mock), patch.object(
shadow, "info", shadow_info_mock
), patch.dict(
shadow.__salt__, {"cmd.retcode": user_exists_mock}
), patch.dict(
shadow.__grains__, {"os": "CentOS"}
):
assert shadow.set_password(user, password, use_usermod=False)
filehandles = shadow_mock.filehandles["/etc/shadow"]
# We should only have opened twice, once to read the contents and once
# to write.
assert len(filehandles) == 2
# We're rewriting the entire file
assert filehandles[1].mode == "w+"
# We should be calling writelines instead of write, to rewrite the
# entire file.
assert len(filehandles[1].writelines_calls) == 1
# Make sure we wrote the correct info
lines = filehandles[1].writelines_calls[0]
# Should only have the same three users in the file
assert len(lines) == 3
# The first and third line should be unchanged
assert lines[0] == original_lines[0]
assert lines[2] == original_lines[2]
# The second line should have the new password hash, and it should have
# gotten "fixed" by adding another colon.
fixed = lines[1].split(":")
assert fixed[:2] == [user, password]
assert len(fixed) == 9
#
# CASE 2: Fix an entry with too many fields
#
user = "toolong"
user_exists_mock = MagicMock(
side_effect=lambda x, **y: 0 if x == ["id", user] else DEFAULT
)
with patch(
"salt.utils.files.fopen", mock_open(read_data=data)
) as shadow_mock, patch("os.path.isfile", isfile_mock), patch.object(
shadow, "info", shadow_info_mock
), patch.dict(
shadow.__salt__, {"cmd.retcode": user_exists_mock}
), patch.dict(
shadow.__grains__, {"os": "CentOS"}
):
assert shadow.set_password(user, password, use_usermod=False)
filehandles = shadow_mock.filehandles["/etc/shadow"]
# We should only have opened twice, once to read the contents and once
# to write.
assert len(filehandles) == 2
# We're rewriting the entire file
assert filehandles[1].mode == "w+"
# We should be calling writelines instead of write, to rewrite the
# entire file.
assert len(filehandles[1].writelines_calls) == 1
# Make sure we wrote the correct info
lines = filehandles[1].writelines_calls[0]
# Should only have the same three users in the file
assert len(lines) == 3
# The first and second line should be unchanged
assert lines[0] == original_lines[0]
assert lines[1] == original_lines[1]
# The third line should have the new password hash, and it should have
# gotten "fixed" by reducing it to 9 fields instead of 10.
fixed = lines[2].split(":")
assert fixed[:2] == [user, password]
assert len(fixed) == 9
@pytest.mark.skip_if_not_root
def test_list_users():
"""
Test if it returns a list of all users
"""
assert shadow.list_users()

View file

@ -1,312 +0,0 @@
"""
:codeauthor: Erik Johnson <erik@saltstack.com>
"""
import textwrap
import pytest
from tests.support.mixins import LoaderModuleMockMixin
from tests.support.mock import DEFAULT, MagicMock, mock_open, patch
from tests.support.unit import TestCase
try:
import spwd
except ImportError:
pass
try:
import salt.modules.linux_shadow as shadow
HAS_SHADOW = True
except ImportError:
HAS_SHADOW = False
_PASSWORD = "lamepassword"
# Not testing blowfish as it is not available on most Linux distros
_HASHES = dict(
md5=dict(pw_salt="TgIp9OTu", pw_hash="$1$TgIp9OTu$.d0FFP6jVi5ANoQmk6GpM1"),
sha256=dict(
pw_salt="3vINbSrC",
pw_hash="$5$3vINbSrC$hH8A04jAY3bG123yU4FQ0wvP678QDTvWBhHHFbz6j0D",
),
sha512=dict(
pw_salt="PiGA3V2o",
pw_hash="$6$PiGA3V2o$/PrntRYufz49bRV/V5Eb1V6DdHaS65LB0fu73Tp/xxmDFr6HWJKptY2TvHRDViXZugWpnAcOnrbORpOgZUGTn.",
),
)
@pytest.mark.skip_unless_on_linux
@pytest.mark.skipif(HAS_SHADOW is False, reason="shadow module is not available")
class LinuxShadowTest(TestCase, LoaderModuleMockMixin):
def setup_loader_modules(self):
return {shadow: {}}
def test_gen_password(self):
"""
Test shadow.gen_password
"""
self.assertTrue(HAS_SHADOW)
for algorithm, hash_info in _HASHES.items():
self.assertEqual(
shadow.gen_password(
_PASSWORD, crypt_salt=hash_info["pw_salt"], algorithm=algorithm
),
hash_info["pw_hash"],
)
def test_set_password(self):
"""
Test the corner case in which shadow.set_password is called for a user
that has an entry in /etc/passwd but not /etc/shadow.
"""
original_file = textwrap.dedent(
"""\
foo:orighash:17955::::::
bar:somehash:17955::::::
"""
)
original_lines = original_file.splitlines(True)
data = {
"/etc/shadow": original_file,
"*": Exception("Attempted to open something other than /etc/shadow"),
}
isfile_mock = MagicMock(
side_effect=lambda x: True if x == "/etc/shadow" else DEFAULT
)
password = "newhash"
shadow_info_mock = MagicMock(return_value={"passwd": password})
#
# CASE 1: Normal password change
#
user = "bar"
user_exists_mock = MagicMock(
side_effect=lambda x, **y: 0 if x == ["id", user] else DEFAULT
)
with patch(
"salt.utils.files.fopen", mock_open(read_data=data)
) as shadow_mock, patch("os.path.isfile", isfile_mock), patch.object(
shadow, "info", shadow_info_mock
), patch.dict(
shadow.__salt__, {"cmd.retcode": user_exists_mock}
), patch.dict(
shadow.__grains__, {"os": "CentOS"}
):
result = shadow.set_password(user, password, use_usermod=False)
assert result
filehandles = shadow_mock.filehandles["/etc/shadow"]
# We should only have opened twice, once to read the contents and once
# to write.
assert len(filehandles) == 2
# We're rewriting the entire file
assert filehandles[1].mode == "w+"
# We should be calling writelines instead of write, to rewrite the
# entire file.
assert len(filehandles[1].writelines_calls) == 1
# Make sure we wrote the correct info
lines = filehandles[1].writelines_calls[0]
# Should only have the same two users in the file
assert len(lines) == 2
# The first line should be unchanged
assert lines[0] == original_lines[0]
# The second line should have the new password hash
assert lines[1].split(":")[:2] == [user, password]
#
# CASE 2: Corner case: no /etc/shadow entry for user
#
user = "baz"
user_exists_mock = MagicMock(
side_effect=lambda x, **y: 0 if x == ["id", user] else DEFAULT
)
with patch(
"salt.utils.files.fopen", mock_open(read_data=data)
) as shadow_mock, patch("os.path.isfile", isfile_mock), patch.object(
shadow, "info", shadow_info_mock
), patch.dict(
shadow.__salt__, {"cmd.retcode": user_exists_mock}
), patch.dict(
shadow.__grains__, {"os": "CentOS"}
):
result = shadow.set_password(user, password, use_usermod=False)
assert result
filehandles = shadow_mock.filehandles["/etc/shadow"]
# We should only have opened twice, once to read the contents and once
# to write.
assert len(filehandles) == 2
# We're just appending to the file, not rewriting
assert filehandles[1].mode == "a+"
# We should only have written to the file once
assert len(filehandles[1].write_calls) == 1
# Make sure we wrote the correct info
assert filehandles[1].write_calls[0].split(":")[:2] == [user, password]
def test_info(self):
"""
Test if info shows the correct user information
"""
# First test is with a succesful call
expected_result = [
("expire", -1),
("inact", -1),
("lstchg", 31337),
("max", 99999),
("min", 0),
("name", "foo"),
("passwd", _HASHES["sha512"]["pw_hash"]),
("warn", 7),
]
getspnam_return = spwd.struct_spwd(
["foo", _HASHES["sha512"]["pw_hash"], 31337, 0, 99999, 7, -1, -1, -1]
)
with patch("spwd.getspnam", return_value=getspnam_return):
result = shadow.info("foo")
self.assertEqual(
expected_result, sorted(result.items(), key=lambda x: x[0])
)
# The next two is for a non-existent user
expected_result = [
("expire", ""),
("inact", ""),
("lstchg", ""),
("max", ""),
("min", ""),
("name", ""),
("passwd", ""),
("warn", ""),
]
# We get KeyError exception for non-existent users in glibc based systems
getspnam_return = KeyError
with patch("spwd.getspnam", side_effect=getspnam_return):
result = shadow.info("foo")
self.assertEqual(
expected_result, sorted(result.items(), key=lambda x: x[0])
)
# And FileNotFoundError in musl based systems
getspnam_return = FileNotFoundError
with patch("spwd.getspnam", side_effect=getspnam_return):
result = shadow.info("foo")
self.assertEqual(
expected_result, sorted(result.items(), key=lambda x: x[0])
)
@pytest.mark.skip_if_not_root
def test_set_password_malformed_shadow_entry(self):
"""
Test that Salt will repair a malformed shadow entry (that is, one that
doesn't have the correct number of fields).
"""
original_file = textwrap.dedent(
"""\
valid:s00persekr1thash:17955::::::
tooshort:orighash:17955:::::
toolong:orighash:17955:::::::
"""
)
original_lines = original_file.splitlines(True)
data = {
"/etc/shadow": original_file,
"*": Exception("Attempted to open something other than /etc/shadow"),
}
isfile_mock = MagicMock(
side_effect=lambda x: True if x == "/etc/shadow" else DEFAULT
)
password = "newhash"
shadow_info_mock = MagicMock(return_value={"passwd": password})
#
# CASE 1: Fix an entry with too few fields
#
user = "tooshort"
user_exists_mock = MagicMock(
side_effect=lambda x, **y: 0 if x == ["id", user] else DEFAULT
)
with patch(
"salt.utils.files.fopen", mock_open(read_data=data)
) as shadow_mock, patch("os.path.isfile", isfile_mock), patch.object(
shadow, "info", shadow_info_mock
), patch.dict(
shadow.__salt__, {"cmd.retcode": user_exists_mock}
), patch.dict(
shadow.__grains__, {"os": "CentOS"}
):
result = shadow.set_password(user, password, use_usermod=False)
assert result
filehandles = shadow_mock.filehandles["/etc/shadow"]
# We should only have opened twice, once to read the contents and once
# to write.
assert len(filehandles) == 2
# We're rewriting the entire file
assert filehandles[1].mode == "w+"
# We should be calling writelines instead of write, to rewrite the
# entire file.
assert len(filehandles[1].writelines_calls) == 1
# Make sure we wrote the correct info
lines = filehandles[1].writelines_calls[0]
# Should only have the same three users in the file
assert len(lines) == 3
# The first and third line should be unchanged
assert lines[0] == original_lines[0]
assert lines[2] == original_lines[2]
# The second line should have the new password hash, and it should have
# gotten "fixed" by adding another colon.
fixed = lines[1].split(":")
assert fixed[:2] == [user, password]
assert len(fixed) == 9
#
# CASE 2: Fix an entry with too many fields
#
user = "toolong"
user_exists_mock = MagicMock(
side_effect=lambda x, **y: 0 if x == ["id", user] else DEFAULT
)
with patch(
"salt.utils.files.fopen", mock_open(read_data=data)
) as shadow_mock, patch("os.path.isfile", isfile_mock), patch.object(
shadow, "info", shadow_info_mock
), patch.dict(
shadow.__salt__, {"cmd.retcode": user_exists_mock}
), patch.dict(
shadow.__grains__, {"os": "CentOS"}
):
result = shadow.set_password(user, password, use_usermod=False)
assert result
filehandles = shadow_mock.filehandles["/etc/shadow"]
# We should only have opened twice, once to read the contents and once
# to write.
assert len(filehandles) == 2
# We're rewriting the entire file
assert filehandles[1].mode == "w+"
# We should be calling writelines instead of write, to rewrite the
# entire file.
assert len(filehandles[1].writelines_calls) == 1
# Make sure we wrote the correct info
lines = filehandles[1].writelines_calls[0]
# Should only have the same three users in the file
assert len(lines) == 3
# The first and second line should be unchanged
assert lines[0] == original_lines[0]
assert lines[1] == original_lines[1]
# The third line should have the new password hash, and it should have
# gotten "fixed" by reducing it to 9 fields instead of 10.
fixed = lines[2].split(":")
assert fixed[:2] == [user, password]
assert len(fixed) == 9
@pytest.mark.skip_if_not_root
def test_list_users(self):
"""
Test if it returns a list of all users
"""
self.assertTrue(shadow.list_users())