Merge "rest: string → UUID conversion for resource.id to be unique per user"

This commit is contained in:
Jenkins 2017-02-02 14:26:47 +00:00 committed by Gerrit Code Review
commit 1ebdf3ec8e
9 changed files with 123 additions and 64 deletions

View File

@ -13,7 +13,7 @@
# under the License. # under the License.
# #
"""no-more-slash """Remove slashes from original resource IDs, recompute their id with creator
Revision ID: 397987e38570 Revision ID: 397987e38570
Revises: aba5a217ca9b Revises: aba5a217ca9b
@ -49,7 +49,8 @@ resource_table = sa.Table(
sqlalchemy_utils.types.uuid.UUIDType(), sqlalchemy_utils.types.uuid.UUIDType(),
nullable=False), nullable=False),
sa.Column('original_resource_id', sa.String(255)), sa.Column('original_resource_id', sa.String(255)),
sa.Column('type', sa.String(255)) sa.Column('type', sa.String(255)),
sa.Column('creator', sa.String(255))
) )
resourcehistory_table = sa.Table( resourcehistory_table = sa.Table(
@ -100,15 +101,28 @@ def upgrade():
nullable=False), nullable=False),
) )
for resource in connection.execute(resource_table.select().where( for resource in connection.execute(resource_table.select()):
resource_table.c.original_resource_id.like('%/%'))):
if resource_table.c.original_resource_id is None:
# statsd resource has no original_resource_id and is NULL
continue
try:
orig_as_uuid = uuid.UUID(
str(resource_table.c.original_resource_id))
except ValueError:
pass
else:
if orig_as_uuid == resource_table.c.id:
continue
new_original_resource_id = resource.original_resource_id.replace( new_original_resource_id = resource.original_resource_id.replace(
'/', '_') '/', '_')
if six.PY2: if six.PY2:
new_original_resource_id = new_original_resource_id.encode('utf-8') new_original_resource_id = new_original_resource_id.encode('utf-8')
new_id = sa.literal(uuidtype.process_bind_param( new_id = sa.literal(uuidtype.process_bind_param(
str(uuid.uuid5(utils.RESOURCE_ID_NAMESPACE, str(utils.ResourceUUID(
new_original_resource_id)), new_original_resource_id, resource.creator)),
connection.dialect)) connection.dialect))
# resource table # resource table

View File

