Add JSON Schema to application credentials and validation decorators

to application credential resource.

Change-Id: I232e4ea42d62a2fac629d2cb6a12ba5f698f53be
This commit is contained in:
Antonia Gaete
2024-08-01 11:02:04 -07:00
committed by Artem Goncharov
parent 516a341b90
commit aa0cdc5397
3 changed files with 267 additions and 58 deletions

View File

@@ -668,6 +668,12 @@ class UserAppCredListCreateResource(ks_flask.ResourceBase):
roles = token.roles
return roles
@validation.request_query_schema(
app_cred_schema.application_credential_index_request_query
)
@validation.response_body_schema(
app_cred_schema.application_credential_index_response_body
)
def get(self, user_id):
"""List application credentials for user.
@@ -682,6 +688,12 @@ class UserAppCredListCreateResource(ks_flask.ResourceBase):
refs = app_cred_api.list_application_credentials(user_id, hints=hints)
return self.wrap_collection(refs, hints=hints)
@validation.request_body_schema(
app_cred_schema.application_credential_create_request_body
)
@validation.response_body_schema(
app_cred_schema.application_credential_create_response_body
)
def post(self, user_id):
"""Create application credential.
@@ -691,9 +703,6 @@ class UserAppCredListCreateResource(ks_flask.ResourceBase):
app_cred_data = self.request_body_json.get(
'application_credential', {}
)
ks_validation.lazy_validate(
app_cred_schema.application_credential_create, app_cred_data
)
token = self.auth_context['token']
_check_unrestricted_application_credential(token)
if self.oslo_context.user_id != user_id:
@@ -743,6 +752,12 @@ class UserAppCredGetDeleteResource(ks_flask.ResourceBase):
collection_key = 'application_credentials'
member_key = 'application_credential'
@validation.request_body_schema(
app_cred_schema.application_credential_request_body
)
@validation.response_body_schema(
app_cred_schema.application_credential_response_body
)
def get(self, user_id, application_credential_id):
"""Get application credential resource.

View File

@@ -12,26 +12,11 @@
# License for the specific language governing permissions and limitations
# under the License.
import copy
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 as ks_parameter_types
_role_properties = {
'type': 'array',
'items': {
'type': 'object',
'properties': {
'id': parameter_types.id_string,
'name': parameter_types.name,
},
'minProperties': 1,
'maxProperties': 1,
'additionalProperties': False,
},
}
# Individual properties of 'Access Rule'
_access_rules_properties = {
@@ -80,7 +65,8 @@ access_rule_schema: dict[str, Any] = {
"additionalProperties": False,
}
# Query parameters of the `/users/{user_d}/access_rules` API
# Query parameters of the `/users/{user_id}/access_rules` and
# `/application_credentials/{application_credential_id}` APIs
index_request_query: dict[str, Any] = {
"type": "object",
"properties": {},
@@ -118,19 +104,213 @@ rule_show_response_body: dict[str, Any] = {
"additionalProperties": False,
}
# Individual properties of 'Application Credential'
_application_credential_properties = {
'name': ks_parameter_types.name,
'description': validation.nullable(ks_parameter_types.description),
'secret': {'type': ['null', 'string']},
'expires_at': {'type': ['null', 'string']},
'roles': _role_properties,
'unrestricted': ks_parameter_types.boolean,
'access_rules': _access_rules_properties,
"name": {
**parameter_types.name,
"description": (
"The name of the application credential. Must be unique to a user."
),
},
"description": {
"type": ["string", "null"],
"description": (
"A description of the application credential's purpose."
),
},
"expires_at": {
"type": ["string", "null"],
"description": (
"The expiration time of the application credential, if one "
"was specified."
),
},
"project_id": {
"type": "string",
"description": (
"The ID of the project the application credential was "
"created for and that authentication requests using this "
"application credential will be scoped to."
),
},
"access_rules": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": parameter_types.id_string,
**_access_rules_properties,
},
},
"description": "A list of access_rules objects.",
},
"unrestricted": {
"type": ["boolean", "null"],
"description": (
"A flag indicating whether the application credential "
"may be used for creation or destruction of other "
"application credentials or trusts."
),
},
"system": {"type": ["string", "null"]},
}
application_credential_create = {
'type': 'object',
'properties': _application_credential_properties,
'required': ['name'],
'additionalProperties': True,
# Common schema of `Application Credential` resource
application_credential_schema: dict[str, Any] = {
"type": "object",
"description": "An application credential object.",
"properties": {
"id": {
"type": "string",
"readOnly": True,
"description": "The UUID of the application credential",
},
"roles": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": parameter_types.id_string,
"name": parameter_types.name,
"domain_id": {
"type": ["string", "null"],
"description": "The ID of the domain of the role.",
},
"description": {
"type": ["string", "null"],
"description": "A description about the role.",
},
"options": {
"type": ["object", "null"],
"description": (
"The resource options for the role. "
"Available resource options are immutable."
),
},
},
"additionalProperties": False,
},
"description": (
"A list of one or more roles that this application "
"credential has associated with its project. A token "
"using this application credential will have these "
"same roles."
),
},
"user_id": {"type": "string", "description": "The ID of the user."},
"links": response_types.resource_links,
**_application_credential_properties,
},
"additionalProperties": False,
}
# Query parameters of `/application_credentials` API
application_credential_index_request_query: dict[str, Any] = {
"type": "object",
"properties": {
"name": {
"type": "string",
"description": (
"The name of the application credential. "
"Must be unique to a user."
),
}
},
"additionalProperties": False,
}
# Response of the `/application_credentials` API
application_credential_index_response_body: dict[str, Any] = {
"type": "object",
"properties": {
"application_credentials": {
"type": "array",
"items": application_credential_schema,
"description": "A list of application credentials",
},
"links": response_types.links,
"truncated": response_types.truncated,
},
"additionalProperties": False,
}
application_credential_request_body: dict[str, Any] = {
"type": "object",
"properties": {
"id": {
"type": "string",
"description": "The UUID of the application credential",
}
},
"additionalProperties": False,
}
# Response of `/application_credentials/{application_credential_id}`
# API returning single access rule
application_credential_response_body: dict[str, Any] = {
"type": "object",
"description": "An application credential object.",
"properties": {"application_credential": application_credential_schema},
"additionalProperties": False,
}
# Request body of the `POST /application_credentials` operation
application_credential_create_request_body: dict[str, Any] = {
"type": "object",
"description": "An application credential object.",
"properties": {
"application_credential": {
"type": "object",
"properties": {
"id": {
"type": "string",
"description": "The UUID for the credential.",
},
"secret": {
"type": ["string", "null"],
"description": (
"The secret that the application credential "
"will be created with. If not provided, one "
"will be generated."
),
},
**_application_credential_properties,
"roles": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": parameter_types.id_string,
"name": parameter_types.name,
},
"minProperties": 1,
"maxProperties": 1,
"additionalProperties": False,
},
"description": (
"A list of one or more roles that this application "
"credential has associated with its project. A token "
"using this application credential will have these "
"same roles."
),
},
},
"additionalProperties": False,
"required": ["name"],
}
},
"required": ["application_credential"],
"additionalProperties": False,
}
application_credential_create_response_body = copy.deepcopy(
application_credential_response_body
)
application_credential_create_response_body["properties"][
"application_credential"
]["properties"]["secret"] = {
"type": "string",
"description": (
"The secret that the application credential will be created with."
),
}

View File

@@ -3482,23 +3482,27 @@ class ApplicationCredentialValidatorTestCase(unit.TestCase):
def setUp(self):
super().setUp()
create = app_cred_schema.application_credential_create
create = app_cred_schema.application_credential_create_request_body
self.create_app_cred_validator = validators.SchemaValidator(create)
def test_validate_app_cred_request(self):
request_to_validate = {
'name': 'myappcred',
'description': 'My App Cred',
'roles': [{'name': 'member'}],
'expires_at': 'tomorrow',
'application_credential': {
'name': 'myappcred',
'description': 'My App Cred',
'roles': [{'name': 'member'}],
'expires_at': 'tomorrow',
}
}
self.create_app_cred_validator.validate(request_to_validate)
def test_validate_app_cred_request_without_name_fails(self):
request_to_validate = {
'description': 'My App Cred',
'roles': [{'name': 'member'}],
'expires_at': 'tomorrow',
'application_credential': {
'description': 'My App Cred',
'roles': [{'name': 'member'}],
'expires_at': 'tomorrow',
}
}
self.assertRaises(
exception.SchemaValidationError,
@@ -3508,10 +3512,12 @@ class ApplicationCredentialValidatorTestCase(unit.TestCase):
def test_validate_app_cred_with_invalid_expires_at_fails(self):
request_to_validate = {
'name': 'myappcred',
'description': 'My App Cred',
'roles': [{'name': 'member'}],
'expires_at': 3,
'application_credential': {
'name': 'myappcred',
'description': 'My App Cred',
'roles': [{'name': 'member'}],
'expires_at': 3,
}
}
self.assertRaises(
exception.SchemaValidationError,
@@ -3521,36 +3527,44 @@ class ApplicationCredentialValidatorTestCase(unit.TestCase):
def test_validate_app_cred_with_null_expires_at_succeeds(self):
request_to_validate = {
'name': 'myappcred',
'description': 'My App Cred',
'roles': [{'name': 'member'}],
'application_credential': {
'name': 'myappcred',
'description': 'My App Cred',
'roles': [{'name': 'member'}],
}
}
self.create_app_cred_validator.validate(request_to_validate)
def test_validate_app_cred_with_unrestricted_flag_succeeds(self):
request_to_validate = {
'name': 'myappcred',
'description': 'My App Cred',
'roles': [{'name': 'member'}],
'unrestricted': True,
'application_credential': {
'name': 'myappcred',
'description': 'My App Cred',
'roles': [{'name': 'member'}],
'unrestricted': True,
}
}
self.create_app_cred_validator.validate(request_to_validate)
def test_validate_app_cred_with_secret_succeeds(self):
request_to_validate = {
'name': 'myappcred',
'description': 'My App Cred',
'roles': [{'name': 'member'}],
'secret': 'secretsecretsecretsecret',
'application_credential': {
'name': 'myappcred',
'description': 'My App Cred',
'roles': [{'name': 'member'}],
'secret': 'secretsecretsecretsecret',
}
}
self.create_app_cred_validator.validate(request_to_validate)
def test_validate_app_cred_invalid_roles_fails(self):
for role in self._invalid_roles:
request_to_validate = {
'name': 'myappcred',
'description': 'My App Cred',
'roles': [role],
'application_credential': {
'name': 'myappcred',
'description': 'My App Cred',
'roles': [role],
}
}
self.assertRaises(
exception.SchemaValidationError,