From 73e4296a389893c750f7c70a477ec828e4360197 Mon Sep 17 00:00:00 2001
From: Paulo Ewerton <pauloewerton@lsd.ufcg.edu.br>
Date: Tue, 29 Mar 2016 19:10:42 +0000
Subject: [PATCH] Adding keystoneauth sessions support

This patch allows authentication in swiftclient with a keystonauth
session.

Co-Authored-By: Tim Burke <tim@swiftstack.com>

Change-Id: Ia3fd947ff619c11ff0ce474897533dcf7b49d9b3
Closes-Bug: 1518938
---
 doc/source/client-api.rst      | 22 ++++++++++++++++++
 swiftclient/client.py          | 35 ++++++++++++++++++++--------
 tests/unit/test_swiftclient.py | 42 ++++++++++++++++++++++++++++++++++
 3 files changed, 89 insertions(+), 10 deletions(-)

diff --git a/doc/source/client-api.rst b/doc/source/client-api.rst
index 5677f70d..13b3056a 100644
--- a/doc/source/client-api.rst
+++ b/doc/source/client-api.rst
@@ -18,6 +18,28 @@ version are detailed below, but are
 just a subset of those that can be used to successfully authenticate. These
 are the most common and recommended combinations.
 
+Keystone Session
+~~~~~~~~~~~~~~~~
+
+.. code-block:: python
+
+    from keystoneauth1 import session
+    from keystoneauth1 import v3
+
+    # Create a password auth plugin
+    auth = v3.Password(auth_url='http://127.0.0.1:5000/v3/',
+                       username='tester',
+                       password='testing',
+                       user_domain_name='Default',
+                       project_name='Default',
+                       project_domain_name='Default')
+
+    # Create session
+    session = session.Session(auth=auth)
+
+    # Create swiftclient Connection
+    swift_conn = Connection(session=session)
+
 Keystone v3
 ~~~~~~~~~~~
 
diff --git a/swiftclient/client.py b/swiftclient/client.py
index 744a876b..8370764d 100644
--- a/swiftclient/client.py
+++ b/swiftclient/client.py
@@ -609,6 +609,7 @@ def get_auth(auth_url, user, key, **kwargs):
     use of this network path causes no bandwidth charges but requires the
     client to be running on Rackspace's ServiceNet network.
     """
+    session = kwargs.get('session', None)
     auth_version = kwargs.get('auth_version', '1')
     os_options = kwargs.get('os_options', {})
 
@@ -617,7 +618,14 @@ def get_auth(auth_url, user, key, **kwargs):
     cert = kwargs.get('cert')
     cert_key = kwargs.get('cert_key')
     timeout = kwargs.get('timeout', None)
-    if auth_version in AUTH_VERSIONS_V1:
+
+    if session:
+        service_type = os_options.get('service_type', 'object-store')
+        interface = os_options.get('endpoint_type', 'public')
+        storage_url = session.get_endpoint(service_type=service_type,
+                                           interface=interface)
+        token = session.get_token()
+    elif auth_version in AUTH_VERSIONS_V1:
         storage_url, token = get_auth_1_0(auth_url,
                                           user,
                                           key,
@@ -654,8 +662,8 @@ def get_auth(auth_url, user, key, **kwargs):
                                                timeout=timeout,
                                                auth_version=auth_version)
     else:
-        raise ClientException('Unknown auth_version %s specified.'
-                              % auth_version)
+        raise ClientException('Unknown auth_version %s specified and no '
+                              'session found.' % auth_version)
 
     # Override storage url, if necessary
     if os_options.get('object_storage_url'):
@@ -1414,7 +1422,7 @@ class Connection(object):
                  os_options=None, auth_version="1", cacert=None,
                  insecure=False, cert=None, cert_key=None,
                  ssl_compression=True, retry_on_ratelimit=False,
-                 timeout=None):
+                 timeout=None, session=None):
         """
         :param authurl: authentication URL
         :param user: user name to authenticate as
@@ -1449,7 +1457,9 @@ class Connection(object):
                                    this parameter to True will cause a retry
                                    after a backoff.
         :param timeout: The connect timeout for the HTTP connection.
