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 #####################################################
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 #######################################################

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:
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)

View File

@ -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):

View File

@ -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):

View File

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

View File

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

View File

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

View File

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