Merge "Enable JsonSchema validation for project
"
This commit is contained in:
commit
f7ffacb7ad
@ -17,10 +17,11 @@ import http.client
|
||||
|
||||
import flask
|
||||
|
||||
from keystone.api import validation
|
||||
from keystone.common import json_home
|
||||
from keystone.common import provider_api
|
||||
from keystone.common import rbac_enforcer
|
||||
from keystone.common import validation
|
||||
from keystone.common import validation as ks_validation
|
||||
import keystone.conf
|
||||
from keystone import exception
|
||||
from keystone.i18n import _
|
||||
@ -54,6 +55,7 @@ class ProjectResource(ks_flask.ResourceBase):
|
||||
)
|
||||
|
||||
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_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/HEAD /v3/projects/{project_id}
|
||||
@ -113,7 +117,51 @@ class ProjectResource(ks_flask.ResourceBase):
|
||||
self._expand_project_ref(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.
|
||||
|
||||
GET/HEAD /v3/projects
|
||||
@ -148,17 +196,8 @@ class ProjectResource(ks_flask.ResourceBase):
|
||||
filtered_refs = refs
|
||||
return self.wrap_collection(filtered_refs, hints=hints)
|
||||
|
||||
def get(self, project_id=None):
|
||||
"""Get project or list projects.
|
||||
|
||||
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()
|
||||
|
||||
@validation.request_body_schema(schema.project_create_request_body)
|
||||
@validation.response_body_schema(schema.project_create_response_body)
|
||||
def post(self):
|
||||
"""Create project.
|
||||
|
||||
@ -169,7 +208,6 @@ class ProjectResource(ks_flask.ResourceBase):
|
||||
ENFORCER.enforce_call(
|
||||
action='identity:create_project', target_attr=target
|
||||
)
|
||||
validation.lazy_validate(schema.project_create, project)
|
||||
project = self._assign_unique_id(project)
|
||||
if not project.get('is_domain'):
|
||||
project = self._normalize_domain_id(project)
|
||||
@ -187,37 +225,6 @@ class ProjectResource(ks_flask.ResourceBase):
|
||||
raise exception.ValidationError(e)
|
||||
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):
|
||||
collection_key = 'projects'
|
||||
@ -237,6 +244,8 @@ class _ProjectTagResourceBase(ks_flask.ResourceBase):
|
||||
|
||||
|
||||
class ProjectTagsResource(_ProjectTagResourceBase):
|
||||
@validation.request_body_schema(None)
|
||||
@validation.response_body_schema(schema.tags_response_body)
|
||||
def get(self, project_id):
|
||||
"""List tags associated with a given project.
|
||||
|
||||
@ -249,6 +258,8 @@ class ProjectTagsResource(_ProjectTagResourceBase):
|
||||
ref = PROVIDERS.resource_api.list_project_tags(project_id)
|
||||
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):
|
||||
"""Update all tags associated with a given project.
|
||||
|
||||
@ -259,12 +270,14 @@ class ProjectTagsResource(_ProjectTagResourceBase):
|
||||
build_target=_build_project_target_enforcement,
|
||||
)
|
||||
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(
|
||||
project_id, tags, 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 all tags associated with a given project.
|
||||
|
||||
@ -279,6 +292,8 @@ class ProjectTagsResource(_ProjectTagResourceBase):
|
||||
|
||||
|
||||
class ProjectTagResource(_ProjectTagResourceBase):
|
||||
@validation.request_body_schema(None)
|
||||
@validation.response_body_schema(None)
|
||||
def get(self, project_id, value):
|
||||
"""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)
|
||||
return None, http.client.NO_CONTENT
|
||||
|
||||
@validation.request_body_schema(None)
|
||||
@validation.response_body_schema(None)
|
||||
def put(self, project_id, value):
|
||||
"""Add a single tag to a project.
|
||||
|
||||
@ -300,11 +317,11 @@ class ProjectTagResource(_ProjectTagResourceBase):
|
||||
action='identity:create_project_tag',
|
||||
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
|
||||
tags = PROVIDERS.resource_api.list_project_tags(project_id)
|
||||
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(
|
||||
project_id, value, initiator=self.audit_initiator
|
||||
)
|
||||
@ -313,6 +330,8 @@ class ProjectTagResource(_ProjectTagResourceBase):
|
||||
response.headers['Location'] = url
|
||||
return response
|
||||
|
||||
@validation.request_body_schema(None)
|
||||
@validation.response_body_schema(None)
|
||||
def delete(self, project_id, value):
|
||||
"""Delete a single tag from a project.
|
||||
|
||||
@ -566,8 +585,21 @@ class ProjectGroupListGrantResource(_ProjectGrantResourceBase):
|
||||
class ProjectAPI(ks_flask.APIBase):
|
||||
_name = 'projects'
|
||||
_import_name = __name__
|
||||
resources = [ProjectResource]
|
||||
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(
|
||||
resource=ProjectTagsResource,
|
||||
url='/projects/<string:project_id>/tags',
|
||||
|
@ -13,6 +13,8 @@
|
||||
"""Common parameter types for validating API requests."""
|
||||
from typing import Any
|
||||
|
||||
empty: dict[str, Any] = {"type": "null"}
|
||||
|
||||
name: dict[str, Any] = {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
|
@ -39,3 +39,12 @@ truncated: dict[str, Any] = {
|
||||
"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
|
||||
# License for the specific language governing permissions and limitations
|
||||
# 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.validation import parameter_types
|
||||
from keystone.common.validation import parameter_types as old_parameter_types
|
||||
from keystone.resource.backends import resource_options as ro
|
||||
|
||||
_name_properties = {
|
||||
@ -41,12 +44,12 @@ _project_tags_list_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.
|
||||
'domain_id': validation.nullable(parameter_types.id_string),
|
||||
'domain_id': validation.nullable(old_parameter_types.id_string),
|
||||
'enabled': 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,
|
||||
'tags': _project_tags_list_properties,
|
||||
'options': ro.PROJECT_OPTIONS_REGISTRY.json_schema,
|
||||
@ -77,7 +80,7 @@ project_update = {
|
||||
}
|
||||
|
||||
_domain_properties = {
|
||||
'description': validation.nullable(parameter_types.description),
|
||||
'description': validation.nullable(old_parameter_types.description),
|
||||
'enabled': parameter_types.boolean,
|
||||
'name': _name_properties,
|
||||
'tags': project_tags_update,
|
||||
@ -99,3 +102,91 @@ domain_update = {
|
||||
'minProperties': 1,
|
||||
'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]
|
||||
|
||||
_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]
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user