Merge "Add v1password keystoneauth plugin"
This commit is contained in:
		@@ -51,7 +51,7 @@ master_doc = 'index'
 | 
			
		||||
 | 
			
		||||
# General information about the project.
 | 
			
		||||
project = u'Swiftclient'
 | 
			
		||||
copyright = u'2013 OpenStack, LLC.'
 | 
			
		||||
copyright = u'2013-2016 OpenStack, LLC.'
 | 
			
		||||
 | 
			
		||||
# The version info for the project you're documenting, acts as replacement for
 | 
			
		||||
# |version| and |release|, also used in various other places throughout the
 | 
			
		||||
 
 | 
			
		||||
@@ -5,6 +5,12 @@ swiftclient
 | 
			
		||||
 | 
			
		||||
.. automodule:: swiftclient
 | 
			
		||||
 | 
			
		||||
swiftclient.authv1
 | 
			
		||||
==================
 | 
			
		||||
 | 
			
		||||
.. automodule:: swiftclient.authv1
 | 
			
		||||
   :inherited-members:
 | 
			
		||||
 | 
			
		||||
swiftclient.client
 | 
			
		||||
==================
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -40,6 +40,9 @@ keystone =
 | 
			
		||||
console_scripts =
 | 
			
		||||
    swift = swiftclient.shell:main
 | 
			
		||||
 | 
			
		||||
keystoneauth1.plugin =
 | 
			
		||||
    v1password = swiftclient.authv1:PasswordLoader
 | 
			
		||||
 | 
			
		||||
[build_sphinx]
 | 
			
		||||
source-dir = doc/source
 | 
			
		||||
build-dir = doc/build
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										350
									
								
								swiftclient/authv1.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										350
									
								
								swiftclient/authv1.py
									
									
									
									
									
										Normal file
									
								
							@@ -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'),
 | 
			
		||||
        ]
 | 
			
		||||
							
								
								
									
										246
									
								
								tests/unit/test_authv1.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										246
									
								
								tests/unit/test_authv1.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,246 @@
 | 
			
		||||
# 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.
 | 
			
		||||
 | 
			
		||||
import datetime
 | 
			
		||||
import json
 | 
			
		||||
import mock
 | 
			
		||||
import unittest
 | 
			
		||||
from keystoneauth1 import plugin
 | 
			
		||||
from keystoneauth1 import loading
 | 
			
		||||
from keystoneauth1 import exceptions
 | 
			
		||||