@ -14,6 +14,7 @@
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations # License for the specific language governing permissions and limitations
# under the License. # under the License.
import functools
import itertools import itertools
import uuid import uuid
@ -849,8 +850,10 @@ class ResourceController(rest.RestController):
def __init__(self, resource_type, id): def __init__(self, resource_type, id):
self._resource_type = resource_type self._resource_type = resource_type
creator = pecan.request.auth_helper.get_current_user(
pecan.request.headers)
try: try:
self.id = utils.ResourceUUID(id) self.id = utils.ResourceUUID(id, creator)
except ValueError: except ValueError:
abort(404, indexer.NoSuchResource(id)) abort(404, indexer.NoSuchResource(id))
self.metric = NamedMetricController(str(self.id), self._resource_type) self.metric = NamedMetricController(str(self.id), self._resource_type)
@ -930,8 +933,15 @@ def schema_for(resource_type):
return ResourceSchema(resource_type.schema) return ResourceSchema(resource_type.schema)
def ResourceID(value): def ResourceUUID(value, creator):
return (six.text_type(value), utils.ResourceUUID(value)) try:
return utils.ResourceUUID(value, creator)
except ValueError as e:
raise voluptuous.Invalid(e)
def ResourceID(value, creator):
return (six.text_type(value), ResourceUUID(value, creator))
class ResourcesController(rest.RestController): class ResourcesController(rest.RestController):
@ -947,7 +957,9 @@ class ResourcesController(rest.RestController):
# NOTE(sileht): we need to copy the dict because when change it # NOTE(sileht): we need to copy the dict because when change it
# and we don't want that next patch call have the "id" # and we don't want that next patch call have the "id"
schema = dict(schema_for(self._resource_type)) schema = dict(schema_for(self._resource_type))
schema["id"] = ResourceID creator = pecan.request.auth_helper.get_current_user(
pecan.request.headers)
schema["id"] = functools.partial(ResourceID, creator=creator)
body = deserialize_and_validate(schema) body = deserialize_and_validate(schema)
body["original_resource_id"], body["id"] = body["id"] body["original_resource_id"], body["id"] = body["id"]
@ -957,8 +969,6 @@ class ResourcesController(rest.RestController):
} }
target.update(body) target.update(body)
enforce("create resource", target) enforce("create resource", target)
creator = pecan.request.auth_helper.get_current_user(
pecan.request.headers)
rid = body['id'] rid = body['id']
del body['id'] del body['id']
try: try:
@ -1004,8 +1014,7 @@ class ResourcesController(rest.RestController):
def delete(self, **kwargs): def delete(self, **kwargs):
# NOTE(sileht): Don't allow empty filter, this is going to delete # NOTE(sileht): Don't allow empty filter, this is going to delete
# the entire database. # the entire database.
attr_filter = deserialize_and_validate( attr_filter = deserialize_and_validate(ResourceSearchSchema)
SearchResourceTypeController.ResourceSearchSchema)
# the voluptuous checks everything, but it is better to # the voluptuous checks everything, but it is better to
# have this here. # have this here.
@ -1130,16 +1139,16 @@ class QueryStringSearchAttrFilter(object):
return cls._parsed_query2dict(parsed_query) return cls._parsed_query2dict(parsed_query)
def _ResourceSearchSchema(v): def ResourceSearchSchema(v):
"""Helper method to indirect the recursivity of the search schema""" return _ResourceSearchSchema()(v)
return SearchResourceTypeController.ResourceSearchSchema(v)
class SearchResourceTypeController(rest.RestController): def _ResourceSearchSchema():
def __init__(self, resource_type): user = pecan.request.auth_helper.get_current_user(
self._resource_type = resource_type pecan.request.headers)
_ResourceUUID = functools.partial(ResourceUUID, creator=user)
ResourceSearchSchema = voluptuous.Schema( return voluptuous.Schema(
voluptuous.All( voluptuous.All(
voluptuous.Length(min=0, max=1), voluptuous.Length(min=0, max=1),
{ {
@ -1156,31 +1165,36 @@ class SearchResourceTypeController(rest.RestController):
voluptuous.Length(min=1, max=1), voluptuous.Length(min=1, max=1),
voluptuous.Any( voluptuous.Any(
{"id": voluptuous.Any( {"id": voluptuous.Any(
utils.ResourceUUID, [utils.ResourceUUID]), [_ResourceUUID], _ResourceUUID),
voluptuous.Extra: voluptuous.Extra})), voluptuous.Extra: voluptuous.Extra})),
voluptuous.Any( voluptuous.Any(
u"and", u"", u"and", u"",
u"or", u"", u"or", u"",
u"not", u"not",
): voluptuous.All( ): voluptuous.All(
[_ResourceSearchSchema], voluptuous.Length(min=1) [ResourceSearchSchema], voluptuous.Length(min=1)
) )
} }
) )
) )
@classmethod
def parse_and_validate_qs_filter(cls, query): class SearchResourceTypeController(rest.RestController):
def __init__(self, resource_type):
self._resource_type = resource_type
@staticmethod
def parse_and_validate_qs_filter(query):
try: try:
attr_filter = QueryStringSearchAttrFilter.parse(query) attr_filter = QueryStringSearchAttrFilter.parse(query)
except InvalidQueryStringSearchAttrFilter as e: except InvalidQueryStringSearchAttrFilter as e:
raise abort(400, e) raise abort(400, e)
return voluptuous.Schema(cls.ResourceSearchSchema, return voluptuous.Schema(ResourceSearchSchema,
required=True)(attr_filter) required=True)(attr_filter)
def _search(self, **kwargs): def _search(self, **kwargs):
if pecan.request.body: if pecan.request.body:
attr_filter = deserialize_and_validate(self.ResourceSearchSchema) attr_filter = deserialize_and_validate(ResourceSearchSchema)
elif kwargs.get("filter"): elif kwargs.get("filter"):
attr_filter = self.parse_and_validate_qs_filter(kwargs["filter"]) attr_filter = self.parse_and_validate_qs_filter(kwargs["filter"])
else: else:
@ -1329,13 +1343,16 @@ class SearchMetricController(rest.RestController):
class ResourcesMetricsMeasuresBatchController(rest.RestController): class ResourcesMetricsMeasuresBatchController(rest.RestController):
MeasuresBatchSchema = voluptuous.Schema(
{ResourceID: {six.text_type: MeasuresListSchema}}
)
@pecan.expose('json') @pecan.expose('json')
def post(self, create_metrics=False): def post(self, create_metrics=False):
body = deserialize_and_validate(self.MeasuresBatchSchema) creator = pecan.request.auth_helper.get_current_user(
pecan.request.headers)
MeasuresBatchSchema = voluptuous.Schema(
{functools.partial(ResourceID, creator=creator):
{six.text_type: MeasuresListSchema}}
)
body = deserialize_and_validate(MeasuresBatchSchema)
known_metrics = [] known_metrics = []
unknown_metrics = [] unknown_metrics = []
@ -1350,8 +1367,6 @@ class ResourcesMetricsMeasuresBatchController(rest.RestController):
known_names = [m.name for m in metrics] known_names = [m.name for m in metrics]
if strutils.bool_from_string(create_metrics): if strutils.bool_from_string(create_metrics):
creator = pecan.request.auth_helper.get_current_user(
pecan.request.headers)
already_exists_names = [] already_exists_names = []
for name in names: for name in names:
if name not in known_names: if name not in known_names:

