Support Zaqar signed queue URLs

This adds new resource to generate a signed URL for a Zaqar queue.

Change-Id: Ib42f11bf13d1e836563023a9fa587b93c875ec61
Closes-Bug: #1628691
This commit is contained in:
Thomas Herve 2016-10-31 14:19:40 +01:00
parent d30965a015
commit 571740f07d
2 changed files with 157 additions and 0 deletions

View File

@ -11,8 +11,11 @@
# License for the specific language governing permissions and limitations
# under the License.
from oslo_serialization import jsonutils
from heat.common.i18n import _
from heat.engine import attributes
from heat.engine import constraints
from heat.engine import properties
from heat.engine import resource
from heat.engine import support
@ -129,7 +132,108 @@ class ZaqarQueue(resource.Resource):
}
class ZaqarSignedQueueURL(resource.Resource):
"""A resource for managing signed URLs of Zaqar queues.
Signed URLs allow to give specific access to queues, for example to be used
as alarm notifications.
"""
default_client_name = "zaqar"
support_status = support.SupportStatus(version='8.0.0')
PROPERTIES = (
QUEUE, PATHS, TTL, METHODS,
) = (
'queue', 'paths', 'ttl', 'methods',
)
ATTRIBUTES = (
SIGNATURE, EXPIRES, PATHS_ATTR, METHODS_ATTR,
) = (
'signature', 'expires', 'paths', 'methods',
)
properties_schema = {
QUEUE: properties.Schema(
properties.Schema.STRING,
_("Name of the queue instance to create a URL for."),
required=True),
PATHS: properties.Schema(
properties.Schema.LIST,
description=_("List of allowed paths to be accessed. "
"Default to allow queue messages URL.")),
TTL: properties.Schema(
properties.Schema.INTEGER,
description=_("Time validity of the URL, in seconds. "
"Default to one day.")),
METHODS: properties.Schema(
properties.Schema.LIST,
description=_("List of allowed HTTP methods to be used. "
"Default to allow GET."),
schema=properties.Schema(
properties.Schema.STRING,
constraints=[
constraints.AllowedValues(['GET', 'DELETE', 'PATCH',
'POST', 'PUT']),
],
))
}
attributes_schema = {
SIGNATURE: attributes.Schema(
_("Signature of the URL built by Zaqar.")
),
EXPIRES: attributes.Schema(
_("Expiration date of the URL.")
),
PATHS_ATTR: attributes.Schema(
_("Comma-delimited list of paths for convenience.")
),
METHODS_ATTR: attributes.Schema(
_("Comma-delimited list of methods for convenience.")
),
}
def handle_create(self):
queue = self.client().queue(self.properties[self.QUEUE])
signed_url = queue.signed_url(paths=self.properties[self.PATHS],
methods=self.properties[self.METHODS],
ttl_seconds=self.properties[self.TTL])
self.data_set(self.SIGNATURE, signed_url['signature'])
self.data_set(self.EXPIRES, signed_url['expires'])
self.data_set(self.PATHS, jsonutils.dumps(signed_url['paths']))
self.data_set(self.METHODS, jsonutils.dumps(signed_url['methods']))
self.resource_id_set(self._url(signed_url))
def _url(self, data):
"""Return a URL that can be used for example for alarming."""
return ('zaqar://?signature={0}&expires={1}&paths={2}'
'&methods={3}&project_id={4}&queue_name={5}'.format(
data['signature'], data['expires'],
','.join(data['paths']), ','.join(data['methods']),
data['project'], self.properties[self.QUEUE]))
def handle_delete(self):
# We can't delete a signed URL
return True
def _resolve_attribute(self, name):
if not self.resource_id:
return
if name == self.SIGNATURE:
return self.data()[self.SIGNATURE]
elif name == self.EXPIRES:
return self.data()[self.EXPIRES]
elif name == self.PATHS:
return jsonutils.loads(self.data()[self.PATHS])
elif name == self.METHODS:
return jsonutils.loads(self.data()[self.METHODS])
def resource_mapping():
return {
'OS::Zaqar::Queue': ZaqarQueue,
'OS::Zaqar::SignedQueueURL': ZaqarSignedQueueURL,
}

View File

@ -270,3 +270,56 @@ class ZaqarMessageQueueTest(common.HeatTestCase):
queue._show_resource()))
self.m.VerifyAll()
class ZaqarSignedQueueURLTest(common.HeatTestCase):
tmpl = '''
heat_template_version: 2015-10-15
resources:
signed_url:
type: OS::Zaqar::SignedQueueURL
properties:
queue: foo
ttl: 60
paths:
- messages
- subscription
methods:
- POST
- DELETE
'''
@mock.patch('zaqarclient.queues.v2.queues.Queue.signed_url')
def test_create(self, mock_signed_url):
mock_signed_url.return_value = {
'expires': '2020-01-01',
'signature': 'secret',
'project': 'project_id',
'paths': ['/v2/foo/messages', '/v2/foo/sub'],
'methods': ['DELETE', 'POST']}
self.t = template_format.parse(self.tmpl)
self.stack = utils.parse_stack(self.t)
self.rsrc = self.stack['signed_url']
self.assertIsNone(self.rsrc.validate())
self.stack.create()
self.assertEqual(self.rsrc.CREATE, self.rsrc.action)
self.assertEqual(self.rsrc.COMPLETE, self.rsrc.status)
self.assertEqual(self.stack.CREATE, self.stack.action)
self.assertEqual(self.stack.COMPLETE, self.stack.status)
mock_signed_url.assert_called_once_with(
paths=['messages', 'subscription'],
methods=['POST', 'DELETE'],
ttl_seconds=60)
self.assertEqual('secret', self.rsrc.FnGetAtt('signature'))
self.assertEqual('2020-01-01', self.rsrc.FnGetAtt('expires'))
self.assertEqual(['/v2/foo/messages', '/v2/foo/sub'],
self.rsrc.FnGetAtt('paths'))
self.assertEqual(['DELETE', 'POST'], self.rsrc.FnGetAtt('methods'))
self.assertEqual(
'zaqar://?signature=secret&expires=2020-01-01'
'&paths=/v2/foo/messages,/v2/foo/sub&methods=DELETE,POST'
'&project_id=project_id&queue_name=foo',
self.rsrc.resource_id)