test_rest_tornado.py 39 KB

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