Added discoverable capabilities.

Swift can now optionally be configured to allow requests to '/info',
providing information about the swift cluster.  Additionally a HMAC
signed requests to
'/info?swiftinfo_sig=<sign>&swiftinfo_expires=<expires>' can be
configured allowing privileged access to more sensitive information
not meant to be public.

DocImpact
Change-Id: I2379360fbfe3d9e9e8b25f1dc34517d199574495
Implements: blueprint capabilities
Closes-Bug: #1245694
This commit is contained in:
Richard (Rick) Hawkins 2013-10-16 19:28:37 -05:00
parent 34eb76fc57
commit 2c4bf81464
22 changed files with 741 additions and 47 deletions

View File

@ -15,7 +15,7 @@
import hmac
from hashlib import sha1
from os.path import basename
from sys import argv, exit
from sys import argv, exit, stderr
from time import time
@ -59,9 +59,11 @@ if __name__ == '__main__':
# have '/'s.
if len(parts) != 5 or parts[0] or parts[1] != 'v1' or not parts[2] or \
not parts[3] or not parts[4].strip('/'):
print '<path> must point to an object.'
print 'For example: /v1/account/container/object'
exit(1)
stderr.write(
'WARNING: "%s" does not refer to an object '
'(e.g. /v1/account/container/object).\n' % path)
stderr.write(
'WARNING: Non-object paths will be rejected by tempurl.\n')
sig = hmac.new(key, '%s\n%s\n%s' % (method, expires, path),
sha1).hexdigest()
print '%s?temp_url_sig=%s&temp_url_expires=%s' % (path, sig, expires)

View File

@ -216,3 +216,22 @@ List Endpoints
.. automodule:: swift.common.middleware.list_endpoints
:members:
:show-inheritance:
Discoverability
===============
Swift can optionally be configured to provide clients with an interface
providing details about the installation. If configured, a GET request to
/info will return configuration data in JSON format. An example
response::
{"swift": {"version": "1.8.1"}, "staticweb": {}, "tempurl": {}}
This would signify to the client that swift version 1.8.1 is running and that
staticweb and tempurl are available in this installation.
There may be administrator-only information available via /info. To
retrieve it, one must use an HMAC-signed request, similar to TempURL.
The signature may be produced like so:
swift-temp-url GET 3600 /info secret 2>/dev/null | sed s/temp_url/swiftinfo/g

View File

@ -5,7 +5,20 @@
# backlog = 4096
# swift_dir = /etc/swift
# user = swift
# Enables exposing configuration settings via HTTP GET /info.
# expose_info = true
# Key to use for admin calls that are HMAC signed. Default is empty,
# which will disable admin calls to /info.
# admin_key = secret_admin_key
#
# Allows the ability to withhold sections from showing up in the public
# calls to /info. The following would cause the sections 'container_quotas'
# and 'tempurl' to not be listed. Default is empty, allowing all registered
# fetures to be listed via HTTP GET /info.
# disallowed_sections = container_quotas, tempurl
# Use an integer to override the number of pre-forked processes that will
# accept connections. Should default to the number of effective cpu
# cores in the system. It's worth noting that individual workers will

View File

@ -48,6 +48,7 @@ post -m quota-bytes:
from swift.common.swob import HTTPForbidden, HTTPRequestEntityTooLarge, \
HTTPBadRequest, wsgify
from swift.common.utils import register_swift_info
from swift.proxy.controllers.base import get_account_info, get_object_info
@ -132,6 +133,8 @@ class AccountQuotaMiddleware(object):
def filter_factory(global_conf, **local_conf):
"""Returns a WSGI filter app for use with paste.deploy."""
register_swift_info('account_quotas')
def account_quota_filter(app):
return AccountQuotaMiddleware(app)
return account_quota_filter

View File

@ -22,7 +22,7 @@ from swift.common.swob import Request, HTTPBadGateway, \
HTTPCreated, HTTPBadRequest, HTTPNotFound, HTTPUnauthorized, HTTPOk, \
HTTPPreconditionFailed, HTTPRequestEntityTooLarge, HTTPNotAcceptable, \
HTTPLengthRequired, HTTPException, HTTPServerError, wsgify
from swift.common.utils import json, get_logger
from swift.common.utils import json, get_logger, register_swift_info
from swift.common.constraints import check_utf8, MAX_FILE_SIZE
from swift.common.http import HTTP_UNAUTHORIZED, HTTP_NOT_FOUND
from swift.common.constraints import MAX_OBJECT_NAME_LENGTH, \
@ -542,6 +542,7 @@ class Bulk(object):
def filter_factory(global_conf, **local_conf):
conf = global_conf.copy()
conf.update(local_conf)
register_swift_info('bulk')
def bulk_filter(app):
return Bulk(app, conf)

View File

