test_vault.py 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428
  1. # -*- coding: utf-8 -*-
  2. """
  3. Test case for the vault utils module
  4. """
  5. # Import python libs
  6. from __future__ import absolute_import, print_function, unicode_literals
  7. import json
  8. import logging
  9. import os
  10. from copy import copy
  11. # Import Salt libs
  12. import salt.utils.vault as vault
  13. from tests.support.mixins import LoaderModuleMockMixin
  14. from tests.support.mock import ANY, MagicMock, Mock, mock_open, patch
  15. # Import Salt Testing libs
  16. from tests.support.unit import TestCase
  17. class RequestMock(Mock):
  18. """
  19. Request Mock
  20. """
  21. def get(self, *args, **kwargs):
  22. return {}
  23. class TestVaultUtils(LoaderModuleMockMixin, TestCase):
  24. """
  25. Test case for the vault utils module
  26. """
  27. json_success = {
  28. "request_id": "35df4df1-c3d8-b270-0682-ddb0160c7450",
  29. "lease_id": "",
  30. "renewable": False,
  31. "lease_duration": 0,
  32. "data": {
  33. "data": {"something": "myvalue"},
  34. "metadata": {
  35. "created_time": "2020-05-02T07:26:12.180848003Z",
  36. "deletion_time": "",
  37. "destroyed": False,
  38. "version": 1,
  39. },
  40. },
  41. "wrap_info": None,
  42. "warnings": None,
  43. "auth": None,
  44. }
  45. json_denied = {"errors": ["permission denied"]}
  46. cache_single = {
  47. "url": "http://127.0.0.1:8200",
  48. "token": "test",
  49. "verify": None,
  50. "uses": 1,
  51. "lease_duration": 100,
  52. "issued": 3000,
  53. }
  54. cache_uses = {
  55. "url": "http://127.0.0.1:8200",
  56. "token": "test",
  57. "verify": None,
  58. "uses": 10,
  59. "lease_duration": 100,
  60. "issued": 3000,
  61. "unlimited_use_token": False,
  62. }
  63. cache_uses_last = {
  64. "url": "http://127.0.0.1:8200",
  65. "token": "test",
  66. "verify": None,
  67. "uses": 1,
  68. "lease_duration": 100,
  69. "issued": 3000,
  70. "unlimited_use_token": False,
  71. }
  72. cache_unlimited = {
  73. "url": "http://127.0.0.1:8200",
  74. "token": "test",
  75. "verify": None,
  76. "uses": 0,
  77. "lease_duration": 100,
  78. "issued": 3000,
  79. "unlimited_use_token": True,
  80. }
  81. metadata_v2 = {
  82. "accessor": "kv_f8731f1b",
  83. "config": {
  84. "default_lease_ttl": 0,
  85. "force_no_cache": False,
  86. "max_lease_ttl": 0,
  87. },
  88. "description": "key/value secret storage",
  89. "external_entropy_access": False,
  90. "local": False,
  91. "options": {"version": "2"},
  92. "path": "secret/",
  93. "seal_wrap": False,
  94. "type": "kv",
  95. "uuid": "1d9431ac-060a-9b63-4572-3ca7ffd78347",
  96. }
  97. cache_secret_meta = {"vault_secret_path_metadata": {"secret/mything": metadata_v2}}
  98. def setup_loader_modules(self):
  99. return {
  100. vault: {
  101. "__opts__": {
  102. "vault": {
  103. "url": "http://127.0.0.1",
  104. "auth": {
  105. "token": "test",
  106. "method": "token",
  107. "uses": 15,
  108. "ttl": 500,
  109. },
  110. },
  111. "file_client": "local",
  112. "cachedir": "somepath",
  113. },
  114. "__grains__": {"id": "test-minion"},
  115. "__context__": {},
  116. }
  117. }
  118. def _mock_json_response(self, data, status_code=200, reason=""):
  119. """
  120. Mock helper for http response
  121. """
  122. response = MagicMock()
  123. response.json = MagicMock(return_value=data)
  124. response.status_code = status_code
  125. response.reason = reason
  126. if status_code == 200:
  127. response.ok = True
  128. else:
  129. response.ok = False
  130. return Mock(return_value=response)
  131. def test_make_request_single_use_token_run_ok(self):
  132. """
  133. Given single use token in __context__, function should run successful secret lookup with no other modifications
  134. """
  135. mock = self._mock_json_response(self.json_success)
  136. supplied_context = {"vault_token": copy(self.cache_single)}
  137. expected_headers = {"X-Vault-Token": "test", "Content-Type": "application/json"}
  138. with patch.dict(vault.__context__, supplied_context):
  139. with patch("requests.request", mock):
  140. vault_return = vault.make_request("/secret/my/secret", "key")
  141. self.assertEqual(vault.__context__, {})
  142. mock.assert_called_with(
  143. "/secret/my/secret",
  144. "http://127.0.0.1:8200/key",
  145. headers=expected_headers,
  146. verify=ANY,
  147. )
  148. self.assertEqual(vault_return.json(), self.json_success)
  149. def test_make_request_single_use_token_run_auth_error(self):
  150. """
  151. Given single use token in __context__ and login error, function should request token and re-run
  152. """
  153. # Disable logging because simulated http failures are logged as errors
  154. logging.disable(logging.CRITICAL)
  155. mock = self._mock_json_response(self.json_denied, status_code=400)
  156. supplied_context = {"vault_token": copy(self.cache_single)}
  157. expected_headers = {"X-Vault-Token": "test", "Content-Type": "application/json"}
  158. with patch.dict(vault.__context__, supplied_context):
  159. with patch("requests.request", mock):
  160. with patch.object(vault, "del_cache") as mock_del_cache:
  161. vault_return = vault.make_request("/secret/my/secret", "key")
  162. self.assertEqual(vault.__context__, {})
  163. mock.assert_called_with(
  164. "/secret/my/secret",
  165. "http://127.0.0.1:8200/key",
  166. headers=expected_headers,
  167. verify=ANY,
  168. )
  169. self.assertEqual(vault_return.json(), self.json_denied)
  170. mock_del_cache.assert_called()
  171. self.assertEqual(mock.call_count, 2)
  172. logging.disable(logging.NOTSET)
  173. def test_multi_use_token_successful_run(self):
  174. """
  175. Given multi-use token, function should get secret and decrement token
  176. """
  177. expected_cache_write = {
  178. "url": "http://127.0.0.1:8200",
  179. "token": "test",
  180. "verify": None,
  181. "uses": 9,
  182. "lease_duration": 100,
  183. "issued": 3000,
  184. "unlimited_use_token": False,
  185. }
  186. mock = self._mock_json_response(self.json_success)
  187. expected_headers = {"X-Vault-Token": "test", "Content-Type": "application/json"}
  188. with patch.object(vault, "get_cache") as mock_get_cache:
  189. mock_get_cache.return_value = copy(self.cache_uses)
  190. with patch("requests.request", mock):
  191. with patch.object(vault, "del_cache") as mock_del_cache:
  192. with patch.object(vault, "write_cache") as mock_write_cache:
  193. vault_return = vault.make_request("/secret/my/secret", "key")
  194. mock.assert_called_with(
  195. "/secret/my/secret",
  196. "http://127.0.0.1:8200/key",
  197. headers=expected_headers,
  198. verify=ANY,
  199. )
  200. mock_write_cache.assert_called_with(expected_cache_write)
  201. self.assertEqual(vault_return.json(), self.json_success)
  202. self.assertEqual(mock.call_count, 1)
  203. def test_multi_use_token_last_use(self):
  204. """
  205. Given last use of multi-use token, function should succeed and flush token cache
  206. """
  207. mock = self._mock_json_response(self.json_success)
  208. expected_headers = {"X-Vault-Token": "test", "Content-Type": "application/json"}
  209. with patch.object(vault, "get_cache") as mock_get_cache:
  210. mock_get_cache.return_value = self.cache_uses_last
  211. with patch("requests.request", mock):
  212. with patch.object(vault, "del_cache") as mock_del_cache:
  213. with patch.object(vault, "write_cache") as mock_write_cache:
  214. vault_return = vault.make_request("/secret/my/secret", "key")
  215. mock.assert_called_with(
  216. "/secret/my/secret",
  217. "http://127.0.0.1:8200/key",
  218. headers=expected_headers,
  219. verify=ANY,
  220. )
  221. mock_del_cache.assert_called()
  222. self.assertEqual(vault_return.json(), self.json_success)
  223. self.assertEqual(mock.call_count, 1)
  224. def test_unlimited_use_token_no_decrement(self):
  225. """
  226. Given unlimited-use token, function should succeed not del cache or decrement
  227. """
  228. mock = self._mock_json_response(self.json_success)
  229. expected_headers = {"X-Vault-Token": "test", "Content-Type": "application/json"}
  230. with patch.object(vault, "get_cache") as mock_get_cache:
  231. mock_get_cache.return_value = self.cache_unlimited
  232. with patch("requests.request", mock):
  233. with patch.object(vault, "del_cache") as mock_del_cache:
  234. with patch.object(vault, "write_cache") as mock_write_cache:
  235. vault_return = vault.make_request("/secret/my/secret", "key")
  236. mock.assert_called_with(
  237. "/secret/my/secret",
  238. "http://127.0.0.1:8200/key",
  239. headers=expected_headers,
  240. verify=ANY,
  241. )
  242. assert (
  243. not mock_del_cache.called
  244. ), "del cache should not be called for unlimited use token"
  245. assert (
  246. not mock_write_cache.called
  247. ), "write cache should not be called for unlimited use token"
  248. self.assertEqual(vault_return.json(), self.json_success)
  249. self.assertEqual(mock.call_count, 1)
  250. def test_get_cache_standard(self):
  251. """
  252. test standard first run of no cache file. Should generate new connection and write cache
  253. """
  254. with patch.object(vault, "_read_cache_file") as mock_read_cache:
  255. mock_read_cache.return_value = {}
  256. with patch.object(
  257. vault, "get_vault_connection"
  258. ) as mock_get_vault_connection:
  259. mock_get_vault_connection.return_value = copy(self.cache_single)
  260. with patch.object(vault, "write_cache") as mock_write_cache:
  261. cache_result = vault.get_cache()
  262. mock_write_cache.assert_called_with(copy(self.cache_single))
  263. def test_get_cache_existing_cache_valid(self):
  264. """
  265. test standard valid cache file
  266. """
  267. with patch("time.time", return_value=1234):
  268. with patch.object(vault, "_read_cache_file") as mock_read_cache:
  269. mock_read_cache.return_value = self.cache_uses
  270. with patch.object(vault, "write_cache") as mock_write_cache:
  271. with patch.object(vault, "del_cache") as mock_del_cache:
  272. cache_result = vault.get_cache()
  273. assert not mock_write_cache.called
  274. assert not mock_del_cache.called
  275. self.assertEqual(cache_result, self.cache_uses)
  276. def test_get_cache_existing_cache_old(self):
  277. """
  278. test old cache file
  279. """
  280. with patch("time.time", return_value=3101):
  281. with patch.object(
  282. vault, "get_vault_connection"
  283. ) as mock_get_vault_connection:
  284. mock_get_vault_connection.return_value = self.cache_uses
  285. with patch.object(vault, "_read_cache_file") as mock_read_cache:
  286. mock_read_cache.return_value = self.cache_uses
  287. with patch.object(vault, "write_cache") as mock_write_cache:
  288. with patch.object(vault, "del_cache") as mock_del_cache:
  289. cache_result = vault.get_cache()
  290. assert mock_del_cache.called
  291. assert mock_write_cache.called
  292. self.assertEqual(cache_result, self.cache_uses)
  293. def test_write_cache_standard(self):
  294. """
  295. Test write cache with standard single use token
  296. """
  297. function_response = vault.write_cache(copy(self.cache_single))
  298. self.assertEqual(vault.__context__["vault_token"], copy(self.cache_single))
  299. self.assertTrue(function_response)
  300. def test_write_cache_multi_use_token(self):
  301. """
  302. Test write cache with multi-use token
  303. """
  304. expected_write = {
  305. "url": "http://127.0.0.1:8200",
  306. "token": "test",
  307. "verify": None,
  308. "uses": 10,
  309. "lease_duration": 100,
  310. "issued": 3000,
  311. "unlimited_use_token": False,
  312. }
  313. with patch("salt.utils.files.fpopen", mock_open()) as mock_fpopen:
  314. function_response = vault.write_cache(self.cache_uses)
  315. assert mock_fpopen.call_count == 1
  316. self.assertListEqual(
  317. list(mock_fpopen.filehandles),
  318. [os.path.join("somepath", "salt_vault_token")],
  319. )
  320. opens = mock_fpopen.filehandles[
  321. os.path.join("somepath", "salt_vault_token")
  322. ]
  323. write_calls_output = json.loads(opens[0].write_calls[0])
  324. self.assertDictEqual(write_calls_output, expected_write)
  325. self.assertTrue(function_response)
  326. def test_write_cache_unlimited_token(self):
  327. """
  328. Test write cache with unlimited use token
  329. """
  330. write_data = {
  331. "url": "http://127.0.0.1:8200",
  332. "token": "test",
  333. "verify": None,
  334. "uses": 0,
  335. "lease_duration": 100,
  336. "issued": 3000,
  337. }
  338. expected_write = {
  339. "url": "http://127.0.0.1:8200",
  340. "token": "test",
  341. "verify": None,
  342. "uses": 0,
  343. "lease_duration": 100,
  344. "issued": 3000,
  345. "unlimited_use_token": True,
  346. }
  347. with patch("salt.utils.files.fpopen", mock_open()) as mock_fpopen:
  348. function_response = vault.write_cache(write_data)
  349. assert mock_fpopen.call_count == 1
  350. self.assertListEqual(
  351. list(mock_fpopen.filehandles),
  352. [os.path.join("somepath", "salt_vault_token")],
  353. )
  354. opens = mock_fpopen.filehandles[
  355. os.path.join("somepath", "salt_vault_token")
  356. ]
  357. write_calls_output = json.loads(opens[0].write_calls[0])
  358. self.assertEqual(write_calls_output, expected_write)
  359. self.assertTrue(function_response)
  360. def test_path_is_v2(self):
  361. """
  362. Validated v2 path is detected as vault kv v2
  363. """
  364. expected_return = {
  365. "v2": True,
  366. "data": "secret/data/mything",
  367. "metadata": "secret/metadata/mything",
  368. "delete": "secret/mything",
  369. "type": "kv",
  370. "destroy": "secret/destroy/mything",
  371. }
  372. with patch.object(vault, "_get_secret_path_metadata") as mock_get_metadata:
  373. mock_get_metadata.return_value = self.metadata_v2
  374. function_return = vault.is_v2("secret/mything")
  375. self.assertEqual(function_return, expected_return)
  376. def test_get_secret_path_metadata_no_cache(self):
  377. """
  378. test with no cache file
  379. """
  380. make_request_response = {
  381. "request_id": "b82f2df7-a9b6-920c-0ed2-a3463b996f9e",
  382. "lease_id": "",
  383. "renewable": False,
  384. "lease_duration": 0,
  385. "data": self.metadata_v2,
  386. "wrap_info": None,
  387. "warnings": None,
  388. "auth": None,
  389. }
  390. cache_object = copy(self.cache_uses)
  391. expected_cache_object = copy(self.cache_uses)
  392. expected_cache_object.update(copy(self.cache_secret_meta))
  393. secret_path = "secret/mything"
  394. mock = self._mock_json_response(make_request_response)
  395. with patch.object(vault, "_read_cache_file") as mock_read_cache:
  396. mock_read_cache.return_value = cache_object
  397. with patch.object(vault, "write_cache") as mock_write_cache:
  398. with patch("salt.utils.vault.make_request", mock):
  399. function_result = vault._get_secret_path_metadata(secret_path)
  400. self.assertEqual(function_result, self.metadata_v2)
  401. mock_write_cache.assert_called_with(cache_object)
  402. self.assertEqual(cache_object, expected_cache_object)