diff --git a/keystoneclient/auth/identity/base.py b/keystoneclient/auth/identity/base.py index 7d1c1d56..0b8314f2 100644 --- a/keystoneclient/auth/identity/base.py +++ b/keystoneclient/auth/identity/base.py @@ -22,6 +22,9 @@ LOG = logging.getLogger(__name__) @six.add_metaclass(abc.ABCMeta) 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, auth_url=None, username=None, @@ -32,13 +35,15 @@ class BaseIdentityPlugin(base.BaseAuthPlugin): super(BaseIdentityPlugin, self).__init__() 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.password = password self.token = token self.trust_id = trust_id - self.auth_ref = None - @abc.abstractmethod def get_auth_ref(self, session, **kwargs): """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 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. """ 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) - return self.auth_ref.auth_token + return self.auth_ref diff --git a/keystoneclient/auth/identity/v2.py b/keystoneclient/auth/identity/v2.py new file mode 100644 index 00000000..80c95f57 --- /dev/null +++ b/keystoneclient/auth/identity/v2.py @@ -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}} diff --git a/keystoneclient/exceptions.py b/keystoneclient/exceptions.py index a1c75f27..c4620740 100644 --- a/keystoneclient/exceptions.py +++ b/keystoneclient/exceptions.py @@ -51,3 +51,8 @@ class VersionNotAvailable(DiscoveryFailure): class MissingAuthPlugin(ClientException): """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.""" diff --git a/keystoneclient/session.py b/keystoneclient/session.py index 116c81ee..32c30b7f 100644 --- a/keystoneclient/session.py +++ b/keystoneclient/session.py @@ -314,8 +314,17 @@ class Session(object): user_agent=kwargs.pop('user_agent', None)) 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: raise exceptions.MissingAuthPlugin("Token Required") - return self.auth.get_token(self) + try: + return self.auth.get_token(self) + except exceptions.HTTPError as exc: + raise exceptions.AuthorizationFailure("Authentication failure: " + "%s" % exc)