test_vault.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338
  1. # -*- coding: utf-8 -*-
  2. """
  3. Unit tests for the Vault runner
  4. """
  5. # Import Python Libs
  6. from __future__ import absolute_import, print_function, unicode_literals
  7. import logging
  8. import salt.runners.vault as vault
  9. # Import salt libs
  10. from salt.ext import six
  11. # Import Salt Testing Libs
  12. from tests.support.mixins import LoaderModuleMockMixin
  13. from tests.support.mock import ANY, MagicMock, Mock, call, patch
  14. from tests.support.unit import TestCase
  15. log = logging.getLogger(__name__)
  16. class VaultTest(TestCase, LoaderModuleMockMixin):
  17. """
  18. Tests for the runner module of the Vault integration
  19. """
  20. def setup_loader_modules(self):
  21. return {vault: {}}
  22. def setUp(self):
  23. self.grains = {
  24. "id": "test-minion",
  25. "roles": ["web", "database"],
  26. "aux": ["foo", "bar"],
  27. "deep": {"foo": {"bar": {"baz": ["hello", "world"]}}},
  28. "mixedcase": "UP-low-UP",
  29. }
  30. def tearDown(self):
  31. del self.grains
  32. def test_pattern_list_expander(self):
  33. """
  34. Ensure _expand_pattern_lists works as intended:
  35. - Expand list-valued patterns
  36. - Do not change non-list-valued tokens
  37. """
  38. cases = {
  39. "no-tokens-to-replace": ["no-tokens-to-replace"],
  40. "single-dict:{minion}": ["single-dict:{minion}"],
  41. "single-list:{grains[roles]}": ["single-list:web", "single-list:database"],
  42. "multiple-lists:{grains[roles]}+{grains[aux]}": [
  43. "multiple-lists:web+foo",
  44. "multiple-lists:web+bar",
  45. "multiple-lists:database+foo",
  46. "multiple-lists:database+bar",
  47. ],
  48. "single-list-with-dicts:{grains[id]}+{grains[roles]}+{grains[id]}": [
  49. "single-list-with-dicts:{grains[id]}+web+{grains[id]}",
  50. "single-list-with-dicts:{grains[id]}+database+{grains[id]}",
  51. ],
  52. "deeply-nested-list:{grains[deep][foo][bar][baz]}": [
  53. "deeply-nested-list:hello",
  54. "deeply-nested-list:world",
  55. ],
  56. }
  57. # The mappings dict is assembled in _get_policies, so emulate here
  58. mappings = {"minion": self.grains["id"], "grains": self.grains}
  59. for case, correct_output in six.iteritems(cases):
  60. output = vault._expand_pattern_lists(
  61. case, **mappings
  62. ) # pylint: disable=protected-access
  63. diff = set(output).symmetric_difference(set(correct_output))
  64. if diff:
  65. log.debug("Test %s failed", case)
  66. log.debug("Expected:\n\t%s\nGot\n\t%s", output, correct_output)
  67. log.debug("Difference:\n\t%s", diff)
  68. self.assertEqual(output, correct_output)
  69. def test_get_policies_for_nonexisting_minions(self):
  70. minion_id = "salt_master"
  71. # For non-existing minions, or the master-minion, grains will be None
  72. cases = {
  73. "no-tokens-to-replace": ["no-tokens-to-replace"],
  74. "single-dict:{minion}": ["single-dict:{0}".format(minion_id)],
  75. "single-list:{grains[roles]}": [],
  76. }
  77. with patch(
  78. "salt.utils.minions.get_minion_data",
  79. MagicMock(return_value=(None, None, None)),
  80. ):
  81. for case, correct_output in six.iteritems(cases):
  82. test_config = {"policies": [case]}
  83. output = vault._get_policies(
  84. minion_id, test_config
  85. ) # pylint: disable=protected-access
  86. diff = set(output).symmetric_difference(set(correct_output))
  87. if diff:
  88. log.debug("Test %s failed", case)
  89. log.debug("Expected:\n\t%s\nGot\n\t%s", output, correct_output)
  90. log.debug("Difference:\n\t%s", diff)
  91. self.assertEqual(output, correct_output)
  92. def test_get_policies(self):
  93. """
  94. Ensure _get_policies works as intended, including expansion of lists
  95. """
  96. cases = {
  97. "no-tokens-to-replace": ["no-tokens-to-replace"],
  98. "single-dict:{minion}": ["single-dict:test-minion"],
  99. "single-list:{grains[roles]}": ["single-list:web", "single-list:database"],
  100. "multiple-lists:{grains[roles]}+{grains[aux]}": [
  101. "multiple-lists:web+foo",
  102. "multiple-lists:web+bar",
  103. "multiple-lists:database+foo",
  104. "multiple-lists:database+bar",
  105. ],
  106. "single-list-with-dicts:{grains[id]}+{grains[roles]}+{grains[id]}": [
  107. "single-list-with-dicts:test-minion+web+test-minion",
  108. "single-list-with-dicts:test-minion+database+test-minion",
  109. ],
  110. "deeply-nested-list:{grains[deep][foo][bar][baz]}": [
  111. "deeply-nested-list:hello",
  112. "deeply-nested-list:world",
  113. ],
  114. "should-not-cause-an-exception,but-result-empty:{foo}": [],
  115. "Case-Should-Be-Lowered:{grains[mixedcase]}": [
  116. "case-should-be-lowered:up-low-up"
  117. ],
  118. }
  119. with patch(
  120. "salt.utils.minions.get_minion_data",
  121. MagicMock(return_value=(None, self.grains, None)),
  122. ):
  123. for case, correct_output in six.iteritems(cases):
  124. test_config = {"policies": [case]}
  125. output = vault._get_policies(
  126. "test-minion", test_config
  127. ) # pylint: disable=protected-access
  128. diff = set(output).symmetric_difference(set(correct_output))
  129. if diff:
  130. log.debug("Test %s failed", case)
  131. log.debug("Expected:\n\t%s\nGot\n\t%s", output, correct_output)
  132. log.debug("Difference:\n\t%s", diff)
  133. self.assertEqual(output, correct_output)
  134. def test_get_token_create_url(self):
  135. """
  136. Ensure _get_token_create_url parses config correctly
  137. """
  138. self.assertEqual(
  139. vault._get_token_create_url( # pylint: disable=protected-access
  140. {"url": "http://127.0.0.1"}
  141. ),
  142. "http://127.0.0.1/v1/auth/token/create",
  143. )
  144. self.assertEqual(
  145. vault._get_token_create_url( # pylint: disable=protected-access
  146. {"url": "https://127.0.0.1/"}
  147. ),
  148. "https://127.0.0.1/v1/auth/token/create",
  149. )
  150. self.assertEqual(
  151. vault._get_token_create_url( # pylint: disable=protected-access
  152. {"url": "http://127.0.0.1:8200", "role_name": "therole"}
  153. ),
  154. "http://127.0.0.1:8200/v1/auth/token/create/therole",
  155. )
  156. self.assertEqual(
  157. vault._get_token_create_url( # pylint: disable=protected-access
  158. {"url": "https://127.0.0.1/test", "role_name": "therole"}
  159. ),
  160. "https://127.0.0.1/test/v1/auth/token/create/therole",
  161. )
  162. def _mock_json_response(data, status_code=200, reason=""):
  163. """
  164. Mock helper for http response
  165. """
  166. response = MagicMock()
  167. response.json = MagicMock(return_value=data)
  168. response.status_code = status_code
  169. response.reason = reason
  170. return Mock(return_value=response)
  171. class VaultTokenAuthTest(TestCase, LoaderModuleMockMixin):
  172. """
  173. Tests for the runner module of the Vault with token setup
  174. """
  175. def setup_loader_modules(self):
  176. return {
  177. vault: {
  178. "__opts__": {
  179. "vault": {
  180. "url": "http://127.0.0.1",
  181. "auth": {
  182. "token": "test",
  183. "method": "token",
  184. "allow_minion_override": True,
  185. },
  186. }
  187. }
  188. }
  189. }
  190. @patch("salt.runners.vault._validate_signature", MagicMock(return_value=None))
  191. @patch(
  192. "salt.runners.vault._get_token_create_url",
  193. MagicMock(return_value="http://fake_url"),
  194. )
  195. def test_generate_token(self):
  196. """
  197. Basic tests for test_generate_token: all exits
  198. """
  199. mock = _mock_json_response(
  200. {"auth": {"client_token": "test", "renewable": False, "lease_duration": 0}}
  201. )
  202. with patch("requests.post", mock):
  203. result = vault.generate_token("test-minion", "signature")
  204. log.debug("generate_token result: %s", result)
  205. self.assertTrue(isinstance(result, dict))
  206. self.assertFalse("error" in result)
  207. self.assertTrue("token" in result)
  208. self.assertEqual(result["token"], "test")
  209. mock.assert_called_with(
  210. "http://fake_url", headers=ANY, json=ANY, verify=ANY
  211. )
  212. # Test uses
  213. num_uses = 6
  214. result = vault.generate_token("test-minion", "signature", uses=num_uses)
  215. self.assertTrue("uses" in result)
  216. self.assertEqual(result["uses"], num_uses)
  217. json_request = {
  218. "policies": ["saltstack/minion/test-minion", "saltstack/minions"],
  219. "num_uses": num_uses,
  220. "meta": {
  221. "saltstack-jid": "<no jid set>",
  222. "saltstack-minion": "test-minion",
  223. "saltstack-user": "<no user set>",
  224. },
  225. }
  226. mock.assert_called_with(
  227. "http://fake_url", headers=ANY, json=json_request, verify=ANY
  228. )
  229. # Test ttl
  230. expected_ttl = "6h"
  231. result = vault.generate_token("test-minion", "signature", ttl=expected_ttl)
  232. self.assertTrue(result["uses"] == 1)
  233. json_request = {
  234. "policies": ["saltstack/minion/test-minion", "saltstack/minions"],
  235. "num_uses": 1,
  236. "explicit_max_ttl": expected_ttl,
  237. "meta": {
  238. "saltstack-jid": "<no jid set>",
  239. "saltstack-minion": "test-minion",
  240. "saltstack-user": "<no user set>",
  241. },
  242. }
  243. mock.assert_called_with(
  244. "http://fake_url", headers=ANY, json=json_request, verify=ANY
  245. )
  246. mock = _mock_json_response({}, status_code=403, reason="no reason")
  247. with patch("requests.post", mock):
  248. result = vault.generate_token("test-minion", "signature")
  249. self.assertTrue(isinstance(result, dict))
  250. self.assertTrue("error" in result)
  251. self.assertEqual(result["error"], "no reason")
  252. with patch("salt.runners.vault._get_policies", MagicMock(return_value=[])):
  253. result = vault.generate_token("test-minion", "signature")
  254. self.assertTrue(isinstance(result, dict))
  255. self.assertTrue("error" in result)
  256. self.assertEqual(result["error"], "No policies matched minion")
  257. with patch(
  258. "requests.post", MagicMock(side_effect=Exception("Test Exception Reason"))
  259. ):
  260. result = vault.generate_token("test-minion", "signature")
  261. self.assertTrue(isinstance(result, dict))
  262. self.assertTrue("error" in result)
  263. self.assertEqual(result["error"], "Test Exception Reason")
  264. class VaultAppRoleAuthTest(TestCase, LoaderModuleMockMixin):
  265. """
  266. Tests for the runner module of the Vault with approle setup
  267. """
  268. def setup_loader_modules(self):
  269. return {
  270. vault: {
  271. "__opts__": {
  272. "vault": {
  273. "url": "http://127.0.0.1",
  274. "auth": {
  275. "method": "approle",
  276. "role_id": "role",
  277. "secret_id": "secret",
  278. },
  279. }
  280. }
  281. }
  282. }
  283. @patch("salt.runners.vault._validate_signature", MagicMock(return_value=None))
  284. @patch(
  285. "salt.runners.vault._get_token_create_url",
  286. MagicMock(return_value="http://fake_url"),
  287. )
  288. def test_generate_token(self):
  289. """
  290. Basic test for test_generate_token with approle (two vault calls)
  291. """
  292. mock = _mock_json_response(
  293. {"auth": {"client_token": "test", "renewable": False, "lease_duration": 0}}
  294. )
  295. with patch("requests.post", mock):
  296. result = vault.generate_token("test-minion", "signature")
  297. log.debug("generate_token result: %s", result)
  298. self.assertTrue(isinstance(result, dict))
  299. self.assertFalse("error" in result)
  300. self.assertTrue("token" in result)
  301. self.assertEqual(result["token"], "test")
  302. calls = [
  303. call("http://127.0.0.1/v1/auth/approle/login", json=ANY, verify=ANY),
  304. call("http://fake_url", headers=ANY, json=ANY, verify=ANY),
  305. ]
  306. mock.assert_has_calls(calls)