View File

@ -134,13 +134,13 @@ tests:
project_id: f3d41b77-0cc1-4f0b-b94a-1d5be9c0e3ea project_id: f3d41b77-0cc1-4f0b-b94a-1d5be9c0e3ea
status: 201 status: 201
response_headers: response_headers:
location: $SCHEME://$NETLOC/v1/resource/generic/8d835270-2834-5e55-a693-fd0cf91cba3d location: $SCHEME://$NETLOC/v1/resource/generic/2d869568-70d4-5ed6-9891-7d7a3bbf572d
response_json_paths: response_json_paths:
type: generic type: generic
started_at: "2014-01-03T02:02:02+00:00" started_at: "2014-01-03T02:02:02+00:00"
project_id: f3d41b77-0cc1-4f0b-b94a-1d5be9c0e3ea project_id: f3d41b77-0cc1-4f0b-b94a-1d5be9c0e3ea
created_by_project_id: 99d13f22-3618-4288-82b8-6512ded77e4f created_by_project_id: 99d13f22-3618-4288-82b8-6512ded77e4f
id: 8d835270-2834-5e55-a693-fd0cf91cba3d id: 2d869568-70d4-5ed6-9891-7d7a3bbf572d
original_resource_id: 1.2.3.4 original_resource_id: 1.2.3.4
- name: get status denied - name: get status denied

View File

@ -244,7 +244,7 @@ tests:
response_json_paths: response_json_paths:
$.description.cause: "Unknown resources" $.description.cause: "Unknown resources"
$.description.detail: $.description.detail:
- resource_id: "301dbf9a-4fce-52b6-9010-4484c469dcec" - resource_id: "6b8e287d-c01a-538c-979b-a819ee49de5d"
original_resource_id: "foobar" original_resource_id: "foobar"
- name: push measurements to named metrics and resource with create_metrics with wrong measure objects - name: push measurements to named metrics and resource with create_metrics with wrong measure objects

View File

