test_verify.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325
  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 getpass
  8. import os
  9. import sys
  10. import stat
  11. import shutil
  12. import tempfile
  13. import socket
  14. # Import third party libs
  15. if sys.platform.startswith('win'):
  16. import win32file
  17. else:
  18. import resource
  19. # Import Salt Testing libs
  20. from tests.support.unit import skipIf, TestCase
  21. from tests.support.paths import TMP
  22. from tests.support.helpers import (
  23. requires_network,
  24. TestsLoggingHandler
  25. )
  26. from tests.support.mock import (
  27. MagicMock,
  28. patch,
  29. NO_MOCK,
  30. NO_MOCK_REASON
  31. )
  32. # Import salt libs
  33. import salt.utils.files
  34. import salt.utils.platform
  35. from salt.utils.verify import (
  36. check_user,
  37. verify_env,
  38. verify_socket,
  39. zmq_version,
  40. check_max_open_files,
  41. valid_id,
  42. log,
  43. verify_log,
  44. verify_logs_filter,
  45. verify_log_files,
  46. )
  47. # Import 3rd-party libs
  48. from salt.ext import six
  49. from salt.ext.six.moves import range # pylint: disable=import-error,redefined-builtin
  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=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 TestsLoggingHandler() as handler:
  129. logmsg_dbg = (
  130. 'DEBUG:This salt-master instance has accepted {0} minion keys.'
  131. )
  132. logmsg_chk = (
  133. '{0}:The number of accepted minion keys({1}) should be lower '
  134. 'than 1/4 of the max open files soft setting({2}). According '
  135. 'to the system\'s hard limit, there\'s still a margin of {3} '
  136. 'to raise the salt\'s max_open_files setting. Please consider '
  137. 'raising this value.'
  138. )
  139. logmsg_crash = (
  140. '{0}:The number of accepted minion keys({1}) should be lower '
  141. 'than 1/4 of the max open files soft setting({2}). '
  142. 'salt-master will crash pretty soon! According to the '
  143. 'system\'s hard limit, there\'s still a margin of {3} to '
  144. 'raise the salt\'s max_open_files setting. Please consider '
  145. 'raising this value.'
  146. )
  147. if sys.platform.startswith('win'):
  148. logmsg_crash = (
  149. '{0}:The number of accepted minion keys({1}) should be lower '
  150. 'than 1/4 of the max open files soft setting({2}). '
  151. 'salt-master will crash pretty soon! Please consider '
  152. 'raising this value.'
  153. )
  154. if sys.platform.startswith('win'):
  155. # Check the Windows API for more detail on this
  156. # http://msdn.microsoft.com/en-us/library/xt874334(v=vs.71).aspx
  157. # and the python binding http://timgolden.me.uk/pywin32-docs/win32file.html
  158. mof_s = mof_h = win32file._getmaxstdio()
  159. else:
  160. mof_s, mof_h = resource.getrlimit(resource.RLIMIT_NOFILE)
  161. tempdir = tempfile.mkdtemp(prefix='fake-keys')
  162. keys_dir = os.path.join(tempdir, 'minions')
  163. os.makedirs(keys_dir)
  164. mof_test = 256
  165. if sys.platform.startswith('win'):
  166. win32file._setmaxstdio(mof_test)
  167. else:
  168. resource.setrlimit(resource.RLIMIT_NOFILE, (mof_test, mof_h))
  169. try:
  170. prev = 0
  171. for newmax, level in ((24, None), (66, 'INFO'),
  172. (127, 'WARNING'), (196, 'CRITICAL')):
  173. for n in range(prev, newmax):
  174. kpath = os.path.join(keys_dir, six.text_type(n))
  175. with salt.utils.files.fopen(kpath, 'w') as fp_:
  176. fp_.write(str(n)) # future lint: disable=blacklisted-function
  177. opts = {
  178. 'max_open_files': newmax,
  179. 'pki_dir': tempdir
  180. }
  181. check_max_open_files(opts)
  182. if level is None:
  183. # No log message is triggered, only the DEBUG one which
  184. # tells us how many minion keys were accepted.
  185. self.assertEqual(
  186. [logmsg_dbg.format(newmax)], handler.messages
  187. )
  188. else:
  189. self.assertIn(
  190. logmsg_dbg.format(newmax), handler.messages
  191. )
  192. self.assertIn(
  193. logmsg_chk.format(
  194. level,
  195. newmax,
  196. mof_test,
  197. mof_test - newmax if sys.platform.startswith('win') else mof_h - newmax,
  198. ),
  199. handler.messages
  200. )
  201. handler.clear()
  202. prev = newmax
  203. newmax = mof_test
  204. for n in range(prev, newmax):
  205. kpath = os.path.join(keys_dir, six.text_type(n))
  206. with salt.utils.files.fopen(kpath, 'w') as fp_:
  207. fp_.write(str(n)) # future lint: disable=blacklisted-function
  208. opts = {
  209. 'max_open_files': newmax,
  210. 'pki_dir': tempdir
  211. }
  212. check_max_open_files(opts)
  213. self.assertIn(logmsg_dbg.format(newmax), handler.messages)
  214. self.assertIn(
  215. logmsg_crash.format(
  216. 'CRITICAL',
  217. newmax,
  218. mof_test,
  219. mof_test - newmax if sys.platform.startswith('win') else mof_h - newmax,
  220. ),
  221. handler.messages
  222. )
  223. handler.clear()
  224. except IOError as err:
  225. if err.errno == 24:
  226. # Too many open files
  227. self.skipTest('We\'ve hit the max open files setting')
  228. raise
  229. finally:
  230. if sys.platform.startswith('win'):
  231. win32file._setmaxstdio(mof_h)
  232. else:
  233. resource.setrlimit(resource.RLIMIT_NOFILE, (mof_s, mof_h))
  234. shutil.rmtree(tempdir)
  235. @skipIf(NO_MOCK, NO_MOCK_REASON)
  236. def test_verify_log(self):
  237. '''
  238. Test that verify_log works as expected
  239. '''
  240. message = 'Insecure logging configuration detected! Sensitive data may be logged.'
  241. mock_cheese = MagicMock()
  242. with patch.object(log, 'warning', mock_cheese):
  243. verify_log({'log_level': 'cheeseshop'})
  244. mock_cheese.assert_called_once_with(message)
  245. mock_trace = MagicMock()
  246. with patch.object(log, 'warning', mock_trace):
  247. verify_log({'log_level': 'trace'})
  248. mock_trace.assert_called_once_with(message)
  249. mock_none = MagicMock()
  250. with patch.object(log, 'warning', mock_none):
  251. verify_log({})
  252. mock_none.assert_called_once_with(message)
  253. mock_info = MagicMock()
  254. with patch.object(log, 'warning', mock_info):
  255. verify_log({'log_level': 'info'})
  256. self.assertTrue(mock_info.call_count == 0)
  257. class TestVerifyLog(TestCase):
  258. def setUp(self):
  259. self.tmpdir = tempfile.mkdtemp()
  260. def tearDown(self):
  261. shutil.rmtree(self.tmpdir)
  262. def test_verify_logs_filter(self):
  263. filtered = verify_logs_filter(
  264. ['udp://foo', 'tcp://bar', '/tmp/foo', 'file://tmp/bar']
  265. )
  266. assert filtered == ['/tmp/foo'], filtered
  267. @skipIf(salt.utils.platform.is_windows(), 'Not applicable on Windows')
  268. def test_verify_log_files_udp_scheme(self):
  269. verify_log_files(['udp://foo'], getpass.getuser())
  270. self.assertFalse(os.path.isdir(os.path.join(os.getcwd(), 'udp:')))
  271. @skipIf(salt.utils.platform.is_windows(), 'Not applicable on Windows')
  272. def test_verify_log_files_tcp_scheme(self):
  273. verify_log_files(['udp://foo'], getpass.getuser())
  274. self.assertFalse(os.path.isdir(os.path.join(os.getcwd(), 'tcp:')))
  275. @skipIf(salt.utils.platform.is_windows(), 'Not applicable on Windows')
  276. def test_verify_log_files_file_scheme(self):
  277. verify_log_files(['file://{}'], getpass.getuser())
  278. self.assertFalse(os.path.isdir(os.path.join(os.getcwd(), 'file:')))
  279. @skipIf(salt.utils.platform.is_windows(), 'Not applicable on Windows')
  280. def test_verify_log_files(self):
  281. path = os.path.join(self.tmpdir, 'foo', 'bar.log')
  282. self.assertFalse(os.path.exists(path))
  283. verify_log_files([path], getpass.getuser())
  284. self.assertTrue(os.path.exists(path))