diff --git a/keystone/api/users.py b/keystone/api/users.py index 15b864a390..e428e82a06 100644 --- a/keystone/api/users.py +++ b/keystone/api/users.py @@ -679,6 +679,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. @@ -693,6 +699,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. @@ -702,9 +714,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: @@ -754,6 +763,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. diff --git a/keystone/application_credential/schema.py b/keystone/application_credential/schema.py index 7bb00c908e..886d567b52 100644 --- a/keystone/application_credential/schema.py +++ b/keystone/application_credential/schema.py @@ -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." + ), } diff --git a/keystone/tests/unit/test_validation.py b/keystone/tests/unit/test_validation.py index b9ae2971ca..e501c20ce1 100644 --- a/keystone/tests/unit/test_validation.py +++ b/keystone/tests/unit/test_validation.py @@ -3623,23 +3623,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, @@ -3649,10 +3653,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, @@ -3662,36 +3668,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,