Support signed URLs in WebSocket

Change-Id: Iab0b719f00fdcf4c84740d06a7d774cac410181c
This commit is contained in:
Thomas Herve 2015-08-19 11:48:44 +02:00
parent d55f1de3bc
commit 02207d8e78
6 changed files with 126 additions and 5 deletions

View File

@ -66,7 +66,9 @@
} else if (action == 'message_list') { } else if (action == 'message_list') {
var messages = data['body']['messages']; var messages = data['body']['messages'];
display_messages(messages); display_messages(messages);
} else if (action == 'queue_create' || action == 'queue_delete' || action == 'authenticate') { } else if (action == 'queue_create' || action == 'queue_delete') {
list_queues();
} else if (action == 'authenticate' && data["headers"]["status"] == 200) {
list_queues(); list_queues();
} else if (action == 'message_post' || action == 'message_delete') { } else if (action == 'message_post' || action == 'message_delete') {
list_messages(); list_messages();
@ -188,6 +190,7 @@
<button class='pure-button' onclick='delete_queue()'>Delete</button> <button class='pure-button' onclick='delete_queue()'>Delete</button>
<button class='pure-button' onclick='list_messages()'>List messages</button> <button class='pure-button' onclick='list_messages()'>List messages</button>
<button class='pure-button' onclick='subscribe_queue()'>Subscribe</button> <button class='pure-button' onclick='subscribe_queue()'>Subscribe</button>
<button class='pure-button' onclick='list_queues()'>Refresh</button>
</fieldset> </fieldset>
</form> </form>
</div> </div>

View File

@ -18,6 +18,7 @@ from zaqar.api.v1_1 import request as schema_validator
from zaqar.common.api import request from zaqar.common.api import request
from zaqar.common.api import response from zaqar.common.api import response
from zaqar.common import errors from zaqar.common import errors
from zaqar.common import urls
class Handler(object): class Handler(object):
@ -26,6 +27,15 @@ class Handler(object):
The handler validates and process the requests The handler validates and process the requests
""" """
_actions_mapping = {
'message_list': 'GET',
'message_get': 'GET',
'message_get_many': 'GET',
'message_post': 'POST',
'message_delete': 'DELETE',
'message_delete_many': 'DELETE'
}
def __init__(self, storage, control, validate, defaults): def __init__(self, storage, control, validate, defaults):
self.v1_1_endpoints = endpoints.Endpoints(storage, control, self.v1_1_endpoints = endpoints.Endpoints(storage, control,
validate, defaults) validate, defaults)
@ -73,3 +83,31 @@ class Handler(object):
def get_defaults(self): def get_defaults(self):
return self.v1_1_endpoints._defaults return self.v1_1_endpoints._defaults
def verify_signature(self, key, payload):
action = payload.get('action')
method = self._actions_mapping.get(action)
queue_name = payload.get('body', {}).get('queue_name')
path = '/v2/queues/%(queue_name)s/messages' % {
'queue_name': queue_name}
headers = payload.get('headers', {})
project = headers.get('X-Project-ID')
expires = headers.get('URL-Expires')
methods = headers.get('URL-Methods')
signature = headers.get('URL-Signature')
if not method or method not in methods:
return False
try:
verified = urls.verify_signed_headers_data(key, path,
project=project,
methods=methods,
expires=expires,
signature=signature)
except ValueError:
return False
return verified

View File

@ -19,6 +19,7 @@ import uuid
from keystonemiddleware import auth_token from keystonemiddleware import auth_token
import mock import mock
from zaqar.common import urls
from zaqar.tests.unit.transport.websocket import base from zaqar.tests.unit.transport.websocket import base
from zaqar.tests.unit.transport.websocket import utils as test_utils from zaqar.tests.unit.transport.websocket import utils as test_utils
@ -30,6 +31,7 @@ class AuthTest(base.V1_1Base):
def setUp(self): def setUp(self):
super(AuthTest, self).setUp() super(AuthTest, self).setUp()
self.protocol = self.transport.factory() self.protocol = self.transport.factory()
self.protocol.factory._secret_key = 'secret'
self.default_message_ttl = 3600 self.default_message_ttl = 3600
@ -119,3 +121,70 @@ class AuthTest(base.V1_1Base):
self.assertEqual(2, len(responses)) self.assertEqual(2, len(responses))
self.assertIn('cancelled', repr(handle)) self.assertIn('cancelled', repr(handle))
self.assertNotIn('cancelled', repr(self.protocol._deauth_handle)) self.assertNotIn('cancelled', repr(self.protocol._deauth_handle))
def test_signed_url(self):
send_mock = mock.Mock()
self.protocol.sendMessage = send_mock
data = urls.create_signed_url('secret', '/v2/queues/myqueue/messages',
project=self.project_id, methods=['GET'])
headers = self.headers.copy()
headers.update({
'URL-Signature': data['signature'],
'URL-Expires': data['expires'],
'URL-Methods': ['GET']
})
req = json.dumps({'action': 'message_list',
'body': {'queue_name': 'myqueue'},
'headers': headers})
self.protocol.onMessage(req, False)
self.assertEqual(1, send_mock.call_count)
resp = json.loads(send_mock.call_args[0][0])
self.assertEqual(200, resp['headers']['status'])
def test_signed_url_wrong_queue(self):
send_mock = mock.Mock()
self.protocol.sendMessage = send_mock
data = urls.create_signed_url('secret', '/v2/queues/myqueue/messages',
project=self.project_id, methods=['GET'])
headers = self.headers.copy()
headers.update({
'URL-Signature': data['signature'],
'URL-Expires': data['expires'],
'URL-Methods': ['GET']
})
req = json.dumps({'action': 'message_list',
'body': {'queue_name': 'otherqueue'},
'headers': headers})
self.protocol.onMessage(req, False)
self.assertEqual(1, send_mock.call_count)
resp = json.loads(send_mock.call_args[0][0])
self.assertEqual(403, resp['headers']['status'])
def test_signed_url_wrong_method(self):
send_mock = mock.Mock()
self.protocol.sendMessage = send_mock
data = urls.create_signed_url('secret', '/v2/queues/myqueue/messages',
project=self.project_id, methods=['GET'])
headers = self.headers.copy()
headers.update({
'URL-Signature': data['signature'],
'URL-Expires': data['expires'],
'URL-Methods': ['GET']
})
req = json.dumps({'action': 'message_delete',
'body': {'queue_name': 'myqueue',
'message_id': '123'},
'headers': headers})
self.protocol.onMessage(req, False)
self.assertEqual(1, send_mock.call_count)
resp = json.loads(send_mock.call_args[0][0])
self.assertEqual(403, resp['headers']['status'])

View File

@ -76,7 +76,8 @@ class Driver(base.DriverBase):
handler=self._api, handler=self._api,
external_port=self._ws_conf.external_port, external_port=self._ws_conf.external_port,
auth_strategy=self._auth_strategy, auth_strategy=self._auth_strategy,
loop=asyncio.get_event_loop()) loop=asyncio.get_event_loop(),
secret_key=self._conf.signed_url.secret_key)
def listen(self): def listen(self):
"""Self-host using 'bind' and 'port' from the WS config group.""" """Self-host using 'bind' and 'port' from the WS config group."""

