From 1789c2654d604f8f5befc39638d2e8860dadc6ef Mon Sep 17 00:00:00 2001
From: Stuart McLaren <stuart.mclaren@hp.com>
Date: Wed, 13 May 2015 09:48:41 +0000
Subject: [PATCH] Add minimal working service token support.

Add client changes to allow accessing alternative
reseller_prefixes via a service token.

ie client changes for this server side spec:
 https://review.openstack.org/#/c/105228

We assume that the service storage url has been passed in
as a preauthurl.

We rely on get_auth preserving this url.

Change-Id: I1cfda178f0b6c8add46cfebd6bf38440caae2036
---
 swiftclient/client.py          | 122 +++++++++++--
 tests/unit/test_swiftclient.py | 307 ++++++++++++++++++++++++++++++++-
 2 files changed, 409 insertions(+), 20 deletions(-)

diff --git a/swiftclient/client.py b/swiftclient/client.py
index 74e60c0a..4819c124 100644
--- a/swiftclient/client.py
+++ b/swiftclient/client.py
@@ -489,7 +489,8 @@ def store_response(resp, response_dict):
 
 
 def get_account(url, token, marker=None, limit=None, prefix=None,
-                end_marker=None, http_conn=None, full_listing=False):
+                end_marker=None, http_conn=None, full_listing=False,
+                service_token=None):
     """
     Get a listing of containers for the account.
 
@@ -503,6 +504,7 @@ def get_account(url, token, marker=None, limit=None, prefix=None,
                       conn object)
     :param full_listing: if True, return a full listing, else returns a max
                          of 10000 listings
+    :param service_token: service auth token
     :returns: a tuple of (response headers, a list of containers) The response
               headers will be a dict and all header names will be lowercase.
     :raises ClientException: HTTP GET request failed
@@ -532,6 +534,8 @@ def get_account(url, token, marker=None, limit=None, prefix=None,
         qs += '&end_marker=%s' % quote(end_marker)
     full_path = '%s?%s' % (parsed.path, qs)
     headers = {'X-Auth-Token': token}
+    if service_token:
+        headers['X-Service-Token'] = service_token
     method = 'GET'
     conn.request(method, full_path, '', headers)
     resp = conn.getresponse()
@@ -552,7 +556,7 @@ def get_account(url, token, marker=None, limit=None, prefix=None,
     return resp_headers, parse_api_response(resp_headers, body)
 
 
-def head_account(url, token, http_conn=None):
+def head_account(url, token, http_conn=None, service_token=None):
     """
     Get account stats.
 
@@ -560,6 +564,7 @@ def head_account(url, token, http_conn=None):
     :param token: auth token
     :param http_conn: HTTP connection object (If None, it will create the
                       conn object)
+    :param service_token: service auth token
     :returns: a dict containing the response's headers (all header names will
               be lowercase)
     :raises ClientException: HTTP HEAD request failed
@@ -570,6 +575,8 @@ def head_account(url, token, http_conn=None):
         parsed, conn = http_connection(url)
     method = "HEAD"
     headers = {'X-Auth-Token': token}
+    if service_token:
+        headers['X-Service-Token'] = service_token
     conn.request(method, parsed.path, '', headers)
     resp = conn.getresponse()
     body = resp.read()
@@ -585,7 +592,8 @@ def head_account(url, token, http_conn=None):
     return resp_headers
 
 
-def post_account(url, token, headers, http_conn=None, response_dict=None):
+def post_account(url, token, headers, http_conn=None, response_dict=None,
+                 service_token=None):
     """
     Update an account's metadata.
 
@@ -596,6 +604,7 @@ def post_account(url, token, headers, http_conn=None, response_dict=None):
                       conn object)
     :param response_dict: an optional dictionary into which to place
                      the response - status, reason and headers
+    :param service_token: service auth token
     :raises ClientException: HTTP POST request failed
     """
     if http_conn:
@@ -604,6 +613,8 @@ def post_account(url, token, headers, http_conn=None, response_dict=None):
         parsed, conn = http_connection(url)
     method = 'POST'
     headers['X-Auth-Token'] = token
+    if service_token:
+        headers['X-Service-Token'] = service_token
     conn.request(method, parsed.path, '', headers)
     resp = conn.getresponse()
     body = resp.read()
