From 0e52d4e706e43099efc2fb5df16f5bd9466d9b30 Mon Sep 17 00:00:00 2001 From: Colleen Murphy Date: Sat, 17 Feb 2018 21:29:40 +0100 Subject: [PATCH] Add tests for application credentials Application credentials were implemented in keystone in Queens. This patch adds test for create, retrieval, and deleting application credentials and ensures that application credentials that are created can be used for authentication. Updating application credentials is not supported. bp application-credentials Change-Id: I3272fee2881fb918fe83961774f4bd27e30cee02 --- ...lication-credentials-df69b1f617db1bb9.yaml | 9 + .../admin/v3/test_application_credentials.py | 48 ++++++ tempest/api/identity/base.py | 29 ++++ .../v3/test_application_credentials.py | 85 ++++++++++ tempest/clients.py | 2 + tempest/config.py | 8 +- tempest/lib/services/identity/v3/__init__.py | 18 +- .../v3/application_credentials_client.py | 83 ++++++++++ .../lib/services/identity/v3/token_client.py | 10 +- .../v3/test_application_credentials_client.py | 156 ++++++++++++++++++ 10 files changed, 438 insertions(+), 10 deletions(-) create mode 100644 releasenotes/notes/bp-application-credentials-df69b1f617db1bb9.yaml create mode 100644 tempest/api/identity/admin/v3/test_application_credentials.py create mode 100644 tempest/api/identity/v3/test_application_credentials.py create mode 100644 tempest/lib/services/identity/v3/application_credentials_client.py create mode 100644 tempest/tests/lib/services/identity/v3/test_application_credentials_client.py diff --git a/releasenotes/notes/bp-application-credentials-df69b1f617db1bb9.yaml b/releasenotes/notes/bp-application-credentials-df69b1f617db1bb9.yaml new file mode 100644 index 0000000000..53125efd30 --- /dev/null +++ b/releasenotes/notes/bp-application-credentials-df69b1f617db1bb9.yaml @@ -0,0 +1,9 @@ +--- +features: + - | + [`blueprint application-credentials `_] + Tempest can test keystone's application credentials interface. A new client + library is added for application credentials, and a new config option, + ``[identity-feature-enabled]/application_credentials``, can control whether + the application credentials feature is tested (defaults to False, + indicating the feature is not enabled in the cloud under test). diff --git a/tempest/api/identity/admin/v3/test_application_credentials.py b/tempest/api/identity/admin/v3/test_application_credentials.py new file mode 100644 index 0000000000..4a74ef813d --- /dev/null +++ b/tempest/api/identity/admin/v3/test_application_credentials.py @@ -0,0 +1,48 @@ +# Copyright 2018 SUSE Linux GmbH +# +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# 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 tempest.api.identity import base +from tempest import config +from tempest.lib import decorators + + +CONF = config.CONF + + +class ApplicationCredentialsV3AdminTest(base.BaseApplicationCredentialsV3Test, + base.BaseIdentityV3AdminTest): + + @decorators.idempotent_id('3b3dd48f-3388-406a-a9e6-4d078a552d0e') + def test_create_application_credential_with_roles(self): + role = self.setup_test_role() + self.os_admin.roles_v3_client.create_user_role_on_project( + self.project_id, + self.user_id, + role['id'] + ) + + app_cred = self.create_application_credential( + roles=[{'id': role['id']}]) + secret = app_cred['secret'] + + # Check that the application credential is functional + token_id, resp = self.non_admin_token.get_token( + app_cred_id=app_cred['id'], + app_cred_secret=secret, + auth_data=True + ) + self.assertEqual(resp['project']['id'], self.project_id) + self.assertEqual(resp['roles'][0]['id'], role['id']) diff --git a/tempest/api/identity/base.py b/tempest/api/identity/base.py index 6edb8f3b18..68f2c07746 100644 --- a/tempest/api/identity/base.py +++ b/tempest/api/identity/base.py @@ -190,6 +190,8 @@ class BaseIdentityV3Test(BaseIdentityTest): cls.non_admin_catalog_client = cls.os_primary.catalog_client cls.non_admin_versions_client =\ cls.os_primary.identity_versions_v3_client + cls.non_admin_app_creds_client = \ + cls.os_primary.application_credentials_client class BaseIdentityV3AdminTest(BaseIdentityV3Test): @@ -289,3 +291,30 @@ class BaseIdentityV3AdminTest(BaseIdentityV3Test): test_utils.call_and_ignore_notfound_exc, self.delete_domain, domain['id']) return domain + + +class BaseApplicationCredentialsV3Test(BaseIdentityV3Test): + + @classmethod + def skip_checks(cls): + super(BaseApplicationCredentialsV3Test, cls).skip_checks() + if not CONF.identity_feature_enabled.application_credentials: + raise cls.skipException("Application credentials are not available" + " in this environment") + + @classmethod + def resource_setup(cls): + super(BaseApplicationCredentialsV3Test, cls).resource_setup() + cls.user_id = cls.os_primary.credentials.user_id + cls.project_id = cls.os_primary.credentials.project_id + + def create_application_credential(self, name=None, **kwargs): + name = name or data_utils.rand_name('application_credential') + application_credential = ( + self.non_admin_app_creds_client.create_application_credential( + self.user_id, name=name, **kwargs))['application_credential'] + self.addCleanup( + self.non_admin_app_creds_client.delete_application_credential, + self.user_id, + application_credential['id']) + return application_credential diff --git a/tempest/api/identity/v3/test_application_credentials.py b/tempest/api/identity/v3/test_application_credentials.py new file mode 100644 index 0000000000..caf0b1e8ae --- /dev/null +++ b/tempest/api/identity/v3/test_application_credentials.py @@ -0,0 +1,85 @@ +# Copyright 2018 SUSE Linux GmbH +# +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import datetime + +from oslo_utils import timeutils + +from tempest.api.identity import base +from tempest import config +from tempest.lib import decorators + + +CONF = config.CONF + + +class ApplicationCredentialsV3Test(base.BaseApplicationCredentialsV3Test): + + def _list_app_creds(self, name=None): + kwargs = dict(user_id=self.user_id) + if name: + kwargs.update(name=name) + return self.non_admin_app_creds_client.list_application_credentials( + **kwargs)['application_credentials'] + + @decorators.idempotent_id('8080c75c-eddc-4786-941a-c2da7039ae61') + def test_create_application_credential(self): + app_cred = self.create_application_credential() + + # Check that the secret appears in the create response + secret = app_cred['secret'] + + # Check that the secret is not retrievable after initial create + app_cred = self.non_admin_app_creds_client.show_application_credential( + user_id=self.user_id, + application_credential_id=app_cred['id'] + )['application_credential'] + self.assertNotIn('secret', app_cred) + + # Check that the application credential is functional + token_id, resp = self.non_admin_token.get_token( + app_cred_id=app_cred['id'], + app_cred_secret=secret, + auth_data=True + ) + self.assertEqual(resp['project']['id'], self.project_id) + + @decorators.idempotent_id('852daf0c-42b5-4239-8466-d193d0543ed3') + def test_create_application_credential_expires(self): + expires_at = timeutils.utcnow() + datetime.timedelta(hours=1) + + app_cred = self.create_application_credential(expires_at=expires_at) + + expires_str = expires_at.isoformat() + self.assertEqual(expires_str, app_cred['expires_at']) + + @decorators.idempotent_id('ff0cd457-6224-46e7-b79e-0ada4964a8a6') + def test_list_application_credentials(self): + self.create_application_credential() + self.create_application_credential() + + app_creds = self._list_app_creds() + self.assertEqual(2, len(app_creds)) + + @decorators.idempotent_id('9bb5e5cc-5250-493a-8869-8b665f6aa5f6') + def test_query_application_credentials(self): + self.create_application_credential() + app_cred_two = self.create_application_credential() + app_cred_two_name = app_cred_two['name'] + + app_creds = self._list_app_creds(name=app_cred_two_name) + self.assertEqual(1, len(app_creds)) + self.assertEqual(app_cred_two_name, app_creds[0]['name']) diff --git a/tempest/clients.py b/tempest/clients.py index d75a7122fa..0d16748b44 100644 --- a/tempest/clients.py +++ b/tempest/clients.py @@ -199,6 +199,8 @@ class Manager(clients.ServiceClients): self.catalog_client = self.identity_v3.CatalogClient(**params_v3) self.project_tags_client = self.identity_v3.ProjectTagsClient( **params_v3) + self.application_credentials_client = \ + self.identity_v3.ApplicationCredentialsClient(**params_v3) # Token clients do not use the catalog. They only need default_params. # They read auth_url, so they should only be set if the corresponding diff --git a/tempest/config.py b/tempest/config.py index a2ccb84b58..7133b3d876 100644 --- a/tempest/config.py +++ b/tempest/config.py @@ -240,7 +240,13 @@ IdentityFeatureGroup = [ 'settings enabled?'), cfg.BoolOpt('project_tags', default=False, - help='Is the project tags identity v3 API available?') + help='Is the project tags identity v3 API available?'), + # Application credentials is a default feature in Queens. This config + # option can removed once Pike is EOL. + cfg.BoolOpt('application_credentials', + default=False, + help='Does the environment have application credentials ' + 'enabled?') ] compute_group = cfg.OptGroup(name='compute', diff --git a/tempest/lib/services/identity/v3/__init__.py b/tempest/lib/services/identity/v3/__init__.py index f302455c83..da1c51c4ee 100644 --- a/tempest/lib/services/identity/v3/__init__.py +++ b/tempest/lib/services/identity/v3/__init__.py @@ -12,6 +12,8 @@ # License for the specific language governing permissions and limitations under # the License. +from tempest.lib.services.identity.v3.application_credentials_client import \ + ApplicationCredentialsClient from tempest.lib.services.identity.v3.catalog_client import \ CatalogClient from tempest.lib.services.identity.v3.credentials_client import \ @@ -46,11 +48,11 @@ from tempest.lib.services.identity.v3.trusts_client import TrustsClient from tempest.lib.services.identity.v3.users_client import UsersClient from tempest.lib.services.identity.v3.versions_client import VersionsClient -__all__ = ['CatalogClient', 'CredentialsClient', 'DomainsClient', - 'DomainConfigurationClient', 'EndPointGroupsClient', - 'EndPointsClient', 'EndPointsFilterClient', 'GroupsClient', - 'IdentityClient', 'InheritedRolesClient', 'OAUTHConsumerClient', - 'OAUTHTokenClient', 'PoliciesClient', 'ProjectsClient', - 'ProjectTagsClient', 'RegionsClient', 'RoleAssignmentsClient', - 'RolesClient', 'ServicesClient', 'V3TokenClient', 'TrustsClient', - 'UsersClient', 'VersionsClient'] +__all__ = ['ApplicationCredentialsClient', 'CatalogClient', + 'CredentialsClient', 'DomainsClient', 'DomainConfigurationClient', + 'EndPointGroupsClient', 'EndPointsClient', 'EndPointsFilterClient', + 'GroupsClient', 'IdentityClient', 'InheritedRolesClient', + 'OAUTHConsumerClient', 'OAUTHTokenClient', 'PoliciesClient', + 'ProjectsClient', 'ProjectTagsClient', 'RegionsClient', + 'RoleAssignmentsClient', 'RolesClient', 'ServicesClient', + 'V3TokenClient', 'TrustsClient', 'UsersClient', 'VersionsClient'] diff --git a/tempest/lib/services/identity/v3/application_credentials_client.py b/tempest/lib/services/identity/v3/application_credentials_client.py new file mode 100644 index 0000000000..557aa9efa1 --- /dev/null +++ b/tempest/lib/services/identity/v3/application_credentials_client.py @@ -0,0 +1,83 @@ +# Copyright 2018 SUSE Linux GmbH +# +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +""" +https://developer.openstack.org/api-ref/identity/v3/index.html#application-credentials +""" + +from oslo_serialization import jsonutils as json +from six.moves.urllib import parse as urllib + +from tempest.lib.common import rest_client + + +class ApplicationCredentialsClient(rest_client.RestClient): + api_version = "v3" + + def create_application_credential(self, user_id, **kwargs): + """Creates an application credential. + + For a full list of available parameters, please refer to the official + API reference: + https://developer.openstack.org/api-ref/identity/v3/index.html#create-application-credential + """ + post_body = json.dumps({'application_credential': kwargs}) + resp, body = self.post('users/%s/application_credentials' % user_id, + post_body) + self.expected_success(201, resp.status) + body = json.loads(body) + return rest_client.ResponseBody(resp, body) + + def show_application_credential(self, user_id, application_credential_id): + """Gets details of an application credential. + + For a full list of available parameters, please refer to the official + API reference: + https://developer.openstack.org/api-ref/identity/v3/index.html#show-application-credential-details + """ + resp, body = self.get('users/%s/application_credentials/%s' % + (user_id, application_credential_id)) + self.expected_success(200, resp.status) + body = json.loads(body) + return rest_client.ResponseBody(resp, body) + + def list_application_credentials(self, user_id, **params): + """Lists out all of a user's application credentials. + + For a full list of available parameters, please refer to the official + API reference: + https://developer.openstack.org/api-ref/identity/v3/index.html#list-application-credentials + """ + url = 'users/%s/application_credentials' % user_id + if params: + url += '?%s' % urllib.urlencode(params) + resp, body = self.get(url) + self.expected_success(200, resp.status) + body = json.loads(body) + return rest_client.ResponseBody(resp, body) + + def delete_application_credential(self, user_id, + application_credential_id): + """Deletes an application credential. + + For a full list of available parameters, please refer to the official + API reference: + https://developer.openstack.org/api-ref/identity/v3/index.html#delete-application-credential + """ + resp, body = self.delete('users/%s/application_credentials/%s' % + (user_id, application_credential_id)) + self.expected_success(204, resp.status) + return rest_client.ResponseBody(resp, body) diff --git a/tempest/lib/services/identity/v3/token_client.py b/tempest/lib/services/identity/v3/token_client.py index 33f6f16fb7..d591f03b42 100644 --- a/tempest/lib/services/identity/v3/token_client.py +++ b/tempest/lib/services/identity/v3/token_client.py @@ -51,7 +51,8 @@ class V3TokenClient(rest_client.RestClient): def auth(self, user_id=None, username=None, password=None, project_id=None, project_name=None, user_domain_id=None, user_domain_name=None, project_domain_id=None, project_domain_name=None, domain_id=None, - domain_name=None, token=None): + domain_name=None, token=None, app_cred_id=None, + app_cred_secret=None): """Obtains a token from the authentication service :param user_id: user id @@ -109,6 +110,13 @@ class V3TokenClient(rest_client.RestClient): if _domain: id_obj['password']['user']['domain'] = _domain + if app_cred_id and app_cred_secret: + id_obj['methods'].append('application_credential') + id_obj['application_credential'] = { + 'id': app_cred_id, + 'secret': app_cred_secret, + } + if (project_id or project_name): _project = dict() diff --git a/tempest/tests/lib/services/identity/v3/test_application_credentials_client.py b/tempest/tests/lib/services/identity/v3/test_application_credentials_client.py new file mode 100644 index 0000000000..9bf9b680fa --- /dev/null +++ b/tempest/tests/lib/services/identity/v3/test_application_credentials_client.py @@ -0,0 +1,156 @@ +# Copyright 2018 SUSE Linux GmbH +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT 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 tempest.lib.services.identity.v3 import application_credentials_client +from tempest.tests.lib import fake_auth_provider +from tempest.tests.lib.services import base + + +class TestApplicationCredentialsClient(base.BaseServiceTest): + FAKE_CREATE_APP_CRED = { + "application_credential": { + "description": "fake application credential", + "roles": [ + { + "id": "c60fdd45", + "domain_id": None, + "name": "Member" + } + ], + "expires_at": "2019-02-27T18:30:59.999999Z", + "secret": "_BVq0xU5L", + "unrestricted": None, + "project_id": "ddef321", + "id": "5499a186", + "name": "one" + } + } + + FAKE_LIST_APP_CREDS = { + "application_credentials": [ + { + "description": "fake application credential", + "roles": [ + { + "domain_id": None, + "name": "Member", + "id": "c60fdd45", + } + ], + "expires_at": "2018-02-27T18:30:59.999999Z", + "unrestricted": None, + "project_id": "ddef321", + "id": "5499a186", + "name": "one" + }, + { + "description": None, + "roles": [ + { + "id": "0f1837c8", + "domain_id": None, + "name": "anotherrole" + }, + { + "id": "c60fdd45", + "domain_id": None, + "name": "Member" + } + ], + "expires_at": None, + "unrestricted": None, + "project_id": "c5403d938", + "id": "d441c904f", + "name": "two" + } + ] + } + + FAKE_APP_CRED_INFO = { + "application_credential": { + "description": None, + "roles": [ + { + "domain_id": None, + "name": "Member", + "id": "c60fdd45", + } + ], + "expires_at": None, + "unrestricted": None, + "project_id": "ddef321", + "id": "5499a186", + "name": "one" + } + } + + def setUp(self): + super(TestApplicationCredentialsClient, self).setUp() + fake_auth = fake_auth_provider.FakeAuthProvider() + self.client = \ + application_credentials_client.ApplicationCredentialsClient( + fake_auth, 'identity', 'regionOne') + + def _test_create_app_cred(self, bytes_body=False): + self.check_service_client_function( + self.client.create_application_credential, + 'tempest.lib.common.rest_client.RestClient.post', + self.FAKE_CREATE_APP_CRED, + bytes_body, + status=201, + user_id="123456") + + def _test_show_app_cred(self, bytes_body=False): + self.check_service_client_function( + self.client.show_application_credential, + 'tempest.lib.common.rest_client.RestClient.get', + self.FAKE_APP_CRED_INFO, + bytes_body, + user_id="123456", + application_credential_id="5499a186") + + def _test_list_app_creds(self, bytes_body=False): + self.check_service_client_function( + self.client.list_application_credentials, + 'tempest.lib.common.rest_client.RestClient.get', + self.FAKE_LIST_APP_CREDS, + bytes_body, + user_id="123456") + + def test_create_application_credential_with_str_body(self): + self._test_create_app_cred() + + def test_create_application_credential_with_bytes_body(self): + self._test_create_app_cred(bytes_body=True) + + def test_show_application_credential_with_str_body(self): + self._test_show_app_cred() + + def test_show_application_credential_with_bytes_body(self): + self._test_show_app_cred(bytes_body=True) + + def test_list_application_credential_with_str_body(self): + self._test_list_app_creds() + + def test_list_application_credential_with_bytes_body(self): + self._test_list_app_creds(bytes_body=True) + + def test_delete_trust(self): + self.check_service_client_function( + self.client.delete_application_credential, + 'tempest.lib.common.rest_client.RestClient.delete', + {}, + user_id="123456", + application_credential_id="5499a186", + status=204)