Merge "Create V2 Auth Plugins"
This commit is contained in:
@@ -22,6 +22,9 @@ LOG = logging.getLogger(__name__)
|
|||||||
@six.add_metaclass(abc.ABCMeta)
|
@six.add_metaclass(abc.ABCMeta)
|
||||||
class BaseIdentityPlugin(base.BaseAuthPlugin):
|
class BaseIdentityPlugin(base.BaseAuthPlugin):
|
||||||
|
|
||||||
|
# we count a token as valid if it is valid for at least this many seconds
|
||||||
|
MIN_TOKEN_LIFE_SECONDS = 1
|
||||||
|
|
||||||
def __init__(self,
|
def __init__(self,
|
||||||
auth_url=None,
|
auth_url=None,
|
||||||
username=None,
|
username=None,
|
||||||
@@ -32,13 +35,15 @@ class BaseIdentityPlugin(base.BaseAuthPlugin):
|
|||||||
super(BaseIdentityPlugin, self).__init__()
|
super(BaseIdentityPlugin, self).__init__()
|
||||||
|
|
||||||
self.auth_url = auth_url
|
self.auth_url = auth_url
|
||||||
|
self.auth_ref = None
|
||||||
|
|
||||||
|
# NOTE(jamielennox): DEPRECATED. The following should not really be set
|
||||||
|
# here but handled by the individual auth plugin.
|
||||||
self.username = username
|
self.username = username
|
||||||
self.password = password
|
self.password = password
|
||||||
self.token = token
|
self.token = token
|
||||||
self.trust_id = trust_id
|
self.trust_id = trust_id
|
||||||
|
|
||||||
self.auth_ref = None
|
|
||||||
|
|
||||||
@abc.abstractmethod
|
@abc.abstractmethod
|
||||||
def get_auth_ref(self, session, **kwargs):
|
def get_auth_ref(self, session, **kwargs):
|
||||||
"""Obtain a token from an OpenStack Identity Service.
|
"""Obtain a token from an OpenStack Identity Service.
|
||||||
@@ -48,11 +53,40 @@ class BaseIdentityPlugin(base.BaseAuthPlugin):
|
|||||||
This function should not be called independently and is expected to be
|
This function should not be called independently and is expected to be
|
||||||
invoked via the do_authenticate function.
|
invoked via the do_authenticate function.
|
||||||
|
|
||||||
|
This function will be invoked if the AcessInfo object cached by the
|
||||||
|
plugin is not valid. Thus plugins should always fetch a new AccessInfo
|
||||||
|
when invoked. If you are looking to just retrieve the current auth
|
||||||
|
data then you should use get_access.
|
||||||
|
|
||||||
|
:raises HTTPError: An error from an invalid HTTP response.
|
||||||
|
|
||||||
:returns AccessInfo: Token access information.
|
:returns AccessInfo: Token access information.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def get_token(self, session, **kwargs):
|
def get_token(self, session, **kwargs):
|
||||||
if not self.auth_ref or self.auth_ref.will_expire_soon(1):
|
"""Return a valid auth token.
|
||||||
|
|
||||||
|
If a valid token is not present then a new one will be fetched using
|
||||||
|
the session and kwargs.
|
||||||
|
|
||||||
|
:raises HTTPError: An error from an invalid HTTP response.
|
||||||
|
|
||||||
|
:return string: A valid token.
|
||||||
|
"""
|
||||||
|
return self.get_access(session, **kwargs).auth_token
|
||||||
|
|
||||||
|
def get_access(self, session, **kwargs):
|
||||||
|
"""Fetch or return a current AccessInfo object.
|
||||||
|
|
||||||
|
If a valid AccessInfo is present then it is returned otherwise kwargs
|
||||||
|
and session are used to fetch a new one.
|
||||||
|
|
||||||
|
:raises HTTPError: An error from an invalid HTTP response.
|
||||||
|
|
||||||
|
:returns AccessInfo: Valid AccessInfo
|
||||||
|
"""
|
||||||
|
if (not self.auth_ref or
|
||||||
|
self.auth_ref.will_expire_soon(self.MIN_TOKEN_LIFE_SECONDS)):
|
||||||
self.auth_ref = self.get_auth_ref(session, **kwargs)
|
self.auth_ref = self.get_auth_ref(session, **kwargs)
|
||||||
|
|
||||||
return self.auth_ref.auth_token
|
return self.auth_ref
|
||||||
|
129
keystoneclient/auth/identity/v2.py
Normal file
129
keystoneclient/auth/identity/v2.py
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||||
|
|
||||||
|
# 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 abc
|
||||||
|
|
||||||
|
import six
|
||||||
|
|
||||||
|
from keystoneclient import access
|
||||||
|
from keystoneclient.auth.identity import base
|
||||||
|
from keystoneclient import exceptions
|
||||||
|
|
||||||
|
|
||||||
|
@six.add_metaclass(abc.ABCMeta)
|
||||||
|
class Auth(base.BaseIdentityPlugin):
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def factory(auth_url, **kwargs):
|
||||||
|
"""Construct a plugin appropriate to your available arguments.
|
||||||
|
|
||||||
|
This function should only be used for loading authentication from a
|
||||||
|
config file or other source where you do not know the type of plugin
|
||||||
|
that is required.
|
||||||
|
|
||||||
|
If you know the style of authorization you require then you should
|
||||||
|
construct that plugin directly.
|
||||||
|
|
||||||
|
:raises NoMatchingPlugin: if a plugin cannot be constructed.
|
||||||
|
|
||||||
|
return Auth: a plugin that can be passed to a session.
|
||||||
|
"""
|
||||||
|
username = kwargs.pop('username', None)
|
||||||
|
password = kwargs.pop('password', None)
|
||||||
|
token = kwargs.pop('token', None)
|
||||||
|
|
||||||
|
if token:
|
||||||
|
return Token(auth_url, token, **kwargs)
|
||||||
|
elif username and password:
|
||||||
|
return Password(auth_url, username, password, **kwargs)
|
||||||
|
|
||||||
|
msg = 'A username and password or token is required.'
|
||||||
|
raise exceptions.NoMatchingPlugin(msg)
|
||||||
|
|
||||||
|
def __init__(self, auth_url,
|
||||||
|
trust_id=None,
|
||||||
|
tenant_id=None,
|
||||||
|
tenant_name=None):
|
||||||
|
"""Construct an Identity V2 Authentication Plugin.
|
||||||
|
|
||||||
|
:param string auth_url: Identity service endpoint for authorization.
|
||||||
|
:param string trust_id: Trust ID for trust scoping.
|
||||||
|
:param string tenant_id: Tenant ID for project scoping.
|
||||||
|
:param string tenant_name: Tenant name for project scoping.
|
||||||
|
"""
|
||||||
|
super(Auth, self).__init__(auth_url=auth_url)
|
||||||
|
|
||||||
|
self.trust_id = trust_id
|
||||||
|
self.tenant_id = tenant_id
|
||||||
|
self.tenant_name = tenant_name
|
||||||
|
|
||||||
|
def get_auth_ref(self, session, **kwargs):
|
||||||
|
headers = {}
|
||||||
|
url = self.auth_url + '/tokens'
|
||||||
|
params = {'auth': self.get_auth_data(headers)}
|
||||||
|
|
||||||
|
if self.tenant_id:
|
||||||
|
params['auth']['tenantId'] = self.tenant_id
|
||||||
|
elif self.tenant_name:
|
||||||
|
params['auth']['tenantName'] = self.tenant_name
|
||||||
|
if self.trust_id:
|
||||||
|
params['auth']['trust_id'] = self.trust_id
|
||||||
|
|
||||||
|
resp = session.post(url, json=params, headers=headers,
|
||||||
|
authenticated=False)
|
||||||
|
return access.AccessInfoV2(**resp.json()['access'])
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def get_auth_data(self, headers=None):
|
||||||
|
"""Return the authentication section of an auth plugin.
|
||||||
|
|
||||||
|
:param dict headers: The headers that will be sent with the auth
|
||||||
|
request if a plugin needs to add to them.
|
||||||
|
:return dict: A dict of authentication data for the auth type.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class Password(Auth):
|
||||||
|
|
||||||
|
def __init__(self, auth_url, username, password, **kwargs):
|
||||||
|
"""A plugin for authenticating with a username and password.
|
||||||
|
|
||||||
|
:param string auth_url: Identity service endpoint for authorization.
|
||||||
|
:param string username: Username for authentication.
|
||||||
|
:param string password: Password for authentication.
|
||||||
|
"""
|
||||||
|
super(Password, self).__init__(auth_url, **kwargs)
|
||||||
|
self.username = username
|
||||||
|
self.password = password
|
||||||
|
|
||||||
|
def get_auth_data(self, headers=None):
|
||||||
|
return {'passwordCredentials': {'username': self.username,
|
||||||
|
'password': self.password}}
|
||||||
|
|
||||||
|
|
||||||
|
class Token(Auth):
|
||||||
|
|
||||||
|
def __init__(self, auth_url, token, **kwargs):
|
||||||
|
"""A plugin for authenticating with an existing token.
|
||||||
|
|
||||||
|
:param string auth_url: Identity service endpoint for authorization.
|
||||||
|
:param string token: Existing token for authentication.
|
||||||
|
"""
|
||||||
|
super(Token, self).__init__(auth_url, **kwargs)
|
||||||
|
self.token = token
|
||||||
|
|
||||||
|
def get_auth_data(self, headers=None):
|
||||||
|
if headers is not None:
|
||||||
|
headers['X-Auth-Token'] = self.token
|
||||||
|
return {'token': {'id': self.token}}
|
@@ -51,3 +51,8 @@ class VersionNotAvailable(DiscoveryFailure):
|
|||||||
|
|
||||||
class MissingAuthPlugin(ClientException):
|
class MissingAuthPlugin(ClientException):
|
||||||
"""An authenticated request is required but no plugin available."""
|
"""An authenticated request is required but no plugin available."""
|
||||||
|
|
||||||
|
|
||||||
|
class NoMatchingPlugin(ClientException):
|
||||||
|
"""There were no auth plugins that could be created from the parameters
|
||||||
|
provided."""
|
||||||
|
@@ -314,8 +314,17 @@ class Session(object):
|
|||||||
user_agent=kwargs.pop('user_agent', None))
|
user_agent=kwargs.pop('user_agent', None))
|
||||||
|
|
||||||
def get_token(self):
|
def get_token(self):
|
||||||
"""Return a token as provided by the auth plugin."""
|
"""Return a token as provided by the auth plugin.
|
||||||
|
|
||||||
|
:raises AuthorizationFailure: if a new token fetch fails.
|
||||||
|
|
||||||
|
:returns string: A valid token.
|
||||||
|
"""
|
||||||
if not self.auth:
|
if not self.auth:
|
||||||
raise exceptions.MissingAuthPlugin("Token Required")
|
raise exceptions.MissingAuthPlugin("Token Required")
|
||||||
|
|
||||||
|
try:
|
||||||
return self.auth.get_token(self)
|
return self.auth.get_token(self)
|
||||||
|
except exceptions.HTTPError as exc:
|
||||||
|
raise exceptions.AuthorizationFailure("Authentication failure: "
|
||||||
|
"%s" % exc)
|
||||||
|
114
keystoneclient/tests/auth/test_identity_v2.py
Normal file
114
keystoneclient/tests/auth/test_identity_v2.py
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||||
|
|
||||||
|
# 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 httpretty
|
||||||
|
|
||||||
|
from keystoneclient.auth.identity import v2
|
||||||
|
from keystoneclient import exceptions
|
||||||
|
from keystoneclient import session
|
||||||
|
from keystoneclient.tests import utils
|
||||||
|
|
||||||
|
|
||||||
|
class V2IdentityPlugin(utils.TestCase):
|
||||||
|
|
||||||
|
TEST_ROOT_URL = 'http://127.0.0.1:5000/'
|
||||||
|
TEST_URL = '%s%s' % (TEST_ROOT_URL, 'v2.0')
|
||||||
|
TEST_ROOT_ADMIN_URL = 'http://127.0.0.1:35357/'
|
||||||
|
TEST_ADMIN_URL = '%s%s' % (TEST_ROOT_ADMIN_URL, 'v2.0')
|
||||||
|
|
||||||
|
TEST_PASS = 'password'
|
||||||
|
|
||||||
|
TEST_SERVICE_CATALOG = []
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super(V2IdentityPlugin, self).setUp()
|
||||||
|
self.TEST_RESPONSE_DICT = {
|
||||||
|
"access": {
|
||||||
|
"token": {
|
||||||
|
"expires": "2020-01-01T00:00:10.000123Z",
|
||||||
|
"id": self.TEST_TOKEN,
|
||||||
|
"tenant": {
|
||||||
|
"id": self.TEST_TENANT_ID
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"user": {
|
||||||
|
"id": self.TEST_USER
|
||||||
|
},
|
||||||
|
"serviceCatalog": self.TEST_SERVICE_CATALOG,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
def _plugin(self, auth_url=TEST_URL, **kwargs):
|
||||||
|
return v2.Auth.factory(auth_url, **kwargs)
|
||||||
|
|
||||||
|
def _session(self, **kwargs):
|
||||||
|
return session.Session(auth=self._plugin(**kwargs))
|
||||||
|
|
||||||
|
def stub_auth(self, **kwargs):
|
||||||
|
self.stub_url(httpretty.POST, ['tokens'], **kwargs)
|
||||||
|
|
||||||
|
@httpretty.activate
|
||||||
|
def test_authenticate_with_username_password(self):
|
||||||
|
self.stub_auth(json=self.TEST_RESPONSE_DICT)
|
||||||
|
s = self._session(username=self.TEST_USER, password=self.TEST_PASS)
|
||||||
|
self.assertIsInstance(s.auth, v2.Password)
|
||||||
|
s.get_token()
|
||||||
|
|
||||||
|
req = {'auth': {'passwordCredentials': {'username': self.TEST_USER,
|
||||||
|
'password': self.TEST_PASS}}}
|
||||||
|
self.assertRequestBodyIs(json=req)
|
||||||
|
self.assertEqual(s.auth.auth_ref.auth_token, self.TEST_TOKEN)
|
||||||
|
|
||||||
|
@httpretty.activate
|
||||||
|
def test_authenticate_with_username_password_scoped(self):
|
||||||
|
self.stub_auth(json=self.TEST_RESPONSE_DICT)
|
||||||
|
s = self._session(username=self.TEST_USER, password=self.TEST_PASS,
|
||||||
|
tenant_id=self.TEST_TENANT_ID)
|
||||||
|
self.assertIsInstance(s.auth, v2.Password)
|
||||||
|
s.get_token()
|
||||||
|
|
||||||
|
req = {'auth': {'passwordCredentials': {'username': self.TEST_USER,
|
||||||
|
'password': self.TEST_PASS},
|
||||||
|
'tenantId': self.TEST_TENANT_ID}}
|
||||||
|
self.assertRequestBodyIs(json=req)
|
||||||
|
self.assertEqual(s.auth.auth_ref.auth_token, self.TEST_TOKEN)
|
||||||
|
|
||||||
|
@httpretty.activate
|
||||||
|
def test_authenticate_with_token(self):
|
||||||
|
self.stub_auth(json=self.TEST_RESPONSE_DICT)
|
||||||
|
s = self._session(token='foo')
|
||||||
|
self.assertIsInstance(s.auth, v2.Token)
|
||||||
|
s.get_token()
|
||||||
|
|
||||||
|
req = {'auth': {'token': {'id': 'foo'}}}
|
||||||
|
self.assertRequestBodyIs(json=req)
|
||||||
|
self.assertRequestHeaderEqual('x-Auth-Token', 'foo')
|
||||||
|
self.assertEqual(s.auth.auth_ref.auth_token, self.TEST_TOKEN)
|
||||||
|
|
||||||
|
def test_missing_auth_params(self):
|
||||||
|
self.assertRaises(exceptions.NoMatchingPlugin, self._plugin)
|
||||||
|
|
||||||
|
@httpretty.activate
|
||||||
|
def test_with_trust_id(self):
|
||||||
|
self.stub_auth(json=self.TEST_RESPONSE_DICT)
|
||||||
|
s = self._session(username=self.TEST_USER, password=self.TEST_PASS,
|
||||||
|
trust_id='trust')
|
||||||
|
s.get_token()
|
||||||
|
|
||||||
|
req = {'auth': {'passwordCredentials': {'username': self.TEST_USER,
|
||||||
|
'password': self.TEST_PASS},
|
||||||
|
'trust_id': 'trust'}}
|
||||||
|
|
||||||
|
self.assertRequestBodyIs(json=req)
|
||||||
|
self.assertEqual(s.auth.auth_ref.auth_token, self.TEST_TOKEN)
|
@@ -15,6 +15,7 @@
|
|||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
|
from keystoneclient.auth.identity import v2 as v2_auth
|
||||||
from keystoneclient import exceptions
|
from keystoneclient import exceptions
|
||||||
from keystoneclient import httpclient
|
from keystoneclient import httpclient
|
||||||
from keystoneclient.v2_0 import ec2
|
from keystoneclient.v2_0 import ec2
|
||||||
@@ -137,7 +138,9 @@ class Client(httpclient.HTTPClient):
|
|||||||
# extensions
|
# extensions
|
||||||
self.ec2 = ec2.CredentialsManager(self)
|
self.ec2 = ec2.CredentialsManager(self)
|
||||||
|
|
||||||
if self.management_url is None:
|
# DEPRECATED: if session is passed then we go to the new behaviour of
|
||||||
|
# authenticating on the first required call.
|
||||||
|
if not kwargs.get('session') and self.management_url is None:
|
||||||
self.authenticate()
|
self.authenticate()
|
||||||
|
|
||||||
def get_raw_token_from_identity_service(self, auth_url, username=None,
|
def get_raw_token_from_identity_service(self, auth_url, username=None,
|
||||||
@@ -148,53 +151,26 @@ class Client(httpclient.HTTPClient):
|
|||||||
**kwargs):
|
**kwargs):
|
||||||
"""Authenticate against the v2 Identity API.
|
"""Authenticate against the v2 Identity API.
|
||||||
|
|
||||||
:returns: (``resp``, ``body``) if authentication was successful.
|
:returns: access.AccessInfo if authentication was successful.
|
||||||
:raises: AuthorizationFailure if unable to authenticate or validate
|
:raises: AuthorizationFailure if unable to authenticate or validate
|
||||||
the existing authorization token
|
the existing authorization token
|
||||||
:raises: ValueError if insufficient parameters are used.
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
return self._base_authN(auth_url,
|
if auth_url is None:
|
||||||
|
raise ValueError("Cannot authenticate without an auth_url")
|
||||||
|
|
||||||
|
a = v2_auth.Auth.factory(auth_url,
|
||||||
username=username,
|
username=username,
|
||||||
tenant_id=project_id or tenant_id,
|
|
||||||
tenant_name=project_name or tenant_name,
|
|
||||||
password=password,
|
password=password,
|
||||||
|
token=token,
|
||||||
trust_id=trust_id,
|
trust_id=trust_id,
|
||||||
token=token)
|
tenant_id=project_id or tenant_id,
|
||||||
|
tenant_name=project_name or tenant_name)
|
||||||
|
|
||||||
|
return a.get_auth_ref(self.session)
|
||||||
except (exceptions.AuthorizationFailure, exceptions.Unauthorized):
|
except (exceptions.AuthorizationFailure, exceptions.Unauthorized):
|
||||||
_logger.debug("Authorization Failed.")
|
_logger.debug("Authorization Failed.")
|
||||||
raise
|
raise
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise exceptions.AuthorizationFailure("Authorization Failed: "
|
raise exceptions.AuthorizationFailure("Authorization Failed: "
|
||||||
"%s" % e)
|
"%s" % e)
|
||||||
|
|
||||||
def _base_authN(self, auth_url, username=None, password=None,
|
|
||||||
tenant_name=None, tenant_id=None, trust_id=None,
|
|
||||||
token=None):
|
|
||||||
"""Takes a username, password, and optionally a tenant_id or
|
|
||||||
tenant_name to get an authentication token from keystone.
|
|
||||||
May also take a token and a tenant_id to re-scope a token
|
|
||||||
to a tenant, or a token, tenant_id and trust_id and re-scope
|
|
||||||
the token to the trust
|
|
||||||
"""
|
|
||||||
headers = {}
|
|
||||||
if auth_url is None:
|
|
||||||
raise ValueError("Cannot authenticate without a valid auth_url")
|
|
||||||
url = auth_url + "/tokens"
|
|
||||||
if token:
|
|
||||||
headers['X-Auth-Token'] = token
|
|
||||||
params = {"auth": {"token": {"id": token}}}
|
|
||||||
elif username and password:
|
|
||||||
params = {"auth": {"passwordCredentials": {"username": username,
|
|
||||||
"password": password}}}
|
|
||||||
else:
|
|
||||||
raise ValueError('A username and password or token is required.')
|
|
||||||
if tenant_id:
|
|
||||||
params['auth']['tenantId'] = tenant_id
|
|
||||||
elif tenant_name:
|
|
||||||
params['auth']['tenantName'] = tenant_name
|
|
||||||
if trust_id:
|
|
||||||
params['auth']['trust_id'] = trust_id
|
|
||||||
resp, body = self.request(url, 'POST', body=params, headers=headers)
|
|
||||||
return resp, body
|
|
||||||
|
Reference in New Issue
Block a user