@ -43,8 +43,9 @@ set:
"""
from swift.common.http import is_success
from swift.proxy.controllers.base import get_container_info, get_object_info
from swift.common.swob import Response, HTTPBadRequest, wsgify
from swift.common.utils import register_swift_info
from swift.proxy.controllers.base import get_container_info, get_object_info
class ContainerQuotaMiddleware(object):
@ -113,6 +114,8 @@ class ContainerQuotaMiddleware(object):
def filter_factory(global_conf, **local_conf):
register_swift_info('container_quotas')
def container_quota_filter(app):
return ContainerQuotaMiddleware(app)
return container_quota_filter

View File

@ -14,6 +14,7 @@
# limitations under the License.
from swift.common.swob import Request, Response
from swift.common.utils import register_swift_info
class CrossDomainMiddleware(object):
@ -84,6 +85,7 @@ class CrossDomainMiddleware(object):
def filter_factory(global_conf, **local_conf):
conf = global_conf.copy()
conf.update(local_conf)
register_swift_info('crossdomain')
def crossdomain_filter(app):
return CrossDomainMiddleware(app, conf)

View File

@ -110,7 +110,7 @@ from time import time
from urllib import quote
from swift.common.middleware.tempurl import get_tempurl_keys_from_metadata
from swift.common.utils import streq_const_time
from swift.common.utils import streq_const_time, register_swift_info
from swift.common.wsgi import make_pre_authed_env
from swift.common.swob import HTTPUnauthorized
from swift.proxy.controllers.base import get_account_info
@ -502,4 +502,5 @@ def filter_factory(global_conf, **local_conf):
"""Returns the WSGI filter for use with paste.deploy."""
conf = global_conf.copy()
conf.update(local_conf)
register_swift_info('formpost')
return lambda app: FormPost(app, conf)

View File

@ -15,6 +15,7 @@
from swift.common import utils as swift_utils
from swift.common.middleware import acl as swift_acl
from swift.common.swob import HTTPNotFound, HTTPForbidden, HTTPUnauthorized
from swift.common.utils import register_swift_info
class KeystoneAuth(object):
@ -334,6 +335,7 @@ def filter_factory(global_conf, **local_conf):
"""Returns a WSGI filter app for use with paste.deploy."""
conf = global_conf.copy()
conf.update(local_conf)
register_swift_info('keystoneauth')
def auth_filter(app):
return KeystoneAuth(app, conf)

View File

@ -17,7 +17,7 @@ from swift import gettext_ as _
import eventlet
from swift.common.utils import cache_from_env, get_logger
from swift.common.utils import cache_from_env, get_logger, register_swift_info
from swift.proxy.controllers.base import get_container_memcache_key
from swift.common.memcached import MemcacheConnectionError
from swift.common.swob import Request, Response
@ -274,6 +274,7 @@ def filter_factory(global_conf, **local_conf):
"""
conf = global_conf.copy()
conf.update(local_conf)
register_swift_info('ratelimit')
def limit_filter(app):
return RateLimitMiddleware(app, conf)

View File

@ -143,7 +143,8 @@ from swift.common.swob import Request, HTTPBadRequest, HTTPServerError, \
HTTPMethodNotAllowed, HTTPRequestEntityTooLarge, HTTPLengthRequired, \
HTTPOk, HTTPPreconditionFailed, HTTPException, HTTPNotFound, \
HTTPUnauthorized
from swift.common.utils import json, get_logger, config_true_value
from swift.common.utils import (json, get_logger, config_true_value,
register_swift_info)
from swift.common.constraints import check_utf8, MAX_BUFFERED_SLO_SEGMENTS
from swift.common.http import HTTP_NOT_FOUND, HTTP_UNAUTHORIZED
from swift.common.wsgi import WSGIContext
@ -461,6 +462,7 @@ class StaticLargeObject(object):
def filter_factory(global_conf, **local_conf):
conf = global_conf.copy()
conf.update(local_conf)
register_swift_info('slo')
def slo_filter(app):
return StaticLargeObject(app, conf)

View File

@ -120,7 +120,7 @@ import cgi
import time
from swift.common.utils import human_readable, split_path, config_true_value, \
json, quote, get_valid_utf8_str
json, quote, get_valid_utf8_str, register_swift_info
from swift.common.wsgi import make_pre_authed_env, WSGIContext
from swift.common.http import is_success, is_redirection, HTTP_NOT_FOUND
from swift.common.swob import Response, HTTPMovedPermanently, HTTPNotFound
@ -468,6 +468,7 @@ def filter_factory(global_conf, **local_conf):
"""Returns a Static Web WSGI filter for use with paste.deploy."""
conf = global_conf.copy()
conf.update(local_conf)
register_swift_info('staticweb')
def staticweb_filter(app):
return StaticWeb(app, conf)

View File

@ -28,7 +28,7 @@ from swift.common.swob import HTTPBadRequest, HTTPForbidden, HTTPNotFound, \
from swift.common.middleware.acl import clean_acl, parse_acl, referrer_allowed
from swift.common.utils import cache_from_env, get_logger, \
split_path, config_true_value
split_path, config_true_value, register_swift_info
class TempAuth(object):
@ -510,6 +510,7 @@ def filter_factory(global_conf, **local_conf):
"""Returns a WSGI filter app for use with paste.deploy."""
conf = global_conf.copy()
conf.update(local_conf)
register_swift_info('tempauth')
def auth_filter(app):
return TempAuth(app, conf)