+        :param session: A keystoneauth session object.
         """
+        self.session = session
         self.authurl = authurl
         self.user = user
         self.key = key
@@ -1493,7 +1503,7 @@ class Connection(object):
 
     def get_auth(self):
         self.url, self.token = get_auth(self.authurl, self.user, self.key,
-                                        snet=self.snet,
+                                        session=self.session, snet=self.snet,
                                         auth_version=self.auth_version,
                                         os_options=self.os_options,
                                         cacert=self.cacert,
@@ -1512,8 +1522,8 @@ class Connection(object):
                                                          None)
         service_user = opts.get('service_username', None)
         service_key = opts.get('service_key', None)
-        return get_auth(self.authurl, service_user,
-                        service_key,
+        return get_auth(self.authurl, service_user, service_key,
+                        session=self.session,
                         snet=self.snet,
                         auth_version=self.auth_version,
                         os_options=service_options,
@@ -1577,10 +1587,15 @@ class Connection(object):
                     logger.exception(err)
                     raise
                 if err.http_status == 401:
+                    if self.session:
+                        should_retry = self.session.invalidate()
+                    else:
+                        # Without a proper session, just check for auth creds
+                        should_retry = all((self.authurl, self.user, self.key))
+
                     self.url = self.token = self.service_token = None
-                    if retried_auth or not all((self.authurl,
-                                                self.user,
-                                                self.key)):
+
+                    if retried_auth or not should_retry:
                         logger.exception(err)
                         raise
                     retried_auth = True
diff --git a/tests/unit/test_swiftclient.py b/tests/unit/test_swiftclient.py
index cbb95dbe..07ae5021 100644
--- a/tests/unit/test_swiftclient.py
+++ b/tests/unit/test_swiftclient.py
@@ -561,6 +561,15 @@ class TestGetAuth(MockHttpTest):
         self.assertTrue(url.startswith("http"))
         self.assertTrue(token)
 
+    def test_auth_with_session(self):
+        mock_session = mock.MagicMock()
+        mock_session.get_endpoint.return_value = 'http://storagehost/v1/acct'
+        mock_session.get_token.return_value = 'token'
+        url, token = c.get_auth('http://www.test.com', 'asdf', 'asdf',
+                                session=mock_session)
+        self.assertEqual(url, 'http://storagehost/v1/acct')
+        self.assertTrue(token)
+
 
 class TestGetAccount(MockHttpTest):
 
@@ -1868,6 +1877,39 @@ class TestConnection(MockHttpTest):
             ('HEAD', '/v1/AUTH_test', '', {'x-auth-token': 'token'}),
         ])
 
+    def test_session_no_invalidate(self):
+        mock_session = mock.MagicMock()
+        mock_session.get_endpoint.return_value = 'http://storagehost/v1/acct'
+        mock_session.get_token.return_value = 'expired'
+        mock_session.invalidate.return_value = False
+        conn = c.Connection(session=mock_session)
+        fake_conn = self.fake_http_connection(401)
+        with mock.patch.multiple('swiftclient.client',
+                                 http_connection=fake_conn,
+                                 sleep=mock.DEFAULT):
+            self.assertRaises(c.ClientException, conn.head_account)
+        self.assertEqual(mock_session.get_token.mock_calls, [mock.call()])
+        self.assertEqual(mock_session.invalidate.mock_calls, [mock.call()])
+
+    def test_session_can_invalidate(self):
+        mock_session = mock.MagicMock()
+        mock_session.get_endpoint.return_value = 'http://storagehost/v1/acct'
+        mock_session.get_token.side_effect = ['expired', 'token']
+        mock_session.invalidate.return_value = True
+        conn = c.Connection(session=mock_session)
+        fake_conn = self.fake_http_connection(401, 200)
+        with mock.patch.multiple('swiftclient.client',
+                                 http_connection=fake_conn,
+                                 sleep=mock.DEFAULT):
+            conn.head_account()
+        self.assertRequests([
+            ('HEAD', '/v1/acct', '', {'x-auth-token': 'expired'}),
+            ('HEAD', '/v1/acct', '', {'x-auth-token': 'token'}),
+        ])
+        self.assertEqual(mock_session.get_token.mock_calls, [
+            mock.call(), mock.call()])
+        self.assertEqual(mock_session.invalidate.mock_calls, [mock.call()])
+
     def test_preauth_token_with_no_storage_url_requires_auth(self):
         conn = c.Connection(
             'http://auth.example.com', 'user', 'password',