@@ -624,7 +635,7 @@ def post_account(url, token, headers, http_conn=None, response_dict=None):
 def get_container(url, token, container, marker=None, limit=None,
                   prefix=None, delimiter=None, end_marker=None,
                   path=None, http_conn=None,
-                  full_listing=False):
+                  full_listing=False, service_token=None):
     """
     Get a listing of objects for the container.
 
@@ -641,6 +652,7 @@ def get_container(url, token, container, marker=None, limit=None,
                       conn object)
     :param full_listing: if True, return a full listing, else returns a max
                          of 10000 listings
+    :param service_token: service auth token
     :returns: a tuple of (response headers, a list of objects) The response
               headers will be a dict and all header names will be lowercase.
     :raises ClientException: HTTP GET request failed
@@ -649,7 +661,8 @@ def get_container(url, token, container, marker=None, limit=None,
         http_conn = http_connection(url)
     if full_listing:
         rv = get_container(url, token, container, marker, limit, prefix,
-                           delimiter, end_marker, path, http_conn)
+                           delimiter, end_marker, path, http_conn,
+                           service_token)
         listing = rv[1]
         while listing:
             if not delimiter:
@@ -658,7 +671,7 @@ def get_container(url, token, container, marker=None, limit=None,
                 marker = listing[-1].get('name', listing[-1].get('subdir'))
             listing = get_container(url, token, container, marker, limit,
                                     prefix, delimiter, end_marker, path,
-                                    http_conn)[1]
+                                    http_conn, service_token)[1]
             if listing:
                 rv[1].extend(listing)
         return rv
@@ -678,6 +691,8 @@ def get_container(url, token, container, marker=None, limit=None,
     if path:
         qs += '&path=%s' % quote(path)
     headers = {'X-Auth-Token': token}
+    if service_token:
+        headers['X-Service-Token'] = service_token
     method = 'GET'
     conn.request(method, '%s?%s' % (cont_path, qs), '', headers)
     resp = conn.getresponse()
@@ -702,7 +717,8 @@ def get_container(url, token, container, marker=None, limit=None,
     return resp_headers, parse_api_response(resp_headers, body)
 
 
-def head_container(url, token, container, http_conn=None, headers=None):
+def head_container(url, token, container, http_conn=None, headers=None,
+                   service_token=None):
     """
     Get container stats.
 
@@ -711,6 +727,7 @@ def head_container(url, token, container, http_conn=None, headers=None):
     :param container: container name to get stats for
     :param http_conn: HTTP connection object (If None, it will create the
                       conn object)
+    :param service_token: service auth token
     :returns: a dict containing the response's headers (all header names will
               be lowercase)
     :raises ClientException: HTTP HEAD request failed
@@ -722,6 +739,8 @@ def head_container(url, token, container, http_conn=None, headers=None):
     path = '%s/%s' % (parsed.path, quote(container))
     method = 'HEAD'
     req_headers = {'X-Auth-Token': token}
+    if service_token:
+        req_headers['X-Service-Token'] = service_token
     if headers:
         req_headers.update(headers)
     conn.request(method, path, '', req_headers)
@@ -743,7 +762,7 @@ def head_container(url, token, container, http_conn=None, headers=None):
 
 
 def put_container(url, token, container, headers=None, http_conn=None,
-                  response_dict=None):
+                  response_dict=None, service_token=None):
     """
     Create a container
 
@@ -755,6 +774,7 @@ def put_container(url, token, container, headers=None, http_conn=None,
                       conn object)
     :param response_dict: an optional dictionary into which to place
                      the response - status, reason and headers
+    :param service_token: service auth token
     :raises ClientException: HTTP PUT request failed
     """
     if http_conn:
@@ -766,6 +786,8 @@ def put_container(url, token, container, headers=None, http_conn=None,
     if not headers:
         headers = {}
     headers['X-Auth-Token'] = token
+    if service_token:
+        headers['X-Service-Token'] = service_token
     if 'content-length' not in (k.lower() for k in headers):
         headers['Content-Length'] = '0'
     conn.request(method, path, '', headers)
@@ -785,7 +807,7 @@ def put_container(url, token, container, headers=None, http_conn=None,
 
 
 def post_container(url, token, container, headers, http_conn=None,
-                   response_dict=None):
+                   response_dict=None, service_token=None):
     """
     Update a container's metadata.
 
@@ -797,6 +819,7 @@ def post_container(url, token, container, headers, http_conn=None,
                       conn object)
     :param response_dict: an optional dictionary into which to place
                      the response - status, reason and headers
+    :param service_token: service auth token
     :raises ClientException: HTTP POST request failed
     """
     if http_conn:
@@ -806,6 +829,8 @@ def post_container(url, token, container, headers, http_conn=None,
     path = '%s/%s' % (parsed.path, quote(container))
     method = 'POST'
     headers['X-Auth-Token'] = token
+    if service_token:
+        headers['X-Service-Token'] = service_token
     if 'content-length' not in (k.lower() for k in headers):
         headers['Content-Length'] = '0'
     conn.request(method, path, '', headers)
@@ -825,7 +850,7 @@ def post_container(url, token, container, headers, http_conn=None,
 
 
 def delete_container(url, token, container, http_conn=None,
-                     response_dict=None):
+                     response_dict=None, service_token=None):
     """
     Delete a container
 
@@ -836,6 +861,7 @@ def delete_container(url, token, container, http_conn=None,
                       conn object)
     :param response_dict: an optional dictionary into which to place
                      the response - status, reason and headers
+    :param service_token: service auth token
     :raises ClientException: HTTP DELETE request failed
     """
     if http_conn:
@@ -844,6 +870,8 @@ def delete_container(url, token, container, http_conn=None,
         parsed, conn = http_connection(url)
     path = '%s/%s' % (parsed.path, quote(container))
     headers = {'X-Auth-Token': token}
+    if service_token:
+        headers['X-Service-Token'] = service_token
     method = 'DELETE'
     conn.request(method, path, '', headers)
     resp = conn.getresponse()
@@ -863,7 +891,7 @@ def delete_container(url, token, container, http_conn=None,
 
 def get_object(url, token, container, name, http_conn=None,
                resp_chunk_size=None, query_string=None,
-               response_dict=None, headers=None):
+               response_dict=None, headers=None, service_token=None):
     """
     Get an object
 
@@ -882,6 +910,7 @@ def get_object(url, token, container, name, http_conn=None,
                      the response - status, reason and headers
     :param headers: an optional dictionary with additional headers to include
                     in the request
+    :param service_token: service auth token
     :returns: a tuple of (response headers, the object's contents) The response
               headers will be a dict and all header names will be lowercase.
     :raises ClientException: HTTP GET request failed
@@ -896,6 +925,8 @@ def get_object(url, token, container, name, http_conn=None,
     method = 'GET'
     headers = headers.copy() if headers else {}
     headers['X-Auth-Token'] = token
+    if service_token:
+        headers['X-Service-Token'] = service_token
     conn.request(method, path, '', headers)
     resp = conn.getresponse()
 
@@ -923,7 +954,8 @@ def get_object(url, token, container, name, http_conn=None,
     return parsed_response['headers'], object_body
 
 
-def head_object(url, token, container, name, http_conn=None):
+def head_object(url, token, container, name, http_conn=None,
+                service_token=None):
     """
     Get object info
 
@@ -933,6 +965,7 @@ def head_object(url, token, container, name, http_conn=None):
     :param name: object name to get info for
     :param http_conn: HTTP connection object (If None, it will create the
                       conn object)
+    :param service_token: service auth token
     :returns: a dict containing the response's headers (all header names will
               be lowercase)
     :raises ClientException: HTTP HEAD request failed
@@ -944,6 +977,8 @@ def head_object(url, token, container, name, http_conn=None):
     path = '%s/%s/%s' % (parsed.path, quote(container), quote(name))
     method = 'HEAD'
     headers = {'X-Auth-Token': token}
+    if service_token:
+        headers['X-Service-Token'] = service_token
     conn.request(method, path, '', headers)
     resp = conn.getresponse()
     body = resp.read()
@@ -963,7 +998,7 @@ def head_object(url, token, container, name, http_conn=None):
 def put_object(url, token=None, container=None, name=None, contents=None,
                content_length=None, etag=None, chunk_size=None,
                content_type=None, headers=None, http_conn=None, proxy=None,
-               query_string=None, response_dict=None):
+               query_string=None, response_dict=None, service_token=None):
     """
     Put an object
 
@@ -994,6 +1029,7 @@ def put_object(url, token=None, container=None, name=None, contents=None,
     :param query_string: if set will be appended with '?' to generated path
     :param response_dict: an optional dictionary into which to place
                      the response - status, reason and headers
+    :param service_token: service auth token
     :returns: etag
     :raises ClientException: HTTP PUT request failed
     """
@@ -1014,6 +1050,8 @@ def put_object(url, token=None, container=None, name=None, contents=None,
         headers = {}
     if token:
         headers['X-Auth-Token'] = token
+    if service_token:
+        headers['X-Service-Token'] = service_token
     if etag:
         headers['ETag'] = etag.strip('"')
     if content_length is not None:
@@ -1067,7 +1105,7 @@ def put_object(url, token=None, container=None, name=None, contents=None,
 
 
 def post_object(url, token, container, name, headers, http_conn=None,
-                response_dict=None):
+                response_dict=None, service_token=None):
     """
     Update object metadata
 
@@ -1080,6 +1118,7 @@ def post_object(url, token, container, name, headers, http_conn=None,
                       conn object)
     :param response_dict: an optional dictionary into which to place
                      the response - status, reason and headers
+    :param service_token: service auth token
     :raises ClientException: HTTP POST request failed
     """
     if http_conn:
@@ -1088,6 +1127,8 @@ def post_object(url, token, container, name, headers, http_conn=None,
         parsed, conn = http_connection(url)
     path = '%s/%s/%s' % (parsed.path, quote(container), quote(name))
     headers['X-Auth-Token'] = token
+    if service_token:
+        headers['X-Service-Token'] = service_token
     conn.request('POST', path, '', headers)
     resp = conn.getresponse()
     body = resp.read()
@@ -1105,7 +1146,7 @@ def post_object(url, token, container, name, headers, http_conn=None,
 
 def delete_object(url, token=None, container=None, name=None, http_conn=None,
                   headers=None, proxy=None, query_string=None,
-                  response_dict=None):
+                  response_dict=None, service_token=None):
     """
     Delete object
 
@@ -1123,6 +1164,7 @@ def delete_object(url, token=None, container=None, name=None, http_conn=None,
     :param query_string: if set will be appended with '?' to generated path
     :param response_dict: an optional dictionary into which to place
                      the response - status, reason and headers
+    :param service_token: service auth token
     :raises ClientException: HTTP DELETE request failed
     """
     if http_conn:
@@ -1142,6 +1184,8 @@ def delete_object(url, token=None, container=None, name=None, http_conn=None,
         headers = {}
     if token:
         headers['X-Auth-Token'] = token
+    if service_token:
+        headers['X-Service-Token'] = service_token
     conn.request('DELETE', path, '', headers)
     resp = conn.getresponse()
     body = resp.read()
@@ -1184,7 +1228,19 @@ def get_capabilities(http_conn):
 
 
 class Connection(object):
-    """Convenience class to make requests that will also retry the request"""
+
+    """
+    Convenience class to make requests that will also retry the request
+
+    Requests will have an X-Auth-Token header whose value is either
+    the preauthtoken or a token obtained from the auth service using
+    the user credentials provided as args to the constructor. If
+    os_options includes a service_username then requests will also have
+    an X-Service-Token header whose value is a token obtained from the
+    auth service using the service credentials. In this case the request
+    url will be set to the storage_url obtained from the auth service
+    for the service user, unless this is overridden by a preauthurl.
+    """
 
     def __init__(self, authurl=None, user=None, key=None, retries=5,
                  preauthurl=None, preauthtoken=None, snet=False,
@@ -1209,7 +1265,8 @@ class Connection(object):
                             to an auth 2.0 system.
         :param os_options: The OpenStack options which can have tenant_id,
                            auth_token, service_type, endpoint_type,
-                           tenant_name, object_storage_url, region_name
+                           tenant_name, object_storage_url, region_name,
+                           service_username, service_project_name, service_key
         :param insecure: Allow to access servers without checking SSL certs.
                          The server's certificate will not be verified.
         :param ssl_compression: Whether to enable compression at the SSL layer.
@@ -1240,6 +1297,11 @@ class Connection(object):
             self.os_options['object_storage_url'] = preauthurl
         self.url = preauthurl or self.os_options.get('object_storage_url')
         self.token = preauthtoken or self.os_options.get('auth_token')
+        if self.os_options.get('service_username', None):
+            self.service_auth = True
+        else:
+            self.service_auth = False
+        self.service_token = None
         self.cacert = cacert
         self.insecure = insecure
         self.ssl_compression = ssl_compression
@@ -1267,6 +1329,24 @@ class Connection(object):
                                         timeout=self.timeout)
         return self.url, self.token
 
+    def get_service_auth(self):
+        opts = self.os_options
+        service_options = {}
+        service_options['tenant_name'] = opts.get('service_project_name', None)
+        service_options['region_name'] = opts.get('region_name', None)
+        service_options['object_storage_url'] = opts.get('object_storage_url',
+                                                         None)
+        service_user = opts.get('service_username', None)
+        service_key = opts.get('service_key', None)
+        return get_auth(self.authurl, service_user,
+                        service_key,
+                        snet=self.snet,
+                        auth_version=self.auth_version,
+                        os_options=service_options,
+                        cacert=self.cacert,
+                        insecure=self.insecure,
+                        timeout=self.timeout)
+
     def http_connection(self, url=None):
         return http_connection(url if url else self.url,
                                cacert=self.cacert,
@@ -1294,13 +1374,17 @@ class Connection(object):
                 if not self.url or not self.token:
                     self.url, self.token = self.get_auth()
                     self.http_conn = None
+                if self.service_auth and not self.service_token:
+                    self.url, self.service_token = self.get_service_auth()
+                    self.http_conn = None
                 self.auth_end_time = time()
                 if not self.http_conn:
                     self.http_conn = self.http_connection()
                 kwargs['http_conn'] = self.http_conn
                 if caller_response_dict is not None:
                     kwargs['response_dict'] = {}
-                rv = func(self.url, self.token, *args, **kwargs)
+                rv = func(self.url, self.token, *args,
+                          service_token=self.service_token, **kwargs)
                 self._add_response_dict(caller_response_dict, kwargs)
                 return rv
             except SSLError:
@@ -1317,7 +1401,7 @@ class Connection(object):
                     logger.exception(err)
                     raise
                 if err.http_status == 401:
-                    self.url = self.token = None
+                    self.url = self.token = self.service_token = None
                     if retried_auth or not all((self.authurl,
                                                 self.user,
                                                 self.key)):
diff --git a/tests/unit/test_swiftclient.py b/tests/unit/test_swiftclient.py
index cefae089..a65e712f 100644
--- a/tests/unit/test_swiftclient.py
+++ b/tests/unit/test_swiftclient.py
@@ -1333,7 +1333,7 @@ class TestConnection(MockHttpTest):
             # represenative of the unit under test.  The real get_auth
             # method will always return the os_option dict's
             # object_storage_url which will be overridden by the
-            # preauthurl paramater to Connection if it is provided.
+            # preauthurl parameter to Connection if it is provided.
             return 'http://www.new.com', 'new'
 
         def swap_sleep(*args):
@@ -1806,3 +1806,308 @@ class TestCloseConnection(MockHttpTest):
         self.assertIsInstance(http_conn_obj, c.HTTPConnection)
         self.assertFalse(hasattr(http_conn_obj, 'close'))
         conn.close()
+
+
+class TestServiceToken(MockHttpTest):
+
+    def setUp(self):
+        super(TestServiceToken, self).setUp()
+        self.os_options = {
+            'object_storage_url': 'http://storage_url.com',
+            'service_username': 'service_username',
+            'service_project_name': 'service_project_name',
+            'service_key': 'service_key'}
+
+    def get_connection(self):
+        conn = c.Connection('http://www.test.com', 'asdf', 'asdf',
+                            os_options=self.os_options)
+
+        self.assertTrue(isinstance(conn, c.Connection))
+        conn.get_auth = self.get_auth
+        conn.get_service_auth = self.get_service_auth
+
+        self.assertEqual(conn.attempts, 0)
+        self.assertEqual(conn.service_token, None)
+
+        self.assertTrue(isinstance(conn, c.Connection))
+        return conn
+
+    def get_auth(self):
+        # The real get_auth function will always return the os_option
+        # dict's object_storage_url which will be overridden by the
+        # preauthurl paramater to Connection if it is provided.
+        return self.os_options.get('object_storage_url'), 'token'
+
+    def get_service_auth(self):
+        # The real get_auth function will always return the os_option
+        # dict's object_storage_url which will be overridden by the
+        # preauthurl parameter to Connection if it is provided.
+        return self.os_options.get('object_storage_url'), 'stoken'
+
+    def test_service_token_reauth(self):
+        get_auth_call_list = []
+
+        def get_auth(url, user, key, **kwargs):
+            # The real get_auth function will always return the os_option
+            # dict's object_storage_url which will be overridden by the
+            # preauthurl parameter to Connection if it is provided.
+            args = {'url': url, 'user': user, 'key': key, 'kwargs': kwargs}
+            get_auth_call_list.append(args)
+            return_dict = {'asdf': 'new', 'service_username': 'newserv'}
+            storage_url = kwargs['os_options'].get('object_storage_url')
+            return storage_url, return_dict[user]
+
+        def swap_sleep(*args):
+            self.swap_sleep_called = True
+            c.get_auth = get_auth
+
+        with mock.patch('swiftclient.client.http_connection',
+                        self.fake_http_connection(401, 200)):
+            with mock.patch('swiftclient.client.sleep', swap_sleep):
+                self.swap_sleep_called = False
+
+                conn = c.Connection('http://www.test.com', 'asdf', 'asdf',
+                                    preauthurl='http://www.old.com',
+                                    preauthtoken='old',
+                                    os_options=self.os_options)
+
+                self.assertEqual(conn.attempts, 0)
+                self.assertEqual(conn.url, 'http://www.old.com')
+                self.assertEqual(conn.token, 'old')
+
+                conn.head_account()
+
+        self.assertTrue(self.swap_sleep_called)
+        self.assertEqual(conn.attempts, 2)
+        # The original 'preauth' storage URL *must* be preserved
+        self.assertEqual(conn.url, 'http://www.old.com')
+        self.assertEqual(conn.token, 'new')
+        self.assertEqual(conn.service_token, 'newserv')
+
+        # Check get_auth was called with expected args
+        auth_args = get_auth_call_list[0]
+        auth_kwargs = get_auth_call_list[0]['kwargs']
+        self.assertEqual('asdf', auth_args['user'])
+        self.assertEqual('asdf', auth_args['key'])
+        self.assertEqual('service_key',
+                         auth_kwargs['os_options']['service_key'])
+        self.assertEqual('service_username',
+                         auth_kwargs['os_options']['service_username'])
+        self.assertEqual('service_project_name',
+                         auth_kwargs['os_options']['service_project_name'])
+
+        auth_args = get_auth_call_list[1]
+        auth_kwargs = get_auth_call_list[1]['kwargs']
+        self.assertEqual('service_username', auth_args['user'])
+        self.assertEqual('service_key', auth_args['key'])
+        self.assertEqual('service_project_name',
+                         auth_kwargs['os_options']['tenant_name'])
+
+    def test_service_token_get_account(self):
+        with mock.patch('swiftclient.client.http_connection',
+                        self.fake_http_connection(200)):
+            with mock.patch('swiftclient.client.parse_api_response'):
+                conn = self.get_connection()
+                conn.get_account()
+        self.assertEqual(1, len(self.request_log), self.request_log)
+        for actual in self.iter_request_log():
+            self.assertEqual('GET', actual['method'])
+            actual_hdrs = actual['headers']
+            self.assertTrue('X-Service-Token' in actual_hdrs)
+            self.assertEqual('stoken', actual_hdrs['X-Service-Token'])
+            self.assertEqual('token', actual_hdrs['X-Auth-Token'])
+            self.assertEqual('http://storage_url.com/?format=json',
+                             actual['full_path'])
+        self.assertEqual(conn.attempts, 1)
+
+    def test_service_token_head_account(self):
+        with mock.patch('swiftclient.client.http_connection',
+                        self.fake_http_connection(200)):
+            conn = self.get_connection()
+            conn.head_account()
+        self.assertEqual(1, len(self.request_log), self.request_log)
+        for actual in self.iter_request_log():
+            self.assertEqual('HEAD', actual['method'])
+            actual_hdrs = actual['headers']
+            self.assertTrue('X-Service-Token' in actual_hdrs)
+            self.assertEqual('stoken', actual_hdrs['X-Service-Token'])
+            self.assertEqual('token', actual_hdrs['X-Auth-Token'])
+            self.assertEqual('http://storage_url.com', actual['full_path'])
+
+        self.assertEqual(conn.attempts, 1)
+
+    def test_service_token_post_account(self):
+        with mock.patch('swiftclient.client.http_connection',
+                        self.fake_http_connection(201)):
+            conn = self.get_connection()
+            conn.post_account(headers={})
+        self.assertEqual(1, len(self.request_log), self.request_log)
+        for actual in self.iter_request_log():
+            self.assertEqual('POST', actual['method'])
+            actual_hdrs = actual['headers']
+            self.assertTrue('X-Service-Token' in actual_hdrs)
+            self.assertEqual('stoken', actual_hdrs['X-Service-Token'])
+            self.assertEqual('token', actual_hdrs['X-Auth-Token'])
+            self.assertEqual('http://storage_url.com', actual['full_path'])
+        self.assertEqual(conn.attempts, 1)
+
+    def test_service_token_delete_container(self):
+        with mock.patch('swiftclient.client.http_connection',
+                        self.fake_http_connection(204)):
+            conn = self.get_connection()
+            conn.delete_container('container1')
+        self.assertEqual(1, len(self.request_log), self.request_log)
+        for actual in self.iter_request_log():
+            self.assertEqual('DELETE', actual['method'])
+            actual_hdrs = actual['headers']
+            self.assertTrue('X-Service-Token' in actual_hdrs)
+            self.assertEqual('stoken', actual_hdrs['X-Service-Token'])
+            self.assertEqual('token', actual_hdrs['X-Auth-Token'])
+            self.assertEqual('http://storage_url.com/container1',
+                             actual['full_path'])
+        self.assertEqual(conn.attempts, 1)
+
+    def test_service_token_get_container(self):
+        with mock.patch('swiftclient.client.http_connection',
+                        self.fake_http_connection(200)):
+            with mock.patch('swiftclient.client.parse_api_response'):
+                conn = self.get_connection()
+                conn.get_container('container1')
+        self.assertEqual(1, len(self.request_log), self.request_log)
+        for actual in self.iter_request_log():
+            self.assertEqual('GET', actual['method'])
+            actual_hdrs = actual['headers']
+            self.assertTrue('X-Service-Token' in actual_hdrs)
+            self.assertEqual('stoken', actual_hdrs['X-Service-Token'])
+            self.assertEqual('token', actual_hdrs['X-Auth-Token'])
+            self.assertEqual('http://storage_url.com/container1?format=json',
+                             actual['full_path'])
+        self.assertEqual(conn.attempts, 1)
+
+    def test_service_token_head_container(self):
+        with mock.patch('swiftclient.client.http_connection',
+                        self.fake_http_connection(200)):
+            conn = self.get_connection()
+            conn.head_container('container1')
+        self.assertEqual(1, len(self.request_log), self.request_log)
+        for actual in self.iter_request_log():
+            self.assertEqual('HEAD', actual['method'])
+            actual_hdrs = actual['headers']
+            self.assertTrue('X-Service-Token' in actual_hdrs)
+            self.assertEqual('stoken', actual_hdrs['X-Service-Token'])
+            self.assertEqual('token', actual_hdrs['X-Auth-Token'])
+            self.assertEqual('http://storage_url.com/container1',
+                             actual['full_path'])
+        self.assertEqual(conn.attempts, 1)
+
+    def test_service_token_post_container(self):
+        with mock.patch('swiftclient.client.http_connection',
+                        self.fake_http_connection(201)):
+            conn = self.get_connection()
+            conn.post_container('container1', {})
+        self.assertEqual(1, len(self.request_log), self.request_log)
+        for actual in self.iter_request_log():
+            self.assertEqual('POST', actual['method'])
+            actual_hdrs = actual['headers']
+            self.assertTrue('X-Service-Token' in actual_hdrs)
+            self.assertEqual('stoken', actual_hdrs['X-Service-Token'])
+            self.assertEqual('token', actual_hdrs['X-Auth-Token'])
+            self.assertEqual('http://storage_url.com/container1',
+                             actual['full_path'])
+        self.assertEqual(conn.attempts, 1)
+
+    def test_service_token_put_container(self):
+        with mock.patch('swiftclient.client.http_connection',
+                        self.fake_http_connection(200)):
+            conn = self.get_connection()
+            conn.put_container('container1')
+        self.assertEqual(1, len(self.request_log), self.request_log)
+        for actual in self.iter_request_log():
+            self.assertEqual('PUT', actual['method'])
+            actual_hdrs = actual['headers']
+            self.assertTrue('X-Service-Token' in actual_hdrs)
+            self.assertEqual('stoken', actual_hdrs['X-Service-Token'])
+            self.assertEqual('token', actual_hdrs['X-Auth-Token'])
+            self.assertEqual('http://storage_url.com/container1',
+                             actual['full_path'])
+        self.assertEqual(conn.attempts, 1)
+
+    def test_service_token_get_object(self):
+        with mock.patch('swiftclient.client.http_connection',
+                        self.fake_http_connection(200)):
+            conn = self.get_connection()
+            conn.get_object('container1', 'obj1')
+        self.assertEqual(1, len(self.request_log), self.request_log)
+        for actual in self.iter_request_log():
+            self.assertEqual('GET', actual['method'])
+            actual_hdrs = actual['headers']
+            self.assertTrue('X-Service-Token' in actual_hdrs)
+            self.assertEqual('stoken', actual_hdrs['X-Service-Token'])
+            self.assertEqual('token', actual_hdrs['X-Auth-Token'])
+            self.assertEqual('http://storage_url.com/container1/obj1',
+                             actual['full_path'])
+        self.assertEqual(conn.attempts, 1)
+
+    def test_service_token_head_object(self):
+        with mock.patch('swiftclient.client.http_connection',
+                        self.fake_http_connection(200)):
+            conn = self.get_connection()
+            conn.head_object('container1', 'obj1')
+        self.assertEqual(1, len(self.request_log), self.request_log)
+        for actual in self.iter_request_log():
+            self.assertEqual('HEAD', actual['method'])
+            actual_hdrs = actual['headers']
+            self.assertTrue('X-Service-Token' in actual_hdrs)
+            self.assertEqual('stoken', actual_hdrs['X-Service-Token'])
+            self.assertEqual('token', actual_hdrs['X-Auth-Token'])
+            self.assertEqual('http://storage_url.com/container1/obj1',
+                             actual['full_path'])
+        self.assertEqual(conn.attempts, 1)
+
+    def test_service_token_put_object(self):
+        with mock.patch('swiftclient.client.http_connection',
+                        self.fake_http_connection(200)):
+            conn = self.get_connection()
+            conn.put_object('container1', 'obj1', 'a_string')
+        self.assertEqual(1, len(self.request_log), self.request_log)
+        for actual in self.iter_request_log():
+            self.assertEqual('PUT', actual['method'])
+            actual_hdrs = actual['headers']
+            self.assertTrue('X-Service-Token' in actual_hdrs)
+            self.assertEqual('stoken', actual_hdrs['X-Service-Token'])
+            self.assertEqual('token', actual_hdrs['X-Auth-Token'])
+            self.assertEqual('http://storage_url.com/container1/obj1',
+                             actual['full_path'])
+        self.assertEqual(conn.attempts, 1)
+
+    def test_service_token_post_object(self):
+        with mock.patch('swiftclient.client.http_connection',
+                        self.fake_http_connection(202)):
+            conn = self.get_connection()
+            conn.post_object('container1', 'obj1', {})
+        self.assertEqual(1, len(self.request_log), self.request_log)
+        for actual in self.iter_request_log():
+            self.assertEqual('POST', actual['method'])
+            actual_hdrs = actual['headers']
+            self.assertTrue('X-Service-Token' in actual_hdrs)
+            self.assertEqual('stoken', actual_hdrs['X-Service-Token'])
+            self.assertEqual('token', actual_hdrs['X-Auth-Token'])
+            self.assertEqual('http://storage_url.com/container1/obj1',
+                             actual['full_path'])
+        self.assertEqual(conn.attempts, 1)
+
+    def test_service_token_delete_object(self):
+        with mock.patch('swiftclient.client.http_connection',
+                        self.fake_http_connection(202)):
+            conn = self.get_connection()
+            conn.delete_object('container1', 'obj1', 'a_string')
+        self.assertEqual(1, len(self.request_log), self.request_log)
+        for actual in self.iter_request_log():
+            self.assertEqual('DELETE', actual['method'])
+            actual_hdrs = actual['headers']
+            self.assertTrue('X-Service-Token' in actual_hdrs)
+            self.assertEqual('stoken', actual_hdrs['X-Service-Token'])
+            self.assertEqual('token', actual_hdrs['X-Auth-Token'])
+            self.assertEqual('http://storage_url.com/container1/obj1?a_string',
+                             actual['full_path'])
+        self.assertEqual(conn.attempts, 1)