1
0

test_verify.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407
  1. # -*- coding: utf-8 -*-
  2. """
  3. Test the verification routines
  4. """
  5. # Import Python libs
  6. from __future__ import absolute_import, print_function, unicode_literals
  7. import ctypes
  8. import getpass
  9. import os
  10. import shutil
  11. import socket
  12. import stat
  13. import sys
  14. import tempfile
  15. # Import salt libs
  16. import salt.utils.files
  17. import salt.utils.platform
  18. # Import 3rd-party libs
  19. from salt.ext import six
  20. from salt.ext.six.moves import range # pylint: disable=import-error,redefined-builtin
  21. from salt.utils.verify import (
  22. check_max_open_files,
  23. check_user,
  24. clean_path,
  25. log,
  26. valid_id,
  27. verify_env,
  28. verify_log,
  29. verify_log_files,
  30. verify_logs_filter,
  31. verify_socket,
  32. zmq_version,
  33. )
  34. from tests.support.helpers import TstSuiteLoggingHandler, requires_network
  35. from tests.support.mock import MagicMock, patch
  36. # Import Salt Testing libs
  37. # Import Salt Testing libs
  38. from tests.support.runtests import RUNTIME_VARS
  39. from tests.support.unit import TestCase, skipIf
  40. # Import third party libs
  41. if sys.platform.startswith("win"):
  42. import win32file
  43. else:
  44. import resource
  45. # Import third party libs
  46. if sys.platform.startswith("win"):
  47. import win32file
  48. else:
  49. import resource
  50. class TestVerify(TestCase):
  51. """
  52. Verify module tests
  53. """
  54. def test_valid_id_exception_handler(self):
  55. """
  56. Ensure we just return False if we pass in invalid or undefined paths.
  57. Refs #8259
  58. """
  59. opts = {"pki_dir": "/tmp/whatever"}
  60. self.assertFalse(valid_id(opts, None))
  61. def test_valid_id_pathsep(self):
  62. """
  63. Path separators in id should make it invalid
  64. """
  65. opts = {"pki_dir": "/tmp/whatever"}
  66. # We have to test both path separators because os.path.normpath will
  67. # convert forward slashes to backslashes on Windows.
  68. for pathsep in ("/", "\\"):
  69. self.assertFalse(valid_id(opts, pathsep.join(("..", "foobar"))))
  70. def test_zmq_verify(self):
  71. self.assertTrue(zmq_version())
  72. def test_zmq_verify_insufficient(self):
  73. import zmq
  74. with patch.object(zmq, "__version__", "2.1.0"):
  75. self.assertFalse(zmq_version())
  76. def test_user(self):
  77. self.assertTrue(check_user(getpass.getuser()))
  78. def test_no_user(self):
  79. # Catch sys.stderr here since no logging is configured and
  80. # check_user WILL write to sys.stderr
  81. class FakeWriter(object):
  82. def __init__(self):
  83. self.output = ""
  84. def write(self, data):
  85. self.output += data
  86. stderr = sys.stderr
  87. writer = FakeWriter()
  88. sys.stderr = writer
  89. # Now run the test
  90. if sys.platform.startswith("win"):
  91. self.assertTrue(check_user("nouser"))
  92. else:
  93. self.assertFalse(check_user("nouser"))
  94. # Restore sys.stderr
  95. sys.stderr = stderr
  96. if writer.output != 'CRITICAL: User not found: "nouser"\n':
  97. # If there's a different error catch, write it to sys.stderr
  98. sys.stderr.write(writer.output)
  99. @skipIf(salt.utils.platform.is_windows(), "No verify_env Windows")
  100. def test_verify_env(self):
  101. root_dir = tempfile.mkdtemp(dir=RUNTIME_VARS.TMP)
  102. var_dir = os.path.join(root_dir, "var", "log", "salt")
  103. key_dir = os.path.join(root_dir, "key_dir")
  104. verify_env([var_dir], getpass.getuser(), root_dir=root_dir)
  105. self.assertTrue(os.path.exists(var_dir))
  106. dir_stat = os.stat(var_dir)
  107. self.assertEqual(dir_stat.st_uid, os.getuid())
  108. self.assertEqual(dir_stat.st_mode & stat.S_IRWXU, stat.S_IRWXU)
  109. self.assertEqual(dir_stat.st_mode & stat.S_IRWXG, 40)
  110. self.assertEqual(dir_stat.st_mode & stat.S_IRWXO, 5)
  111. @requires_network(only_local_network=True)
  112. def test_verify_socket(self):
  113. self.assertTrue(verify_socket("", 18000, 18001))
  114. if socket.has_ipv6:
  115. # Only run if Python is built with IPv6 support; otherwise
  116. # this will just fail.
  117. try:
  118. self.assertTrue(verify_socket("::", 18000, 18001))
  119. except socket.error as serr:
  120. # Python has IPv6 enabled, but the system cannot create
  121. # IPv6 sockets (otherwise the test would return a bool)
  122. # - skip the test
  123. #
  124. # FIXME - possibly emit a message that the system does
  125. # not support IPv6.
  126. pass
  127. def test_max_open_files(self):
  128. with TstSuiteLoggingHandler() as handler:
  129. logmsg_dbg = "DEBUG:This salt-master instance has accepted {0} minion keys."
  130. logmsg_chk = (
  131. "{0}:The number of accepted minion keys({1}) should be lower "
  132. "than 1/4 of the max open files soft setting({2}). According "
  133. "to the system's hard limit, there's still a margin of {3} "
  134. "to raise the salt's max_open_files setting. Please consider "
  135. "raising this value."
  136. )
  137. logmsg_crash = (
  138. "{0}:The number of accepted minion keys({1}) should be lower "
  139. "than 1/4 of the max open files soft setting({2}). "
  140. "salt-master will crash pretty soon! According to the "
  141. "system's hard limit, there's still a margin of {3} to "
  142. "raise the salt's max_open_files setting. Please consider "
  143. "raising this value."
  144. )
  145. if sys.platform.startswith("win"):
  146. logmsg_crash = (
  147. "{0}:The number of accepted minion keys({1}) should be lower "
  148. "than 1/4 of the max open files soft setting({2}). "
  149. "salt-master will crash pretty soon! Please consider "
  150. "raising this value."
  151. )
  152. if sys.platform.startswith("win"):
  153. # Check the Windows API for more detail on this
  154. # http://msdn.microsoft.com/en-us/library/xt874334(v=vs.71).aspx
  155. # and the python binding http://timgolden.me.uk/pywin32-docs/win32file.html
  156. mof_s = mof_h = win32file._getmaxstdio()
  157. else:
  158. mof_s, mof_h = resource.getrlimit(resource.RLIMIT_NOFILE)
  159. tempdir = tempfile.mkdtemp(prefix="fake-keys")
  160. keys_dir = os.path.join(tempdir, "minions")
  161. os.makedirs(keys_dir)
  162. mof_test = 256
  163. if sys.platform.startswith("win"):
  164. win32file._setmaxstdio(mof_test)
  165. else:
  166. resource.setrlimit(resource.RLIMIT_NOFILE, (mof_test, mof_h))
  167. try:
  168. prev = 0
  169. for newmax, level in (
  170. (24, None),
  171. (66, "INFO"),
  172. (127, "WARNING"),
  173. (196, "CRITICAL"),
  174. ):
  175. for n in range(prev, newmax):
  176. kpath = os.path.join(keys_dir, six.text_type(n))
  177. with salt.utils.files.fopen(kpath, "w") as fp_:
  178. fp_.write(
  179. str(n)
  180. ) # future lint: disable=blacklisted-function
  181. opts = {"max_open_files": newmax, "pki_dir": tempdir}
  182. check_max_open_files(opts)
  183. if level is None:
  184. # No log message is triggered, only the DEBUG one which
  185. # tells us how many minion keys were accepted.
  186. self.assertEqual([logmsg_dbg.format(newmax)], handler.messages)
  187. else:
  188. self.assertIn(logmsg_dbg.format(newmax), handler.messages)
  189. self.assertIn(
  190. logmsg_chk.format(
  191. level,
  192. newmax,
  193. mof_test,
  194. mof_test - newmax
  195. if sys.platform.startswith("win")
  196. else mof_h - newmax,
  197. ),
  198. handler.messages,
  199. )
  200. handler.clear()
  201. prev = newmax
  202. newmax = mof_test
  203. for n in range(prev, newmax):
  204. kpath = os.path.join(keys_dir, six.text_type(n))
  205. with salt.utils.files.fopen(kpath, "w") as fp_:
  206. fp_.write(str(n)) # future lint: disable=blacklisted-function
  207. opts = {"max_open_files": newmax, "pki_dir": tempdir}
  208. check_max_open_files(opts)
  209. self.assertIn(logmsg_dbg.format(newmax), handler.messages)
  210. self.assertIn(
  211. logmsg_crash.format(
  212. "CRITICAL",
  213. newmax,
  214. mof_test,
  215. mof_test - newmax
  216. if sys.platform.startswith("win")
  217. else mof_h - newmax,
  218. ),
  219. handler.messages,
  220. )
  221. handler.clear()
  222. except IOError as err:
  223. if err.errno == 24:
  224. # Too many open files
  225. self.skipTest("We've hit the max open files setting")
  226. raise
  227. finally:
  228. if sys.platform.startswith("win"):
  229. win32file._setmaxstdio(mof_h)
  230. else:
  231. resource.setrlimit(resource.RLIMIT_NOFILE, (mof_s, mof_h))
  232. shutil.rmtree(tempdir)
  233. def test_verify_log(self):
  234. """
  235. Test that verify_log works as expected
  236. """
  237. message = (
  238. "Insecure logging configuration detected! Sensitive data may be logged."
  239. )
  240. mock_cheese = MagicMock()
  241. with patch.object(log, "warning", mock_cheese):
  242. verify_log({"log_level": "cheeseshop"})
  243. mock_cheese.assert_called_once_with(message)
  244. mock_trace = MagicMock()
  245. with patch.object(log, "warning", mock_trace):
  246. verify_log({"log_level": "trace"})
  247. mock_trace.assert_called_once_with(message)
  248. mock_none = MagicMock()
  249. with patch.object(log, "warning", mock_none):
  250. verify_log({})
  251. mock_none.assert_called_once_with(message)
  252. mock_info = MagicMock()
  253. with patch.object(log, "warning", mock_info):
  254. verify_log({"log_level": "info"})
  255. self.assertTrue(mock_info.call_count == 0)
  256. class TestVerifyLog(TestCase):
  257. def setUp(self):
  258. self.tmpdir = tempfile.mkdtemp()
  259. def tearDown(self):
  260. shutil.rmtree(self.tmpdir)
  261. def test_verify_logs_filter(self):
  262. filtered = verify_logs_filter(
  263. ["udp://foo", "tcp://bar", "/tmp/foo", "file://tmp/bar"]
  264. )
  265. assert filtered == ["/tmp/foo"], filtered
  266. @skipIf(salt.utils.platform.is_windows(), "Not applicable on Windows")
  267. def test_verify_log_files_udp_scheme(self):
  268. verify_log_files(["udp://foo"], getpass.getuser())
  269. self.assertFalse(os.path.isdir(os.path.join(os.getcwd(), "udp:")))
  270. @skipIf(salt.utils.platform.is_windows(), "Not applicable on Windows")
  271. def test_verify_log_files_tcp_scheme(self):
  272. verify_log_files(["udp://foo"], getpass.getuser())
  273. self.assertFalse(os.path.isdir(os.path.join(os.getcwd(), "tcp:")))
  274. @skipIf(salt.utils.platform.is_windows(), "Not applicable on Windows")
  275. def test_verify_log_files_file_scheme(self):
  276. verify_log_files(["file://{}"], getpass.getuser())
  277. self.assertFalse(os.path.isdir(os.path.join(os.getcwd(), "file:")))
  278. @skipIf(salt.utils.platform.is_windows(), "Not applicable on Windows")
  279. def test_verify_log_files(self):
  280. path = os.path.join(self.tmpdir, "foo", "bar.log")
  281. self.assertFalse(os.path.exists(path))
  282. verify_log_files([path], getpass.getuser())
  283. self.assertTrue(os.path.exists(path))
  284. class TestCleanPath(TestCase):
  285. """
  286. salt.utils.clean_path works as expected
  287. """
  288. def setUp(self):
  289. self.tmpdir = tempfile.mkdtemp()
  290. def tearDown(self):
  291. shutil.rmtree(self.tmpdir)
  292. def test_clean_path_valid(self):
  293. path_a = os.path.join(self.tmpdir, "foo")
  294. path_b = os.path.join(self.tmpdir, "foo", "bar")
  295. assert clean_path(path_a, path_b) == path_b
  296. def test_clean_path_invalid(self):
  297. path_a = os.path.join(self.tmpdir, "foo")
  298. path_b = os.path.join(self.tmpdir, "baz", "bar")
  299. assert clean_path(path_a, path_b) == ""
  300. __CSL = None
  301. def symlink(source, link_name):
  302. """
  303. symlink(source, link_name) Creates a symbolic link pointing to source named
  304. link_name
  305. """
  306. global __CSL
  307. if __CSL is None:
  308. csl = ctypes.windll.kernel32.CreateSymbolicLinkW
  309. csl.argtypes = (ctypes.c_wchar_p, ctypes.c_wchar_p, ctypes.c_uint32)
  310. csl.restype = ctypes.c_ubyte
  311. __CSL = csl
  312. flags = 0
  313. if source is not None and os.path.isdir(source):
  314. flags = 1
  315. if __CSL(link_name, source, flags) == 0:
  316. raise ctypes.WinError()
  317. @skipIf(six.PY2 and salt.utils.platform.is_windows(), "Skipped on windows py2")
  318. class TestCleanPathLink(TestCase):
  319. """
  320. Ensure salt.utils.clean_path works with symlinked directories and files
  321. """
  322. def setUp(self):
  323. self.tmpdir = tempfile.mkdtemp()
  324. self.to_path = os.path.join(self.tmpdir, "linkto")
  325. self.from_path = os.path.join(self.tmpdir, "linkfrom")
  326. if six.PY2 or salt.utils.platform.is_windows():
  327. kwargs = {}
  328. else:
  329. kwargs = {"target_is_directory": True}
  330. if salt.utils.platform.is_windows():
  331. symlink(self.to_path, self.from_path, **kwargs)
  332. else:
  333. os.symlink(self.to_path, self.from_path, **kwargs)
  334. def tearDown(self):
  335. shutil.rmtree(self.tmpdir)
  336. def test_clean_path_symlinked_src(self):
  337. test_path = os.path.join(self.from_path, "test")
  338. expect_path = os.path.join(self.to_path, "test")
  339. ret = clean_path(self.from_path, test_path)
  340. assert ret == expect_path, "{} is not {}".format(ret, expect_path)
  341. def test_clean_path_symlinked_tgt(self):
  342. test_path = os.path.join(self.to_path, "test")
  343. expect_path = os.path.join(self.to_path, "test")
  344. ret = clean_path(self.from_path, test_path)
  345. assert ret == expect_path, "{} is not {}".format(ret, expect_path)