From c2ae9e298e2acdc4e653bce18735294cf4df2454 Mon Sep 17 00:00:00 2001 From: Colleen Murphy Date: Tue, 16 Jan 2018 23:30:06 +0100 Subject: [PATCH] Add support for application credentials Add new auth classes and loading options for application credentials. Change-Id: If267c17eecc2c4acaf62e27276afc185c1ae3616 --- keystoneauth1/identity/__init__.py | 6 +- keystoneauth1/identity/v3/__init__.py | 6 +- .../identity/v3/application_credential.py | 89 +++++++++++++++++++ keystoneauth1/loading/_plugins/identity/v3.py | 40 +++++++++ .../tests/unit/identity/test_identity_v3.py | 48 ++++++++++ keystoneauth1/tests/unit/loading/test_v3.py | 64 +++++++++++++ setup.cfg | 1 + 7 files changed, 252 insertions(+), 2 deletions(-) create mode 100644 keystoneauth1/identity/v3/application_credential.py diff --git a/keystoneauth1/identity/__init__.py b/keystoneauth1/identity/__init__.py index 18207ce3..af917b6b 100644 --- a/keystoneauth1/identity/__init__.py +++ b/keystoneauth1/identity/__init__.py @@ -55,6 +55,9 @@ V3TOTP = v3.TOTP V3TokenlessAuth = v3.TokenlessAuth """See :class:`keystoneauth1.identity.v3.TokenlessAuth`""" +V3ApplicationCredential = v3.ApplicationCredential +"""See :class:`keystoneauth1.identity.v3.ApplicationCredential`""" + __all__ = ('BaseIdentityPlugin', 'Password', 'Token', @@ -66,4 +69,5 @@ __all__ = ('BaseIdentityPlugin', 'V3OidcAuthorizationCode', 'V3OidcAccessToken', 'V3TOTP', - 'V3TokenlessAuth') + 'V3TokenlessAuth', + 'V3ApplicationCredential') diff --git a/keystoneauth1/identity/v3/__init__.py b/keystoneauth1/identity/v3/__init__.py index 38e78db8..49a69742 100644 --- a/keystoneauth1/identity/v3/__init__.py +++ b/keystoneauth1/identity/v3/__init__.py @@ -10,6 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. +from keystoneauth1.identity.v3.application_credential import * # noqa from keystoneauth1.identity.v3.base import * # noqa from keystoneauth1.identity.v3.federation import * # noqa from keystoneauth1.identity.v3.k2k import * # noqa @@ -20,7 +21,10 @@ from keystoneauth1.identity.v3.totp import * # noqa from keystoneauth1.identity.v3.tokenless_auth import * # noqa -__all__ = ('Auth', +__all__ = ('ApplicationCredential', + 'ApplicationCredentialMethod', + + 'Auth', 'AuthConstructor', 'AuthMethod', 'BaseAuth', diff --git a/keystoneauth1/identity/v3/application_credential.py b/keystoneauth1/identity/v3/application_credential.py new file mode 100644 index 00000000..8dc2e570 --- /dev/null +++ b/keystoneauth1/identity/v3/application_credential.py @@ -0,0 +1,89 @@ +# 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 keystoneauth1.identity.v3 import base + + +__all__ = ('ApplicationCredentialMethod', 'ApplicationCredential') + + +class ApplicationCredentialMethod(base.AuthMethod): + """Construct a User/Passcode based authentication method. + + :param string application_credential_secret: Application credential secret. + :param string application_credential_id: Application credential id. + :param string application_credential_name: The name of the application + credential, if an ID is not + provided. + :param string username: Username for authentication, if an application + credential ID is not provided. + :param string user_id: User ID for authentication, if an application + credential ID is not provided. + :param string user_domain_id: User's domain ID for authentication, if an + application credential ID is not provided. + :param string user_domain_name: User's domain name for authentication, if + an application credential ID is not + provided. + """ + + _method_parameters = ['application_credential_secret', + 'application_credential_id', + 'application_credential_name', + 'user_id', + 'username', + 'user_domain_id', + 'user_domain_name'] + + def get_auth_data(self, session, auth, headers, **kwargs): + auth_data = {'secret': self.application_credential_secret} + + if self.application_credential_id: + auth_data['id'] = self.application_credential_id + else: + auth_data['name'] = self.application_credential_name + auth_data['user'] = {} + if self.user_id: + auth_data['user']['id'] = self.user_id + elif self.username: + auth_data['user']['name'] = self.username + + if self.user_domain_id: + auth_data['user']['domain'] = {'id': self.user_domain_id} + elif self.user_domain_name: + auth_data['user']['domain'] = { + 'name': self.user_domain_name} + + return 'application_credential', auth_data + + def get_cache_id_elements(self): + return dict(('application_credential_%s' % p, getattr(self, p)) + for p in self._method_parameters) + + +class ApplicationCredential(base.AuthConstructor): + """A plugin for authenticating with an application credential. + + :param string auth_url: Identity service endpoint for authentication. + :param string application_credential_secret: Application credential secret. + :param string application_credential_id: Application credential ID. + :param string application_credential_name: Application credential name. + :param string username: Username for authentication. + :param string user_id: User ID for authentication. + :param string user_domain_id: User's domain ID for authentication. + :param string user_domain_name: User's domain name for authentication. + :param bool reauthenticate: Allow fetching a new token if the current one + is going to expire. (optional) default True + """ + + _auth_method_class = ApplicationCredentialMethod diff --git a/keystoneauth1/loading/_plugins/identity/v3.py b/keystoneauth1/loading/_plugins/identity/v3.py index cc0ae1fb..3131b9a9 100644 --- a/keystoneauth1/loading/_plugins/identity/v3.py +++ b/keystoneauth1/loading/_plugins/identity/v3.py @@ -254,3 +254,43 @@ class TokenlessAuth(loading.BaseLoader): raise exceptions.OptionError(m) return super(TokenlessAuth, self).load_from_options(**kwargs) + + +class ApplicationCredential(loading.BaseV3Loader): + + @property + def plugin_class(self): + return identity.V3ApplicationCredential + + def get_options(self): + options = super(ApplicationCredential, self).get_options() + _add_common_identity_options(options) + + options.extend([ + loading.Opt('application_credential_secret', secret=True, + required=True, + help="Application credential auth secret"), + ]), + options.extend([ + loading.Opt('application_credential_id', + help='Application credential ID'), + ]), + options.extend([ + loading.Opt('application_credential_name', + help='Application credential name'), + ]) + + return options + + def load_from_options(self, **kwargs): + _assert_identity_options(kwargs) + if (not kwargs.get('application_credential_id') and + not kwargs.get('application_credential_name')): + m = ('You must provide either an application credential ID or an ' + 'application credential name and user.') + raise exceptions.OptionError(m) + if not kwargs.get('application_credential_secret'): + m = ('You must provide an auth secret.') + raise exceptions.OptionError(m) + + return super(ApplicationCredential, self).load_from_options(**kwargs) diff --git a/keystoneauth1/tests/unit/identity/test_identity_v3.py b/keystoneauth1/tests/unit/identity/test_identity_v3.py index 5c88105b..1147feef 100644 --- a/keystoneauth1/tests/unit/identity/test_identity_v3.py +++ b/keystoneauth1/tests/unit/identity/test_identity_v3.py @@ -33,6 +33,9 @@ class V3IdentityPlugin(utils.TestCase): TEST_PASS = 'password' + TEST_APP_CRED_ID = 'appcredid' + TEST_APP_CRED_SECRET = 'secret' + TEST_SERVICE_CATALOG = [{ "endpoints": [{ "url": "http://cdn.admin-nets.local:8774/v1.0/", @@ -186,6 +189,35 @@ class V3IdentityPlugin(utils.TestCase): "self": "https://identity:5000/v3/projects", } } + self.TEST_APP_CRED_TOKEN_RESPONSE = { + "token": { + "methods": [ + "application_credential" + ], + + "expires_at": "2020-01-01T00:00:10.000123Z", + "project": { + "domain": { + "id": self.TEST_DOMAIN_ID, + "name": self.TEST_DOMAIN_NAME + }, + "id": self.TEST_TENANT_ID, + "name": self.TEST_TENANT_NAME + }, + "user": { + "domain": { + "id": self.TEST_DOMAIN_ID, + "name": self.TEST_DOMAIN_NAME + }, + "id": self.TEST_USER, + "name": self.TEST_USER + }, + "issued_at": "2013-05-29T16:55:21.468960Z", + "catalog": self.TEST_SERVICE_CATALOG, + "service_providers": self.TEST_SERVICE_PROVIDERS, + "application_credential_restricted": True + }, + } def stub_auth(self, subject_token=None, **kwargs): if not subject_token: @@ -370,6 +402,22 @@ class V3IdentityPlugin(utils.TestCase): domain_id='x', trust_id='x') self.assertRaises(exceptions.AuthorizationFailure, a.get_auth_ref, s) + def test_application_credential_method(self): + self.stub_auth(json=self.TEST_APP_CRED_TOKEN_RESPONSE) + ac = v3.ApplicationCredential( + self.TEST_URL, application_credential_id=self.TEST_APP_CRED_ID, + application_credential_secret=self.TEST_APP_CRED_SECRET) + req = {'auth': {'identity': + {'methods': ['application_credential'], + 'application_credential': { + 'id': self.TEST_APP_CRED_ID, + 'secret': self.TEST_APP_CRED_SECRET}}}} + s = session.Session(auth=ac) + self.assertEqual({'X-Auth-Token': self.TEST_TOKEN}, + s.get_auth_headers()) + self.assertRequestBodyIs(json=req) + self.assertEqual(s.auth.auth_ref.auth_token, self.TEST_TOKEN) + def _do_service_url_test(self, base_url, endpoint_filter): self.stub_auth(json=self.TEST_RESPONSE_DICT) self.stub_url('GET', ['path'], diff --git a/keystoneauth1/tests/unit/loading/test_v3.py b/keystoneauth1/tests/unit/loading/test_v3.py index 516c0ec9..b44b745e 100644 --- a/keystoneauth1/tests/unit/loading/test_v3.py +++ b/keystoneauth1/tests/unit/loading/test_v3.py @@ -363,3 +363,67 @@ class V3TokenlessAuthTests(utils.TestCase): self.assertRaises(exceptions.OptionError, self.create, project_name=uuid.uuid4().hex) + + +class V3ApplicationCredentialTests(utils.TestCase): + + def setUp(self): + super(V3ApplicationCredentialTests, self).setUp() + + self.auth_url = uuid.uuid4().hex + + def create(self, **kwargs): + kwargs.setdefault('auth_url', self.auth_url) + loader = loading.get_plugin_loader('v3applicationcredential') + return loader.load_from_options(**kwargs) + + def test_basic(self): + id = uuid.uuid4().hex + secret = uuid.uuid4().hex + + app_cred = self.create(application_credential_id=id, + application_credential_secret=secret) + + ac_method = app_cred.auth_methods[0] + + self.assertEqual(id, ac_method.application_credential_id) + self.assertEqual(secret, ac_method.application_credential_secret) + + def test_with_name(self): + name = uuid.uuid4().hex + secret = uuid.uuid4().hex + username = uuid.uuid4().hex + user_domain_id = uuid.uuid4().hex + + app_cred = self.create(application_credential_name=name, + application_credential_secret=secret, + username=username, + user_domain_id=user_domain_id) + + ac_method = app_cred.auth_methods[0] + + self.assertEqual(name, ac_method.application_credential_name) + self.assertEqual(secret, ac_method.application_credential_secret) + self.assertEqual(username, ac_method.username) + self.assertEqual(user_domain_id, ac_method.user_domain_id) + + def test_without_user_domain(self): + self.assertRaises(exceptions.OptionError, + self.create, + application_credential_name=uuid.uuid4().hex, + username=uuid.uuid4().hex, + application_credential_secret=uuid.uuid4().hex) + + def test_without_name_or_id(self): + self.assertRaises(exceptions.OptionError, + self.create, + username=uuid.uuid4().hex, + user_domain_id=uuid.uuid4().hex, + application_credential_secret=uuid.uuid4().hex) + + def test_without_secret(self): + self.assertRaises(exceptions.OptionError, + self.create, + application_credential_id=uuid.uuid4().hex, + username=uuid.uuid4().hex, + user_domain_id=uuid.uuid4().hex) diff --git a/setup.cfg b/setup.cfg index 0b878996..d7921932 100644 --- a/setup.cfg +++ b/setup.cfg @@ -56,6 +56,7 @@ keystoneauth1.plugin = v3tokenlessauth = keystoneauth1.loading._plugins.identity.v3:TokenlessAuth v3adfspassword = keystoneauth1.extras._saml2._loading:ADFSPassword v3samlpassword = keystoneauth1.extras._saml2._loading:Saml2Password + v3applicationcredential = keystoneauth1.loading._plugins.identity.v3:ApplicationCredential [build_sphinx] source-dir = doc/source