@ -6,6 +6,11 @@
fixtures: fixtures:
- ConfigFixture - ConfigFixture
defaults:
request_headers:
x-user-id: 0fbb231484614b1a80131fc22f6afc9c
x-project-id: f3d41b770cc14f0bb94a1d5be9c0e3ea
tests: tests:
- name: list resource type - name: list resource type
@ -222,8 +227,6 @@ tests:
- name: post invalid resource - name: post invalid resource
POST: /v1/resource/my_custom_resource POST: /v1/resource/my_custom_resource
request_headers: request_headers:
x-user-id: 0fbb2314-8461-4b1a-8013-1fc22f6afc9c
x-project-id: f3d41b77-0cc1-4f0b-b94a-1d5be9c0e3ea
content-type: application/json content-type: application/json
data: data:
id: d11edfca-4393-4fda-b94d-b05a3a1b3747 id: d11edfca-4393-4fda-b94d-b05a3a1b3747
@ -239,8 +242,6 @@ tests:
- name: post invalid resource uuid - name: post invalid resource uuid
POST: $LAST_URL POST: $LAST_URL
request_headers: request_headers:
x-user-id: 0fbb2314-8461-4b1a-8013-1fc22f6afc9c
x-project-id: f3d41b77-0cc1-4f0b-b94a-1d5be9c0e3ea
content-type: application/json content-type: application/json
data: data:
id: d11edfca-4393-4fda-b94d-b05a3a1b3747 id: d11edfca-4393-4fda-b94d-b05a3a1b3747
@ -258,8 +259,6 @@ tests:
- name: post custom resource - name: post custom resource
POST: $LAST_URL POST: $LAST_URL
request_headers: request_headers:
x-user-id: 0fbb2314-8461-4b1a-8013-1fc22f6afc9c
x-project-id: f3d41b77-0cc1-4f0b-b94a-1d5be9c0e3ea
content-type: application/json content-type: application/json
data: data:
id: d11edfca-4393-4fda-b94d-b05a3a1b3747 id: d11edfca-4393-4fda-b94d-b05a3a1b3747
@ -276,8 +275,6 @@ tests:
- name: patch custom resource - name: patch custom resource
PATCH: /v1/resource/my_custom_resource/d11edfca-4393-4fda-b94d-b05a3a1b3747 PATCH: /v1/resource/my_custom_resource/d11edfca-4393-4fda-b94d-b05a3a1b3747
request_headers: request_headers:
x-user-id: 0fbb2314-8461-4b1a-8013-1fc22f6afc9c
x-project-id: f3d41b77-0cc1-4f0b-b94a-1d5be9c0e3ea
content-type: application/json content-type: application/json
data: data:
name: foo name: foo
@ -303,8 +300,6 @@ tests:
- name: post resource with default - name: post resource with default
POST: /v1/resource/my_custom_resource POST: /v1/resource/my_custom_resource
request_headers: request_headers:
x-user-id: 0fbb2314-8461-4b1a-8013-1fc22f6afc9c
x-project-id: f3d41b77-0cc1-4f0b-b94a-1d5be9c0e3ea
content-type: application/json content-type: application/json
data: data:
id: c4110aec-6e5c-43fa-b8c5-ffdfbca3ce59 id: c4110aec-6e5c-43fa-b8c5-ffdfbca3ce59

View File

@ -66,8 +66,26 @@ tests:
project_id: f3d41b770cc14f0bb94a1d5be9c0e3ea project_id: f3d41b770cc14f0bb94a1d5be9c0e3ea
status: 400 status: 400
response_strings: response_strings:
- "Invalid input: not a valid value for dictionary value @ data[" - "'/' is not supported in resource id"
- "'id'] "
- name: post new resource non uuid again different user
POST: /v1/resource/generic
request_headers:
x-user-id: 0fbb231484614b1a80131fc22f6afc9b
x-project-id: f3d41b770cc14f0bb94a1d5be9c0e3ea
data:
id: generic zero
metrics:
cpu.util:
archive_policy_name: medium
status: 201
response_json_paths:
created_by_user_id: 0fbb231484614b1a80131fc22f6afc9b
created_by_project_id: f3d41b770cc14f0bb94a1d5be9c0e3ea
response_headers:
# is a UUID
location: /v1/resource/generic/[a-f0-9-]{36}/
- name: post new resource non uuid - name: post new resource non uuid
POST: /v1/resource/generic POST: /v1/resource/generic
@ -151,7 +169,7 @@ tests:
archive_policy_name: medium archive_policy_name: medium
status: 400 status: 400
response_strings: response_strings:
- not a valid value for - transformable resource id >255 max allowed characters for dictionary value
- name: post long non uuid resource id - name: post long non uuid resource id
POST: $LAST_URL POST: $LAST_URL