View File

@ -23,12 +23,13 @@ class ProtocolFactory(websocket.WebSocketServerFactory):
protocol = protocol.MessagingProtocol protocol = protocol.MessagingProtocol
def __init__(self, uri, debug, handler, external_port, auth_strategy, def __init__(self, uri, debug, handler, external_port, auth_strategy,
loop): loop, secret_key):
websocket.WebSocketServerFactory.__init__( websocket.WebSocketServerFactory.__init__(
self, url=uri, debug=debug, externalPort=external_port) self, url=uri, debug=debug, externalPort=external_port)
self._handler = handler self._handler = handler
self._auth_strategy = auth_strategy self._auth_strategy = auth_strategy
self._loop = loop self._loop = loop
self._secret_key = secret_key
def __call__(self): def __call__(self):
proto = self.protocol(self._handler, self._auth_strategy, self._loop) proto = self.protocol(self._handler, self._auth_strategy, self._loop)

View File

@ -73,8 +73,17 @@ class MessagingProtocol(websocket.WebSocketServerProtocol):
if resp is None: if resp is None:
if self._auth_strategy and not self._authentified: if self._auth_strategy and not self._authentified:
if self._auth_app or payload.get('action') != 'authenticate': if self._auth_app or payload.get('action') != 'authenticate':
body = {'error': 'Not authentified.'} if 'URL-Signature' in payload.get('headers', {}):
resp = self._handler.create_response(403, body, req) if self._handler.verify_signature(
self.factory._secret_key, payload):
resp = self._handler.process_request(req)
else:
body = {'error': 'Not authentified.'}
resp = self._handler.create_response(
403, body, req)
else:
body = {'error': 'Not authentified.'}
resp = self._handler.create_response(403, body, req)
else: else:
return self._authenticate(payload) return self._authenticate(payload)
elif payload.get('action') == 'authenticate': elif payload.get('action') == 'authenticate':