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') {
var messages = data['body']['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();
} else if (action == 'message_post' || action == 'message_delete') {
list_messages();
@ -188,6 +190,7 @@
<button class='pure-button' onclick='delete_queue()'>Delete</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='list_queues()'>Refresh</button>
</fieldset>
</form>
</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 response
from zaqar.common import errors
from zaqar.common import urls
class Handler(object):
@ -26,6 +27,15 @@ class Handler(object):
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):
self.v1_1_endpoints = endpoints.Endpoints(storage, control,
validate, defaults)
@ -73,3 +83,31 @@ class Handler(object):
def get_defaults(self):
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
import mock
from zaqar.common import urls
from zaqar.tests.unit.transport.websocket import base
from zaqar.tests.unit.transport.websocket import utils as test_utils
@ -30,6 +31,7 @@ class AuthTest(base.V1_1Base):
def setUp(self):
super(AuthTest, self).setUp()
self.protocol = self.transport.factory()
self.protocol.factory._secret_key = 'secret'
self.default_message_ttl = 3600
@ -119,3 +121,70 @@ class AuthTest(base.V1_1Base):
self.assertEqual(2, len(responses))
self.assertIn('cancelled', repr(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,
external_port=self._ws_conf.external_port,
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):
"""Self-host using 'bind' and 'port' from the WS config group."""

View File

@ -23,12 +23,13 @@ class ProtocolFactory(websocket.WebSocketServerFactory):
protocol = protocol.MessagingProtocol
def __init__(self, uri, debug, handler, external_port, auth_strategy,
loop):
loop, secret_key):
websocket.WebSocketServerFactory.__init__(
self, url=uri, debug=debug, externalPort=external_port)
self._handler = handler
self._auth_strategy = auth_strategy
self._loop = loop
self._secret_key = secret_key
def __call__(self):
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 self._auth_strategy and not self._authentified:
if self._auth_app or payload.get('action') != 'authenticate':
body = {'error': 'Not authentified.'}
resp = self._handler.create_response(403, body, req)
if 'URL-Signature' in payload.get('headers', {}):
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:
return self._authenticate(payload)
elif payload.get('action') == 'authenticate':