diff --git a/bin/swift-temp-url b/bin/swift-temp-url index 60576c9931..041c0129fa 100755 --- a/bin/swift-temp-url +++ b/bin/swift-temp-url @@ -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 ' 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) diff --git a/doc/source/misc.rst b/doc/source/misc.rst index e42a91193d..3aea382458 100644 --- a/doc/source/misc.rst +++ b/doc/source/misc.rst @@ -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 diff --git a/etc/proxy-server.conf-sample b/etc/proxy-server.conf-sample index d381d5fc14..26e31828c6 100644 --- a/etc/proxy-server.conf-sample +++ b/etc/proxy-server.conf-sample @@ -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 diff --git a/swift/common/middleware/account_quotas.py b/swift/common/middleware/account_quotas.py index 7515a33e1d..174cd87978 100644 --- a/swift/common/middleware/account_quotas.py +++ b/swift/common/middleware/account_quotas.py @@ -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 diff --git a/swift/common/middleware/bulk.py b/swift/common/middleware/bulk.py index 62218fa910..7dcb79e782 100644 --- a/swift/common/middleware/bulk.py +++ b/swift/common/middleware/bulk.py @@ -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) diff --git a/swift/common/middleware/container_quotas.py b/swift/common/middleware/container_quotas.py index 275c246944..e7bbd1acfe 100644 --- a/swift/common/middleware/container_quotas.py +++ b/swift/common/middleware/container_quotas.py @@ -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 diff --git a/swift/common/middleware/crossdomain.py b/swift/common/middleware/crossdomain.py index ea9b759e09..81eb8a2459 100644 --- a/swift/common/middleware/crossdomain.py +++ b/swift/common/middleware/crossdomain.py @@ -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) diff --git a/swift/common/middleware/formpost.py b/swift/common/middleware/formpost.py index b6c0a2961a..b81b75d619 100644 --- a/swift/common/middleware/formpost.py +++ b/swift/common/middleware/formpost.py @@ -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) diff --git a/swift/common/middleware/keystoneauth.py b/swift/common/middleware/keystoneauth.py index 1573e59911..096af45083 100644 --- a/swift/common/middleware/keystoneauth.py +++ b/swift/common/middleware/keystoneauth.py @@ -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) diff --git a/swift/common/middleware/ratelimit.py b/swift/common/middleware/ratelimit.py index d60b602211..2f8a6b5d71 100644 --- a/swift/common/middleware/ratelimit.py +++ b/swift/common/middleware/ratelimit.py @@ -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) diff --git a/swift/common/middleware/slo.py b/swift/common/middleware/slo.py index 31d1618963..6918c51375 100644 --- a/swift/common/middleware/slo.py +++ b/swift/common/middleware/slo.py @@ -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) diff --git a/swift/common/middleware/staticweb.py b/swift/common/middleware/staticweb.py index b8a9daaac8..7b2d7dfa75 100644 --- a/swift/common/middleware/staticweb.py +++ b/swift/common/middleware/staticweb.py @@ -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) diff --git a/swift/common/middleware/tempauth.py b/swift/common/middleware/tempauth.py index 4600e5c7dd..6dde03c77f 100644 --- a/swift/common/middleware/tempauth.py +++ b/swift/common/middleware/tempauth.py @@ -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) diff --git a/swift/common/middleware/tempurl.py b/swift/common/middleware/tempurl.py index 72de21ff19..59ad206545 100644 --- a/swift/common/middleware/tempurl.py +++ b/swift/common/middleware/tempurl.py @@ -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 @@ -389,31 +388,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): """ @@ -492,4 +470,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) diff --git a/swift/common/utils.py b/swift/common/utils.py index 390e57c129..a33067fdb8 100644 --- a/swift/common/utils.py +++ b/swift/common/utils.py @@ -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, diff --git a/swift/proxy/controllers/__init__.py b/swift/proxy/controllers/__init__.py index 327376016c..de4c0145b0 100644 --- a/swift/proxy/controllers/__init__.py +++ b/swift/proxy/controllers/__init__.py @@ -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', ] diff --git a/swift/proxy/controllers/info.py b/swift/proxy/controllers/info.py new file mode 100644 index 0000000000..4986302eec --- /dev/null +++ b/swift/proxy/controllers/info.py @@ -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') diff --git a/swift/proxy/server.py b/swift/proxy/server.py index b489ed9e7c..d78aa78ff9 100644 --- a/swift/proxy/server.py +++ b/swift/proxy/server.py @@ -23,13 +23,15 @@ import itertools 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 @@ -164,6 +166,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): """ @@ -174,6 +182,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, diff --git a/test/unit/common/middleware/test_tempurl.py b/test/unit/common/middleware/test_tempurl.py index f63fd2691b..a462b7a64b 100644 --- a/test/unit/common/middleware/test_tempurl.py +++ b/test/unit/common/middleware/test_tempurl.py @@ -687,17 +687,17 @@ class TestTempURL(unittest.TestCase): s, e)}), (s, 0, None, 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): diff --git a/test/unit/common/test_utils.py b/test/unit/common/test_utils.py index 3ecd175dcb..e7eb0b99a4 100644 --- a/test/unit/common/test_utils.py +++ b/test/unit/common/test_utils.py @@ -1662,6 +1662,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): diff --git a/test/unit/proxy/controllers/test_info.py b/test/unit/proxy/controllers/test_info.py new file mode 100644 index 0000000000..f33beba024 --- /dev/null +++ b/test/unit/proxy/controllers/test_info.py @@ -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() diff --git a/test/unit/proxy/test_server.py b/test/unit/proxy/test_server.py index a796bc6238..f6f587113a 100644 --- a/test/unit/proxy/test_server.py +++ b/test/unit/proxy/test_server.py @@ -650,6 +650,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):