From 53290711743e8c35297543afa38a51ccaa2843e8 Mon Sep 17 00:00:00 2001 From: Gage Hugo Date: Mon, 17 Jul 2017 15:37:44 -0500 Subject: [PATCH] Add JSON schema validation for project tags This change adds json schema for project tags validation, with the limits for number of characters per tag as well as the limit for the number of tags a project can have per update. Co-Authored-By: Jaewoo Park Co-Authored-By: Nicolas Change-Id: I5f8e4a53089b9fcc38084bb958d09f63ccc59d2a Partially-Implements: bp project-tags --- keystone/resource/schema.py | 31 ++++++- keystone/tests/unit/test_validation.py | 124 +++++++++++++++++++++++++ 2 files changed, 153 insertions(+), 2 deletions(-) diff --git a/keystone/resource/schema.py b/keystone/resource/schema.py index 865f567659..49d5e789d0 100644 --- a/keystone/resource/schema.py +++ b/keystone/resource/schema.py @@ -20,6 +20,25 @@ _name_properties = { 'pattern': '[\S]+' } +_project_tag_name_properties = { + 'type': 'string', + 'minLength': 1, + 'maxLength': 255, + # NOTE(gagehugo) This pattern is for tags which follows the + # guidelines as set by the API-WG, which matches anything that + # does not contain a '/' or ','. + # https://specs.openstack.org/openstack/api-wg/guidelines/tags.html + 'pattern': '^[^,/]*$' +} + +_project_tags_list_properties = { + 'type': 'array', + 'items': _project_tag_name_properties, + 'required': [], + 'maxItems': 80, + 'uniqueItems': True +} + _project_properties = { 'description': validation.nullable(parameter_types.description), # NOTE(htruta): domain_id is nullable for projects acting as a domain. @@ -27,9 +46,16 @@ _project_properties = { 'enabled': parameter_types.boolean, 'is_domain': parameter_types.boolean, 'parent_id': validation.nullable(parameter_types.id_string), - 'name': _name_properties + 'name': _name_properties, + 'tags': _project_tags_list_properties } +# This is for updating a single project tag via the URL +project_tag_create = _project_tag_name_properties + +# This is for updaing a project with a list of tags +project_tags_update = _project_tags_list_properties + project_create = { 'type': 'object', 'properties': _project_properties, @@ -51,7 +77,8 @@ project_update = { _domain_properties = { 'description': validation.nullable(parameter_types.description), 'enabled': parameter_types.boolean, - 'name': _name_properties + 'name': _name_properties, + 'tags': project_tags_update } domain_create = { diff --git a/keystone/tests/unit/test_validation.py b/keystone/tests/unit/test_validation.py index 0602e56449..56c980cea9 100644 --- a/keystone/tests/unit/test_validation.py +++ b/keystone/tests/unit/test_validation.py @@ -381,6 +381,37 @@ class ProjectValidationTestCase(unit.BaseTestCase): self.create_project_validator.validate, request_to_validate) + def test_validate_project_create_with_tags(self): + request_to_validate = {'name': uuid.uuid4().hex, + 'tags': ['foo', 'bar']} + self.create_project_validator.validate(request_to_validate) + + def test_validate_project_create_with_tags_invalid_char(self): + invalid_chars = [',', '/', ',foo', 'foo/bar'] + for char in invalid_chars: + tag = uuid.uuid4().hex + char + request_to_validate = {'name': uuid.uuid4().hex, + 'tags': ['foo', tag]} + self.assertRaises(exception.SchemaValidationError, + self.create_project_validator.validate, + request_to_validate) + + def test_validate_project_create_with_tag_name_too_long(self): + invalid_name = 'a' * 256 + request_to_validate = {'name': uuid.uuid4().hex, + 'tags': ['foo', invalid_name]} + self.assertRaises(exception.SchemaValidationError, + self.create_project_validator.validate, + request_to_validate) + + def test_validate_project_create_with_too_many_tags(self): + tags = [uuid.uuid4().hex for _ in range(81)] + request_to_validate = {'name': uuid.uuid4().hex, + 'tags': tags} + self.assertRaises(exception.SchemaValidationError, + self.create_project_validator.validate, + request_to_validate) + def test_validate_project_request_with_valid_parent_id(self): """Test that we validate `parent_id` in create project requests.""" # parent_id is nullable @@ -432,6 +463,37 @@ class ProjectValidationTestCase(unit.BaseTestCase): self.update_project_validator.validate, request_to_validate) + def test_validate_project_update_with_tags(self): + request_to_validate = {'name': uuid.uuid4().hex, + 'tags': ['foo', 'bar']} + self.update_project_validator.validate(request_to_validate) + + def test_validate_project_update_with_tags_invalid_char(self): + invalid_chars = [',', '/'] + for char in invalid_chars: + tag = uuid.uuid4().hex + char + request_to_validate = {'name': uuid.uuid4().hex, + 'tags': ['foo', tag]} + self.assertRaises(exception.SchemaValidationError, + self.update_project_validator.validate, + request_to_validate) + + def test_validate_project_update_with_tag_name_too_long(self): + invalid_name = 'a' * 256 + request_to_validate = {'name': uuid.uuid4().hex, + 'tags': ['foo', invalid_name]} + self.assertRaises(exception.SchemaValidationError, + self.update_project_validator.validate, + request_to_validate) + + def test_validate_project_update_with_too_many_tags(self): + tags = [uuid.uuid4().hex for _ in range(81)] + request_to_validate = {'name': uuid.uuid4().hex, + 'tags': tags} + self.assertRaises(exception.SchemaValidationError, + self.update_project_validator.validate, + request_to_validate) + def test_validate_project_create_request_with_valid_domain_id(self): """Test that we validate `domain_id` in create project requests.""" # domain_id is nullable @@ -521,6 +583,37 @@ class DomainValidationTestCase(unit.BaseTestCase): self.create_domain_validator.validate, request_to_validate) + def test_validate_domain_create_with_tags(self): + request_to_validate = {'name': uuid.uuid4().hex, + 'tags': ['foo', 'bar']} + self.create_domain_validator.validate(request_to_validate) + + def test_validate_domain_create_with_tags_invalid_char(self): + invalid_chars = [',', '/'] + for char in invalid_chars: + tag = uuid.uuid4().hex + char + request_to_validate = {'name': uuid.uuid4().hex, + 'tags': ['foo', tag]} + self.assertRaises(exception.SchemaValidationError, + self.create_domain_validator.validate, + request_to_validate) + + def test_validate_domain_create_with_tag_name_too_long(self): + invalid_name = 'a' * 256 + request_to_validate = {'name': uuid.uuid4().hex, + 'tags': ['foo', invalid_name]} + self.assertRaises(exception.SchemaValidationError, + self.create_domain_validator.validate, + request_to_validate) + + def test_validate_domain_create_with_too_many_tags(self): + tags = [uuid.uuid4().hex for _ in range(81)] + request_to_validate = {'name': uuid.uuid4().hex, + 'tags': tags} + self.assertRaises(exception.SchemaValidationError, + self.create_domain_validator.validate, + request_to_validate) + def test_validate_domain_update_request(self): """Test that we validate a domain update request.""" request_to_validate = {'domain_id': uuid.uuid4().hex} @@ -549,6 +642,37 @@ class DomainValidationTestCase(unit.BaseTestCase): self.update_domain_validator.validate, request_to_validate) + def test_validate_domain_update_with_tags(self): + request_to_validate = {'name': uuid.uuid4().hex, + 'tags': ['foo', 'bar']} + self.update_domain_validator.validate(request_to_validate) + + def test_validate_domain_update_with_tags_invalid_char(self): + invalid_chars = [',', '/'] + for char in invalid_chars: + tag = uuid.uuid4().hex + char + request_to_validate = {'name': uuid.uuid4().hex, + 'tags': ['foo', tag]} + self.assertRaises(exception.SchemaValidationError, + self.update_domain_validator.validate, + request_to_validate) + + def test_validate_domain_update_with_tag_name_too_long(self): + invalid_name = 'a' * 256 + request_to_validate = {'name': uuid.uuid4().hex, + 'tags': ['foo', invalid_name]} + self.assertRaises(exception.SchemaValidationError, + self.update_domain_validator.validate, + request_to_validate) + + def test_validate_domain_update_with_too_many_tags(self): + tags = [uuid.uuid4().hex for _ in range(81)] + request_to_validate = {'name': uuid.uuid4().hex, + 'tags': tags} + self.assertRaises(exception.SchemaValidationError, + self.update_domain_validator.validate, + request_to_validate) + class RoleValidationTestCase(unit.BaseTestCase): """Test for V3 Role API validation."""