From fff82e7a1178238ce72a82c87e410f89e020deae Mon Sep 17 00:00:00 2001 From: wanghao Date: Fri, 18 May 2018 15:33:08 +0800 Subject: [PATCH] Remove format constraint of client id Since some clients use different format of client id not only uuid, so Zaqar will support this function. Add one option 'client_id_uuid_safe' to allow user to control the validation of client id. Add two options 'min_length_client_id' and 'max_length_client_id' to allow user to control the length of client id if not using uuid. This also requires user to ensure the client id is immutable. Implements: blueprint remove-format-constraint-of-client-id Change-Id: I96bc2620b09394419b66a733484ff3d8f0d56313 --- api-ref/source/parameters.yaml | 19 +++++--- ...ntraint-of-client-id-ab787960df6e1606.yaml | 9 ++++ zaqar/api/v2/endpoints.py | 5 +- zaqar/common/api/utils.py | 7 +-- zaqar/common/transport/wsgi/helpers.py | 34 +++++++++---- zaqar/conf/transport.py | 24 +++++++++- .../wsgi/v2_0/test_queue_lifecycle.py | 48 +++++++++++++++++++ zaqar/transport/validation.py | 19 ++++++++ zaqar/transport/wsgi/driver.py | 6 ++- 9 files changed, 146 insertions(+), 25 deletions(-) create mode 100644 releasenotes/notes/remove-format-contraint-of-client-id-ab787960df6e1606.yaml diff --git a/api-ref/source/parameters.yaml b/api-ref/source/parameters.yaml index b072f82dc..b0d6c7b5b 100644 --- a/api-ref/source/parameters.yaml +++ b/api-ref/source/parameters.yaml @@ -1,16 +1,21 @@ #### variables in header ##################################################### client_id: - type: UUID + type: string in: header description: | - A UUID for each client instance. The UUID must be submitted in its + The identification for each client instance. The format of client id is + UUID by default, but Zaqar also supports a Non-UUID string by setting + configuration "client_id_uuid_safe=off". The UUID must be submitted in its canonical form (for example, 3381af92-2b9e-11e3-b191-71861300734c). The - client generates the Client-ID once. Client-ID persists between restarts - of the client so the client should reuse that same Client-ID. Note: All - message-related operations require the use of ``Client-ID`` in the headers - to ensure that messages are not echoed back to the client that posted - them, unless the client explicitly requests this. + string must be longer than "min_length_client_id=20" and smaller than + "max_length_client_id=300" by default. User can control the length of + client id by using those two options. The client generates the Client-ID + once. Client-ID persists between restarts of the client so the client + should reuse that same Client-ID. Note: All message-related operations + require the use of ``Client-ID`` in the headers to ensure that messages + are not echoed back to the client that posted them, unless the client + explicitly requests this. #### variables in path ####################################################### diff --git a/releasenotes/notes/remove-format-contraint-of-client-id-ab787960df6e1606.yaml b/releasenotes/notes/remove-format-contraint-of-client-id-ab787960df6e1606.yaml new file mode 100644 index 000000000..ce9fd62c6 --- /dev/null +++ b/releasenotes/notes/remove-format-contraint-of-client-id-ab787960df6e1606.yaml @@ -0,0 +1,9 @@ +--- +features: + - Since some clients use different format of client id not only uuid, like + user id of ldap, so Zaqar will remove the format contrain of client id. + Add one option 'client_id_uuid_safe' to allow user to control the + validation of client id. Add two options 'min_length_client_id' and + 'max_length_client_id' to allow user to control the length of client id + if not using uuid. This also requires user to ensure the client id is + immutable. diff --git a/zaqar/api/v2/endpoints.py b/zaqar/api/v2/endpoints.py index 9dd69933e..888988a56 100644 --- a/zaqar/api/v2/endpoints.py +++ b/zaqar/api/v2/endpoints.py @@ -293,6 +293,7 @@ class Endpoints(object): try: kwargs = api_utils.get_headers(req) + self._validate.client_id_uuid_safe(req._headers.get('Client-ID')) client_uuid = api_utils.get_client_uuid(req) self._validate.message_listing(**kwargs) @@ -468,6 +469,7 @@ class Endpoints(object): return api_utils.error_response(req, ex, headers) try: + self._validate.client_id_uuid_safe(req._headers.get('Client-ID')) client_uuid = api_utils.get_client_uuid(req) self._validate.message_posting(messages) @@ -477,7 +479,8 @@ class Endpoints(object): messages=messages, project=project_id, client_uuid=client_uuid) - except (api_errors.BadRequest, validation.ValidationFailed) as ex: + except (ValueError, api_errors.BadRequest, + validation.ValidationFailed) as ex: LOG.debug(ex) headers = {'status': 400} return api_utils.error_response(req, ex, headers) diff --git a/zaqar/common/api/utils.py b/zaqar/common/api/utils.py index 56e9b91e9..c05854946 100644 --- a/zaqar/common/api/utils.py +++ b/zaqar/common/api/utils.py @@ -136,16 +136,13 @@ def get_client_uuid(req): """Read a required Client-ID from a request. :param req: Request object - :raises BadRequest: if the Client-ID header is missing or - does not represent a valid UUID - :returns: A UUID object + :returns: A UUID object or A string of client id """ try: return uuid.UUID(req._headers.get('Client-ID')) except ValueError: - description = _(u'Malformed hexadecimal UUID.') - raise api_errors.BadRequest(description) + return req._headers.get('Client-ID') def get_headers(req): diff --git a/zaqar/common/transport/wsgi/helpers.py b/zaqar/common/transport/wsgi/helpers.py index 9bc397574..9e9a2ecce 100644 --- a/zaqar/common/transport/wsgi/helpers.py +++ b/zaqar/common/transport/wsgi/helpers.py @@ -67,17 +67,13 @@ def get_client_uuid(req): """Read a required Client-ID from a request. :param req: A falcon.Request object - :raises HTTPBadRequest: if the Client-ID header is missing or - does not represent a valid UUID - :returns: A UUID object + :returns: A UUID object or A string of client id """ try: return uuid.UUID(req.get_header('Client-ID', required=True)) - except ValueError: - description = _(u'Malformed hexadecimal UUID.') - raise falcon.HTTPBadRequest('Wrong UUID value', description) + return req.get_header('Client-ID', required=True) def extract_project_id(req, resp, params): @@ -112,10 +108,13 @@ def extract_project_id(req, resp, params): _(u'The header X-PROJECT-ID was missing')) -def require_client_id(req, resp, params): +def require_client_id(validate, req, resp, params): """Makes sure the header `Client-ID` is present in the request Use as a before hook. + :param validate: A validator function that will + be used to check the format of client id against configured + limits. :param req: request sent :type req: falcon.request.Request :param resp: response object to return @@ -126,9 +125,24 @@ def require_client_id(req, resp, params): """ if req.path.startswith('/v1.1/') or req.path.startswith('/v2/'): - # NOTE(flaper87): `get_client_uuid` already raises 400 - # it the header is missing. - get_client_uuid(req) + try: + validate(req.get_header('Client-ID', required=True)) + except ValueError: + description = _(u'Malformed hexadecimal UUID.') + raise falcon.HTTPBadRequest('Wrong UUID value', description) + except validation.ValidationFailed as ex: + raise falcon.HTTPBadRequest(six.text_type(ex)) + else: + # NOTE(wanghao): Since we changed the get_client_uuid to support + # other format of client id, so need to check the uuid here for + # v1 API. + try: + client_id = req.get_header('Client-ID') + if client_id or client_id == '': + uuid.UUID(client_id) + except ValueError: + description = _(u'Malformed hexadecimal UUID.') + raise falcon.HTTPBadRequest('Wrong UUID value', description) def validate_queue_identification(validate, req, resp, params): diff --git a/zaqar/conf/transport.py b/zaqar/conf/transport.py index 0a5fd71a9..9201a2268 100644 --- a/zaqar/conf/transport.py +++ b/zaqar/conf/transport.py @@ -124,6 +124,25 @@ max_pools_per_page = cfg.IntOpt( help='Defines the maximum number of pools per page.') +client_id_uuid_safe = cfg.StrOpt( + 'client_id_uuid_safe', default='strict', choices=['strict', 'off'], + help='Defines the format of client id, the value could be ' + '"strict" or "off". "strict" means the format of client id' + ' must be uuid, "off" means the restriction be removed.') + + +min_length_client_id = cfg.IntOpt( + 'min_length_client_id', default='10', + help='Defines the minimum length of client id if remove the ' + 'uuid restriction. Default is 10.') + + +max_length_client_id = cfg.IntOpt( + 'max_length_client_id', default='36', + help='Defines the maximum length of client id if remove the ' + 'uuid restriction. Default is 36.') + + GROUP_NAME = 'transport' ALL_OPTS = [ default_message_ttl, @@ -143,7 +162,10 @@ ALL_OPTS = [ max_claim_grace, subscriber_types, max_flavors_per_page, - max_pools_per_page + max_pools_per_page, + client_id_uuid_safe, + min_length_client_id, + max_length_client_id ] diff --git a/zaqar/tests/unit/transport/wsgi/v2_0/test_queue_lifecycle.py b/zaqar/tests/unit/transport/wsgi/v2_0/test_queue_lifecycle.py index b230abe35..a040cf005 100644 --- a/zaqar/tests/unit/transport/wsgi/v2_0/test_queue_lifecycle.py +++ b/zaqar/tests/unit/transport/wsgi/v2_0/test_queue_lifecycle.py @@ -127,6 +127,54 @@ class TestQueueLifecycleMongoDB(base.V2Base): self.simulate_get(gumshoe_queue_path_stats, headers=headers) self.assertEqual(falcon.HTTP_200, self.srmock.status) + @ddt.data('1234567890', '11111111111111111111111111111111111') + def test_basics_thoroughly_with_different_client_id(self, client_id): + self.conf.set_override('client_id_uuid_safe', 'off', 'transport') + headers = { + 'Client-ID': client_id, + 'X-Project-ID': '480924' + } + gumshoe_queue_path_stats = self.gumshoe_queue_path + '/stats' + + # Stats are empty - queue not created yet + self.simulate_get(gumshoe_queue_path_stats, headers=headers) + self.assertEqual(falcon.HTTP_200, self.srmock.status) + + # Create + doc = '{"messages": {"ttl": 600}}' + self.simulate_put(self.gumshoe_queue_path, + headers=headers, body=doc) + self.assertEqual(falcon.HTTP_201, self.srmock.status) + + location = self.srmock.headers_dict['Location'] + self.assertEqual(location, self.gumshoe_queue_path) + + # Fetch metadata + result = self.simulate_get(self.gumshoe_queue_path, + headers=headers) + result_doc = jsonutils.loads(result[0]) + self.assertEqual(falcon.HTTP_200, self.srmock.status) + ref_doc = jsonutils.loads(doc) + ref_doc['_default_message_ttl'] = 3600 + ref_doc['_max_messages_post_size'] = 262144 + ref_doc['_default_message_delay'] = 0 + ref_doc['_dead_letter_queue'] = None + ref_doc['_dead_letter_queue_messages_ttl'] = None + ref_doc['_max_claim_count'] = None + self.assertEqual(ref_doc, result_doc) + + # Stats empty queue + self.simulate_get(gumshoe_queue_path_stats, headers=headers) + self.assertEqual(falcon.HTTP_200, self.srmock.status) + + # Delete + self.simulate_delete(self.gumshoe_queue_path, headers=headers) + self.assertEqual(falcon.HTTP_204, self.srmock.status) + + # Get non-existent stats + self.simulate_get(gumshoe_queue_path_stats, headers=headers) + self.assertEqual(falcon.HTTP_200, self.srmock.status) + def test_name_restrictions(self): self.simulate_put(self.queue_path + '/Nice-Boat_2', headers=self.headers) diff --git a/zaqar/transport/validation.py b/zaqar/transport/validation.py index 396dee885..650c9d70d 100644 --- a/zaqar/transport/validation.py +++ b/zaqar/transport/validation.py @@ -16,6 +16,7 @@ import datetime import re +import uuid from oslo_utils import timeutils import six @@ -649,3 +650,21 @@ class Validator(object): if limit is not None and not (0 < limit <= uplimit): msg = _(u'Limit must be at least 1 and no greater than {0}.') raise ValidationFailed(msg, self._limits_conf.max_pools_per_page) + + def client_id_uuid_safe(self, client_id): + """Restrictions the format of client id + + :param client_id: the client id of request + :raises ValidationFailed: if the limit is exceeded + """ + + if self._limits_conf.client_id_uuid_safe == 'off': + if (len(client_id) < self._limits_conf.min_length_client_id) or \ + (len(client_id) > self._limits_conf.max_length_client_id): + msg = _(u'Length of client id must be at least {0} and no ' + 'greater than {1}.') + raise ValidationFailed(msg, + self._limits_conf.min_length_client_id, + self._limits_conf.max_length_client_id) + if self._limits_conf.client_id_uuid_safe == 'strict': + uuid.UUID(client_id) diff --git a/zaqar/transport/wsgi/driver.py b/zaqar/transport/wsgi/driver.py index b93da0eb9..afcc9cd5f 100644 --- a/zaqar/transport/wsgi/driver.py +++ b/zaqar/transport/wsgi/driver.py @@ -72,6 +72,10 @@ class Driver(transport.DriverBase): return helpers.validate_queue_identification( self._validate.queue_identification, req, resp, params) + def _require_client_id(self, req, resp, params): + return helpers.require_client_id( + self._validate.client_id_uuid_safe, req, resp, params) + @decorators.lazy_property(write=False) def before_hooks(self): """Exposed to facilitate unit testing.""" @@ -79,7 +83,7 @@ class Driver(transport.DriverBase): self._verify_pre_signed_url, helpers.require_content_type_be_non_urlencoded, helpers.require_accepts_json, - helpers.require_client_id, + self._require_client_id, helpers.extract_project_id, # NOTE(jeffrey4l): Depends on the project_id and client_id being