test_linux_shadow.py 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154
  1. # -*- coding: utf-8 -*-
  2. """
  3. :codeauthor: Erik Johnson <erik@saltstack.com>
  4. """
  5. from __future__ import absolute_import, print_function, unicode_literals
  6. import textwrap
  7. import pytest
  8. import salt.utils.platform
  9. from salt.ext import six
  10. from tests.support.mixins import LoaderModuleMockMixin
  11. from tests.support.mock import DEFAULT, MagicMock, mock_open, patch
  12. from tests.support.unit import TestCase, skipIf
  13. try:
  14. import salt.modules.linux_shadow as shadow
  15. HAS_SHADOW = True
  16. except ImportError:
  17. HAS_SHADOW = False
  18. _PASSWORD = "lamepassword"
  19. # Not testing blowfish as it is not available on most Linux distros
  20. _HASHES = dict(
  21. md5=dict(pw_salt="TgIp9OTu", pw_hash="$1$TgIp9OTu$.d0FFP6jVi5ANoQmk6GpM1"),
  22. sha256=dict(
  23. pw_salt="3vINbSrC",
  24. pw_hash="$5$3vINbSrC$hH8A04jAY3bG123yU4FQ0wvP678QDTvWBhHHFbz6j0D",
  25. ),
  26. sha512=dict(
  27. pw_salt="PiGA3V2o",
  28. pw_hash="$6$PiGA3V2o$/PrntRYufz49bRV/V5Eb1V6DdHaS65LB0fu73Tp/xxmDFr6HWJKptY2TvHRDViXZugWpnAcOnrbORpOgZUGTn.",
  29. ),
  30. )
  31. @skipIf(not salt.utils.platform.is_linux(), "minion is not Linux")
  32. @skipIf(not HAS_SHADOW, "shadow module is not available")
  33. class LinuxShadowTest(TestCase, LoaderModuleMockMixin):
  34. def setup_loader_modules(self):
  35. return {shadow: {}}
  36. def test_gen_password(self):
  37. """
  38. Test shadow.gen_password
  39. """
  40. self.assertTrue(HAS_SHADOW)
  41. for algorithm, hash_info in six.iteritems(_HASHES):
  42. self.assertEqual(
  43. shadow.gen_password(
  44. _PASSWORD, crypt_salt=hash_info["pw_salt"], algorithm=algorithm
  45. ),
  46. hash_info["pw_hash"],
  47. )
  48. def test_set_password(self):
  49. """
  50. Test the corner case in which shadow.set_password is called for a user
  51. that has an entry in /etc/passwd but not /etc/shadow.
  52. """
  53. data = {
  54. "/etc/shadow": salt.utils.stringutils.to_bytes(
  55. textwrap.dedent(
  56. """\
  57. foo:orighash:17955::::::
  58. bar:somehash:17955::::::
  59. """
  60. )
  61. ),
  62. "*": Exception("Attempted to open something other than /etc/shadow"),
  63. }
  64. isfile_mock = MagicMock(
  65. side_effect=lambda x: True if x == "/etc/shadow" else DEFAULT
  66. )
  67. password = "newhash"
  68. shadow_info_mock = MagicMock(return_value={"passwd": password})
  69. #
  70. # CASE 1: Normal password change
  71. #
  72. user = "bar"
  73. user_exists_mock = MagicMock(
  74. side_effect=lambda x, **y: 0 if x == ["id", user] else DEFAULT
  75. )
  76. with patch(
  77. "salt.utils.files.fopen", mock_open(read_data=data)
  78. ) as shadow_mock, patch("os.path.isfile", isfile_mock), patch.object(
  79. shadow, "info", shadow_info_mock
  80. ), patch.dict(
  81. shadow.__salt__, {"cmd.retcode": user_exists_mock}
  82. ), patch.dict(
  83. shadow.__grains__, {"os": "CentOS"}
  84. ):
  85. result = shadow.set_password(user, password, use_usermod=False)
  86. assert result
  87. filehandles = shadow_mock.filehandles["/etc/shadow"]
  88. # We should only have opened twice, once to read the contents and once
  89. # to write.
  90. assert len(filehandles) == 2
  91. # We're rewriting the entire file
  92. assert filehandles[1].mode == "w+"
  93. # We should be calling writelines instead of write, to rewrite the
  94. # entire file.
  95. assert len(filehandles[1].writelines_calls) == 1
  96. # Make sure we wrote the correct info
  97. lines = filehandles[1].writelines_calls[0]
  98. # Should only have the same two users in the file
  99. assert len(lines) == 2
  100. # The first line should be unchanged
  101. assert lines[0] == "foo:orighash:17955::::::\n"
  102. # The second line should have the new password hash
  103. assert lines[1].split(":")[:2] == [user, password]
  104. #
  105. # CASE 2: Corner case: no /etc/shadow entry for user
  106. #
  107. user = "baz"
  108. user_exists_mock = MagicMock(
  109. side_effect=lambda x, **y: 0 if x == ["id", user] else DEFAULT
  110. )
  111. with patch(
  112. "salt.utils.files.fopen", mock_open(read_data=data)
  113. ) as shadow_mock, patch("os.path.isfile", isfile_mock), patch.object(
  114. shadow, "info", shadow_info_mock
  115. ), patch.dict(
  116. shadow.__salt__, {"cmd.retcode": user_exists_mock}
  117. ), patch.dict(
  118. shadow.__grains__, {"os": "CentOS"}
  119. ):
  120. result = shadow.set_password(user, password, use_usermod=False)
  121. assert result
  122. filehandles = shadow_mock.filehandles["/etc/shadow"]
  123. # We should only have opened twice, once to read the contents and once
  124. # to write.
  125. assert len(filehandles) == 2
  126. # We're just appending to the file, not rewriting
  127. assert filehandles[1].mode == "a+"
  128. # We should only have written to the file once
  129. assert len(filehandles[1].write_calls) == 1
  130. # Make sure we wrote the correct info
  131. assert filehandles[1].write_calls[0].split(":")[:2] == [user, password]
  132. @pytest.mark.skip_if_not_root
  133. def test_list_users(self):
  134. """
  135. Test if it returns a list of all users
  136. """
  137. self.assertTrue(shadow.list_users())