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
This commit is contained in:
wanghao 2018-05-18 15:33:08 +08:00
parent 788d57ed41
commit fff82e7a11
9 changed files with 146 additions and 25 deletions

View File

@ -1,16 +1,21 @@
#### variables in header ##################################################### #### variables in header #####################################################
client_id: client_id:
type: UUID type: string
in: header in: header
description: | 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 canonical form (for example, 3381af92-2b9e-11e3-b191-71861300734c). The
client generates the Client-ID once. Client-ID persists between restarts string must be longer than "min_length_client_id=20" and smaller than
of the client so the client should reuse that same Client-ID. Note: All "max_length_client_id=300" by default. User can control the length of
message-related operations require the use of ``Client-ID`` in the headers client id by using those two options. The client generates the Client-ID
to ensure that messages are not echoed back to the client that posted once. Client-ID persists between restarts of the client so the client
them, unless the client explicitly requests this. 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 ####################################################### #### variables in path #######################################################

View File

@ -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.

View File

@ -293,6 +293,7 @@ class Endpoints(object):
try: try:
kwargs = api_utils.get_headers(req) 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) client_uuid = api_utils.get_client_uuid(req)
self._validate.message_listing(**kwargs) self._validate.message_listing(**kwargs)
@ -468,6 +469,7 @@ class Endpoints(object):
return api_utils.error_response(req, ex, headers) return api_utils.error_response(req, ex, headers)
try: try:
self._validate.client_id_uuid_safe(req._headers.get('Client-ID'))
client_uuid = api_utils.get_client_uuid(req) client_uuid = api_utils.get_client_uuid(req)
self._validate.message_posting(messages) self._validate.message_posting(messages)
@ -477,7 +479,8 @@ class Endpoints(object):
messages=messages, messages=messages,
project=project_id, project=project_id,
client_uuid=client_uuid) client_uuid=client_uuid)
except (api_errors.BadRequest, validation.ValidationFailed) as ex: except (ValueError, api_errors.BadRequest,
validation.ValidationFailed) as ex:
LOG.debug(ex) LOG.debug(ex)
headers = {'status': 400} headers = {'status': 400}
return api_utils.error_response(req, ex, headers) return api_utils.error_response(req, ex, headers)

View File

@ -136,16 +136,13 @@ def get_client_uuid(req):
"""Read a required Client-ID from a request. """Read a required Client-ID from a request.
:param req: Request object :param req: Request object
:raises BadRequest: if the Client-ID header is missing or :returns: A UUID object or A string of client id
does not represent a valid UUID
:returns: A UUID object
""" """
try: try:
return uuid.UUID(req._headers.get('Client-ID')) return uuid.UUID(req._headers.get('Client-ID'))
except ValueError: except ValueError:
description = _(u'Malformed hexadecimal UUID.') return req._headers.get('Client-ID')
raise api_errors.BadRequest(description)
def get_headers(req): def get_headers(req):

View File

@ -67,17 +67,13 @@ def get_client_uuid(req):
"""Read a required Client-ID from a request. """Read a required Client-ID from a request.
:param req: A falcon.Request object :param req: A falcon.Request object
:raises HTTPBadRequest: if the Client-ID header is missing or :returns: A UUID object or A string of client id
does not represent a valid UUID
:returns: A UUID object
""" """
try: try:
return uuid.UUID(req.get_header('Client-ID', required=True)) return uuid.UUID(req.get_header('Client-ID', required=True))
except ValueError: except ValueError:
description = _(u'Malformed hexadecimal UUID.') return req.get_header('Client-ID', required=True)
raise falcon.HTTPBadRequest('Wrong UUID value', description)
def extract_project_id(req, resp, params): 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')) _(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 """Makes sure the header `Client-ID` is present in the request
Use as a before hook. 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 :param req: request sent
:type req: falcon.request.Request :type req: falcon.request.Request
:param resp: response object to return :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/'): if req.path.startswith('/v1.1/') or req.path.startswith('/v2/'):
# NOTE(flaper87): `get_client_uuid` already raises 400 try:
# it the header is missing. validate(req.get_header('Client-ID', required=True))
get_client_uuid(req) 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): def validate_queue_identification(validate, req, resp, params):

View File

@ -124,6 +124,25 @@ max_pools_per_page = cfg.IntOpt(
help='Defines the maximum number of pools per page.') 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' GROUP_NAME = 'transport'
ALL_OPTS = [ ALL_OPTS = [
default_message_ttl, default_message_ttl,
@ -143,7 +162,10 @@ ALL_OPTS = [
max_claim_grace, max_claim_grace,
subscriber_types, subscriber_types,
max_flavors_per_page, max_flavors_per_page,
max_pools_per_page max_pools_per_page,
client_id_uuid_safe,
min_length_client_id,
max_length_client_id
] ]

View File

@ -127,6 +127,54 @@ class TestQueueLifecycleMongoDB(base.V2Base):
self.simulate_get(gumshoe_queue_path_stats, headers=headers) self.simulate_get(gumshoe_queue_path_stats, headers=headers)
self.assertEqual(falcon.HTTP_200, self.srmock.status) 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): def test_name_restrictions(self):
self.simulate_put(self.queue_path + '/Nice-Boat_2', self.simulate_put(self.queue_path + '/Nice-Boat_2',
headers=self.headers) headers=self.headers)

View File

@ -16,6 +16,7 @@
import datetime import datetime
import re import re
import uuid
from oslo_utils import timeutils from oslo_utils import timeutils
import six import six
@ -649,3 +650,21 @@ class Validator(object):
if limit is not None and not (0 < limit <= uplimit): if limit is not None and not (0 < limit <= uplimit):
msg = _(u'Limit must be at least 1 and no greater than {0}.') msg = _(u'Limit must be at least 1 and no greater than {0}.')
raise ValidationFailed(msg, self._limits_conf.max_pools_per_page) 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)

View File

@ -72,6 +72,10 @@ class Driver(transport.DriverBase):
return helpers.validate_queue_identification( return helpers.validate_queue_identification(
self._validate.queue_identification, req, resp, params) 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) @decorators.lazy_property(write=False)
def before_hooks(self): def before_hooks(self):
"""Exposed to facilitate unit testing.""" """Exposed to facilitate unit testing."""
@ -79,7 +83,7 @@ class Driver(transport.DriverBase):
self._verify_pre_signed_url, self._verify_pre_signed_url,
helpers.require_content_type_be_non_urlencoded, helpers.require_content_type_be_non_urlencoded,
helpers.require_accepts_json, helpers.require_accepts_json,
helpers.require_client_id, self._require_client_id,
helpers.extract_project_id, helpers.extract_project_id,
# NOTE(jeffrey4l): Depends on the project_id and client_id being # NOTE(jeffrey4l): Depends on the project_id and client_id being