Merge "Enable JsonSchema validation for project
"
This commit is contained in:
commit
f7ffacb7ad
@ -17,10 +17,11 @@ import http.client
|
|||||||
|
|
||||||
import flask
|
import flask
|
||||||
|
|
||||||
|
from keystone.api import validation
|
||||||
from keystone.common import json_home
|
from keystone.common import json_home
|
||||||
from keystone.common import provider_api
|
from keystone.common import provider_api
|
||||||
from keystone.common import rbac_enforcer
|
from keystone.common import rbac_enforcer
|
||||||
from keystone.common import validation
|
from keystone.common import validation as ks_validation
|
||||||
import keystone.conf
|
import keystone.conf
|
||||||
from keystone import exception
|
from keystone import exception
|
||||||
from keystone.i18n import _
|
from keystone.i18n import _
|
||||||
@ -54,6 +55,7 @@ class ProjectResource(ks_flask.ResourceBase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def _expand_project_ref(self, ref):
|
def _expand_project_ref(self, ref):
|
||||||
|
# NOTE(gtema): This functionality is not described in the API-Ref
|
||||||
parents_as_list = self.query_filter_is_true('parents_as_list')
|
parents_as_list = self.query_filter_is_true('parents_as_list')
|
||||||
parents_as_ids = self.query_filter_is_true('parents_as_ids')
|
parents_as_ids = self.query_filter_is_true('parents_as_ids')
|
||||||
|
|
||||||
@ -100,7 +102,9 @@ class ProjectResource(ks_flask.ResourceBase):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
def _get_project(self, project_id):
|
@validation.request_body_schema(None)
|
||||||
|
@validation.response_body_schema(schema.project_get_response_body)
|
||||||
|
def get(self, project_id: str):
|
||||||
"""Get project.
|
"""Get project.
|
||||||
|
|
||||||
GET/HEAD /v3/projects/{project_id}
|
GET/HEAD /v3/projects/{project_id}
|
||||||
@ -113,7 +117,51 @@ class ProjectResource(ks_flask.ResourceBase):
|
|||||||
self._expand_project_ref(project)
|
self._expand_project_ref(project)
|
||||||
return self.wrap_member(project)
|
return self.wrap_member(project)
|
||||||
|
|
||||||
def _list_projects(self):
|
@validation.request_body_schema(schema.project_update_request_body)
|
||||||
|
@validation.response_body_schema(schema.project_update_response_body)
|
||||||
|
def patch(self, project_id):
|
||||||
|
"""Update project.
|
||||||
|
|
||||||
|
PATCH /v3/projects/{project_id}
|
||||||
|
"""
|
||||||
|
ENFORCER.enforce_call(
|
||||||
|
action='identity:update_project',
|
||||||
|
build_target=_build_project_target_enforcement,
|
||||||
|
)
|
||||||
|
project = self.request_body_json.get('project', {})
|
||||||
|
self._require_matching_id(project)
|
||||||
|
ref = PROVIDERS.resource_api.update_project(
|
||||||
|
project_id, project, initiator=self.audit_initiator
|
||||||
|
)
|
||||||
|
return self.wrap_member(ref)
|
||||||
|
|
||||||
|
@validation.request_body_schema(None)
|
||||||
|
@validation.response_body_schema(None)
|
||||||
|
def delete(self, project_id):
|
||||||
|
"""Delete project.
|
||||||
|
|
||||||
|
DELETE /v3/projects/{project_id}
|
||||||
|
"""
|
||||||
|
ENFORCER.enforce_call(
|
||||||
|
action='identity:delete_project',
|
||||||
|
build_target=_build_project_target_enforcement,
|
||||||
|
)
|
||||||
|
PROVIDERS.resource_api.delete_project(
|
||||||
|
project_id, initiator=self.audit_initiator
|
||||||
|
)
|
||||||
|
return None, http.client.NO_CONTENT
|
||||||
|
|
||||||
|
|
||||||
|
class ProjectsResource(ks_flask.ResourceBase):
|
||||||
|
collection_key = 'projects'
|
||||||
|
member_key = 'project'
|
||||||
|
get_member_from_driver = PROVIDERS.deferred_provider_lookup(
|
||||||
|
api='resource_api', method='get_project'
|
||||||
|
)
|
||||||
|
|
||||||
|
@validation.request_query_schema(schema.project_index_request_query)
|
||||||
|
@validation.response_body_schema(schema.project_index_response_body)
|
||||||
|
def get(self):
|
||||||
"""List projects.
|
"""List projects.
|
||||||
|
|
||||||
GET/HEAD /v3/projects
|
GET/HEAD /v3/projects
|
||||||
@ -148,17 +196,8 @@ class ProjectResource(ks_flask.ResourceBase):
|
|||||||
filtered_refs = refs
|
filtered_refs = refs
|
||||||
return self.wrap_collection(filtered_refs, hints=hints)
|
return self.wrap_collection(filtered_refs, hints=hints)
|
||||||
|
|
||||||
def get(self, project_id=None):
|
@validation.request_body_schema(schema.project_create_request_body)
|
||||||
"""Get project or list projects.
|
@validation.response_body_schema(schema.project_create_response_body)
|
||||||
|
|
||||||
GET/HEAD /v3/projects
|
|
||||||
GET/HEAD /v3/projects/{project_id}
|
|
||||||
"""
|
|
||||||
if project_id is not None:
|
|
||||||
return self._get_project(project_id)
|
|
||||||
else:
|
|
||||||
return self._list_projects()
|
|
||||||
|
|
||||||
def post(self):
|
def post(self):
|
||||||
"""Create project.
|
"""Create project.
|
||||||
|
|
||||||
@ -169,7 +208,6 @@ class ProjectResource(ks_flask.ResourceBase):
|
|||||||
ENFORCER.enforce_call(
|
ENFORCER.enforce_call(
|
||||||
action='identity:create_project', target_attr=target
|
action='identity:create_project', target_attr=target
|
||||||
)
|
)
|
||||||
validation.lazy_validate(schema.project_create, project)
|
|
||||||
project = self._assign_unique_id(project)
|
project = self._assign_unique_id(project)
|
||||||
if not project.get('is_domain'):
|
if not project.get('is_domain'):
|
||||||
project = self._normalize_domain_id(project)
|
project = self._normalize_domain_id(project)
|
||||||
@ -187,37 +225,6 @@ class ProjectResource(ks_flask.ResourceBase):
|
|||||||
raise exception.ValidationError(e)
|
raise exception.ValidationError(e)
|
||||||
return self.wrap_member(ref), http.client.CREATED
|
return self.wrap_member(ref), http.client.CREATED
|
||||||
|
|
||||||
def patch(self, project_id):
|
|
||||||
"""Update project.
|
|
||||||
|
|
||||||
PATCH /v3/projects/{project_id}
|
|
||||||
"""
|
|
||||||
ENFORCER.enforce_call(
|
|
||||||
action='identity:update_project',
|
|
||||||
build_target=_build_project_target_enforcement,
|
|
||||||
)
|
|
||||||
project = self.request_body_json.get('project', {})
|
|
||||||
validation.lazy_validate(schema.project_update, project)
|
|
||||||
self._require_matching_id(project)
|
|
||||||
ref = PROVIDERS.resource_api.update_project(
|
|
||||||
project_id, project, initiator=self.audit_initiator
|
|
||||||
)
|
|
||||||
return self.wrap_member(ref)
|
|
||||||
|
|
||||||
def delete(self, project_id):
|
|
||||||
"""Delete project.
|
|
||||||
|
|
||||||
DELETE /v3/projects/{project_id}
|
|
||||||
"""
|
|
||||||
ENFORCER.enforce_call(
|
|
||||||
action='identity:delete_project',
|
|
||||||
build_target=_build_project_target_enforcement,
|
|
||||||
)
|
|
||||||
PROVIDERS.resource_api.delete_project(
|
|
||||||
project_id, initiator=self.audit_initiator
|
|
||||||
)
|
|
||||||
return None, http.client.NO_CONTENT
|
|
||||||
|
|
||||||
|
|
||||||
class _ProjectTagResourceBase(ks_flask.ResourceBase):
|
class _ProjectTagResourceBase(ks_flask.ResourceBase):
|
||||||
collection_key = 'projects'
|
collection_key = 'projects'
|
||||||
@ -237,6 +244,8 @@ class _ProjectTagResourceBase(ks_flask.ResourceBase):
|
|||||||
|
|
||||||
|
|
||||||
class ProjectTagsResource(_ProjectTagResourceBase):
|
class ProjectTagsResource(_ProjectTagResourceBase):
|
||||||
|
@validation.request_body_schema(None)
|
||||||
|
@validation.response_body_schema(schema.tags_response_body)
|
||||||
def get(self, project_id):
|
def get(self, project_id):
|
||||||
"""List tags associated with a given project.
|
"""List tags associated with a given project.
|
||||||
|
|
||||||
@ -249,6 +258,8 @@ class ProjectTagsResource(_ProjectTagResourceBase):
|
|||||||
ref = PROVIDERS.resource_api.list_project_tags(project_id)
|
ref = PROVIDERS.resource_api.list_project_tags(project_id)
|
||||||
return self.wrap_member(ref)
|
return self.wrap_member(ref)
|
||||||
|
|
||||||
|
@validation.request_body_schema(schema.tags_update_request_body)
|
||||||
|
@validation.response_body_schema(schema.tags_response_body)
|
||||||
def put(self, project_id):
|
def put(self, project_id):
|
||||||
"""Update all tags associated with a given project.
|
"""Update all tags associated with a given project.
|
||||||
|
|
||||||
@ -259,12 +270,14 @@ class ProjectTagsResource(_ProjectTagResourceBase):
|
|||||||
build_target=_build_project_target_enforcement,
|
build_target=_build_project_target_enforcement,
|
||||||
)
|
)
|
||||||
tags = self.request_body_json.get('tags', {})
|
tags = self.request_body_json.get('tags', {})
|
||||||
validation.lazy_validate(schema.project_tags_update, tags)
|
ks_validation.lazy_validate(schema.project_tags_update, tags)
|
||||||
ref = PROVIDERS.resource_api.update_project_tags(
|
ref = PROVIDERS.resource_api.update_project_tags(
|
||||||
project_id, tags, initiator=self.audit_initiator
|
project_id, tags, initiator=self.audit_initiator
|
||||||
)
|
)
|
||||||
return self.wrap_member(ref)
|
return self.wrap_member(ref)
|
||||||
|
|
||||||
|
@validation.request_body_schema(None)
|
||||||
|
@validation.response_body_schema(None)
|
||||||
def delete(self, project_id):
|
def delete(self, project_id):
|
||||||
"""Delete all tags associated with a given project.
|
"""Delete all tags associated with a given project.
|
||||||
|
|
||||||
@ -279,6 +292,8 @@ class ProjectTagsResource(_ProjectTagResourceBase):
|
|||||||
|
|
||||||
|
|
||||||
class ProjectTagResource(_ProjectTagResourceBase):
|
class ProjectTagResource(_ProjectTagResourceBase):
|
||||||
|
@validation.request_body_schema(None)
|
||||||
|
@validation.response_body_schema(None)
|
||||||
def get(self, project_id, value):
|
def get(self, project_id, value):
|
||||||
"""Get information for a single tag associated with a given project.
|
"""Get information for a single tag associated with a given project.
|
||||||
|
|
||||||
@ -291,6 +306,8 @@ class ProjectTagResource(_ProjectTagResourceBase):
|
|||||||
PROVIDERS.resource_api.get_project_tag(project_id, value)
|
PROVIDERS.resource_api.get_project_tag(project_id, value)
|
||||||
return None, http.client.NO_CONTENT
|
return None, http.client.NO_CONTENT
|
||||||
|
|
||||||
|
@validation.request_body_schema(None)
|
||||||
|
@validation.response_body_schema(None)
|
||||||
def put(self, project_id, value):
|
def put(self, project_id, value):
|
||||||
"""Add a single tag to a project.
|
"""Add a single tag to a project.
|
||||||
|
|
||||||
@ -300,11 +317,11 @@ class ProjectTagResource(_ProjectTagResourceBase):
|
|||||||
action='identity:create_project_tag',
|
action='identity:create_project_tag',
|
||||||
build_target=_build_project_target_enforcement,
|
build_target=_build_project_target_enforcement,
|
||||||
)
|
)
|
||||||
validation.lazy_validate(schema.project_tag_create, value)
|
ks_validation.lazy_validate(schema.project_tag_create, value)
|
||||||
# Check if we will exceed the max number of tags on this project
|
# Check if we will exceed the max number of tags on this project
|
||||||
tags = PROVIDERS.resource_api.list_project_tags(project_id)
|
tags = PROVIDERS.resource_api.list_project_tags(project_id)
|
||||||
tags.append(value)
|
tags.append(value)
|
||||||
validation.lazy_validate(schema.project_tags_update, tags)
|
ks_validation.lazy_validate(schema.project_tags_update, tags)
|
||||||
PROVIDERS.resource_api.create_project_tag(
|
PROVIDERS.resource_api.create_project_tag(
|
||||||
project_id, value, initiator=self.audit_initiator
|
project_id, value, initiator=self.audit_initiator
|
||||||
)
|
)
|
||||||
@ -313,6 +330,8 @@ class ProjectTagResource(_ProjectTagResourceBase):
|
|||||||
response.headers['Location'] = url
|
response.headers['Location'] = url
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
@validation.request_body_schema(None)
|
||||||
|
@validation.response_body_schema(None)
|
||||||
def delete(self, project_id, value):
|
def delete(self, project_id, value):
|
||||||
"""Delete a single tag from a project.
|
"""Delete a single tag from a project.
|
||||||
|
|
||||||
@ -566,8 +585,21 @@ class ProjectGroupListGrantResource(_ProjectGrantResourceBase):
|
|||||||
class ProjectAPI(ks_flask.APIBase):
|
class ProjectAPI(ks_flask.APIBase):
|
||||||
_name = 'projects'
|
_name = 'projects'
|
||||||
_import_name = __name__
|
_import_name = __name__
|
||||||
resources = [ProjectResource]
|
|
||||||
resource_mapping = [
|
resource_mapping = [
|
||||||
|
ks_flask.construct_resource_map(
|
||||||
|
resource=ProjectsResource,
|
||||||
|
url='/projects',
|
||||||
|
resource_kwargs={},
|
||||||
|
rel="projects",
|
||||||
|
path_vars=None,
|
||||||
|
),
|
||||||
|
ks_flask.construct_resource_map(
|
||||||
|
resource=ProjectResource,
|
||||||
|
url='/projects/<string:project_id>',
|
||||||
|
resource_kwargs={},
|
||||||
|
rel="project",
|
||||||
|
path_vars={'project_id': json_home.Parameters.PROJECT_ID},
|
||||||
|
),
|
||||||
ks_flask.construct_resource_map(
|
ks_flask.construct_resource_map(
|
||||||
resource=ProjectTagsResource,
|
resource=ProjectTagsResource,
|
||||||
url='/projects/<string:project_id>/tags',
|
url='/projects/<string:project_id>/tags',
|
||||||
|
@ -13,6 +13,8 @@
|
|||||||
"""Common parameter types for validating API requests."""
|
"""Common parameter types for validating API requests."""
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
|
empty: dict[str, Any] = {"type": "null"}
|
||||||
|
|
||||||
name: dict[str, Any] = {
|
name: dict[str, Any] = {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"minLength": 1,
|
"minLength": 1,
|
||||||
|
@ -39,3 +39,12 @@ truncated: dict[str, Any] = {
|
|||||||
"response limit"
|
"response limit"
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tags: dict[str, Any] = {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"tags": {"type": "array", "items": {"type": "string"}},
|
||||||
|
"links": links,
|
||||||
|
},
|
||||||
|
"additionalProperties": False,
|
||||||
|
}
|
||||||
|
@ -9,9 +9,12 @@
|
|||||||
# 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.
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from keystone.api.validation import parameter_types
|
||||||
|
from keystone.api.validation import response_types
|
||||||
from keystone.common import validation
|
from keystone.common import validation
|
||||||
from keystone.common.validation import parameter_types
|
from keystone.common.validation import parameter_types as old_parameter_types
|
||||||
from keystone.resource.backends import resource_options as ro
|
from keystone.resource.backends import resource_options as ro
|
||||||
|
|
||||||
_name_properties = {
|
_name_properties = {
|
||||||
@ -41,12 +44,12 @@ _project_tags_list_properties = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_project_properties = {
|
_project_properties = {
|
||||||
'description': validation.nullable(parameter_types.description),
|
'description': validation.nullable(old_parameter_types.description),
|
||||||
# NOTE(htruta): domain_id is nullable for projects acting as a domain.
|
# NOTE(htruta): domain_id is nullable for projects acting as a domain.
|
||||||
'domain_id': validation.nullable(parameter_types.id_string),
|
'domain_id': validation.nullable(old_parameter_types.id_string),
|
||||||
'enabled': parameter_types.boolean,
|
'enabled': parameter_types.boolean,
|
||||||
'is_domain': parameter_types.boolean,
|
'is_domain': parameter_types.boolean,
|
||||||
'parent_id': validation.nullable(parameter_types.id_string),
|
'parent_id': validation.nullable(old_parameter_types.id_string),
|
||||||
'name': _name_properties,
|
'name': _name_properties,
|
||||||
'tags': _project_tags_list_properties,
|
'tags': _project_tags_list_properties,
|
||||||
'options': ro.PROJECT_OPTIONS_REGISTRY.json_schema,
|
'options': ro.PROJECT_OPTIONS_REGISTRY.json_schema,
|
||||||
@ -77,7 +80,7 @@ project_update = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_domain_properties = {
|
_domain_properties = {
|
||||||
'description': validation.nullable(parameter_types.description),
|
'description': validation.nullable(old_parameter_types.description),
|
||||||
'enabled': parameter_types.boolean,
|
'enabled': parameter_types.boolean,
|
||||||
'name': _name_properties,
|
'name': _name_properties,
|
||||||
'tags': project_tags_update,
|
'tags': project_tags_update,
|
||||||
@ -99,3 +102,91 @@ domain_update = {
|
|||||||
'minProperties': 1,
|
'minProperties': 1,
|
||||||
'additionalProperties': True,
|
'additionalProperties': True,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
project_index_request_query = {
|
||||||
|
'type': 'object',
|
||||||
|
'properties': {
|
||||||
|
"domain_id": parameter_types.domain_id,
|
||||||
|
"enabled": parameter_types.boolean,
|
||||||
|
"name": _name_properties,
|
||||||
|
"parent_id": parameter_types.parent_id,
|
||||||
|
"is_domain": parameter_types.boolean,
|
||||||
|
"tags": parameter_types.tags,
|
||||||
|
"tags-any": parameter_types.tags,
|
||||||
|
"not-tags": parameter_types.tags,
|
||||||
|
"not-tags-any": parameter_types.tags,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
project_schema: dict[str, Any] = {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"id": {"type": "string", "readOnly": True},
|
||||||
|
"links": response_types.resource_links,
|
||||||
|
**_project_properties,
|
||||||
|
},
|
||||||
|
"additionalProperties": True,
|
||||||
|
}
|
||||||
|
|
||||||
|
project_get_response_body: dict[str, Any] = {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {"project": project_schema},
|
||||||
|
"additionalProperties": False,
|
||||||
|
}
|
||||||
|
|
||||||
|
project_index_response_body: dict[str, Any] = {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"projects": {"type": "array", "items": project_schema},
|
||||||
|
"links": response_types.links,
|
||||||
|
"truncated": response_types.truncated,
|
||||||
|
},
|
||||||
|
"additionalProperties": False,
|
||||||
|
}
|
||||||
|
|
||||||
|
project_create_request_body: dict[str, Any] = {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"project": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": _project_properties,
|
||||||
|
"required": ["name"],
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"additionalProperties": False,
|
||||||
|
}
|
||||||
|
|
||||||
|
project_create_response_body: dict[str, Any] = project_get_response_body
|
||||||
|
|
||||||
|
# Explicitly list attributes allowed to be updated.
|
||||||
|
# Since updating `domain_id` is since very long time marked deprecated do not
|
||||||
|
# even include it in the schema
|
||||||
|
project_update_request_body: dict[str, Any] = {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"project": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
'description': validation.nullable(
|
||||||
|
old_parameter_types.description
|
||||||
|
),
|
||||||
|
'enabled': parameter_types.boolean,
|
||||||
|
'name': _name_properties,
|
||||||
|
'tags': _project_tags_list_properties,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"additionalProperties": False,
|
||||||
|
}
|
||||||
|
|
||||||
|
project_update_response_body: dict[str, Any] = project_get_response_body
|
||||||
|
|
||||||
|
tags_response_body: dict[str, Any] = response_types.tags
|
||||||
|
|
||||||
|
tags_update_request_body: dict[str, Any] = {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"tags": {"type": "array", "items": _project_tag_name_properties}
|
||||||
|
},
|
||||||
|
"additionalProperties": False,
|
||||||
|
}
|
||||||
|
@ -82,7 +82,12 @@ entity_update = {
|
|||||||
|
|
||||||
_VALID_ENABLED_FORMATS = [True, False]
|
_VALID_ENABLED_FORMATS = [True, False]
|
||||||
|
|
||||||
_INVALID_ENABLED_FORMATS = ['some string', 1, 0, 'True', 'False']
|
# NOTE(gtema): During impliementing JsonSchema validation some
|
||||||
|
# normalization has been done and it was noticed that value 'True' for boolean
|
||||||
|
# is explicitly expected in i.e. is_domain while other test verifies it is
|
||||||
|
# rejected. Therefore `True` and `False` values are removed from invalid values
|
||||||
|
# to be more conforming to other OpenStack services.
|
||||||
|
_INVALID_ENABLED_FORMATS = ['some string', 1, 0]
|
||||||
|
|
||||||
_INVALID_DESC_FORMATS = [False, 1, 2.0]
|
_INVALID_DESC_FORMATS = [False, 1, 2.0]
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user