2014-10-31 15:18:45 +00:00
|
|
|
# coding: utf-8
|
|
|
|
|
2014-11-19 22:35:30 +00:00
|
|
|
# Import Python libs
|
2014-11-21 19:05:13 +00:00
|
|
|
from __future__ import absolute_import
|
2014-10-31 15:18:45 +00:00
|
|
|
import json
|
|
|
|
import yaml
|
2015-01-12 22:36:34 +00:00
|
|
|
import os
|
2014-10-31 15:18:45 +00:00
|
|
|
|
2014-11-20 01:48:09 +00:00
|
|
|
# Import Salt Testing Libs
|
|
|
|
from salttesting.unit import skipIf
|
|
|
|
from salttesting.helpers import ensure_in_syspath
|
2014-11-20 17:19:58 +00:00
|
|
|
ensure_in_syspath('../../..')
|
2014-11-20 18:57:09 +00:00
|
|
|
import integration # pylint: disable=import-error
|
2014-11-20 01:48:09 +00:00
|
|
|
|
2014-11-19 22:35:30 +00:00
|
|
|
# Import Salt libs
|
2014-11-20 01:48:09 +00:00
|
|
|
try:
|
|
|
|
from salt.netapi.rest_tornado import saltnado
|
|
|
|
HAS_TORNADO = True
|
|
|
|
except ImportError:
|
|
|
|
HAS_TORNADO = False
|
2014-10-31 15:18:45 +00:00
|
|
|
import salt.auth
|
2014-11-20 01:48:09 +00:00
|
|
|
|
2014-10-31 15:18:45 +00:00
|
|
|
|
2014-11-19 22:35:30 +00:00
|
|
|
# Import 3rd-party libs
|
|
|
|
# pylint: disable=import-error
|
2014-11-20 01:48:09 +00:00
|
|
|
try:
|
|
|
|
import tornado.testing
|
|
|
|
import tornado.concurrent
|
|
|
|
from tornado.testing import AsyncHTTPTestCase
|
|
|
|
HAS_TORNADO = True
|
|
|
|
except ImportError:
|
|
|
|
HAS_TORNADO = False
|
|
|
|
|
|
|
|
# Let's create a fake AsyncHTTPTestCase so we can properly skip the test case
|
|
|
|
class AsyncHTTPTestCase(object):
|
|
|
|
pass
|
|
|
|
|
2014-11-19 22:35:30 +00:00
|
|
|
from salt.ext.six.moves.urllib.parse import urlencode # pylint: disable=no-name-in-module
|
|
|
|
# pylint: enable=import-error
|
2014-11-13 17:33:14 +00:00
|
|
|
|
2014-10-31 15:18:45 +00:00
|
|
|
|
2014-11-20 01:48:09 +00:00
|
|
|
@skipIf(HAS_TORNADO is False, 'The tornado package needs to be installed')
|
|
|
|
class SaltnadoTestCase(integration.ModuleCase, AsyncHTTPTestCase):
|
2014-10-31 15:18:45 +00:00
|
|
|
'''
|
|
|
|
Mixin to hold some shared things
|
|
|
|
'''
|
|
|
|
content_type_map = {'json': 'application/json',
|
|
|
|
'yaml': 'application/x-yaml',
|
|
|
|
'text': 'text/plain',
|
|
|
|
'form': 'application/x-www-form-urlencoded'}
|
|
|
|
auth_creds = (
|
2014-11-05 15:58:30 +00:00
|
|
|
('username', 'saltdev_api'),
|
2014-10-31 15:18:45 +00:00
|
|
|
('password', 'saltdev'),
|
|
|
|
('eauth', 'auto'))
|
|
|
|
|
|
|
|
@property
|
|
|
|
def auth_creds_dict(self):
|
|
|
|
return dict(self.auth_creds)
|
|
|
|
|
2014-10-31 15:49:47 +00:00
|
|
|
@property
|
|
|
|
def opts(self):
|
|
|
|
return self.get_config('master', from_scratch=True)
|
|
|
|
|
|
|
|
@property
|
|
|
|
def auth(self):
|
|
|
|
if not hasattr(self, '__auth'):
|
|
|
|
self.__auth = salt.auth.LoadAuth(self.opts)
|
|
|
|
return self.__auth
|
2014-10-31 15:18:45 +00:00
|
|
|
|
2014-10-31 15:49:47 +00:00
|
|
|
@property
|
|
|
|
def token(self):
|
|
|
|
''' Mint and return a valid token for auth_creds '''
|
|
|
|
return self.auth.mk_token(self.auth_creds_dict)
|
|
|
|
|
2015-01-12 22:36:34 +00:00
|
|
|
def setUp(self):
|
|
|
|
super(SaltnadoTestCase, self).setUp()
|
|
|
|
os.environ['ASYNC_TEST_TIMEOUT'] = str(30)
|
|
|
|
|
|
|
|
def tearDown(self):
|
|
|
|
super(SaltnadoTestCase, self).tearDown()
|
|
|
|
os.environ.pop('ASYNC_TEST_TIMEOUT', None)
|
|
|
|
|
2014-10-31 15:49:47 +00:00
|
|
|
|
|
|
|
class TestBaseSaltAPIHandler(SaltnadoTestCase):
|
2014-10-31 15:18:45 +00:00
|
|
|
def get_app(self):
|
|
|
|
class StubHandler(saltnado.BaseSaltAPIHandler):
|
|
|
|
def get(self):
|
|
|
|
return self.echo_stuff()
|
|
|
|
|
|
|
|
def post(self):
|
|
|
|
return self.echo_stuff()
|
|
|
|
|
|
|
|
def echo_stuff(self):
|
|
|
|
ret_dict = {'foo': 'bar'}
|
|
|
|
attrs = ('token',
|
|
|
|
'start',
|
|
|
|
'connected',
|
|
|
|
'lowstate',
|
|
|
|
)
|
|
|
|
for attr in attrs:
|
|
|
|
ret_dict[attr] = getattr(self, attr)
|
|
|
|
|
|
|
|
self.write(self.serialize(ret_dict))
|
|
|
|
|
|
|
|
return tornado.web.Application([('/', StubHandler)], debug=True)
|
|
|
|
|
|
|
|
def test_content_type(self):
|
|
|
|
'''
|
|
|
|
Test the base handler's accept picking
|
|
|
|
'''
|
|
|
|
|
|
|
|
# send NO accept header, should come back with json
|
|
|
|
response = self.fetch('/')
|
2014-11-20 16:17:12 +00:00
|
|
|
self.assertEqual(response.headers['Content-Type'], self.content_type_map['json'])
|
|
|
|
self.assertEqual(type(json.loads(response.body)), dict)
|
2014-10-31 15:18:45 +00:00
|
|
|
|
|
|
|
# send application/json
|
|
|
|
response = self.fetch('/', headers={'Accept': self.content_type_map['json']})
|
2014-11-20 16:17:12 +00:00
|
|
|
self.assertEqual(response.headers['Content-Type'], self.content_type_map['json'])
|
|
|
|
self.assertEqual(type(json.loads(response.body)), dict)
|
2014-10-31 15:18:45 +00:00
|
|
|
|
|
|
|
# send application/x-yaml
|
|
|
|
response = self.fetch('/', headers={'Accept': self.content_type_map['yaml']})
|
2014-11-20 16:17:12 +00:00
|
|
|
self.assertEqual(response.headers['Content-Type'], self.content_type_map['yaml'])
|
|
|
|
self.assertEqual(type(yaml.load(response.body)), dict)
|
2014-10-31 15:18:45 +00:00
|
|
|
|
|
|
|
def test_token(self):
|
|
|
|
'''
|
|
|
|
Test that the token is returned correctly
|
|
|
|
'''
|
|
|
|
token = json.loads(self.fetch('/').body)['token']
|
2014-11-20 16:17:12 +00:00
|
|
|
self.assertIs(token, None)
|
2014-10-31 15:18:45 +00:00
|
|
|
|
|
|
|
# send a token as a header
|
|
|
|
response = self.fetch('/', headers={saltnado.AUTH_TOKEN_HEADER: 'foo'})
|
|
|
|
token = json.loads(response.body)['token']
|
2014-11-20 16:17:12 +00:00
|
|
|
self.assertEqual(token, 'foo')
|
2014-10-31 15:18:45 +00:00
|
|
|
|
|
|
|
# send a token as a cookie
|
|
|
|
response = self.fetch('/', headers={'Cookie': '{0}=foo'.format(saltnado.AUTH_COOKIE_NAME)})
|
|
|
|
token = json.loads(response.body)['token']
|
2014-11-20 16:17:12 +00:00
|
|
|
self.assertEqual(token, 'foo')
|
2014-10-31 15:18:45 +00:00
|
|
|
|
|
|
|
# send both, make sure its the header
|
|
|
|
response = self.fetch('/', headers={saltnado.AUTH_TOKEN_HEADER: 'foo',
|
|
|
|
'Cookie': '{0}=bar'.format(saltnado.AUTH_COOKIE_NAME)})
|
|
|
|
token = json.loads(response.body)['token']
|
2014-11-20 16:17:12 +00:00
|
|
|
self.assertEqual(token, 'foo')
|
2014-10-31 15:18:45 +00:00
|
|
|
|
|
|
|
def test_deserialize(self):
|
|
|
|
'''
|
|
|
|
Send various encoded forms of lowstates (and bad ones) to make sure we
|
|
|
|
handle deserialization correctly
|
|
|
|
'''
|
|
|
|
valid_lowstate = [{
|
|
|
|
"client": "local",
|
|
|
|
"tgt": "*",
|
|
|
|
"fun": "test.fib",
|
|
|
|
"arg": ["10"]
|
|
|
|
},
|
|
|
|
{
|
|
|
|
"client": "runner",
|
|
|
|
"fun": "jobs.lookup_jid",
|
|
|
|
"jid": "20130603122505459265"
|
|
|
|
}]
|
|
|
|
|
|
|
|
# send as JSON
|
|
|
|
response = self.fetch('/',
|
|
|
|
method='POST',
|
|
|
|
body=json.dumps(valid_lowstate),
|
|
|
|
headers={'Content-Type': self.content_type_map['json']})
|
|
|
|
|
2014-11-20 16:17:12 +00:00
|
|
|
self.assertEqual(valid_lowstate, json.loads(response.body)['lowstate'])
|
2014-10-31 15:18:45 +00:00
|
|
|
|
|
|
|
# send yaml as json (should break)
|
|
|
|
response = self.fetch('/',
|
|
|
|
method='POST',
|
|
|
|
body=yaml.dump(valid_lowstate),
|
|
|
|
headers={'Content-Type': self.content_type_map['json']})
|
2014-11-20 16:17:12 +00:00
|
|
|
self.assertEqual(response.code, 400)
|
2014-10-31 15:18:45 +00:00
|
|
|
|
|
|
|
# send as yaml
|
|
|
|
response = self.fetch('/',
|
|
|
|
method='POST',
|
|
|
|
body=yaml.dump(valid_lowstate),
|
|
|
|
headers={'Content-Type': self.content_type_map['yaml']})
|
2014-11-20 16:17:12 +00:00
|
|
|
self.assertEqual(valid_lowstate, json.loads(response.body)['lowstate'])
|
2014-10-31 15:18:45 +00:00
|
|
|
|
|
|
|
# send json as yaml (works since yaml is a superset of json)
|
|
|
|
response = self.fetch('/',
|
|
|
|
method='POST',
|
|
|
|
body=json.dumps(valid_lowstate),
|
|
|
|
headers={'Content-Type': self.content_type_map['yaml']})
|
2014-11-20 16:17:12 +00:00
|
|
|
self.assertEqual(valid_lowstate, json.loads(response.body)['lowstate'])
|
2014-10-31 15:18:45 +00:00
|
|
|
|
|
|
|
# send json as text/plain
|
|
|
|
response = self.fetch('/',
|
|
|
|
method='POST',
|
|
|
|
body=json.dumps(valid_lowstate),
|
|
|
|
headers={'Content-Type': self.content_type_map['text']})
|
2014-11-20 16:17:12 +00:00
|
|
|
self.assertEqual(valid_lowstate, json.loads(response.body)['lowstate'])
|
2014-10-31 15:18:45 +00:00
|
|
|
|
|
|
|
# send form-urlencoded
|
|
|
|
form_lowstate = (
|
|
|
|
('client', 'local'),
|
|
|
|
('tgt', '*'),
|
|
|
|
('fun', 'test.fib'),
|
|
|
|
('arg', '10'),
|
|
|
|
('arg', 'foo'),
|
|
|
|
)
|
|
|
|
response = self.fetch('/',
|
|
|
|
method='POST',
|
2014-11-19 22:35:30 +00:00
|
|
|
body=urlencode(form_lowstate),
|
2014-10-31 15:18:45 +00:00
|
|
|
headers={'Content-Type': self.content_type_map['form']})
|
|
|
|
returned_lowstate = json.loads(response.body)['lowstate']
|
2014-11-20 16:17:12 +00:00
|
|
|
self.assertEqual(len(returned_lowstate), 1)
|
2014-10-31 15:18:45 +00:00
|
|
|
returned_lowstate = returned_lowstate[0]
|
|
|
|
|
2014-11-20 16:17:12 +00:00
|
|
|
self.assertEqual(returned_lowstate['client'], 'local')
|
|
|
|
self.assertEqual(returned_lowstate['tgt'], '*')
|
|
|
|
self.assertEqual(returned_lowstate['fun'], 'test.fib')
|
|
|
|
self.assertEqual(returned_lowstate['arg'], ['10', 'foo'])
|
2014-10-31 15:18:45 +00:00
|
|
|
|
|
|
|
|
2014-10-31 15:49:47 +00:00
|
|
|
class TestSaltAuthHandler(SaltnadoTestCase):
|
2014-10-31 15:18:45 +00:00
|
|
|
def get_app(self):
|
2014-10-31 15:49:47 +00:00
|
|
|
|
|
|
|
# TODO: make a "GET APPPLICATION" func
|
2014-10-31 15:18:45 +00:00
|
|
|
application = tornado.web.Application([('/login', saltnado.SaltAuthHandler)], debug=True)
|
|
|
|
|
2014-10-31 15:49:47 +00:00
|
|
|
application.auth = self.auth
|
2014-10-31 15:18:45 +00:00
|
|
|
application.opts = self.opts
|
|
|
|
return application
|
|
|
|
|
|
|
|
def test_get(self):
|
|
|
|
'''
|
|
|
|
We don't allow gets, so assert we get 401s
|
|
|
|
'''
|
|
|
|
response = self.fetch('/login')
|
2014-11-20 16:17:12 +00:00
|
|
|
self.assertEqual(response.code, 401)
|
2014-10-31 15:18:45 +00:00
|
|
|
|
|
|
|
def test_login(self):
|
|
|
|
'''
|
|
|
|
Test valid logins
|
|
|
|
'''
|
|
|
|
response = self.fetch('/login',
|
|
|
|
method='POST',
|
2014-11-19 22:35:30 +00:00
|
|
|
body=urlencode(self.auth_creds),
|
2014-10-31 15:18:45 +00:00
|
|
|
headers={'Content-Type': self.content_type_map['form']})
|
|
|
|
|
|
|
|
response_obj = json.loads(response.body)['return'][0]
|
2014-11-20 16:17:12 +00:00
|
|
|
self.assertEqual(response_obj['perms'], self.opts['external_auth']['auto'][self.auth_creds_dict['username']])
|
2014-11-21 02:35:22 +00:00
|
|
|
self.assertIn('token', response_obj) # TODO: verify that its valid?
|
2014-11-20 16:17:12 +00:00
|
|
|
self.assertEqual(response_obj['user'], self.auth_creds_dict['username'])
|
|
|
|
self.assertEqual(response_obj['eauth'], self.auth_creds_dict['eauth'])
|
2014-10-31 15:18:45 +00:00
|
|
|
|
|
|
|
def test_login_missing_password(self):
|
|
|
|
'''
|
|
|
|
Test logins with bad/missing passwords
|
|
|
|
'''
|
|
|
|
bad_creds = []
|
|
|
|
for key, val in self.auth_creds_dict.iteritems():
|
|
|
|
if key == 'password':
|
|
|
|
continue
|
2014-11-13 17:33:14 +00:00
|
|
|
bad_creds.append((key, val))
|
2014-10-31 15:18:45 +00:00
|
|
|
response = self.fetch('/login',
|
|
|
|
method='POST',
|
2014-11-19 22:35:30 +00:00
|
|
|
body=urlencode(bad_creds),
|
2014-10-31 15:18:45 +00:00
|
|
|
headers={'Content-Type': self.content_type_map['form']})
|
|
|
|
|
2014-11-20 16:17:12 +00:00
|
|
|
self.assertEqual(response.code, 400)
|
2014-10-31 15:18:45 +00:00
|
|
|
|
|
|
|
def test_login_bad_creds(self):
|
|
|
|
'''
|
|
|
|
Test logins with bad/missing passwords
|
|
|
|
'''
|
|
|
|
bad_creds = []
|
|
|
|
for key, val in self.auth_creds_dict.iteritems():
|
|
|
|
if key == 'username':
|
|
|
|
val = val + 'foo'
|
2014-11-13 17:33:14 +00:00
|
|
|
bad_creds.append((key, val))
|
2014-10-31 15:18:45 +00:00
|
|
|
response = self.fetch('/login',
|
|
|
|
method='POST',
|
2014-11-19 22:35:30 +00:00
|
|
|
body=urlencode(bad_creds),
|
2014-10-31 15:18:45 +00:00
|
|
|
headers={'Content-Type': self.content_type_map['form']})
|
|
|
|
|
2014-11-20 16:17:12 +00:00
|
|
|
self.assertEqual(response.code, 401)
|
2014-11-20 01:48:09 +00:00
|
|
|
|
|
|
|
|
|
|
|
if __name__ == '__main__':
|
2014-11-20 18:57:09 +00:00
|
|
|
from integration import run_tests # pylint: disable=import-error
|
2014-11-20 01:48:09 +00:00
|
|
|
run_tests(TestBaseSaltAPIHandler, TestSaltAuthHandler, needs_daemon=False)
|