from swiftclient import authv1
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TestDataNoAccount(object):
 | 
			
		||||
    options = dict(
 | 
			
		||||
        auth_url='http://saio:8080/auth/v1.0',
 | 
			
		||||
        username='test:tester',
 | 
			
		||||
        password='testing')
 | 
			
		||||
    storage_url = 'http://saio:8080/v1/AUTH_test'
 | 
			
		||||
    expected_endpoint = storage_url
 | 
			
		||||
    token = 'token'
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TestDataWithAccount(object):
 | 
			
		||||
    options = dict(
 | 
			
		||||
        auth_url='http://saio:8080/auth/v1.0',
 | 
			
		||||
        username='test2:tester2',
 | 
			
		||||
        project_name='SOME_other_account',
 | 
			
		||||
        password='testing2')
 | 
			
		||||
    storage_url = 'http://saio:8080/v1/AUTH_test2'
 | 
			
		||||
    expected_endpoint = 'http://saio:8080/v1/SOME_other_account'
 | 
			
		||||
    token = 'other_token'
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TestPluginLoading(TestDataNoAccount, unittest.TestCase):
 | 
			
		||||
    def test_can_load(self):
 | 
			
		||||
        loader = loading.get_plugin_loader('v1password')
 | 
			
		||||
        self.assertIsInstance(loader, authv1.PasswordLoader)
 | 
			
		||||
 | 
			
		||||
        auth_plugin = loader.load_from_options(**self.options)
 | 
			
		||||
        self.assertIsInstance(auth_plugin, authv1.PasswordPlugin)
 | 
			
		||||
 | 
			
		||||
        self.assertEqual(self.options['auth_url'], auth_plugin.auth_url)
 | 
			
		||||
        self.assertEqual(self.options['username'], auth_plugin.user)
 | 
			
		||||
        self.assertEqual(self.options.get('project_name'), auth_plugin.account)
 | 
			
		||||
        self.assertEqual(self.options['password'], auth_plugin.key)
 | 
			
		||||
 | 
			
		||||
    def test_get_state(self):
 | 
			
		||||
        auth_plugin = authv1.PasswordPlugin(**self.options)
 | 
			
		||||
        self.assertIsNone(auth_plugin.get_auth_state())
 | 
			
		||||
 | 
			
		||||
        with mock.patch('swiftclient.authv1.time.time', return_value=1234.56):
 | 
			
		||||
            auth_plugin.auth_ref = authv1.AccessInfoV1(
 | 
			
		||||
                self.options['auth_url'],
 | 
			
		||||
                self.storage_url,
 | 
			
		||||
                self.options.get('project_name'),
 | 
			
		||||
                self.options['username'],
 | 
			
		||||
                self.token,
 | 
			
		||||
                60)
 | 
			
		||||
 | 
			
		||||
        expected = json.dumps({
 | 
			
		||||
            'auth_url': self.options['auth_url'],
 | 
			
		||||
            'username': self.options['username'],
 | 
			
		||||
            'account': self.options.get('project_name'),
 | 
			
		||||
            'issued': 1234.56,
 | 
			
		||||
            'storage_url': self.storage_url,
 | 
			
		||||
            'auth_token': self.token,
 | 
			
		||||
            'expires': 1234.56 + 60,
 | 
			
		||||
        }, sort_keys=True)
 | 
			
		||||
        self.assertEqual(expected, auth_plugin.auth_ref.get_state())
 | 
			
		||||
        self.assertEqual(expected, auth_plugin.get_auth_state())
 | 
			
		||||
 | 
			
		||||
    def test_set_state(self):
 | 
			
		||||
        auth_plugin = authv1.PasswordPlugin(**self.options)
 | 
			
		||||
        self.assertIsNone(auth_plugin.auth_ref)
 | 
			
		||||
 | 
			
		||||
        auth_plugin.auth_ref = object()
 | 
			
		||||
        auth_plugin.set_auth_state(None)
 | 
			
		||||
        self.assertIsNone(auth_plugin.get_auth_state())
 | 
			
		||||
 | 
			
		||||
        state = json.dumps({
 | 
			
		||||
            'auth_url': self.options['auth_url'],
 | 
			
		||||
            'username': self.options['username'],
 | 
			
		||||
            'account': self.options.get('project_name'),
 | 
			
		||||
            'issued': 1234.56,
 | 
			
		||||
            'storage_url': self.storage_url,
 | 
			
		||||
            'auth_token': self.token,
 | 
			
		||||
            'expires': None,
 | 
			
		||||
        }, sort_keys=True)
 | 
			
		||||
        auth_plugin.set_auth_state(state)
 | 
			
		||||
        self.assertIsInstance(auth_plugin.auth_ref, authv1.AccessInfoV1)
 | 
			
		||||
 | 
			
		||||
        self.assertEqual(self.options['username'],
 | 
			
		||||
                         auth_plugin.auth_ref.username)
 | 
			
		||||
        self.assertEqual(self.options['auth_url'],
 | 
			
		||||
                         auth_plugin.auth_ref.auth_url)
 | 
			
		||||
        self.assertEqual(self.storage_url, auth_plugin.auth_ref.storage_url)
 | 
			
		||||
        self.assertEqual(self.options.get('project_name'), auth_plugin.account)
 | 
			
		||||
        self.assertEqual(self.token, auth_plugin.auth_ref.auth_token)
 | 
			
		||||
        self.assertEqual(1234.56, auth_plugin.auth_ref._issued)
 | 
			
		||||
        self.assertIs(datetime.datetime, type(auth_plugin.auth_ref.issued))
 | 
			
		||||
        self.assertIsNone(auth_plugin.auth_ref._expires)
 | 
			
		||||
        self.assertIsNone(auth_plugin.auth_ref.expires)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TestPluginLoadingWithAccount(TestDataWithAccount, TestPluginLoading):
 | 
			
		||||
    pass
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TestPlugin(TestDataNoAccount, unittest.TestCase):
 | 
			
		||||
    def setUp(self):
 | 
			
		||||
        self.mock_session = mock.MagicMock()
 | 
			
		||||
        self.mock_response = self.mock_session.get.return_value
 | 
			
		||||
        self.mock_response.status_code = 200
 | 
			
		||||
        self.mock_response.headers = {
 | 
			
		||||
            'X-Auth-Token': self.token,
 | 
			
		||||
            'X-Storage-Url': self.storage_url,
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    def test_get_access(self):
 | 
			
		||||
        auth_plugin = authv1.PasswordPlugin(**self.options)
 | 
			
		||||
        with mock.patch('swiftclient.authv1.time.time', return_value=1234.56):
 | 
			
		||||
            access = auth_plugin.get_access(self.mock_session)
 | 
			
		||||
 | 
			
		||||
        self.assertEqual(self.mock_session.get.mock_calls, [mock.call(
 | 
			
		||||
            self.options['auth_url'], authenticated=False, log=False, headers={
 | 
			
		||||
                'X-Auth-User': self.options['username'],
 | 
			
		||||
                'X-Auth-Key': self.options['password'],
 | 
			
		||||
            })])
 | 
			
		||||
 | 
			
		||||
        self.assertEqual(self.options['username'], access.username)
 | 
			
		||||
        # `openstack token issue` requires a user_id property
 | 
			
		||||
        self.assertEqual(self.options['username'], access.user_id)
 | 
			
		||||
        self.assertEqual(self.storage_url, access.storage_url)
 | 
			
		||||
        self.assertEqual(self.token, access.auth_token)
 | 
			
		||||
        self.assertEqual(1234.56, access._issued)
 | 
			
		||||
        self.assertIs(datetime.datetime, type(auth_plugin.auth_ref.issued))
 | 
			
		||||
        self.assertIsNone(access.expires)
 | 
			
		||||
 | 
			
		||||
        # `openstack catalog list/show` require a catalog property
 | 
			
		||||
        catalog = access.service_catalog.catalog
 | 
			
		||||
        self.assertEqual('swift', catalog[0].get('name'))
 | 
			
		||||
        self.assertEqual('object-store', catalog[0].get('type'))
 | 
			
		||||
        self.assertIn('endpoints', catalog[0])
 | 
			
		||||
        self.assertIn(self.storage_url, [
 | 
			
		||||
            e.get('publicURL') for e in catalog[0]['endpoints']])
 | 
			
		||||
 | 
			
		||||
    def test_get_access_with_expiry(self):
 | 
			
		||||
        auth_plugin = authv1.PasswordPlugin(**self.options)
 | 
			
		||||
        self.mock_response.headers['X-Auth-Token-Expires'] = '78.9'
 | 
			
		||||
        with mock.patch('swiftclient.authv1.time.time',
 | 
			
		||||
                        return_value=1234.56) as mock_time:
 | 
			
		||||
            access = auth_plugin.get_access(self.mock_session)
 | 
			
		||||
            self.assertEqual(1234.56 + 78.9, access._expires)
 | 
			
		||||
            self.assertIs(datetime.datetime,
 | 
			
		||||
                          type(auth_plugin.auth_ref.expires))
 | 
			
		||||
 | 
			
		||||
            self.assertIs(True, access.will_expire_soon(90))
 | 
			
		||||
            self.assertIs(False, access.will_expire_soon(60))
 | 
			
		||||
        self.assertEqual(3, len(mock_time.mock_calls))
 | 
			
		||||
 | 
			
		||||
    def test_get_access_bad_expiry(self):
 | 
			
		||||
        auth_plugin = authv1.PasswordPlugin(**self.options)
 | 
			
		||||
        self.mock_response.headers['X-Auth-Token-Expires'] = 'foo'
 | 
			
		||||
        access = auth_plugin.get_access(self.mock_session)
 | 
			
		||||
        self.assertEqual(None, access.expires)
 | 
			
		||||
 | 
			
		||||
        self.assertIs(False, access.will_expire_soon(60))
 | 
			
		||||
        self.assertIs(False, access.will_expire_soon(1e20))
 | 
			
		||||
 | 
			
		||||
    def test_get_access_bad_status(self):
 | 
			
		||||
        auth_plugin = authv1.PasswordPlugin(**self.options)
 | 
			
		||||
        self.mock_response.status_code = 401
 | 
			
		||||
        self.assertRaises(exceptions.InvalidResponse,
 | 
			
		||||
                          auth_plugin.get_access, self.mock_session)
 | 
			
		||||
 | 
			
		||||
    def test_get_access_missing_token(self):
 | 
			
		||||
        auth_plugin = authv1.PasswordPlugin(**self.options)
 | 
			
		||||
        self.mock_response.headers.pop('X-Auth-Token')
 | 
			
		||||
        self.assertRaises(exceptions.InvalidResponse,
 | 
			
		||||
                          auth_plugin.get_access, self.mock_session)
 | 
			
		||||
 | 
			
		||||
    def test_get_access_accepts_storage_token(self):
 | 
			
		||||
        auth_plugin = authv1.PasswordPlugin(**self.options)
 | 
			
		||||
        self.mock_response.headers.pop('X-Auth-Token')
 | 
			
		||||
        self.mock_response.headers['X-Storage-Token'] = 'yet another token'
 | 
			
		||||
        access = auth_plugin.get_access(self.mock_session)
 | 
			
		||||
        self.assertEqual('yet another token', access.auth_token)
 | 
			
		||||
 | 
			
		||||
    def test_get_access_missing_url(self):
 | 
			
		||||
        auth_plugin = authv1.PasswordPlugin(**self.options)
 | 
			
		||||
        self.mock_response.headers.pop('X-Storage-Url')
 | 
			
		||||
        self.assertRaises(exceptions.InvalidResponse,
 | 
			
		||||
                          auth_plugin.get_access, self.mock_session)
 | 
			
		||||
 | 
			
		||||
    def test_get_endpoint(self):
 | 
			
		||||
        auth_plugin = authv1.PasswordPlugin(**self.options)
 | 
			
		||||
 | 
			
		||||
        object_store_endpoint = auth_plugin.get_endpoint(
 | 
			
		||||
            self.mock_session, service_type='object-store')
 | 
			
		||||
        self.assertEqual(object_store_endpoint, self.expected_endpoint)
 | 
			
		||||
 | 
			
		||||
        auth_endpoint = auth_plugin.get_endpoint(
 | 
			
		||||
            self.mock_session, interface=plugin.AUTH_INTERFACE)
 | 
			
		||||
        self.assertEqual(auth_endpoint, self.options['auth_url'])
 | 
			
		||||
 | 
			
		||||
        with self.assertRaises(exceptions.EndpointNotFound) as exc_mgr:
 | 
			
		||||
            auth_plugin.get_endpoint(self.mock_session)
 | 
			
		||||
        self.assertEqual('public endpoint for None service not found',
 | 
			
		||||
                         str(exc_mgr.exception))
 | 
			
		||||
 | 
			
		||||
        with self.assertRaises(exceptions.EndpointNotFound) as exc_mgr:
 | 
			
		||||
            auth_plugin.get_endpoint(
 | 
			
		||||
                self.mock_session, service_type='identity', region_name='DFW')
 | 
			
		||||
        self.assertEqual(
 | 
			
		||||
            'public endpoint for identity service in DFW region not found',
 | 
			
		||||
            str(exc_mgr.exception))
 | 
			
		||||
 | 
			
		||||
        with self.assertRaises(exceptions.EndpointNotFound) as exc_mgr:
 | 
			
		||||
            auth_plugin.get_endpoint(
 | 
			
		||||
                self.mock_session, service_type='image', service_name='glance')
 | 
			
		||||
        self.assertEqual(
 | 
			
		||||
            'public endpoint for image service named glance not found',
 | 
			
		||||
            str(exc_mgr.exception))
 | 
			
		||||
 | 
			
		||||
        with self.assertRaises(exceptions.EndpointNotFound) as exc_mgr:
 | 
			
		||||
            auth_plugin.get_endpoint(
 | 
			
		||||
                self.mock_session, service_type='compute', service_name='nova',
 | 
			
		||||
                region_name='IAD')
 | 
			
		||||
        self.assertEqual('public endpoint for compute service named nova in '
 | 
			
		||||
                         'IAD region not found', str(exc_mgr.exception))
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TestPluginWithAccount(TestDataWithAccount, TestPlugin):
 | 
			
		||||
    pass
 | 
			
		||||
		Reference in New Issue
	
	Block a user