View File

@ -39,23 +39,24 @@ LOG = log.getLogger(__name__)
RESOURCE_ID_NAMESPACE = uuid.UUID('0a7a15ff-aa13-4ac2-897c-9bdf30ce175b') RESOURCE_ID_NAMESPACE = uuid.UUID('0a7a15ff-aa13-4ac2-897c-9bdf30ce175b')
def ResourceUUID(value): def ResourceUUID(value, creator):
if isinstance(value, uuid.UUID): if isinstance(value, uuid.UUID):
return value return value
if '/' in value: if '/' in value:
raise ValueError("'/' is not supported in resource id") raise ValueError("'/' is not supported in resource id")
try: try:
try: return uuid.UUID(value)
return uuid.UUID(value) except ValueError:
except ValueError: if len(value) <= 255:
if len(value) <= 255: # value/creator must be str (unicode) in Python 3 and str (bytes)
if six.PY2: # in Python 2. It's not logical, I know.
value = value.encode('utf-8') if six.PY2:
return uuid.uuid5(RESOURCE_ID_NAMESPACE, value) value = value.encode('utf-8')
raise ValueError( creator = creator.encode('utf-8')
'transformable resource id >255 max allowed characters') return uuid.uuid5(RESOURCE_ID_NAMESPACE,
except Exception as e: value + "\x00" + creator)
raise ValueError(e) raise ValueError(
'transformable resource id >255 max allowed characters')
def UUID(value): def UUID(value):

View File

@ -0,0 +1,12 @@
---
issues:
- >-
The conversion mechanism provided by the API to convert non-UUID resource
id to UUID is now also based on the user creating/accessing the resource.
This makes sure that the conversion generates a unique UUID for the user
and that several users can use the same string as `original_resource_id`.
upgrade:
- >-
Since `original_resource_id` is now unique per creator, that means users
cannot refer to resource by using the `original_resource_id` if the
resource was not created by them.

View File

@ -111,12 +111,16 @@ RESOURCE_IDS=(
"5a301761-cccc-46e2-8900-8b4f6fe6675a" "5a301761-cccc-46e2-8900-8b4f6fe6675a"
) )
# NOTE(sileht): / are now _ # NOTE(sileht): / are now _
[ "$have_resource_type_post" ] && RESOURCE_ID_EXT="5a301761_dddd_46e2_8900_8b4f6fe6675a" # NOTE(jdanjou): and we reencode for admin:admin, but we cannot authenticate as
# admin:admin in basic since ":" is forbidden in any username, so let's use the direct
# computed ID
[ "$have_resource_type_post" ] && RESOURCE_ID_EXT="517920a9-2e50-58b8-88e8-25fd7aae1d8f"
dump_data $GNOCCHI_DATA/new dump_data $GNOCCHI_DATA/new
# NOTE(sileht): change the output of the old gnocchi to compare with the new without '/' # NOTE(sileht): change the output of the old gnocchi to compare with the new without '/'
$GSED -i -e "s,5a301761/dddd/46e2/8900/8b4f6fe6675a,5a301761_dddd_46e2_8900_8b4f6fe6675a,g" \ $GSED -i -e "s,5a301761/dddd/46e2/8900/8b4f6fe6675a,5a301761_dddd_46e2_8900_8b4f6fe6675a,g" \
-e "s,19235bb9-35ca-5f55-b7db-165cfb033c86,fe1bdabf-d94c-5b3a-af1e-06bdff53f228,g" $GNOCCHI_DATA/old/resources.list -e "s,19235bb9-35ca-5f55-b7db-165cfb033c86,517920a9-2e50-58b8-88e8-25fd7aae1d8f,g" $GNOCCHI_DATA/old/resources.list
echo "* Checking output difference between Gnocchi $old_version and $new_version" echo "* Checking output difference between Gnocchi $old_version and $new_version"
diff -uNr $GNOCCHI_DATA/old $GNOCCHI_DATA/new diff -uNr $GNOCCHI_DATA/old $GNOCCHI_DATA/new