View File

@ -89,8 +89,6 @@ __all__ = ['TempURL', 'filter_factory',
'DEFAULT_OUTGOING_ALLOW_HEADERS']
import hmac
from hashlib import sha1
from os.path import basename
from time import time
from urllib import urlencode
@ -98,7 +96,8 @@ from urlparse import parse_qs
from swift.proxy.controllers.base import get_account_info
from swift.common.swob import HeaderKeyDict, HTTPUnauthorized
from swift.common.utils import split_path, get_valid_utf8_str
from swift.common.utils import split_path, get_valid_utf8_str, \
register_swift_info, get_hmac
#: Default headers to remove from incoming requests. Simply a whitespace
@ -377,31 +376,10 @@ class TempURL(object):
:param keys: Key strings, from the X-Account-Meta-Temp-URL-Key[-2] of
the account.
"""
return [self._get_hmac(env, expires, key, request_method)
for key in keys]
def _get_hmac(self, env, expires, key, request_method=None):
"""
Returns the hexdigest string of the HMAC-SHA1 (RFC 2104) for
the request.
:param env: The WSGI environment for the request.
:param expires: Unix timestamp as an int for when the URL
expires.
:param key: Key str, from the X-Account-Meta-Temp-URL-Key of
the account.
:param request_method: Optional override of the request in
the WSGI env. For example, if a HEAD
does not match, you may wish to
override with GET to still allow the
HEAD.
:returns: hexdigest str of the HMAC-SHA1 for the request.
"""
if not request_method:
request_method = env['REQUEST_METHOD']
return hmac.new(
key, '%s\n%s\n%s' % (request_method, expires,
env['PATH_INFO']), sha1).hexdigest()
return [get_hmac(
request_method, env['PATH_INFO'], expires, key) for key in keys]
def _invalid(self, env, start_response):
"""
@ -480,4 +458,5 @@ def filter_factory(global_conf, **local_conf):
"""Returns the WSGI filter for use with paste.deploy."""
conf = global_conf.copy()
conf.update(local_conf)
register_swift_info('tempurl')
return lambda app: TempURL(app, conf)

View File

