Add support for application credentials

Add new auth classes and loading options for application credentials.

Change-Id: If267c17eecc2c4acaf62e27276afc185c1ae3616
This commit is contained in:
Colleen Murphy 2018-01-16 23:30:06 +01:00 committed by Colleen Murphy
parent b7496d8b23
commit c2ae9e298e
7 changed files with 252 additions and 2 deletions

View File

@ -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')

View File

@ -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',

View File

@ -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

View File

@ -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)

View File

@ -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'],

View File

@ -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)

View File

@ -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