test_rest_tornado.py 38 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111
  1. # coding: utf-8
  2. from __future__ import absolute_import
  3. import copy
  4. import hashlib
  5. import os
  6. import shutil
  7. import salt.auth
  8. import salt.utils.event
  9. import salt.utils.json
  10. import salt.utils.yaml
  11. from salt.ext import six
  12. from salt.ext.six.moves import map, range # pylint: disable=import-error
  13. from salt.ext.six.moves.urllib.parse import ( # pylint: disable=no-name-in-module
  14. urlencode,
  15. urlparse,
  16. )
  17. from tests.support.events import eventpublisher_process
  18. from tests.support.helpers import patched_environ, slowTest
  19. from tests.support.mixins import AdaptedConfigurationTestCaseMixin
  20. from tests.support.mock import MagicMock, patch
  21. from tests.support.runtests import RUNTIME_VARS
  22. from tests.support.unit import TestCase, skipIf
  23. try:
  24. HAS_TORNADO = True
  25. except ImportError:
  26. HAS_TORNADO = False
  27. # pylint: disable=import-error
  28. try:
  29. import salt.ext.tornado.escape
  30. import salt.ext.tornado.testing
  31. import salt.ext.tornado.concurrent
  32. from salt.ext.tornado.testing import AsyncTestCase, AsyncHTTPTestCase, gen_test
  33. from salt.ext.tornado.httpclient import HTTPRequest, HTTPError
  34. from salt.ext.tornado.websocket import websocket_connect
  35. import salt.netapi.rest_tornado as rest_tornado
  36. from salt.netapi.rest_tornado import saltnado
  37. HAS_TORNADO = True
  38. except ImportError:
  39. HAS_TORNADO = False
  40. # Create fake test case classes so we can properly skip the test case
  41. class AsyncTestCase(object):
  42. pass
  43. class AsyncHTTPTestCase(object):
  44. pass
  45. # pylint: enable=import-error
  46. @skipIf(
  47. not HAS_TORNADO, "The tornado package needs to be installed"
  48. ) # pylint: disable=W0223
  49. class SaltnadoTestCase(TestCase, AdaptedConfigurationTestCaseMixin, AsyncHTTPTestCase):
  50. """
  51. Mixin to hold some shared things
  52. """
  53. content_type_map = {
  54. "json": "application/json",
  55. "json-utf8": "application/json; charset=utf-8",
  56. "yaml": "application/x-yaml",
  57. "text": "text/plain",
  58. "form": "application/x-www-form-urlencoded",
  59. "xml": "application/xml",
  60. "real-accept-header-json": "application/json, text/javascript, */*; q=0.01",
  61. "real-accept-header-yaml": "application/x-yaml, text/yaml, */*; q=0.01",
  62. }
  63. auth_creds = (
  64. ("username", "saltdev_api"),
  65. ("password", "saltdev"),
  66. ("eauth", "auto"),
  67. )
  68. @property
  69. def auth_creds_dict(self):
  70. return dict(self.auth_creds)
  71. @property
  72. def opts(self):
  73. return self.get_temp_config("client_config")
  74. @property
  75. def mod_opts(self):
  76. return self.get_temp_config("minion")
  77. @property
  78. def auth(self):
  79. if not hasattr(self, "__auth"):
  80. self.__auth = salt.auth.LoadAuth(self.opts)
  81. return self.__auth
  82. @property
  83. def token(self):
  84. """ Mint and return a valid token for auth_creds """
  85. return self.auth.mk_token(self.auth_creds_dict)
  86. def setUp(self):
  87. super(SaltnadoTestCase, self).setUp()
  88. self.patched_environ = patched_environ(ASYNC_TEST_TIMEOUT="30")
  89. self.patched_environ.__enter__()
  90. self.addCleanup(self.patched_environ.__exit__)
  91. def tearDown(self):
  92. super(SaltnadoTestCase, self).tearDown()
  93. if hasattr(self, "http_server"):
  94. del self.http_server
  95. if hasattr(self, "io_loop"):
  96. del self.io_loop
  97. if hasattr(self, "_app"):
  98. del self._app
  99. if hasattr(self, "http_client"):
  100. del self.http_client
  101. if hasattr(self, "__port"):
  102. del self.__port
  103. if hasattr(self, "_AsyncHTTPTestCase__port"):
  104. del self._AsyncHTTPTestCase__port
  105. if hasattr(self, "__auth"):
  106. del self.__auth
  107. if hasattr(self, "_SaltnadoTestCase__auth"):
  108. del self._SaltnadoTestCase__auth
  109. if hasattr(self, "_test_generator"):
  110. del self._test_generator
  111. if hasattr(self, "application"):
  112. del self.application
  113. def build_tornado_app(self, urls):
  114. application = salt.ext.tornado.web.Application(urls, debug=True)
  115. application.auth = self.auth
  116. application.opts = self.opts
  117. application.mod_opts = self.mod_opts
  118. return application
  119. def decode_body(self, response):
  120. if response is None:
  121. return response
  122. if six.PY2:
  123. return response
  124. if response.body:
  125. # Decode it
  126. if response.headers.get("Content-Type") == "application/json":
  127. response._body = response.body.decode("utf-8")
  128. else:
  129. response._body = salt.ext.tornado.escape.native_str(response.body)
  130. return response
  131. def fetch(self, path, **kwargs):
  132. return self.decode_body(super(SaltnadoTestCase, self).fetch(path, **kwargs))
  133. class TestBaseSaltAPIHandler(SaltnadoTestCase):
  134. def get_app(self):
  135. class StubHandler(saltnado.BaseSaltAPIHandler): # pylint: disable=W0223
  136. def get(self, *args, **kwargs):
  137. return self.echo_stuff()
  138. def post(self): # pylint: disable=arguments-differ
  139. return self.echo_stuff()
  140. def echo_stuff(self):
  141. ret_dict = {"foo": "bar"}
  142. attrs = (
  143. "token",
  144. "start",
  145. "connected",
  146. "lowstate",
  147. )
  148. for attr in attrs:
  149. ret_dict[attr] = getattr(self, attr)
  150. self.write(self.serialize(ret_dict))
  151. urls = [("/", StubHandler), ("/(.*)", StubHandler)]
  152. return self.build_tornado_app(urls)
  153. def test_accept_content_type(self):
  154. """
  155. Test the base handler's accept picking
  156. """
  157. # send NO accept header, should come back with json
  158. response = self.fetch("/")
  159. self.assertEqual(
  160. response.headers["Content-Type"], self.content_type_map["json"]
  161. )
  162. self.assertEqual(type(salt.utils.json.loads(response.body)), dict)
  163. # Request application/json
  164. response = self.fetch("/", headers={"Accept": self.content_type_map["json"]})
  165. self.assertEqual(
  166. response.headers["Content-Type"], self.content_type_map["json"]
  167. )
  168. self.assertEqual(type(salt.utils.json.loads(response.body)), dict)
  169. # Request application/x-yaml
  170. response = self.fetch("/", headers={"Accept": self.content_type_map["yaml"]})
  171. self.assertEqual(
  172. response.headers["Content-Type"], self.content_type_map["yaml"]
  173. )
  174. self.assertEqual(type(salt.utils.yaml.safe_load(response.body)), dict)
  175. # Request not supported content-type
  176. response = self.fetch("/", headers={"Accept": self.content_type_map["xml"]})
  177. self.assertEqual(response.code, 406)
  178. # Request some JSON with a browser like Accept
  179. accept_header = self.content_type_map["real-accept-header-json"]
  180. response = self.fetch("/", headers={"Accept": accept_header})
  181. self.assertEqual(
  182. response.headers["Content-Type"], self.content_type_map["json"]
  183. )
  184. self.assertEqual(type(salt.utils.json.loads(response.body)), dict)
  185. # Request some YAML with a browser like Accept
  186. accept_header = self.content_type_map["real-accept-header-yaml"]
  187. response = self.fetch("/", headers={"Accept": accept_header})
  188. self.assertEqual(
  189. response.headers["Content-Type"], self.content_type_map["yaml"]
  190. )
  191. self.assertEqual(type(salt.utils.yaml.safe_load(response.body)), dict)
  192. def test_token(self):
  193. """
  194. Test that the token is returned correctly
  195. """
  196. token = salt.utils.json.loads(self.fetch("/").body)["token"]
  197. self.assertIs(token, None)
  198. # send a token as a header
  199. response = self.fetch("/", headers={saltnado.AUTH_TOKEN_HEADER: "foo"})
  200. token = salt.utils.json.loads(response.body)["token"]
  201. self.assertEqual(token, "foo")
  202. # send a token as a cookie
  203. response = self.fetch(
  204. "/", headers={"Cookie": "{0}=foo".format(saltnado.AUTH_COOKIE_NAME)}
  205. )
  206. token = salt.utils.json.loads(response.body)["token"]
  207. self.assertEqual(token, "foo")
  208. # send both, make sure its the header
  209. response = self.fetch(
  210. "/",
  211. headers={
  212. saltnado.AUTH_TOKEN_HEADER: "foo",
  213. "Cookie": "{0}=bar".format(saltnado.AUTH_COOKIE_NAME),
  214. },
  215. )
  216. token = salt.utils.json.loads(response.body)["token"]
  217. self.assertEqual(token, "foo")
  218. def test_deserialize(self):
  219. """
  220. Send various encoded forms of lowstates (and bad ones) to make sure we
  221. handle deserialization correctly
  222. """
  223. valid_lowstate = [
  224. {"client": "local", "tgt": "*", "fun": "test.fib", "arg": ["10"]},
  225. {
  226. "client": "runner",
  227. "fun": "jobs.lookup_jid",
  228. "jid": "20130603122505459265",
  229. },
  230. ]
  231. # send as JSON
  232. response = self.fetch(
  233. "/",
  234. method="POST",
  235. body=salt.utils.json.dumps(valid_lowstate),
  236. headers={"Content-Type": self.content_type_map["json"]},
  237. )
  238. self.assertEqual(
  239. valid_lowstate, salt.utils.json.loads(response.body)["lowstate"]
  240. )
  241. # send yaml as json (should break)
  242. response = self.fetch(
  243. "/",
  244. method="POST",
  245. body=salt.utils.yaml.safe_dump(valid_lowstate),
  246. headers={"Content-Type": self.content_type_map["json"]},
  247. )
  248. self.assertEqual(response.code, 400)
  249. # send as yaml
  250. response = self.fetch(
  251. "/",
  252. method="POST",
  253. body=salt.utils.yaml.safe_dump(valid_lowstate),
  254. headers={"Content-Type": self.content_type_map["yaml"]},
  255. )
  256. self.assertEqual(
  257. valid_lowstate, salt.utils.json.loads(response.body)["lowstate"]
  258. )
  259. # send json as yaml (works since yaml is a superset of json)
  260. response = self.fetch(
  261. "/",
  262. method="POST",
  263. body=salt.utils.json.dumps(valid_lowstate),
  264. headers={"Content-Type": self.content_type_map["yaml"]},
  265. )
  266. self.assertEqual(
  267. valid_lowstate, salt.utils.json.loads(response.body)["lowstate"]
  268. )
  269. # send json as text/plain
  270. response = self.fetch(
  271. "/",
  272. method="POST",
  273. body=salt.utils.json.dumps(valid_lowstate),
  274. headers={"Content-Type": self.content_type_map["text"]},
  275. )
  276. self.assertEqual(
  277. valid_lowstate, salt.utils.json.loads(response.body)["lowstate"]
  278. )
  279. # send form-urlencoded
  280. form_lowstate = (
  281. ("client", "local"),
  282. ("tgt", "*"),
  283. ("fun", "test.fib"),
  284. ("arg", "10"),
  285. ("arg", "foo"),
  286. )
  287. response = self.fetch(
  288. "/",
  289. method="POST",
  290. body=urlencode(form_lowstate),
  291. headers={"Content-Type": self.content_type_map["form"]},
  292. )
  293. returned_lowstate = salt.utils.json.loads(response.body)["lowstate"]
  294. self.assertEqual(len(returned_lowstate), 1)
  295. returned_lowstate = returned_lowstate[0]
  296. self.assertEqual(returned_lowstate["client"], "local")
  297. self.assertEqual(returned_lowstate["tgt"], "*")
  298. self.assertEqual(returned_lowstate["fun"], "test.fib")
  299. self.assertEqual(returned_lowstate["arg"], ["10", "foo"])
  300. # Send json with utf8 charset
  301. response = self.fetch(
  302. "/",
  303. method="POST",
  304. body=salt.utils.json.dumps(valid_lowstate),
  305. headers={"Content-Type": self.content_type_map["json-utf8"]},
  306. )
  307. self.assertEqual(
  308. valid_lowstate, salt.utils.json.loads(response.body)["lowstate"]
  309. )
  310. def test_get_lowstate(self):
  311. """
  312. Test transformations low data of the function _get_lowstate
  313. """
  314. valid_lowstate = [
  315. {u"client": u"local", u"tgt": u"*", u"fun": u"test.fib", u"arg": [u"10"]}
  316. ]
  317. # Case 1. dictionary type of lowstate
  318. request_lowstate = {
  319. "client": "local",
  320. "tgt": "*",
  321. "fun": "test.fib",
  322. "arg": ["10"],
  323. }
  324. response = self.fetch(
  325. "/",
  326. method="POST",
  327. body=salt.utils.json.dumps(request_lowstate),
  328. headers={"Content-Type": self.content_type_map["json"]},
  329. )
  330. self.assertEqual(
  331. valid_lowstate, salt.utils.json.loads(response.body)["lowstate"]
  332. )
  333. # Case 2. string type of arg
  334. request_lowstate = {
  335. "client": "local",
  336. "tgt": "*",
  337. "fun": "test.fib",
  338. "arg": "10",
  339. }
  340. response = self.fetch(
  341. "/",
  342. method="POST",
  343. body=salt.utils.json.dumps(request_lowstate),
  344. headers={"Content-Type": self.content_type_map["json"]},
  345. )
  346. self.assertEqual(
  347. valid_lowstate, salt.utils.json.loads(response.body)["lowstate"]
  348. )
  349. # Case 3. Combine Case 1 and Case 2.
  350. request_lowstate = {
  351. "client": "local",
  352. "tgt": "*",
  353. "fun": "test.fib",
  354. "arg": "10",
  355. }
  356. # send as json
  357. response = self.fetch(
  358. "/",
  359. method="POST",
  360. body=salt.utils.json.dumps(request_lowstate),
  361. headers={"Content-Type": self.content_type_map["json"]},
  362. )
  363. self.assertEqual(
  364. valid_lowstate, salt.utils.json.loads(response.body)["lowstate"]
  365. )
  366. # send as yaml
  367. response = self.fetch(
  368. "/",
  369. method="POST",
  370. body=salt.utils.yaml.safe_dump(request_lowstate),
  371. headers={"Content-Type": self.content_type_map["yaml"]},
  372. )
  373. self.assertEqual(
  374. valid_lowstate, salt.utils.json.loads(response.body)["lowstate"]
  375. )
  376. # send as plain text
  377. response = self.fetch(
  378. "/",
  379. method="POST",
  380. body=salt.utils.json.dumps(request_lowstate),
  381. headers={"Content-Type": self.content_type_map["text"]},
  382. )
  383. self.assertEqual(
  384. valid_lowstate, salt.utils.json.loads(response.body)["lowstate"]
  385. )
  386. # send as form-urlencoded
  387. request_form_lowstate = (
  388. ("client", "local"),
  389. ("tgt", "*"),
  390. ("fun", "test.fib"),
  391. ("arg", "10"),
  392. )
  393. response = self.fetch(
  394. "/",
  395. method="POST",
  396. body=urlencode(request_form_lowstate),
  397. headers={"Content-Type": self.content_type_map["form"]},
  398. )
  399. self.assertEqual(
  400. valid_lowstate, salt.utils.json.loads(response.body)["lowstate"]
  401. )
  402. def test_cors_origin_wildcard(self):
  403. """
  404. Check that endpoints returns Access-Control-Allow-Origin
  405. """
  406. self._app.mod_opts["cors_origin"] = "*"
  407. headers = self.fetch("/").headers
  408. self.assertEqual(headers["Access-Control-Allow-Origin"], "*")
  409. def test_cors_origin_single(self):
  410. """
  411. Check that endpoints returns the Access-Control-Allow-Origin when
  412. only one origins is set
  413. """
  414. self._app.mod_opts["cors_origin"] = "http://example.foo"
  415. # Example.foo is an authorized origin
  416. headers = self.fetch("/", headers={"Origin": "http://example.foo"}).headers
  417. self.assertEqual(headers["Access-Control-Allow-Origin"], "http://example.foo")
  418. # Example2.foo is not an authorized origin
  419. headers = self.fetch("/", headers={"Origin": "http://example2.foo"}).headers
  420. self.assertEqual(headers.get("Access-Control-Allow-Origin"), None)
  421. def test_cors_origin_multiple(self):
  422. """
  423. Check that endpoints returns the Access-Control-Allow-Origin when
  424. multiple origins are set
  425. """
  426. self._app.mod_opts["cors_origin"] = ["http://example.foo", "http://foo.example"]
  427. # Example.foo is an authorized origin
  428. headers = self.fetch("/", headers={"Origin": "http://example.foo"}).headers
  429. self.assertEqual(headers["Access-Control-Allow-Origin"], "http://example.foo")
  430. # Example2.foo is not an authorized origin
  431. headers = self.fetch("/", headers={"Origin": "http://example2.foo"}).headers
  432. self.assertEqual(headers.get("Access-Control-Allow-Origin"), None)
  433. def test_cors_preflight_request(self):
  434. """
  435. Check that preflight request contains right headers
  436. """
  437. self._app.mod_opts["cors_origin"] = "*"
  438. request_headers = "X-Auth-Token, accept, content-type"
  439. preflight_headers = {
  440. "Access-Control-Request-Headers": request_headers,
  441. "Access-Control-Request-Method": "GET",
  442. }
  443. response = self.fetch("/", method="OPTIONS", headers=preflight_headers)
  444. headers = response.headers
  445. self.assertEqual(response.code, 204)
  446. self.assertEqual(headers["Access-Control-Allow-Headers"], request_headers)
  447. self.assertEqual(headers["Access-Control-Expose-Headers"], "X-Auth-Token")
  448. self.assertEqual(headers["Access-Control-Allow-Methods"], "OPTIONS, GET, POST")
  449. self.assertEqual(response.code, 204)
  450. def test_cors_origin_url_with_arguments(self):
  451. """
  452. Check that preflight requests works with url with components
  453. like jobs or minions endpoints.
  454. """
  455. self._app.mod_opts["cors_origin"] = "*"
  456. request_headers = "X-Auth-Token, accept, content-type"
  457. preflight_headers = {
  458. "Access-Control-Request-Headers": request_headers,
  459. "Access-Control-Request-Method": "GET",
  460. }
  461. response = self.fetch(
  462. "/1234567890", method="OPTIONS", headers=preflight_headers
  463. )
  464. headers = response.headers
  465. self.assertEqual(response.code, 204)
  466. self.assertEqual(headers["Access-Control-Allow-Origin"], "*")
  467. class TestWebhookSaltHandler(SaltnadoTestCase):
  468. def get_app(self):
  469. urls = [
  470. (r"/hook(/.*)?", saltnado.WebhookSaltAPIHandler),
  471. ]
  472. return self.build_tornado_app(urls)
  473. def test_hook_can_handle_get_parameters(self):
  474. with patch("salt.utils.event.get_event") as get_event:
  475. with patch.dict(self._app.mod_opts, {"webhook_disable_auth": True}):
  476. event = MagicMock()
  477. event.fire_event.return_value = True
  478. get_event.return_value = event
  479. response = self.fetch(
  480. "/hook/my_service/?param=1&param=2",
  481. body=salt.utils.json.dumps({}),
  482. method="POST",
  483. headers={"Content-Type": self.content_type_map["json"]},
  484. )
  485. self.assertEqual(response.code, 200, response.body)
  486. host = urlparse(response.effective_url).netloc
  487. event.fire_event.assert_called_once_with(
  488. {
  489. "headers": {
  490. "Content-Length": "2",
  491. "Connection": "close",
  492. "Content-Type": "application/json",
  493. "Host": host,
  494. "Accept-Encoding": "gzip",
  495. },
  496. "post": {},
  497. "get": {"param": ["1", "2"]},
  498. },
  499. "salt/netapi/hook/my_service/",
  500. )
  501. class TestSaltAuthHandler(SaltnadoTestCase):
  502. def get_app(self):
  503. urls = [("/login", saltnado.SaltAuthHandler)]
  504. return self.build_tornado_app(urls)
  505. def test_get(self):
  506. """
  507. We don't allow gets, so assert we get 401s
  508. """
  509. response = self.fetch("/login")
  510. self.assertEqual(response.code, 401)
  511. def test_login(self):
  512. """
  513. Test valid logins
  514. """
  515. # Test in form encoded
  516. response = self.fetch(
  517. "/login",
  518. method="POST",
  519. body=urlencode(self.auth_creds),
  520. headers={"Content-Type": self.content_type_map["form"]},
  521. )
  522. cookies = response.headers["Set-Cookie"]
  523. self.assertEqual(response.code, 200)
  524. response_obj = salt.utils.json.loads(response.body)["return"][0]
  525. token = response_obj["token"]
  526. self.assertIn("session_id={0}".format(token), cookies)
  527. self.assertEqual(
  528. sorted(response_obj["perms"]),
  529. sorted(
  530. self.opts["external_auth"]["auto"][self.auth_creds_dict["username"]]
  531. ),
  532. )
  533. self.assertIn("token", response_obj) # TODO: verify that its valid?
  534. self.assertEqual(response_obj["user"], self.auth_creds_dict["username"])
  535. self.assertEqual(response_obj["eauth"], self.auth_creds_dict["eauth"])
  536. # Test in JSON
  537. response = self.fetch(
  538. "/login",
  539. method="POST",
  540. body=salt.utils.json.dumps(self.auth_creds_dict),
  541. headers={"Content-Type": self.content_type_map["json"]},
  542. )
  543. cookies = response.headers["Set-Cookie"]
  544. self.assertEqual(response.code, 200)
  545. response_obj = salt.utils.json.loads(response.body)["return"][0]
  546. token = response_obj["token"]
  547. self.assertIn("session_id={0}".format(token), cookies)
  548. self.assertEqual(
  549. sorted(response_obj["perms"]),
  550. sorted(
  551. self.opts["external_auth"]["auto"][self.auth_creds_dict["username"]]
  552. ),
  553. )
  554. self.assertIn("token", response_obj) # TODO: verify that its valid?
  555. self.assertEqual(response_obj["user"], self.auth_creds_dict["username"])
  556. self.assertEqual(response_obj["eauth"], self.auth_creds_dict["eauth"])
  557. # Test in YAML
  558. response = self.fetch(
  559. "/login",
  560. method="POST",
  561. body=salt.utils.yaml.safe_dump(self.auth_creds_dict),
  562. headers={"Content-Type": self.content_type_map["yaml"]},
  563. )
  564. cookies = response.headers["Set-Cookie"]
  565. self.assertEqual(response.code, 200)
  566. response_obj = salt.utils.json.loads(response.body)["return"][0]
  567. token = response_obj["token"]
  568. self.assertIn("session_id={0}".format(token), cookies)
  569. self.assertEqual(
  570. sorted(response_obj["perms"]),
  571. sorted(
  572. self.opts["external_auth"]["auto"][self.auth_creds_dict["username"]]
  573. ),
  574. )
  575. self.assertIn("token", response_obj) # TODO: verify that its valid?
  576. self.assertEqual(response_obj["user"], self.auth_creds_dict["username"])
  577. self.assertEqual(response_obj["eauth"], self.auth_creds_dict["eauth"])
  578. def test_login_missing_password(self):
  579. """
  580. Test logins with bad/missing passwords
  581. """
  582. bad_creds = []
  583. for key, val in six.iteritems(self.auth_creds_dict):
  584. if key == "password":
  585. continue
  586. bad_creds.append((key, val))
  587. response = self.fetch(
  588. "/login",
  589. method="POST",
  590. body=urlencode(bad_creds),
  591. headers={"Content-Type": self.content_type_map["form"]},
  592. )
  593. self.assertEqual(response.code, 400)
  594. def test_login_bad_creds(self):
  595. """
  596. Test logins with bad/missing passwords
  597. """
  598. bad_creds = []
  599. for key, val in six.iteritems(self.auth_creds_dict):
  600. if key == "username":
  601. val = val + "foo"
  602. if key == "eauth":
  603. val = "sharedsecret"
  604. bad_creds.append((key, val))
  605. response = self.fetch(
  606. "/login",
  607. method="POST",
  608. body=urlencode(bad_creds),
  609. headers={"Content-Type": self.content_type_map["form"]},
  610. )
  611. self.assertEqual(response.code, 401)
  612. def test_login_invalid_data_structure(self):
  613. """
  614. Test logins with either list or string JSON payload
  615. """
  616. response = self.fetch(
  617. "/login",
  618. method="POST",
  619. body=salt.utils.json.dumps(self.auth_creds),
  620. headers={"Content-Type": self.content_type_map["form"]},
  621. )
  622. self.assertEqual(response.code, 400)
  623. response = self.fetch(
  624. "/login",
  625. method="POST",
  626. body=salt.utils.json.dumps(42),
  627. headers={"Content-Type": self.content_type_map["form"]},
  628. )
  629. self.assertEqual(response.code, 400)
  630. response = self.fetch(
  631. "/login",
  632. method="POST",
  633. body=salt.utils.json.dumps("mystring42"),
  634. headers={"Content-Type": self.content_type_map["form"]},
  635. )
  636. self.assertEqual(response.code, 400)
  637. class TestSaltRunHandler(SaltnadoTestCase):
  638. def get_app(self):
  639. urls = [("/run", saltnado.RunSaltAPIHandler)]
  640. return self.build_tornado_app(urls)
  641. def test_authentication_exception_consistency(self):
  642. """
  643. Test consistency of authentication exception of each clients.
  644. """
  645. valid_response = {"return": ["Failed to authenticate"]}
  646. clients = ["local", "local_async", "runner", "runner_async"]
  647. request_lowstates = map(
  648. lambda client: {
  649. "client": client,
  650. "tgt": "*",
  651. "fun": "test.fib",
  652. "arg": ["10"],
  653. },
  654. clients,
  655. )
  656. for request_lowstate in request_lowstates:
  657. response = self.fetch(
  658. "/run",
  659. method="POST",
  660. body=salt.utils.json.dumps(request_lowstate),
  661. headers={"Content-Type": self.content_type_map["json"]},
  662. )
  663. self.assertEqual(valid_response, salt.utils.json.loads(response.body))
  664. @skipIf(
  665. not HAS_TORNADO, "The tornado package needs to be installed"
  666. ) # pylint: disable=W0223
  667. class TestWebsocketSaltAPIHandler(SaltnadoTestCase):
  668. def get_app(self):
  669. opts = copy.deepcopy(self.opts)
  670. opts.setdefault("rest_tornado", {})["websockets"] = True
  671. return rest_tornado.get_application(opts)
  672. @gen_test
  673. def test_websocket_handler_upgrade_to_websocket(self):
  674. response = yield self.http_client.fetch(
  675. self.get_url("/login"),
  676. method="POST",
  677. body=urlencode(self.auth_creds),
  678. headers={"Content-Type": self.content_type_map["form"]},
  679. )
  680. token = salt.utils.json.loads(self.decode_body(response).body)["return"][0][
  681. "token"
  682. ]
  683. url = "ws://127.0.0.1:{0}/all_events/{1}".format(self.get_http_port(), token)
  684. request = HTTPRequest(
  685. url, headers={"Origin": "http://example.com", "Host": "example.com"}
  686. )
  687. ws = yield websocket_connect(request)
  688. ws.write_message("websocket client ready")
  689. ws.close()
  690. @gen_test
  691. def test_websocket_handler_bad_token(self):
  692. """
  693. A bad token should returns a 401 during a websocket connect
  694. """
  695. token = "A" * len(
  696. getattr(hashlib, self.opts.get("hash_type", "md5"))().hexdigest()
  697. )
  698. url = "ws://127.0.0.1:{0}/all_events/{1}".format(self.get_http_port(), token)
  699. request = HTTPRequest(
  700. url, headers={"Origin": "http://example.com", "Host": "example.com"}
  701. )
  702. try:
  703. ws = yield websocket_connect(request)
  704. except HTTPError as error:
  705. self.assertEqual(error.code, 401)
  706. @gen_test
  707. def test_websocket_handler_cors_origin_wildcard(self):
  708. self._app.mod_opts["cors_origin"] = "*"
  709. response = yield self.http_client.fetch(
  710. self.get_url("/login"),
  711. method="POST",
  712. body=urlencode(self.auth_creds),
  713. headers={"Content-Type": self.content_type_map["form"]},
  714. )
  715. token = salt.utils.json.loads(self.decode_body(response).body)["return"][0][
  716. "token"
  717. ]
  718. url = "ws://127.0.0.1:{0}/all_events/{1}".format(self.get_http_port(), token)
  719. request = HTTPRequest(
  720. url, headers={"Origin": "http://foo.bar", "Host": "example.com"}
  721. )
  722. ws = yield websocket_connect(request)
  723. ws.write_message("websocket client ready")
  724. ws.close()
  725. @gen_test
  726. def test_cors_origin_single(self):
  727. self._app.mod_opts["cors_origin"] = "http://example.com"
  728. response = yield self.http_client.fetch(
  729. self.get_url("/login"),
  730. method="POST",
  731. body=urlencode(self.auth_creds),
  732. headers={"Content-Type": self.content_type_map["form"]},
  733. )
  734. token = salt.utils.json.loads(self.decode_body(response).body)["return"][0][
  735. "token"
  736. ]
  737. url = "ws://127.0.0.1:{0}/all_events/{1}".format(self.get_http_port(), token)
  738. # Example.com should works
  739. request = HTTPRequest(
  740. url, headers={"Origin": "http://example.com", "Host": "example.com"}
  741. )
  742. ws = yield websocket_connect(request)
  743. ws.write_message("websocket client ready")
  744. ws.close()
  745. # But foo.bar not
  746. request = HTTPRequest(
  747. url, headers={"Origin": "http://foo.bar", "Host": "example.com"}
  748. )
  749. try:
  750. ws = yield websocket_connect(request)
  751. except HTTPError as error:
  752. self.assertEqual(error.code, 403)
  753. @gen_test
  754. def test_cors_origin_multiple(self):
  755. self._app.mod_opts["cors_origin"] = ["http://example.com", "http://foo.bar"]
  756. response = yield self.http_client.fetch(
  757. self.get_url("/login"),
  758. method="POST",
  759. body=urlencode(self.auth_creds),
  760. headers={"Content-Type": self.content_type_map["form"]},
  761. )
  762. token = salt.utils.json.loads(self.decode_body(response).body)["return"][0][
  763. "token"
  764. ]
  765. url = "ws://127.0.0.1:{0}/all_events/{1}".format(self.get_http_port(), token)
  766. # Example.com should works
  767. request = HTTPRequest(
  768. url, headers={"Origin": "http://example.com", "Host": "example.com"}
  769. )
  770. ws = yield websocket_connect(request)
  771. ws.write_message("websocket client ready")
  772. ws.close()
  773. # Foo.bar too
  774. request = HTTPRequest(
  775. url, headers={"Origin": "http://foo.bar", "Host": "example.com"}
  776. )
  777. ws = yield websocket_connect(request)
  778. ws.write_message("websocket client ready")
  779. ws.close()
  780. @skipIf(not HAS_TORNADO, "The tornado package needs to be installed")
  781. class TestSaltnadoUtils(AsyncTestCase):
  782. def test_any_future(self):
  783. """
  784. Test that the Any Future does what we think it does
  785. """
  786. # create a few futures
  787. futures = []
  788. for x in range(0, 3):
  789. future = salt.ext.tornado.concurrent.Future()
  790. future.add_done_callback(self.stop)
  791. futures.append(future)
  792. # create an any future, make sure it isn't immediately done
  793. any_ = saltnado.Any(futures)
  794. self.assertIs(any_.done(), False)
  795. # finish one, lets see who finishes
  796. futures[0].set_result("foo")
  797. self.wait()
  798. self.assertIs(any_.done(), True)
  799. self.assertIs(futures[0].done(), True)
  800. self.assertIs(futures[1].done(), False)
  801. self.assertIs(futures[2].done(), False)
  802. # make sure it returned the one that finished
  803. self.assertEqual(any_.result(), futures[0])
  804. futures = futures[1:]
  805. # re-wait on some other futures
  806. any_ = saltnado.Any(futures)
  807. futures[0].set_result("foo")
  808. self.wait()
  809. self.assertIs(any_.done(), True)
  810. self.assertIs(futures[0].done(), True)
  811. self.assertIs(futures[1].done(), False)
  812. @skipIf(not HAS_TORNADO, "The tornado package needs to be installed")
  813. class TestEventListener(AsyncTestCase):
  814. def setUp(self):
  815. self.sock_dir = os.path.join(RUNTIME_VARS.TMP, "test-socks")
  816. if not os.path.exists(self.sock_dir):
  817. os.makedirs(self.sock_dir)
  818. self.addCleanup(shutil.rmtree, self.sock_dir, ignore_errors=True)
  819. super(TestEventListener, self).setUp()
  820. @slowTest
  821. def test_simple(self):
  822. """
  823. Test getting a few events
  824. """
  825. with eventpublisher_process(self.sock_dir):
  826. me = salt.utils.event.MasterEvent(self.sock_dir)
  827. event_listener = saltnado.EventListener(
  828. {}, # we don't use mod_opts, don't save?
  829. {"sock_dir": self.sock_dir, "transport": "zeromq"},
  830. )
  831. self._finished = False # fit to event_listener's behavior
  832. event_future = event_listener.get_event(
  833. self, "evt1", callback=self.stop
  834. ) # get an event future
  835. me.fire_event({"data": "foo2"}, "evt2") # fire an event we don't want
  836. me.fire_event({"data": "foo1"}, "evt1") # fire an event we do want
  837. self.wait() # wait for the future
  838. # check that we got the event we wanted
  839. self.assertTrue(event_future.done())
  840. self.assertEqual(event_future.result()["tag"], "evt1")
  841. self.assertEqual(event_future.result()["data"]["data"], "foo1")
  842. @slowTest
  843. def test_set_event_handler(self):
  844. """
  845. Test subscribing events using set_event_handler
  846. """
  847. with eventpublisher_process(self.sock_dir):
  848. me = salt.utils.event.MasterEvent(self.sock_dir)
  849. event_listener = saltnado.EventListener(
  850. {}, # we don't use mod_opts, don't save?
  851. {"sock_dir": self.sock_dir, "transport": "zeromq"},
  852. )
  853. self._finished = False # fit to event_listener's behavior
  854. event_future = event_listener.get_event(
  855. self, tag="evt", callback=self.stop, timeout=1,
  856. ) # get an event future
  857. me.fire_event({"data": "foo"}, "evt") # fire an event we do want
  858. self.wait()
  859. # check that we subscribed the event we wanted
  860. self.assertEqual(len(event_listener.timeout_map), 0)
  861. @slowTest
  862. def test_timeout(self):
  863. """
  864. Make sure timeouts work correctly
  865. """
  866. with eventpublisher_process(self.sock_dir):
  867. event_listener = saltnado.EventListener(
  868. {}, # we don't use mod_opts, don't save?
  869. {"sock_dir": self.sock_dir, "transport": "zeromq"},
  870. )
  871. self._finished = False # fit to event_listener's behavior
  872. event_future = event_listener.get_event(
  873. self, tag="evt1", callback=self.stop, timeout=1,
  874. ) # get an event future
  875. self.wait()
  876. self.assertTrue(event_future.done())
  877. with self.assertRaises(saltnado.TimeoutException):
  878. event_future.result()
  879. @slowTest
  880. def test_clean_by_request(self):
  881. """
  882. Make sure the method clean_by_request clean up every related data in EventListener
  883. request_future_1 : will be timeout-ed by clean_by_request(self)
  884. request_future_2 : will be finished by me.fire_event ...
  885. dummy_request_future_1 : will be finished by me.fire_event ...
  886. dummy_request_future_2 : will be timeout-ed by clean-by_request(dummy_request)
  887. """
  888. class DummyRequest(object):
  889. """
  890. Dummy request object to simulate the request object
  891. """
  892. @property
  893. def _finished(self):
  894. """
  895. Simulate _finished of the request object
  896. """
  897. return False
  898. # Inner functions never permit modifying primitive values directly
  899. cnt = [0]
  900. def stop():
  901. """
  902. To realize the scenario of this test, define a custom stop method to call
  903. self.stop after finished two events.
  904. """
  905. cnt[0] += 1
  906. if cnt[0] == 2:
  907. self.stop()
  908. with eventpublisher_process(self.sock_dir):
  909. me = salt.utils.event.MasterEvent(self.sock_dir)
  910. event_listener = saltnado.EventListener(
  911. {}, # we don't use mod_opts, don't save?
  912. {"sock_dir": self.sock_dir, "transport": "zeromq"},
  913. )
  914. self.assertEqual(0, len(event_listener.tag_map))
  915. self.assertEqual(0, len(event_listener.request_map))
  916. self._finished = False # fit to event_listener's behavior
  917. dummy_request = DummyRequest()
  918. request_future_1 = event_listener.get_event(self, tag="evt1")
  919. request_future_2 = event_listener.get_event(
  920. self, tag="evt2", callback=lambda f: stop()
  921. )
  922. dummy_request_future_1 = event_listener.get_event(
  923. dummy_request, tag="evt3", callback=lambda f: stop()
  924. )
  925. dummy_request_future_2 = event_listener.get_event(
  926. dummy_request, timeout=10, tag="evt4"
  927. )
  928. self.assertEqual(4, len(event_listener.tag_map))
  929. self.assertEqual(2, len(event_listener.request_map))
  930. me.fire_event({"data": "foo2"}, "evt2")
  931. me.fire_event({"data": "foo3"}, "evt3")
  932. self.wait()
  933. event_listener.clean_by_request(self)
  934. me.fire_event({"data": "foo1"}, "evt1")
  935. self.assertTrue(request_future_1.done())
  936. with self.assertRaises(saltnado.TimeoutException):
  937. request_future_1.result()
  938. self.assertTrue(request_future_2.done())
  939. self.assertEqual(request_future_2.result()["tag"], "evt2")
  940. self.assertEqual(request_future_2.result()["data"]["data"], "foo2")
  941. self.assertTrue(dummy_request_future_1.done())
  942. self.assertEqual(dummy_request_future_1.result()["tag"], "evt3")
  943. self.assertEqual(dummy_request_future_1.result()["data"]["data"], "foo3")
  944. self.assertFalse(dummy_request_future_2.done())
  945. self.assertEqual(2, len(event_listener.tag_map))
  946. self.assertEqual(1, len(event_listener.request_map))
  947. event_listener.clean_by_request(dummy_request)
  948. with self.assertRaises(saltnado.TimeoutException):
  949. dummy_request_future_2.result()
  950. self.assertEqual(0, len(event_listener.tag_map))
  951. self.assertEqual(0, len(event_listener.request_map))