@ -17,6 +17,7 @@
import errno
import fcntl
import hmac
import operator
import os
import pwd
@ -26,7 +27,7 @@ import threading as stdlib_threading
import time
import uuid
import functools
from hashlib import md5
from hashlib import md5, sha1
from random import random, shuffle
from urllib import quote as _quote
from contextlib import contextmanager, closing
@ -100,6 +101,78 @@ if hash_conf.read('/etc/swift/swift.conf'):
pass
def get_hmac(request_method, path, expires, key):
"""
Returns the hexdigest string of the HMAC-SHA1 (RFC 2104) for
the request.
:param request_method: Request method to allow.
:param path: The path to the resource to allow access to.
:param expires: Unix timestamp as an int for when the URL
expires.
:param key: HMAC shared secret.
:returns: hexdigest str of the HMAC-SHA1 for the request.
"""
return hmac.new(
key, '%s\n%s\n%s' % (request_method, expires, path), sha1).hexdigest()
# Used by get_swift_info and register_swift_info to store information about
# the swift cluster.
_swift_info = {}
_swift_admin_info = {}
def get_swift_info(admin=False, disallowed_sections=None):
"""
Returns information about the swift cluster that has been previously
registered with the register_swift_info call.
:param admin: boolean value, if True will additionally return an 'admin'
section with information previously registered as admin
info.
:param disallowed_sections: list of section names to be withheld from the
information returned.
:returns: dictionary of information about the swift cluster.
"""
disallowed_sections = disallowed_sections or []
info = {}
for section in _swift_info:
if section in disallowed_sections:
continue
info[section] = dict(_swift_info[section].items())
if admin:
info['admin'] = dict(_swift_admin_info)
info['admin']['disallowed_sections'] = list(disallowed_sections)
return info
def register_swift_info(name='swift', admin=False, **kwargs):
"""
Registers information about the swift cluster to be retrieved with calls
to get_swift_info.
:param name: string, the section name to place the information under.
:param admin: boolean, if True, information will be registered to an
admin section which can optionally be withheld when
requesting the information.
:param kwargs: key value arguments representing the information to be
added.
"""
if name == 'admin' or name == 'disallowed_sections':
raise ValueError('\'{0}\' is reserved name.'.format(name))
if admin:
dict_to_use = _swift_admin_info
else:
dict_to_use = _swift_info
if name not in dict_to_use:
dict_to_use[name] = {}
for key, val in kwargs.iteritems():
dict_to_use[name][key] = val
def backward(f, blocksize=4096):
"""
A generator returning lines from a file starting with the last line,

View File

@ -12,6 +12,7 @@
# limitations under the License.
from swift.proxy.controllers.base import Controller
from swift.proxy.controllers.info import InfoController
from swift.proxy.controllers.obj import ObjectController
from swift.proxy.controllers.account import AccountController
from swift.proxy.controllers.container import ContainerController
@ -20,5 +21,6 @@ __all__ = [
'AccountController',
'ContainerController',
'Controller',
'InfoController',
'ObjectController',
]

View File

@ -0,0 +1,100 @@
# Copyright (c) 2010-2012 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.
from time import time
from swift.common.utils import public, get_hmac, get_swift_info, json
from swift.proxy.controllers.base import Controller, delay_denial
from swift.common.swob import HTTPOk, HTTPForbidden, HTTPUnauthorized
class InfoController(Controller):
"""WSGI controller for info requests"""
server_type = 'Info'
def __init__(self, app, version, expose_info, disallowed_sections,
admin_key):
Controller.__init__(self, app)
self.expose_info = expose_info
self.disallowed_sections = disallowed_sections
self.admin_key = admin_key
self.allowed_hmac_methods = {
'HEAD': ['HEAD', 'GET'],
'GET': ['GET']}
@public
@delay_denial
def GET(self, req):
return self.GETorHEAD(req)
@public
@delay_denial
def HEAD(self, req):
return self.GETorHEAD(req)
@public
@delay_denial
def OPTIONS(self, req):
return HTTPOk(request=req, headers={'Allow': 'HEAD, GET, OPTIONS'})
def GETorHEAD(self, req):
"""Handler for HTTP GET/HEAD requests."""
"""
Handles requests to /info
Should return a WSGI-style callable (such as swob.Response).
:param req: swob.Request object
"""
if not self.expose_info:
return HTTPForbidden(request=req)
admin_request = False
sig = req.params.get('swiftinfo_sig', '')
expires = req.params.get('swiftinfo_expires', '')
if sig != '' or expires != '':
admin_request = True
if not self.admin_key:
return HTTPForbidden(request=req)
try:
expires = int(expires)
except ValueError:
return HTTPUnauthorized(request=req)
if expires < time():
return HTTPUnauthorized(request=req)
valid_sigs = []
for method in self.allowed_hmac_methods[req.method]:
valid_sigs.append(get_hmac(method,
'/info',
expires,
self.admin_key))
if sig not in valid_sigs:
return HTTPUnauthorized(request=req)
headers = {}
if 'Origin' in req.headers:
headers['Access-Control-Allow-Origin'] = req.headers['Origin']
headers['Access-Control-Expose-Headers'] = ', '.join(
['x-trans-id'])
info = json.dumps(get_swift_info(
admin=admin_request, disallowed_sections=self.disallowed_sections))
return HTTPOk(request=req,
headers=headers,
body=info,
content_type='application/json; charset=UTF-8')

View File

@ -22,13 +22,15 @@ from time import time
from eventlet import Timeout
from swift import __canonical_version__ as swift_version
from swift.common.ring import Ring
from swift.common.utils import cache_from_env, get_logger, \
get_remote_client, split_path, config_true_value, generate_trans_id, \
affinity_key_function, affinity_locality_predicate
affinity_key_function, affinity_locality_predicate, list_from_csv, \
register_swift_info
from swift.common.constraints import check_utf8
from swift.proxy.controllers import AccountController, ObjectController, \
ContainerController
ContainerController, InfoController
from swift.common.swob import HTTPBadRequest, HTTPForbidden, \
HTTPMethodNotAllowed, HTTPNotFound, HTTPPreconditionFailed, \
HTTPServerError, HTTPException, Request
@ -162,6 +164,12 @@ class Application(object):
# ** Because it affects the client as well, currently, we use the
# client chunk size as the govenor and not the object chunk size.
socket._fileobject.default_bufsize = self.client_chunk_size
self.expose_info = config_true_value(
conf.get('expose_info', 'yes'))
self.disallowed_sections = list_from_csv(
conf.get('disallowed_sections'))
self.admin_key = conf.get('admin_key', None)
register_swift_info(version=swift_version)
def get_controller(self, path):
"""
@ -172,6 +180,13 @@ class Application(object):
:raises: ValueError (thrown by split_path) if given invalid path
"""
if path == '/info':
d = dict(version=None,
expose_info=self.expose_info,
disallowed_sections=self.disallowed_sections,
admin_key=self.admin_key)
return InfoController, d
version, account, container, obj = split_path(path, 1, 4, True)
d = dict(version=version,
account_name=account,

View File

@ -643,17 +643,17 @@ class TestTempURL(unittest.TestCase):
s, e)}),
(s, 0, None))
def test_get_hmac(self):
def test_get_hmacs(self):
self.assertEquals(
self.tempurl._get_hmac(
self.tempurl._get_hmacs(
{'REQUEST_METHOD': 'GET', 'PATH_INFO': '/v1/a/c/o'},
1, 'abc'),
'026d7f7cc25256450423c7ad03fc9f5ffc1dab6d')
1, ['abc']),
['026d7f7cc25256450423c7ad03fc9f5ffc1dab6d'])
self.assertEquals(
self.tempurl._get_hmac(
self.tempurl._get_hmacs(
{'REQUEST_METHOD': 'HEAD', 'PATH_INFO': '/v1/a/c/o'},
1, 'abc', request_method='GET'),
'026d7f7cc25256450423c7ad03fc9f5ffc1dab6d')
1, ['abc'], request_method='GET'),
['026d7f7cc25256450423c7ad03fc9f5ffc1dab6d'])
def test_invalid(self):

View File

@ -1613,6 +1613,159 @@ log_name = %(yarr)s'''
self.assertEquals('abc_%EF%BF%BD%EF%BF%BD%EC%BC%9D%EF%BF%BD',
utils.quote(invalid_utf8_str))
def test_get_hmac(self):
self.assertEquals(
utils.get_hmac('GET', '/path', 1, 'abc'),
'b17f6ff8da0e251737aa9e3ee69a881e3e092e2f')
class TestSwiftInfo(unittest.TestCase):
def tearDown(self):
utils._swift_info = {}
utils._swift_admin_info = {}
def test_register_swift_info(self):
utils.register_swift_info(foo='bar')
utils.register_swift_info(lorem='ipsum')
utils.register_swift_info('cap1', cap1_foo='cap1_bar')
utils.register_swift_info('cap1', cap1_lorem='cap1_ipsum')
self.assertTrue('swift' in utils._swift_info)
self.assertTrue('foo' in utils._swift_info['swift'])
self.assertEqual(utils._swift_info['swift']['foo'], 'bar')
self.assertTrue('lorem' in utils._swift_info['swift'])
self.assertEqual(utils._swift_info['swift']['lorem'], 'ipsum')
self.assertTrue('cap1' in utils._swift_info)
self.assertTrue('cap1_foo' in utils._swift_info['cap1'])
self.assertEqual(utils._swift_info['cap1']['cap1_foo'], 'cap1_bar')
self.assertTrue('cap1_lorem' in utils._swift_info['cap1'])
self.assertEqual(utils._swift_info['cap1']['cap1_lorem'], 'cap1_ipsum')
self.assertRaises(ValueError,
utils.register_swift_info, 'admin', foo='bar')
self.assertRaises(ValueError,
utils.register_swift_info, 'disallowed_sections',
disallowed_sections=None)
def test_get_swift_info(self):
utils._swift_info = {'swift': {'foo': 'bar'},
'cap1': {'cap1_foo': 'cap1_bar'}}
utils._swift_admin_info = {'admin_cap1': {'ac1_foo': 'ac1_bar'}}
info = utils.get_swift_info()
self.assertTrue('admin' not in info)
self.assertTrue('swift' in info)
self.assertTrue('foo' in info['swift'])
self.assertEqual(utils._swift_info['swift']['foo'], 'bar')
self.assertTrue('cap1' in info)
self.assertTrue('cap1_foo' in info['cap1'])
self.assertEqual(utils._swift_info['cap1']['cap1_foo'], 'cap1_bar')
def test_get_swift_info_with_disallowed_sections(self):
utils._swift_info = {'swift': {'foo': 'bar'},
'cap1': {'cap1_foo': 'cap1_bar'},
'cap2': {'cap2_foo': 'cap2_bar'},
'cap3': {'cap3_foo': 'cap3_bar'}}
utils._swift_admin_info = {'admin_cap1': {'ac1_foo': 'ac1_bar'}}
info = utils.get_swift_info(disallowed_sections=['cap1', 'cap3'])
self.assertTrue('admin' not in info)
self.assertTrue('swift' in info)
self.assertTrue('foo' in info['swift'])
self.assertEqual(info['swift']['foo'], 'bar')
self.assertTrue('cap1' not in info)
self.assertTrue('cap2' in info)
self.assertTrue('cap2_foo' in info['cap2'])
self.assertEqual(info['cap2']['cap2_foo'], 'cap2_bar')
self.assertTrue('cap3' not in info)
def test_register_swift_admin_info(self):
utils.register_swift_info(admin=True, admin_foo='admin_bar')
utils.register_swift_info(admin=True, admin_lorem='admin_ipsum')
utils.register_swift_info('cap1', admin=True, ac1_foo='ac1_bar')
utils.register_swift_info('cap1', admin=True, ac1_lorem='ac1_ipsum')
self.assertTrue('swift' in utils._swift_admin_info)
self.assertTrue('admin_foo' in utils._swift_admin_info['swift'])
self.assertEqual(
utils._swift_admin_info['swift']['admin_foo'], 'admin_bar')
self.assertTrue('admin_lorem' in utils._swift_admin_info['swift'])
self.assertEqual(
utils._swift_admin_info['swift']['admin_lorem'], 'admin_ipsum')
self.assertTrue('cap1' in utils._swift_admin_info)
self.assertTrue('ac1_foo' in utils._swift_admin_info['cap1'])
self.assertEqual(
utils._swift_admin_info['cap1']['ac1_foo'], 'ac1_bar')
self.assertTrue('ac1_lorem' in utils._swift_admin_info['cap1'])
self.assertEqual(
utils._swift_admin_info['cap1']['ac1_lorem'], 'ac1_ipsum')
self.assertTrue('swift' not in utils._swift_info)
self.assertTrue('cap1' not in utils._swift_info)
def test_get_swift_admin_info(self):
utils._swift_info = {'swift': {'foo': 'bar'},
'cap1': {'cap1_foo': 'cap1_bar'}}
utils._swift_admin_info = {'admin_cap1': {'ac1_foo': 'ac1_bar'}}
info = utils.get_swift_info(admin=True)
self.assertTrue('admin' in info)
self.assertTrue('admin_cap1' in info['admin'])
self.assertTrue('ac1_foo' in info['admin']['admin_cap1'])
self.assertEqual(info['admin']['admin_cap1']['ac1_foo'], 'ac1_bar')
self.assertTrue('swift' in info)
self.assertTrue('foo' in info['swift'])
self.assertEqual(utils._swift_info['swift']['foo'], 'bar')
self.assertTrue('cap1' in info)
self.assertTrue('cap1_foo' in info['cap1'])
self.assertEqual(utils._swift_info['cap1']['cap1_foo'], 'cap1_bar')
def test_get_swift_admin_info_with_disallowed_sections(self):
utils._swift_info = {'swift': {'foo': 'bar'},
'cap1': {'cap1_foo': 'cap1_bar'},
'cap2': {'cap2_foo': 'cap2_bar'},
'cap3': {'cap3_foo': 'cap3_bar'}}
utils._swift_admin_info = {'admin_cap1': {'ac1_foo': 'ac1_bar'}}
info = utils.get_swift_info(
admin=True, disallowed_sections=['cap1', 'cap3'])
self.assertTrue('admin' in info)
self.assertTrue('admin_cap1' in info['admin'])
self.assertTrue('ac1_foo' in info['admin']['admin_cap1'])
self.assertEqual(info['admin']['admin_cap1']['ac1_foo'], 'ac1_bar')
self.assertTrue('disallowed_sections' in info['admin'])
self.assertTrue('cap1' in info['admin']['disallowed_sections'])
self.assertTrue('cap2' not in info['admin']['disallowed_sections'])
self.assertTrue('cap3' in info['admin']['disallowed_sections'])
self.assertTrue('swift' in info)
self.assertTrue('foo' in info['swift'])
self.assertEqual(info['swift']['foo'], 'bar')
self.assertTrue('cap1' not in info)
self.assertTrue('cap2' in info)
self.assertTrue('cap2_foo' in info['cap2'])
self.assertEqual(info['cap2']['cap2_foo'], 'cap2_bar')
self.assertTrue('cap3' not in info)
class TestFileLikeIter(unittest.TestCase):

View File

@ -0,0 +1,293 @@
# Copyright (c) 2010-2012 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 unittest
import time
from mock import Mock
from swift.proxy.controllers import InfoController
from swift.proxy.server import Application as ProxyApp
from swift.common import utils
from swift.common.utils import json
from swift.common.swob import Request, HTTPException
class TestInfoController(unittest.TestCase):
def setUp(self):
utils._swift_info = {}
utils._swift_admin_info = {}
def get_controller(self, expose_info=None, disallowed_sections=None,
admin_key=None):
disallowed_sections = disallowed_sections or []
app = Mock(spec=ProxyApp)
return InfoController(app, None, expose_info,
disallowed_sections, admin_key)
def start_response(self, status, headers):
self.got_statuses.append(status)
for h in headers:
self.got_headers.append({h[0]: h[1]})
def test_disabled_info(self):
controller = self.get_controller(expose_info=False)
req = Request.blank(
'/info', environ={'REQUEST_METHOD': 'GET'})
resp = controller.GET(req)
self.assertTrue(isinstance(resp, HTTPException))
self.assertEqual('403 Forbidden', str(resp))
def test_get_info(self):
controller = self.get_controller(expose_info=True)
utils._swift_info = {'foo': {'bar': 'baz'}}
utils._swift_admin_info = {'qux': {'quux': 'corge'}}
req = Request.blank(
'/info', environ={'REQUEST_METHOD': 'GET'})
resp = controller.GET(req)
self.assertTrue(isinstance(resp, HTTPException))
self.assertEqual('200 OK', str(resp))
info = json.loads(resp.body)
self.assertTrue('admin' not in info)
self.assertTrue('foo' in info)
self.assertTrue('bar' in info['foo'])
self.assertEqual(info['foo']['bar'], 'baz')
def test_options_info(self):
controller = self.get_controller(expose_info=True)
req = Request.blank(
'/info', environ={'REQUEST_METHOD': 'GET'})
resp = controller.OPTIONS(req)
self.assertTrue(isinstance(resp, HTTPException))
self.assertEqual('200 OK', str(resp))
self.assertTrue('Allow' in resp.headers)
def test_get_info_cors(self):
controller = self.get_controller(expose_info=True)
utils._swift_info = {'foo': {'bar': 'baz'}}
utils._swift_admin_info = {'qux': {'quux': 'corge'}}
req = Request.blank(
'/info', environ={'REQUEST_METHOD': 'GET'},
headers={'Origin': 'http://example.com'})
resp = controller.GET(req)
self.assertTrue(isinstance(resp, HTTPException))
self.assertEqual('200 OK', str(resp))
info = json.loads(resp.body)
self.assertTrue('admin' not in info)
self.assertTrue('foo' in info)
self.assertTrue('bar' in info['foo'])
self.assertEqual(info['foo']['bar'], 'baz')
self.assertTrue('Access-Control-Allow-Origin' in resp.headers)
self.assertTrue('Access-Control-Expose-Headers' in resp.headers)
def test_head_info(self):
controller = self.get_controller(expose_info=True)
utils._swift_info = {'foo': {'bar': 'baz'}}
utils._swift_admin_info = {'qux': {'quux': 'corge'}}
req = Request.blank(
'/info', environ={'REQUEST_METHOD': 'HEAD'})
resp = controller.HEAD(req)
self.assertTrue(isinstance(resp, HTTPException))
self.assertEqual('200 OK', str(resp))
def test_disallow_info(self):
controller = self.get_controller(expose_info=True,
disallowed_sections=['foo2'])
utils._swift_info = {'foo': {'bar': 'baz'},
'foo2': {'bar2': 'baz2'}}
utils._swift_admin_info = {'qux': {'quux': 'corge'}}
req = Request.blank(
'/info', environ={'REQUEST_METHOD': 'GET'})
resp = controller.GET(req)
self.assertTrue(isinstance(resp, HTTPException))
self.assertEqual('200 OK', str(resp))
info = json.loads(resp.body)
self.assertTrue('foo' in info)
self.assertTrue('bar' in info['foo'])
self.assertEqual(info['foo']['bar'], 'baz')
self.assertTrue('foo2' not in info)
def test_disabled_admin_info(self):
controller = self.get_controller(expose_info=True, admin_key='')
utils._swift_info = {'foo': {'bar': 'baz'}}
utils._swift_admin_info = {'qux': {'quux': 'corge'}}
expires = int(time.time() + 86400)
sig = utils.get_hmac('GET', '/info', expires, '')
path = '/info?swiftinfo_sig={sig}&swiftinfo_expires={expires}'.format(
sig=sig, expires=expires)
req = Request.blank(
path, environ={'REQUEST_METHOD': 'GET'})
resp = controller.GET(req)
self.assertTrue(isinstance(resp, HTTPException))
self.assertEqual('403 Forbidden', str(resp))
def test_get_admin_info(self):
controller = self.get_controller(expose_info=True,
admin_key='secret-admin-key')
utils._swift_info = {'foo': {'bar': 'baz'}}
utils._swift_admin_info = {'qux': {'quux': 'corge'}}
expires = int(time.time() + 86400)
sig = utils.get_hmac('GET', '/info', expires, 'secret-admin-key')
path = '/info?swiftinfo_sig={sig}&swiftinfo_expires={expires}'.format(
sig=sig, expires=expires)
req = Request.blank(
path, environ={'REQUEST_METHOD': 'GET'})
resp = controller.GET(req)
self.assertTrue(isinstance(resp, HTTPException))
self.assertEqual('200 OK', str(resp))
info = json.loads(resp.body)
self.assertTrue('admin' in info)
self.assertTrue('qux' in info['admin'])
self.assertTrue('quux' in info['admin']['qux'])
self.assertEqual(info['admin']['qux']['quux'], 'corge')
def test_head_admin_info(self):
controller = self.get_controller(expose_info=True,
admin_key='secret-admin-key')
utils._swift_info = {'foo': {'bar': 'baz'}}
utils._swift_admin_info = {'qux': {'quux': 'corge'}}
expires = int(time.time() + 86400)
sig = utils.get_hmac('GET', '/info', expires, 'secret-admin-key')
path = '/info?swiftinfo_sig={sig}&swiftinfo_expires={expires}'.format(
sig=sig, expires=expires)
req = Request.blank(
path, environ={'REQUEST_METHOD': 'HEAD'})
resp = controller.GET(req)
self.assertTrue(isinstance(resp, HTTPException))
self.assertEqual('200 OK', str(resp))
expires = int(time.time() + 86400)
sig = utils.get_hmac('HEAD', '/info', expires, 'secret-admin-key')
path = '/info?swiftinfo_sig={sig}&swiftinfo_expires={expires}'.format(
sig=sig, expires=expires)
req = Request.blank(
path, environ={'REQUEST_METHOD': 'HEAD'})
resp = controller.GET(req)
self.assertTrue(isinstance(resp, HTTPException))
self.assertEqual('200 OK', str(resp))
def test_get_admin_info_invalid_method(self):
controller = self.get_controller(expose_info=True,
admin_key='secret-admin-key')
utils._swift_info = {'foo': {'bar': 'baz'}}
utils._swift_admin_info = {'qux': {'quux': 'corge'}}
expires = int(time.time() + 86400)
sig = utils.get_hmac('HEAD', '/info', expires, 'secret-admin-key')
path = '/info?swiftinfo_sig={sig}&swiftinfo_expires={expires}'.format(
sig=sig, expires=expires)
req = Request.blank(
path, environ={'REQUEST_METHOD': 'GET'})
resp = controller.GET(req)
self.assertTrue(isinstance(resp, HTTPException))
self.assertEqual('401 Unauthorized', str(resp))
def test_get_admin_info_invalid_expires(self):
controller = self.get_controller(expose_info=True,
admin_key='secret-admin-key')
utils._swift_info = {'foo': {'bar': 'baz'}}
utils._swift_admin_info = {'qux': {'quux': 'corge'}}
expires = 1
sig = utils.get_hmac('GET', '/info', expires, 'secret-admin-key')
path = '/info?swiftinfo_sig={sig}&swiftinfo_expires={expires}'.format(
sig=sig, expires=expires)
req = Request.blank(
path, environ={'REQUEST_METHOD': 'GET'})
resp = controller.GET(req)
self.assertTrue(isinstance(resp, HTTPException))
self.assertEqual('401 Unauthorized', str(resp))
expires = 'abc'
sig = utils.get_hmac('GET', '/info', expires, 'secret-admin-key')
path = '/info?swiftinfo_sig={sig}&swiftinfo_expires={expires}'.format(
sig=sig, expires=expires)
req = Request.blank(
path, environ={'REQUEST_METHOD': 'GET'})
resp = controller.GET(req)
self.assertTrue(isinstance(resp, HTTPException))
self.assertEqual('401 Unauthorized', str(resp))
def test_get_admin_info_invalid_path(self):
controller = self.get_controller(expose_info=True,
admin_key='secret-admin-key')
utils._swift_info = {'foo': {'bar': 'baz'}}
utils._swift_admin_info = {'qux': {'quux': 'corge'}}
expires = int(time.time() + 86400)
sig = utils.get_hmac('GET', '/foo', expires, 'secret-admin-key')
path = '/info?swiftinfo_sig={sig}&swiftinfo_expires={expires}'.format(
sig=sig, expires=expires)
req = Request.blank(
path, environ={'REQUEST_METHOD': 'GET'})
resp = controller.GET(req)
self.assertTrue(isinstance(resp, HTTPException))
self.assertEqual('401 Unauthorized', str(resp))
def test_get_admin_info_invalid_key(self):
controller = self.get_controller(expose_info=True,
admin_key='secret-admin-key')
utils._swift_info = {'foo': {'bar': 'baz'}}
utils._swift_admin_info = {'qux': {'quux': 'corge'}}
expires = int(time.time() + 86400)
sig = utils.get_hmac('GET', '/foo', expires, 'invalid-admin-key')
path = '/info?swiftinfo_sig={sig}&swiftinfo_expires={expires}'.format(
sig=sig, expires=expires)
req = Request.blank(
path, environ={'REQUEST_METHOD': 'GET'})
resp = controller.GET(req)
self.assertTrue(isinstance(resp, HTTPException))
self.assertEqual('401 Unauthorized', str(resp))
def test_admin_disallow_info(self):
controller = self.get_controller(expose_info=True,
disallowed_sections=['foo2'],
admin_key='secret-admin-key')
utils._swift_info = {'foo': {'bar': 'baz'},
'foo2': {'bar2': 'baz2'}}
utils._swift_admin_info = {'qux': {'quux': 'corge'}}
expires = int(time.time() + 86400)
sig = utils.get_hmac('GET', '/info', expires, 'secret-admin-key')
path = '/info?swiftinfo_sig={sig}&swiftinfo_expires={expires}'.format(
sig=sig, expires=expires)
req = Request.blank(
path, environ={'REQUEST_METHOD': 'GET'})
resp = controller.GET(req)
self.assertTrue(isinstance(resp, HTTPException))
self.assertEqual('200 OK', str(resp))
info = json.loads(resp.body)
self.assertTrue('foo2' not in info)
self.assertTrue('admin' in info)
self.assertTrue('disallowed_sections' in info['admin'])
self.assertTrue('foo2' in info['admin']['disallowed_sections'])
self.assertTrue('qux' in info['admin'])
self.assertTrue('quux' in info['admin']['qux'])
self.assertEqual(info['admin']['qux']['quux'], 'corge')
if __name__ == '__main__':
unittest.main()

View File

@ -641,6 +641,34 @@ class TestProxyServer(unittest.TestCase):
{'region': 2, 'zone': 1, 'ip': '127.0.0.1'}]
self.assertEquals(exp_sorted, app_sorted)
def test_info_defaults(self):
app = proxy_server.Application({}, FakeMemcache(),
account_ring=FakeRing(),
container_ring=FakeRing(),
object_ring=FakeRing())
self.assertTrue(app.expose_info)
self.assertTrue(isinstance(app.disallowed_sections, list))
self.assertEqual(0, len(app.disallowed_sections))
self.assertTrue(app.admin_key is None)
def test_get_info_controller(self):
path = '/info'
app = proxy_server.Application({}, FakeMemcache(),
account_ring=FakeRing(),
container_ring=FakeRing(),
object_ring=FakeRing())
controller, path_parts = app.get_controller(path)
self.assertTrue('version' in path_parts)
self.assertTrue(path_parts['version'] is None)
self.assertTrue('disallowed_sections' in path_parts)
self.assertTrue('expose_info' in path_parts)
self.assertTrue('admin_key' in path_parts)
self.assertEqual(controller.__name__, 'InfoController')
class TestObjectController(unittest.TestCase):