From 14c4b177efc4b53f9ab97fa3f7560ba255bbdb93 Mon Sep 17 00:00:00 2001 From: Colleen Murphy Date: Thu, 3 Jan 2019 13:37:52 +0100 Subject: [PATCH] Add API changes for app cred access rules bp whitelist-extension-for-app-creds Change-Id: Ie022e379d03d0309ec320b6947b987758d87fe5d --- keystone/api/users.py | 13 ++- keystone/application_credential/schema.py | 25 ++++- .../unit/test_v3_application_credential.py | 95 ++++++++++++++++++- 3 files changed, 130 insertions(+), 3 deletions(-) diff --git a/keystone/api/users.py b/keystone/api/users.py index 97626da9b5..3ddcc3291b 100644 --- a/keystone/api/users.py +++ b/keystone/api/users.py @@ -545,7 +545,8 @@ class UserAppCredListCreateResource(ks_flask.ResourceBase): # secret is only exposed after create, it is not stored 'secret', 'links', - 'unrestricted' + 'unrestricted', + 'access_rules' ]) @staticmethod @@ -608,6 +609,16 @@ class UserAppCredListCreateResource(ks_flask.ResourceBase): if app_cred_data.get('expires_at'): app_cred_data['expires_at'] = utils.parse_expiration_date( app_cred_data['expires_at']) + if app_cred_data.get('access_rules'): + for access_rule in app_cred_data['access_rules']: + # If user provides an access rule by ID, it will be looked up + # by ID. If user provides an access rule that is identical to + # an existing one, the ID generated here will be ignored and + # the pre-existing access rule will be used. + if 'id' not in access_rule: + # Generate directly, rather than using _assign_unique_id, + # so that there is no deep copy made + access_rule['id'] = uuid.uuid4().hex app_cred_data = self._normalize_dict(app_cred_data) app_cred_api = PROVIDERS.application_credential_api diff --git a/keystone/application_credential/schema.py b/keystone/application_credential/schema.py index edf6462bde..9f2b50c20b 100644 --- a/keystone/application_credential/schema.py +++ b/keystone/application_credential/schema.py @@ -29,6 +29,28 @@ _role_properties = { } } +_access_rules_properties = { + 'type': 'array', + 'items': { + 'type': 'object', + 'properties': { + 'path': { + 'type': 'string', + 'minLength': 0, + 'maxLength': 225, + 'pattern': '^\/.*' + }, + 'method': { + 'type': 'string', + 'pattern': '^(POST|GET|HEAD|PATCH|PUT|DELETE)$' + }, + 'service': parameter_types.id_string, + 'id': parameter_types.id_string, + }, + 'additionalProperties': False + } +} + _application_credential_properties = { 'name': parameter_types.name, 'description': validation.nullable(parameter_types.description), @@ -39,7 +61,8 @@ _application_credential_properties = { 'type': ['null', 'string'] }, 'roles': _role_properties, - 'unrestricted': parameter_types.boolean + 'unrestricted': parameter_types.boolean, + 'access_rules': _access_rules_properties } application_credential_create = { diff --git a/keystone/tests/unit/test_v3_application_credential.py b/keystone/tests/unit/test_v3_application_credential.py index 4e8899e562..cd87587850 100644 --- a/keystone/tests/unit/test_v3_application_credential.py +++ b/keystone/tests/unit/test_v3_application_credential.py @@ -35,7 +35,8 @@ class ApplicationCredentialTestCase(test_v3.RestfulTestCase): self.config_fixture.config(group='auth', methods='password,application_credential') - def _app_cred_body(self, roles=None, name=None, expires=None, secret=None): + def _app_cred_body(self, roles=None, name=None, expires=None, secret=None, + access_rules=None): name = name or uuid.uuid4().hex description = 'Credential for backups' app_cred_data = { @@ -48,6 +49,8 @@ class ApplicationCredentialTestCase(test_v3.RestfulTestCase): app_cred_data['expires_at'] = expires if secret: app_cred_data['secret'] = secret + if access_rules is not None: + app_cred_data['access_rules'] = access_rules return {'application_credential': app_cred_data} def test_create_application_credential(self): @@ -187,6 +190,96 @@ class ApplicationCredentialTestCase(test_v3.RestfulTestCase): expected_status_code=http_client.CREATED, headers={'x-Auth-Token': token_data.headers['x-subject-token']}) + def test_create_application_credential_with_access_rules(self): + roles = [{'id': self.role_id}] + access_rules = [ + { + 'path': '/v3/projects', + 'method': 'POST', + 'service': 'identity', + } + ] + app_cred_body = self._app_cred_body(roles=roles, + access_rules=access_rules) + with self.test_client() as c: + token = self.get_scoped_token() + resp = c.post('/v3/users/%s/application_credentials' % self.user_id, + headers={'X-Auth-Token': token}, + json=app_cred_body, + expected_status_code=http_client.CREATED) + resp_access_rules = resp.json['application_credential']['access_rules'] + self.assertIn('id', resp_access_rules[0]) + resp_access_rules[0].pop('id') + self.assertEqual(access_rules[0], resp_access_rules[0]) + + def test_create_application_credential_with_duplicate_access_rule(self): + roles = [{'id': self.role_id}] + access_rules = [ + { + 'path': '/v3/projects', + 'method': 'POST', + 'service': 'identity', + } + ] + app_cred_body_1 = self._app_cred_body(roles=roles, + access_rules=access_rules) + with self.test_client() as c: + token = self.get_scoped_token() + resp = c.post('/v3/users/%s/application_credentials' % self.user_id, + headers={'X-Auth-Token': token}, + json=app_cred_body_1, + expected_status_code=http_client.CREATED) + resp_access_rules = resp.json['application_credential']['access_rules'] + self.assertIn('id', resp_access_rules[0]) + access_rule_id = resp_access_rules[0].pop('id') + self.assertEqual(access_rules[0], resp_access_rules[0]) + + app_cred_body_2 = self._app_cred_body(roles=roles, + access_rules=access_rules) + with self.test_client() as c: + token = self.get_scoped_token() + resp = c.post('/v3/users/%s/application_credentials' % self.user_id, + headers={'X-Auth-Token': token}, + json=app_cred_body_2, + expected_status_code=http_client.CREATED) + resp_access_rules = resp.json['application_credential']['access_rules'] + self.assertEqual(access_rule_id, resp_access_rules[0]['id']) + + def test_create_application_credential_with_access_rule_by_id(self): + roles = [{'id': self.role_id}] + access_rules = [ + { + 'path': '/v3/projects', + 'method': 'POST', + 'service': 'identity', + } + ] + app_cred_body_1 = self._app_cred_body(roles=roles, + access_rules=access_rules) + with self.test_client() as c: + token = self.get_scoped_token() + resp = c.post('/v3/users/%s/application_credentials' % self.user_id, + headers={'X-Auth-Token': token}, + json=app_cred_body_1, + expected_status_code=http_client.CREATED) + resp_access_rules = resp.json['application_credential']['access_rules'] + access_rule_id = resp_access_rules + self.assertIn('id', resp_access_rules[0]) + access_rule_id = resp_access_rules[0].pop('id') + self.assertEqual(access_rules[0], resp_access_rules[0]) + + access_rules = [{'id': access_rule_id}] + app_cred_body_2 = self._app_cred_body(roles=roles, + access_rules=access_rules) + with self.test_client() as c: + token = self.get_scoped_token() + resp = c.post('/v3/users/%s/application_credentials' % self.user_id, + headers={'X-Auth-Token': token}, + json=app_cred_body_2, + expected_status_code=http_client.CREATED) + resp_access_rules = resp.json['application_credential']['access_rules'] + self.assertEqual(access_rule_id, resp_access_rules[0]['id']) + def test_list_application_credentials(self): with self.test_client() as c: token = self.get_scoped_token()