def test_split_path(self): """ Test chase.common.utils.split_account_path """ self.assertRaises(ValueError, utils.split_path, '') self.assertRaises(ValueError, utils.split_path, '/') self.assertRaises(ValueError, utils.split_path, '//') self.assertEquals(utils.split_path('/a'), ['a']) self.assertRaises(ValueError, utils.split_path, '//a') self.assertEquals(utils.split_path('/a/'), ['a']) self.assertRaises(ValueError, utils.split_path, '/a/c') self.assertRaises(ValueError, utils.split_path, '//c') self.assertRaises(ValueError, utils.split_path, '/a/c/') self.assertRaises(ValueError, utils.split_path, '/a//') self.assertRaises(ValueError, utils.split_path, '/a', 2) self.assertRaises(ValueError, utils.split_path, '/a', 2, 3) self.assertRaises(ValueError, utils.split_path, '/a', 2, 3, True) self.assertEquals(utils.split_path('/a/c', 2), ['a', 'c']) self.assertEquals(utils.split_path('/a/c/o', 3), ['a', 'c', 'o']) self.assertRaises(ValueError, utils.split_path, '/a/c/o/r', 3, 3) self.assertEquals(utils.split_path('/a/c/o/r', 3, 3, True), ['a', 'c', 'o/r']) self.assertEquals(utils.split_path('/a/c', 2, 3, True), ['a', 'c', None]) self.assertRaises(ValueError, utils.split_path, '/a', 5, 4) self.assertEquals(utils.split_path('/a/c/', 2), ['a', 'c']) self.assertEquals(utils.split_path('/a/c/', 2, 3), ['a', 'c', '']) try: utils.split_path('o\nn e', 2) except ValueError, err: self.assertEquals(str(err), 'Invalid path: o%0An%20e')
def DELETE(self, req): """Handle HTTP DELETE request.""" try: drive, part, account = split_path(unquote(req.path), 3) except ValueError, err: return HTTPBadRequest(body=str(err), content_type='text/plain', request=req)
def __call__(self, env, start_response): """ Main hook into the WSGI paste.deploy filter/app pipeline. :param env: The WSGI environment dict. :param start_response: The WSGI start_response hook. """ env['staticweb.start_time'] = time.time() try: (self.version, self.account, self.container, self.obj) = \ split_path(env['PATH_INFO'], 2, 4, True) except ValueError: return self.app(env, start_response) memcache_client = cache_from_env(env) if memcache_client: if env['REQUEST_METHOD'] in ('PUT', 'POST'): if not self.obj and self.container: memcache_key = '/staticweb/%s/%s/%s' % \ (self.version, self.account, self.container) memcache_client.delete(memcache_key) return self.app(env, start_response) if (env['REQUEST_METHOD'] not in ('HEAD', 'GET') or (env.get('REMOTE_USER') and env.get('HTTP_X_WEB_MODE', 'f').lower() not in TRUE_VALUES) or (not env.get('REMOTE_USER') and env.get('HTTP_X_WEB_MODE', 't').lower() not in TRUE_VALUES)): return self.app(env, start_response) if self.obj: return self._handle_object(env, start_response) elif self.container: return self._handle_container(env, start_response) return self.app(env, start_response)
def GET(self, req): """Handle HTTP GET request.""" try: drive, part, account, container, obj = split_path( unquote(req.path), 4, 5, True) except ValueError, err: return HTTPBadRequest(body=str(err), content_type='text/plain', request=req)
def DELETE(self, request): """Handle HTTP DELETE requests for the Chase Object Server.""" try: device, partition, account, container, obj = \ split_path(unquote(request.path), 5, 5, True) except ValueError, e: return HTTPBadRequest(body=str(e), request=request, content_type='text/plain')
def REPLICATE(self, req): """ Handle HTTP REPLICATE request (json-encoded RPC calls for replication.) """ try: post_args = split_path(unquote(req.path), 3) except ValueError, err: return HTTPBadRequest(body=str(err), content_type='text/plain', request=req)
def get_controller(self, path): container, obj = split_path(path, 0, 2, True) d = dict(container_name=container, object_name=obj) if container and obj: return ObjectController, d elif container: return BucketController, d return ServiceController, d
def HEAD(self, request): """Handle HTTP HEAD requests for the Chase Object Server.""" try: device, partition, account, container, obj = \ split_path(unquote(request.path), 5, 5, True) except ValueError, err: resp = HTTPBadRequest(request=request) resp.content_type = 'text/plain' resp.body = str(err) return resp
def REPLICATE(self, request): """ Handle REPLICATE requests for the Chase Object Server. This is used by the object replicator to get hashes for directories. """ try: device, partition, suffix = split_path( unquote(request.path), 2, 3, True) except ValueError, e: return HTTPBadRequest(body=str(e), request=request, content_type='text/plain')
def GET(self, req): error = False root, type = split_path(req.path, 1, 2, False) try: if type == "mem": content = json.dumps(self.get_mem()) elif type == "load": try: content = json.dumps(self.get_load(), sort_keys=True) except IOError as e: error = True content = "load - %s" % e elif type == "async": try: content = json.dumps(self.get_async_info()) except IOError as e: error = True content = "async - %s" % e elif type == "replication": try: content = json.dumps(self.get_replication_info()) except IOError as e: error = True content = "replication - %s" % e elif type == "mounted": content = json.dumps(self.get_mounted()) elif type == "unmounted": content = json.dumps(self.get_unmounted()) elif type == "diskusage": content = json.dumps(self.get_diskusage()) elif type == "ringmd5": content = json.dumps(self.get_ring_md5()) elif type == "quarantined": content = json.dumps(self.get_quarantine_count()) elif type == "sockstat": content = json.dumps(self.get_socket_info()) else: content = "Invalid path: %s" % req.path return Response(request=req, status="400 Bad Request", \ body=content, content_type="text/plain") except ValueError as e: error = True content = "ValueError: %s" % e if not error: return Response(request=req, body=content, \ content_type="application/json") else: msg = 'CRITICAL recon - %s' % str(content) self.logger.critical(msg) body = "Internal server error." return Response(request=req, status="500 Server Error", \ body=body, content_type="text/plain")
def HEAD(self, req): """Handle HTTP HEAD request.""" # TODO(refactor): The account server used to provide a 'account and # container existence check all-in-one' call by doing a HEAD with a # container path. However, container existence is now checked with the # container servers directly so this is no longer needed. We should # refactor out the container existence check here and retest # everything. try: drive, part, account, container = split_path(unquote(req.path), 3, 4) except ValueError, err: return HTTPBadRequest(body=str(err), content_type='text/plain', request=req)
def HEAD(self, req): """Handle HTTP HEAD request.""" # TODO(refactor): The account server used to provide a 'account and # container existence check all-in-one' call by doing a HEAD with a # container path. However, container existence is now checked with the # container servers directly so this is no longer needed. We should # refactor out the container existence check here and retest # everything. try: drive, part, account, container = split_path( unquote(req.path), 3, 4) except ValueError, err: return HTTPBadRequest(body=str(err), content_type='text/plain', request=req)
def authorize(self, req): """ Returns None if the request is authorized to continue or a standard WSGI response callable if not. """ try: version, account, container, obj = split_path(req.path, 1, 4, True) except ValueError: return HTTPNotFound(request=req) if not account or not account.startswith(self.reseller_prefix): return self.denied_response(req) user_groups = (req.remote_user or '').split(',') if '.reseller_admin' in user_groups and \ account != self.reseller_prefix and \ account[len(self.reseller_prefix)] != '.': req.environ['chase_owner'] = True return None if account in user_groups and \ (req.method not in ('DELETE', 'PUT') or container): # If the user is admin for the account and is not trying to do an # account DELETE or PUT... req.environ['chase_owner'] = True return None if (req.environ.get('chase_sync_key') and req.environ['chase_sync_key'] == req.headers.get('x-container-sync-key', None) and 'x-timestamp' in req.headers and (req.remote_addr in self.allowed_sync_hosts or get_remote_client(req) in self.allowed_sync_hosts)): return None referrers, groups = parse_acl(getattr(req, 'acl', None)) if referrer_allowed(req.referer, referrers): if obj or '.rlistings' in groups: return None return self.denied_response(req) if not req.remote_user: return self.denied_response(req) for user_group in user_groups: if user_group in groups: return None return self.denied_response(req)
def authorize(self, req): """ Returns None if the request is authorized to continue or a standard WSGI response callable if not. """ try: version, account, container, obj = split_path(req.path, 1, 4, True) except ValueError: return HTTPNotFound(request=req) if not account or not account.startswith(self.reseller_prefix): return self.denied_response(req) user_groups = (req.remote_user or '').split(',') if '.reseller_admin' in user_groups and \ account != self.reseller_prefix and \ account[len(self.reseller_prefix)] != '.': req.environ['chase_owner'] = True return None if account in user_groups and \ (req.method not in ('DELETE', 'PUT') or container): # If the user is admin for the account and is not trying to do an # account DELETE or PUT... req.environ['chase_owner'] = True return None if (req.environ.get('chase_sync_key') and req.environ['chase_sync_key'] == req.headers.get( 'x-container-sync-key', None) and 'x-timestamp' in req.headers and (req.remote_addr in self.allowed_sync_hosts or get_remote_client(req) in self.allowed_sync_hosts)): return None referrers, groups = parse_acl(getattr(req, 'acl', None)) if referrer_allowed(req.referer, referrers): if obj or '.rlistings' in groups: return None return self.denied_response(req) if not req.remote_user: return self.denied_response(req) for user_group in user_groups: if user_group in groups: return None return self.denied_response(req)
def handle_request(self, req): """ Entry point for auth requests (ones that match the self.auth_prefix). Should return a WSGI-style callable (such as webob.Response). :param req: webob.Request object """ req.start_time = time() handler = None try: version, account, user, _junk = split_path(req.path_info, minsegs=1, maxsegs=4, rest_with_last=True) except ValueError: return HTTPNotFound(request=req) if version in ('v1', 'v1.0', 'auth'): if req.method == 'GET': handler = self.handle_get_token if not handler: req.response = HTTPBadRequest(request=req) else: req.response = handler(req) return req.response
def authorize_via_acl(self, req): """Anon request handling. For now this only allows anon read of objects. Container and account actions are prohibited. """ self.log.debug("authorizing anonymous request") try: version, account, container, obj = split_path(req.path, 1, 4, True) except ValueError: return HTTPNotFound(request=req) if obj: return self._authorize_anon_object(req, account, container, obj) if container: return self._authorize_anon_container(req, account, container) if account: return self._authorize_anon_account(req, account) return self._authorize_anon_toplevel(req)
def authorize_via_acl(self, req): """Anon request handling. For now this only allows anon read of objects. Container and account actions are prohibited. """ self.log.debug('authorizing anonymous request') try: version, account, container, obj = split_path(req.path, 1, 4, True) except ValueError: return HTTPNotFound(request=req) if obj: return self._authorize_anon_object(req, account, container, obj) if container: return self._authorize_anon_container(req, account, container) if account: return self._authorize_anon_account(req, account) return self._authorize_anon_toplevel(req)
def __call__(self, env, start_response): """ WSGI entry point. Wraps env in webob.Request object and passes it down. :param env: WSGI environment dictionary :param start_response: WSGI callable """ req = Request(env) if self.memcache_client is None: self.memcache_client = cache_from_env(env) if not self.memcache_client: self.logger.warning( _('Warning: Cannot ratelimit without a memcached client')) return self.app(env, start_response) try: version, account, container, obj = split_path(req.path, 1, 4, True) except ValueError: return HTTPNotFound()(env, start_response) ratelimit_resp = self.handle_ratelimit(req, account, container, obj) if ratelimit_resp is None: return self.app(env, start_response) else: return ratelimit_resp(env, start_response)
def handle_get_token(self, req): """ Handles the various `request for token and service end point(s)` calls. There are various formats to support the various auth servers in the past. Examples:: GET <auth-prefix>/v1/<act>/auth X-Auth-User: <act>:<usr> or X-Storage-User: <usr> X-Auth-Key: <key> or X-Storage-Pass: <key> GET <auth-prefix>/auth X-Auth-User: <act>:<usr> or X-Storage-User: <act>:<usr> X-Auth-Key: <key> or X-Storage-Pass: <key> GET <auth-prefix>/v1.0 X-Auth-User: <act>:<usr> or X-Storage-User: <act>:<usr> X-Auth-Key: <key> or X-Storage-Pass: <key> On successful authentication, the response will have X-Auth-Token and X-Storage-Token set to the token to use with Chase and X-Storage-URL set to the URL to the default Chase cluster to use. :param req: The webob.Request to process. :returns: webob.Response, 2xx on success with data set as explained above. """ # Validate the request info try: pathsegs = split_path(req.path_info, minsegs=1, maxsegs=3, rest_with_last=True) except ValueError: return HTTPNotFound(request=req) if pathsegs[0] == 'v1' and pathsegs[2] == 'auth': account = pathsegs[1] user = req.headers.get('x-storage-user') if not user: user = req.headers.get('x-auth-user') if not user or ':' not in user: return HTTPUnauthorized(request=req) account2, user = user.split(':', 1) if account != account2: return HTTPUnauthorized(request=req) key = req.headers.get('x-storage-pass') if not key: key = req.headers.get('x-auth-key') elif pathsegs[0] in ('auth', 'v1.0'): user = req.headers.get('x-auth-user') if not user: user = req.headers.get('x-storage-user') if not user or ':' not in user: return HTTPUnauthorized(request=req) account, user = user.split(':', 1) key = req.headers.get('x-auth-key') if not key: key = req.headers.get('x-storage-pass') else: return HTTPBadRequest(request=req) if not all((account, user, key)): return HTTPUnauthorized(request=req) # Authenticate user account_user = account + ':' + user if account_user not in self.users: return HTTPUnauthorized(request=req) if self.users[account_user]['key'] != key: return HTTPUnauthorized(request=req) # Get memcache client memcache_client = cache_from_env(req.environ) if not memcache_client: raise Exception('Memcache required') # See if a token already exists and hasn't expired token = None memcache_user_key = '%s/user/%s' % (self.reseller_prefix, account_user) candidate_token = memcache_client.get(memcache_user_key) if candidate_token: memcache_token_key = \ '%s/token/%s' % (self.reseller_prefix, candidate_token) cached_auth_data = memcache_client.get(memcache_token_key) if cached_auth_data: expires, groups = cached_auth_data if expires > time(): token = candidate_token # Create a new token if one didn't exist if not token: # Generate new token token = '%stk%s' % (self.reseller_prefix, uuid4().hex) expires = time() + self.token_life groups = [account, account_user] groups.extend(self.users[account_user]['groups']) if '.admin' in groups: groups.remove('.admin') account_id = self.users[account_user]['url'].rsplit('/', 1)[-1] groups.append(account_id) groups = ','.join(groups) # Save token memcache_token_key = '%s/token/%s' % (self.reseller_prefix, token) memcache_client.set(memcache_token_key, (expires, groups), timeout=float(expires - time())) # Record the token with the user info for future use. memcache_user_key = \ '%s/user/%s' % (self.reseller_prefix, account_user) memcache_client.set(memcache_user_key, token, timeout=float(expires - time())) return Response(request=req, headers={ 'x-auth-token': token, 'x-storage-token': token, 'x-storage-url': self.users[account_user]['url'] })
def __call__(self, env, start_response): """ Accepts a standard WSGI application call, authenticating the request and installing callback hooks for authorization and ACL header validation. For an authenticated request, REMOTE_USER will be set to a comma separated list of the user's groups. With a non-empty reseller prefix, acts as the definitive auth service for just tokens and accounts that begin with that prefix, but will deny requests outside this prefix if no other auth middleware overrides it. With an empty reseller prefix, acts as the definitive auth service only for tokens that validate to a non-empty set of groups. For all other requests, acts as the fallback auth service when no other auth middleware overrides it. Alternatively, if the request matches the self.auth_prefix, the request will be routed through the internal auth request handler (self.handle). This is to handle granting tokens, etc. """ if env.get('PATH_INFO', '').startswith(self.auth_prefix): return self.handle(env, start_response) s3 = env.get('HTTP_AUTHORIZATION') token = env.get('HTTP_X_AUTH_TOKEN', env.get('HTTP_X_STORAGE_TOKEN')) if s3 or (token and token.startswith(self.reseller_prefix)): # Note: Empty reseller_prefix will match all tokens. groups = self.get_groups(env, token) if groups: env['REMOTE_USER'] = groups user = groups and groups.split(',', 1)[0] or '' # We know the proxy logs the token, so we augment it just a bit # to also log the authenticated user. env['HTTP_X_AUTH_TOKEN'] = \ '%s,%s' % (user, 's3' if s3 else token) env['chase.authorize'] = self.authorize env['chase.clean_acl'] = clean_acl else: # Unauthorized token if self.reseller_prefix: # Because I know I'm the definitive auth for this token, I # can deny it outright. return HTTPUnauthorized()(env, start_response) # Because I'm not certain if I'm the definitive auth for empty # reseller_prefixed tokens, I won't overwrite chase.authorize. elif 'chase.authorize' not in env: env['chase.authorize'] = self.denied_response else: if self.reseller_prefix: # With a non-empty reseller_prefix, I would like to be called # back for anonymous access to accounts I know I'm the # definitive auth for. try: version, rest = split_path(env.get('PATH_INFO', ''), 1, 2, True) except ValueError: return HTTPNotFound()(env, start_response) if rest and rest.startswith(self.reseller_prefix): # Handle anonymous access to accounts I'm the definitive # auth for. env['chase.authorize'] = self.authorize env['chase.clean_acl'] = clean_acl # Not my token, not my account, I can't authorize this request, # deny all is a good idea if not already set... elif 'chase.authorize' not in env: env['chase.authorize'] = self.denied_response # Because I'm not certain if I'm the definitive auth for empty # reseller_prefixed accounts, I won't overwrite chase.authorize. elif 'chase.authorize' not in env: env['chase.authorize'] = self.authorize env['chase.clean_acl'] = clean_acl return self.app(env, start_response)
class TestUtils(unittest.TestCase): """ Tests for chase.common.utils """ def setUp(self): utils.HASH_PATH_SUFFIX = 'endcap' def test_normalize_timestamp(self): """ Test chase.common.utils.normalize_timestamp """ self.assertEquals(utils.normalize_timestamp('1253327593.48174'), "1253327593.48174") self.assertEquals(utils.normalize_timestamp(1253327593.48174), "1253327593.48174") self.assertEquals(utils.normalize_timestamp('1253327593.48'), "1253327593.48000") self.assertEquals(utils.normalize_timestamp(1253327593.48), "1253327593.48000") self.assertEquals(utils.normalize_timestamp('253327593.48'), "0253327593.48000") self.assertEquals(utils.normalize_timestamp(253327593.48), "0253327593.48000") self.assertEquals(utils.normalize_timestamp('1253327593'), "1253327593.00000") self.assertEquals(utils.normalize_timestamp(1253327593), "1253327593.00000") self.assertRaises(ValueError, utils.normalize_timestamp, '') self.assertRaises(ValueError, utils.normalize_timestamp, 'abc') def test_mkdirs(self): testroot = os.path.join(os.path.dirname(__file__), 'mkdirs') try: os.unlink(testroot) except Exception: pass rmtree(testroot, ignore_errors=1) self.assert_(not os.path.exists(testroot)) utils.mkdirs(testroot) self.assert_(os.path.exists(testroot)) utils.mkdirs(testroot) self.assert_(os.path.exists(testroot)) rmtree(testroot, ignore_errors=1) testdir = os.path.join(testroot, 'one/two/three') self.assert_(not os.path.exists(testdir)) utils.mkdirs(testdir) self.assert_(os.path.exists(testdir)) utils.mkdirs(testdir) self.assert_(os.path.exists(testdir)) rmtree(testroot, ignore_errors=1) open(testroot, 'wb').close() self.assert_(not os.path.exists(testdir)) self.assertRaises(OSError, utils.mkdirs, testdir) os.unlink(testroot) def test_split_path(self): """ Test chase.common.utils.split_account_path """ self.assertRaises(ValueError, utils.split_path, '') self.assertRaises(ValueError, utils.split_path, '/') self.assertRaises(ValueError, utils.split_path, '//') self.assertEquals(utils.split_path('/a'), ['a']) self.assertRaises(ValueError, utils.split_path, '//a') self.assertEquals(utils.split_path('/a/'), ['a']) self.assertRaises(ValueError, utils.split_path, '/a/c') self.assertRaises(ValueError, utils.split_path, '//c') self.assertRaises(ValueError, utils.split_path, '/a/c/') self.assertRaises(ValueError, utils.split_path, '/a//') self.assertRaises(ValueError, utils.split_path, '/a', 2) self.assertRaises(ValueError, utils.split_path, '/a', 2, 3) self.assertRaises(ValueError, utils.split_path, '/a', 2, 3, True) self.assertEquals(utils.split_path('/a/c', 2), ['a', 'c']) self.assertEquals(utils.split_path('/a/c/o', 3), ['a', 'c', 'o']) self.assertRaises(ValueError, utils.split_path, '/a/c/o/r', 3, 3) self.assertEquals(utils.split_path('/a/c/o/r', 3, 3, True), ['a', 'c', 'o/r']) self.assertEquals(utils.split_path('/a/c', 2, 3, True), ['a', 'c', None]) self.assertRaises(ValueError, utils.split_path, '/a', 5, 4) self.assertEquals(utils.split_path('/a/c/', 2), ['a', 'c']) self.assertEquals(utils.split_path('/a/c/', 2, 3), ['a', 'c', '']) try: utils.split_path('o\nn e', 2) except ValueError, err: self.assertEquals(str(err), 'Invalid path: o%0An%20e') try: utils.split_path('o\nn e', 2, 3, True) except ValueError, err: self.assertEquals(str(err), 'Invalid path: o%0An%20e')
def handle_get_token(self, req): """ Handles the various `request for token and service end point(s)` calls. There are various formats to support the various auth servers in the past. Examples:: GET <auth-prefix>/v1/<act>/auth X-Auth-User: <act>:<usr> or X-Storage-User: <usr> X-Auth-Key: <key> or X-Storage-Pass: <key> GET <auth-prefix>/auth X-Auth-User: <act>:<usr> or X-Storage-User: <act>:<usr> X-Auth-Key: <key> or X-Storage-Pass: <key> GET <auth-prefix>/v1.0 X-Auth-User: <act>:<usr> or X-Storage-User: <act>:<usr> X-Auth-Key: <key> or X-Storage-Pass: <key> On successful authentication, the response will have X-Auth-Token and X-Storage-Token set to the token to use with Chase and X-Storage-URL set to the URL to the default Chase cluster to use. :param req: The webob.Request to process. :returns: webob.Response, 2xx on success with data set as explained above. """ # Validate the request info try: pathsegs = split_path(req.path_info, minsegs=1, maxsegs=3, rest_with_last=True) except ValueError: return HTTPNotFound(request=req) if pathsegs[0] == 'v1' and pathsegs[2] == 'auth': account = pathsegs[1] user = req.headers.get('x-storage-user') if not user: user = req.headers.get('x-auth-user') if not user or ':' not in user: return HTTPUnauthorized(request=req) account2, user = user.split(':', 1) if account != account2: return HTTPUnauthorized(request=req) key = req.headers.get('x-storage-pass') if not key: key = req.headers.get('x-auth-key') elif pathsegs[0] in ('auth', 'v1.0'): user = req.headers.get('x-auth-user') if not user: user = req.headers.get('x-storage-user') if not user or ':' not in user: return HTTPUnauthorized(request=req) account, user = user.split(':', 1) key = req.headers.get('x-auth-key') if not key: key = req.headers.get('x-storage-pass') else: return HTTPBadRequest(request=req) if not all((account, user, key)): return HTTPUnauthorized(request=req) # Authenticate user account_user = account + ':' + user if account_user not in self.users: return HTTPUnauthorized(request=req) if self.users[account_user]['key'] != key: return HTTPUnauthorized(request=req) # Get memcache client memcache_client = cache_from_env(req.environ) if not memcache_client: raise Exception('Memcache required') # See if a token already exists and hasn't expired token = None memcache_user_key = '%s/user/%s' % (self.reseller_prefix, account_user) candidate_token = memcache_client.get(memcache_user_key) if candidate_token: memcache_token_key = \ '%s/token/%s' % (self.reseller_prefix, candidate_token) cached_auth_data = memcache_client.get(memcache_token_key) if cached_auth_data: expires, groups = cached_auth_data if expires > time(): token = candidate_token # Create a new token if one didn't exist if not token: # Generate new token token = '%stk%s' % (self.reseller_prefix, uuid4().hex) expires = time() + self.token_life groups = [account, account_user] groups.extend(self.users[account_user]['groups']) if '.admin' in groups: groups.remove('.admin') account_id = self.users[account_user]['url'].rsplit('/', 1)[-1] groups.append(account_id) groups = ','.join(groups) # Save token memcache_token_key = '%s/token/%s' % (self.reseller_prefix, token) memcache_client.set(memcache_token_key, (expires, groups), timeout=float(expires - time())) # Record the token with the user info for future use. memcache_user_key = \ '%s/user/%s' % (self.reseller_prefix, account_user) memcache_client.set(memcache_user_key, token, timeout=float(expires - time())) return Response(request=req, headers={'x-auth-token': token, 'x-storage-token': token, 'x-storage-url': self.users[account_user]['url']})