diff options
| author | Jenkins <jenkins@review.openstack.org> | 2016-11-08 18:35:50 +0000 |
|---|---|---|
| committer | Gerrit Code Review <review@openstack.org> | 2016-11-08 18:35:50 +0000 |
| commit | cb922f4dc6c9d1f49069b555d8fbfc3628e78190 (patch) | |
| tree | f571c1cd572583ccccef9f990670a634fa7f4d5c /swiftclient | |
| parent | 41de8b7c0b1e3834e05f291fa6419132311b2452 (diff) | |
| parent | a38efb6031efda7a076886066b6993cdb144f6a3 (diff) | |
| download | python-swiftclient-cb922f4dc6c9d1f49069b555d8fbfc3628e78190.tar.gz | |
Merge "Add v1password keystoneauth plugin"
Diffstat (limited to 'swiftclient')
| -rw-r--r-- | swiftclient/authv1.py | 350 |
1 files changed, 350 insertions, 0 deletions
diff --git a/swiftclient/authv1.py b/swiftclient/authv1.py new file mode 100644 index 0000000..55469ac --- /dev/null +++ b/swiftclient/authv1.py @@ -0,0 +1,350 @@ +# Copyright 2016 OpenStack Foundation +# +# 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. + +""" +Authentication plugin for keystoneauth to support v1 endpoints. + +Way back in the long-long ago, there was no Keystone. Swift used an auth +mechanism now known as "v1", which used only HTTP headers. Auth requests +and responses would look something like:: + + > GET /auth/v1.0 HTTP/1.1 + > Host: <swift server> + > X-Auth-User: <tenant>:<user> + > X-Auth-Key: <password> + > + < HTTP/1.1 200 OK + < X-Storage-Url: http://<swift server>/v1/<tenant account> + < X-Auth-Token: <token> + < X-Storage-Token: <token> + < + +This plugin provides a way for Keystone sessions (and clients that +use them, like python-openstackclient) to communicate with old auth +endpoints that still use this mechanism, such as tempauth, swauth, +or https://identity.api.rackspacecloud.com/v1.0 +""" + +import datetime +import json +import time + +from six.moves.urllib.parse import urljoin + +# Note that while we import keystoneauth1 here, we *don't* need to add it to +# requirements.txt -- this entire module only makes sense (and should only be +# loaded) if keystoneauth is already installed. +from keystoneauth1 import plugin +from keystoneauth1 import exceptions +from keystoneauth1 import loading +from keystoneauth1.identity import base + + +# stupid stdlib... +class _UTC(datetime.tzinfo): + def utcoffset(self, dt): + return datetime.timedelta(0) + + def tzname(self, dt): + return "UTC" + + def dst(self, dt): + return datetime.timedelta(0) + + +UTC = _UTC() +del _UTC + + +class ServiceCatalogV1(object): + def __init__(self, auth_url, storage_url, account): + self.auth_url = auth_url + self._storage_url = storage_url + self._account = account + + @property + def storage_url(self): + if self._account: + return urljoin(self._storage_url.rstrip('/'), self._account) + return self._storage_url + + @property + def catalog(self): + # openstackclient wants this for the `catalog list` and + # `catalog show` commands + endpoints = [{ + 'region': 'default', + 'publicURL': self._storage_url, + }] + if self.storage_url != self._storage_url: + endpoints.insert(0, { + 'region': 'override', + 'publicURL': self.storage_url, + }) + + return [ + { + 'name': 'swift', + 'type': 'object-store', + 'endpoints': endpoints, + }, + { + 'name': 'auth', + 'type': 'identity', + 'endpoints': [{ + 'region': 'default', + 'publicURL': self.auth_url, + }], + } + ] + + def url_for(self, **kwargs): + kwargs.setdefault('interface', 'public') + kwargs.setdefault('service_type', None) + + if kwargs['service_type'] == 'object-store': + return self.storage_url + + # Although our "catalog" includes an identity entry, nothing that uses + # url_for() (including `openstack endpoint list`) will know what to do + # with it. Better to just raise the exception, cribbing error messages + # from keystoneauth1/access/service_catalog.py + + if 'service_name' in kwargs and 'region_name' in kwargs: + msg = ('%(interface)s endpoint for %(service_type)s service ' + 'named %(service_name)s in %(region_name)s region not ' + 'found' % kwargs) + elif 'service_name' in kwargs: + msg = ('%(interface)s endpoint for %(service_type)s service ' + 'named %(service_name)s not found' % kwargs) + elif 'region_name' in kwargs: + msg = ('%(interface)s endpoint for %(service_type)s service ' + 'in %(region_name)s region not found' % kwargs) + else: + msg = ('%(interface)s endpoint for %(service_type)s service ' + 'not found' % kwargs) + + raise exceptions.EndpointNotFound(msg) + + +class AccessInfoV1(object): + """An object for encapsulating a raw v1 auth token.""" + + def __init__(self, auth_url, storage_url, account, username, auth_token, + token_life): + self.auth_url = auth_url + self.storage_url = storage_url + self.account = account + self.service_catalog = ServiceCatalogV1(auth_url, storage_url, account) + self.username = username + self.auth_token = auth_token + self._issued = time.time() + try: + self._expires = self._issued + float(token_life) + except (TypeError, ValueError): + self._expires = None + # following is used by openstackclient + self.project_id = None + + @property + def expires(self): + if self._expires is None: + return None + return datetime.datetime.fromtimestamp(self._expires, UTC) + + @property + def issued(self): + return datetime.datetime.fromtimestamp(self._issued, UTC) + + @property + def user_id(self): + # openstackclient wants this for the `token issue` command + return self.username + + def will_expire_soon(self, stale_duration): + """Determines if expiration is about to occur. + + :returns: true if expiration is within the given duration + """ + if self._expires is None: + return False # assume no expiration + return time.time() + stale_duration > self._expires + + def get_state(self): + """Serialize the current state.""" + return json.dumps({ + 'auth_url': self.auth_url, + 'storage_url': self.storage_url, + 'account': self.account, + 'username': self.username, + 'auth_token': self.auth_token, + 'issued': self._issued, + 'expires': self._expires}, sort_keys=True) + + @classmethod + def from_state(cls, data): + """Deserialize the given state. + + :returns: a new AccessInfoV1 object with the given state + """ + data = json.loads(data) + access = cls( + data['auth_url'], + data['storage_url'], + data['account'], + data['username'], + data['auth_token'], + token_life=None) + access._issued = data['issued'] + access._expires = data['expires'] + return access + + +class PasswordPlugin(base.BaseIdentityPlugin): + """A plugin for authenticating with a username and password. + + Subclassing from BaseIdentityPlugin gets us a few niceties, like handling + token invalidation and locking during authentication. + + :param string auth_url: Identity v1 endpoint for authorization. + :param string username: Username for authentication. + :param string password: Password for authentication. + :param string project_name: Swift account to use after authentication. + We use 'project_name' to be consistent with + other auth plugins. + :param string reauthenticate: Whether to allow re-authentication. + """ + access_class = AccessInfoV1 + + def __init__(self, auth_url, username, password, project_name=None, + reauthenticate=True): + super(PasswordPlugin, self).__init__( + auth_url=auth_url, + reauthenticate=reauthenticate) + self.user = username + self.key = password + self.account = project_name + + def get_auth_ref(self, session, **kwargs): + """Obtain a token from a v1 endpoint. + + 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. + + :param session: A session object that can be used for communication. + + :returns: Token access information. + """ + headers = {'X-Auth-User': self.user, + 'X-Auth-Key': self.key} + + resp = session.get(self.auth_url, headers=headers, + authenticated=False, log=False) + + if resp.status_code // 100 != 2: + raise exceptions.InvalidResponse(response=resp) + + if 'X-Storage-Url' not in resp.headers: + raise exceptions.InvalidResponse(response=resp) + + if 'X-Auth-Token' not in resp.headers and \ + 'X-Storage-Token' not in resp.headers: + raise exceptions.InvalidResponse(response=resp) + token = resp.headers.get('X-Storage-Token', + resp.headers.get('X-Auth-Token')) + return AccessInfoV1( + auth_url=self.auth_url, + storage_url=resp.headers['X-Storage-Url'], + account=self.account, + username=self.user, + auth_token=token, + token_life=resp.headers.get('X-Auth-Token-Expires')) + + def get_cache_id_elements(self): + """Get the elements for this auth plugin that make it unique.""" + return {'auth_url': self.auth_url, + 'user': self.user, + 'key': self.key, + 'account': self.account} + + def get_endpoint(self, session, interface='public', **kwargs): + """Return an endpoint for the client.""" + if interface is plugin.AUTH_INTERFACE: + return self.auth_url + else: + return self.get_access(session).service_catalog.url_for( + interface=interface, **kwargs) + + def get_auth_state(self): + """Retrieve the current authentication state for the plugin. + + :returns: raw python data (which can be JSON serialized) that can be + moved into another plugin (of the same type) to have the + same authenticated state. + """ + if self.auth_ref: + return self.auth_ref.get_state() + + def set_auth_state(self, data): + """Install existing authentication state for a plugin. + + Take the output of get_auth_state and install that authentication state + into the current authentication plugin. + """ + if data: + self.auth_ref = self.access_class.from_state(data) + else: + self.auth_ref = None + + def get_sp_auth_url(self, *args, **kwargs): + raise NotImplementedError() + + def get_sp_url(self, *args, **kwargs): + raise NotImplementedError() + + def get_discovery(self, *args, **kwargs): + raise NotImplementedError() + + +class PasswordLoader(loading.BaseLoader): + """Option handling for the ``v1password`` plugin.""" + plugin_class = PasswordPlugin + + def get_options(self): + """Return the list of parameters associated with the auth plugin. + + This list may be used to generate CLI or config arguments. + """ + return [ + loading.Opt('auth-url', required=True, + help='Authentication URL'), + # overload project-name as a way to specify an alternate account, + # since: + # - in a world of just users & passwords, this seems the closest + # analog to a project, and + # - openstackclient will (or used to?) still require that you + # provide one anyway + loading.Opt('project-name', required=False, + help='Swift account to use'), + loading.Opt('username', required=True, + deprecated=[loading.Opt('user-name')], + help='Username to login with'), + loading.Opt('password', required=True, secret=True, + help='Password to use'), + ] |
