diff --git a/doc/source/deployment_guide.rst b/doc/source/deployment_guide.rst index c72b7a165a..fae53d7584 100644 --- a/doc/source/deployment_guide.rst +++ b/doc/source/deployment_guide.rst @@ -529,6 +529,13 @@ cert_file Path to the ssl .crt. This key_file Path to the ssl .key. This should be enabled for testing purposes only. +cors_allow_origin This is a list of hosts that + are included with any CORS + request by default and + returned with the + Access-Control-Allow-Origin + header in addition to what + the container has set. ============================ =============== ============================= [proxy-server] diff --git a/doc/source/development_auth.rst b/doc/source/development_auth.rst index 14368deba5..e913850b5a 100644 --- a/doc/source/development_auth.rst +++ b/doc/source/development_auth.rst @@ -482,3 +482,12 @@ folks a start on their own code if they want to use repoze.what:: authenticators=[('devauth', DevAuthenticator(conf))], challengers=[('devauth', DevChallenger(conf))]) return auth_filter + +----------------------- +Allowing CORS with Auth +----------------------- + +Cross Origin RequestS require that the auth system allow the OPTIONS method to +pass through without a token. The preflight request will make an OPTIONS call +against the object or container and will not work if the auth system stops it. +See TempAuth for an example of how OPTIONS requests are handled. diff --git a/doc/source/misc.rst b/doc/source/misc.rst index 6044d02ea8..0c0d607267 100644 --- a/doc/source/misc.rst +++ b/doc/source/misc.rst @@ -171,3 +171,35 @@ Proxy Logging .. automodule:: swift.common.middleware.proxy_logging :members: :show-inheritance: + +CORS Headers +============ + +Cross Origin RequestS or CORS allows the browser to make requests against +Swift from another origin via the browser. This enables the use of HTML5 +forms and javascript uploads to swift. The owner of a container can set +three headers: + ++---------------------------------------------+-------------------------------+ +|Metadata | Use | ++=============================================+===============================+ +|X-Container-Meta-Access-Control-Allow-Origin | Origins to be allowed to | +| | make Cross Origin Requests, | +| | space separated | ++---------------------------------------------+-------------------------------+ +|X-Container-Meta-Access-Control-Max-Age | Max age for the Origin to | +| | hold the preflight results. | ++---------------------------------------------+-------------------------------+ +|X-Container-Meta-Access-Control-Allow-Headers| Headers to be allowed in | +| | actual request by browser. | ++---------------------------------------------+-------------------------------+ + +When the browser does a request it can issue a preflight request. The +preflight request is the OPTIONS call that verifies the Origin is allowed +to make the request. + +* Browser makes OPTIONS request to Swift +* Swift returns 200/401 to browser based on allowed origins +* If 200, browser makes PUT, POST, DELETE, HEAD, GET request to Swift + +CORS should be used in conjunction with TempURL and FormPost. diff --git a/doc/source/overview_auth.rst b/doc/source/overview_auth.rst index a07b1a872c..44eb9134c5 100644 --- a/doc/source/overview_auth.rst +++ b/doc/source/overview_auth.rst @@ -39,6 +39,8 @@ Additionally, if the auth system sets the request environ's swift_owner key to True, the proxy will return additional header information in some requests, such as the X-Container-Sync-Key for a container GET or HEAD. +TempAuth will now allow OPTIONS requests to go through without a token. + The user starts a session by sending a ReST request to the auth system to receive the auth token and a URL to the Swift system. diff --git a/etc/proxy-server.conf-sample b/etc/proxy-server.conf-sample index d6110823cb..7b8135c2fe 100644 --- a/etc/proxy-server.conf-sample +++ b/etc/proxy-server.conf-sample @@ -27,6 +27,8 @@ # log_statsd_port = 8125 # log_statsd_default_sample_rate = 1 # log_statsd_metric_prefix = +# Use a comma separated list of full url (http://foo.bar:1234,https://foo.bar) +# cors_allow_origin = [pipeline:main] pipeline = catch_errors healthcheck cache ratelimit tempauth proxy-logging proxy-server diff --git a/swift/common/middleware/tempauth.py b/swift/common/middleware/tempauth.py index d9cfbcb42d..57c99e6a8a 100644 --- a/swift/common/middleware/tempauth.py +++ b/swift/common/middleware/tempauth.py @@ -84,7 +84,8 @@ class TempAuth(object): if self.auth_prefix[-1] != '/': self.auth_prefix += '/' self.token_life = int(conf.get('token_life', 86400)) - self.allowed_sync_hosts = [h.strip() + self.allowed_sync_hosts = [ + h.strip() for h in conf.get('allowed_sync_hosts', '127.0.0.1').split(',') if h.strip()] self.allow_overrides = \ @@ -244,6 +245,7 @@ class TempAuth(object): 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: @@ -270,6 +272,9 @@ class TempAuth(object): (req.remote_addr in self.allowed_sync_hosts or get_remote_client(req) in self.allowed_sync_hosts)): return None + if req.method == 'OPTIONS': + #allow OPTIONS requests to proceed as normal + return None referrers, groups = parse_acl(getattr(req, 'acl', None)) if referrer_allowed(req.referer, referrers): if obj or '.rlistings' in groups: @@ -341,8 +346,11 @@ class TempAuth(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) + version, account, user, _junk = split_path( + req.path_info, + minsegs=1, + maxsegs=4, + rest_with_last=True) except ValueError: self.logger.increment('errors') return HTTPNotFound(request=req) @@ -464,8 +472,10 @@ class TempAuth(object): 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']}) + headers={ + 'x-auth-token': token, + 'x-storage-token': token, + 'x-storage-url': self.users[account_user]['url']}) def posthooklogger(self, env, req): if not req.path.startswith(self.auth_prefix): @@ -490,12 +500,13 @@ class TempAuth(object): if getattr(req, 'client_disconnect', False) or \ getattr(response, 'client_disconnect', False): status_int = HTTP_CLIENT_CLOSED_REQUEST - self.logger.info(' '.join(quote(str(x)) for x in (client or '-', + self.logger.info( + ' '.join(quote(str(x)) for x in (client or '-', req.remote_addr or '-', strftime('%d/%b/%Y/%H/%M/%S', gmtime()), req.method, the_request, req.environ['SERVER_PROTOCOL'], status_int, req.referer or '-', req.user_agent or '-', req.headers.get('x-auth-token', - req.headers.get('x-auth-admin-user', '-')), + req.headers.get('x-auth-admin-user', '-')), getattr(req, 'bytes_transferred', 0) or '-', getattr(response, 'bytes_transferred', 0) or '-', req.headers.get('etag', '-'), diff --git a/swift/proxy/controllers/base.py b/swift/proxy/controllers/base.py index 12196a2de5..231a016a88 100644 --- a/swift/proxy/controllers/base.py +++ b/swift/proxy/controllers/base.py @@ -278,6 +278,25 @@ class Controller(object): return partition, nodes, container_count return None, None, None + def headers_to_container_info(self, headers): + headers = dict(headers) + return { + 'read_acl': headers.get('x-container-read'), + 'write_acl': headers.get('x-container-write'), + 'sync_key': headers.get('x-container-sync-key'), + 'count': headers.get('x-container-object-count'), + 'bytes': headers.get('x-container-bytes-used'), + 'versions': headers.get('x-versions-location'), + 'cors': { + 'allow_origin': headers.get( + 'x-container-meta-access-control-allow-origin'), + 'allow_headers': headers.get( + 'x-container-meta-access-control-allow-headers'), + 'max_age': headers.get( + 'x-container-meta-access-control-max-age') + } + } + def container_info(self, account, container, account_autocreate=False): """ Get container information and thusly verify container existance. @@ -324,14 +343,9 @@ class Controller(object): resp = conn.getresponse() body = resp.read() if is_success(resp.status): - container_info.update({ - 'status': HTTP_OK, - 'read_acl': resp.getheader('x-container-read'), - 'write_acl': resp.getheader('x-container-write'), - 'sync_key': resp.getheader('x-container-sync-key'), - 'count': resp.getheader('x-container-object-count'), - 'bytes': resp.getheader('x-container-bytes-used'), - 'versions': resp.getheader('x-versions-location')}) + container_info.update( + self.headers_to_container_info(resp.getheaders())) + container_info['status'] = HTTP_OK break elif resp.status == HTTP_NOT_FOUND: container_info['status'] = HTTP_NOT_FOUND @@ -661,3 +675,37 @@ class Controller(object): return res return self.best_response(req, statuses, reasons, bodies, '%s %s' % (server_type, req.method)) + + def OPTIONS_base(self, req): + """ + Base handler for OPTIONS requests + + :param req: swob.Request object + :returns: swob.Response object + """ + container_info = \ + self.container_info(self.account_name, self.container_name) + cors = container_info.get('cors', {}) + allowed_origins = set() + if cors.get('allow_origin'): + allowed_origins.update(cors['allow_origin'].split(' ')) + if self.app.cors_allow_origin: + allowed_origins.update(self.app.cors_allow_origin) + if not allowed_origins: + return Response(status=401, request=req) + headers = {} + if req.headers.get('Origin') in allowed_origins \ + or '*' in allowed_origins: + headers['access-control-allow-origin'] = ' '.join(allowed_origins) + headers['access-control-max-age'] = cors.get('max_age') + headers['access-control-allow-methods'] = \ + 'GET, POST, PUT, DELETE, HEAD' + headers['access-control-allow-headers'] = \ + cors.get('allow_headers') + return Response(status=200, headers=headers, request=req) + else: + return Response(status=401, request=req) + + @public + def OPTIONS(self, req): + return self.OPTIONS_base(req) diff --git a/swift/proxy/server.py b/swift/proxy/server.py index 80bdd1a7a9..e573d445e9 100644 --- a/swift/proxy/server.py +++ b/swift/proxy/server.py @@ -79,9 +79,9 @@ class Application(object): self.resellers_conf.read(os.path.join(swift_dir, 'resellers.conf')) self.object_ring = object_ring or Ring(swift_dir, ring_name='object') self.container_ring = container_ring or Ring(swift_dir, - ring_name='container') + ring_name='container') self.account_ring = account_ring or Ring(swift_dir, - ring_name='account') + ring_name='account') self.memcache = memcache mimetypes.init(mimetypes.knownfiles + [os.path.join(swift_dir, 'mime.types')]) @@ -94,10 +94,12 @@ class Application(object): int(conf.get('expiring_objects_container_divisor') or 86400) self.max_containers_per_account = \ int(conf.get('max_containers_per_account') or 0) - self.max_containers_whitelist = [a.strip() + self.max_containers_whitelist = [ + a.strip() for a in conf.get('max_containers_whitelist', '').split(',') if a.strip()] - self.deny_host_headers = [host.strip() for host in + self.deny_host_headers = [ + host.strip() for host in conf.get('deny_host_headers', '').split(',') if host.strip()] self.rate_limit_after_segment = \ int(conf.get('rate_limit_after_segment', 10)) @@ -105,6 +107,10 @@ class Application(object): int(conf.get('rate_limit_segments_per_sec', 1)) self.log_handoffs = \ conf.get('log_handoffs', 'true').lower() in TRUE_VALUES + self.cors_allow_origin = [ + a.strip() + for a in conf.get('cors_allow_origin', '').split(',') + if a.strip()] def get_controller(self, path): """ @@ -117,9 +123,9 @@ class Application(object): """ version, account, container, obj = split_path(path, 1, 4, True) d = dict(version=version, - account_name=account, - container_name=container, - object_name=obj) + account_name=account, + container_name=container, + object_name=obj) if obj and container and account: return ObjectController, d elif container and account: @@ -146,7 +152,7 @@ class Application(object): return err(env, start_response) except (Exception, Timeout): start_response('500 Server Error', - [('Content-Type', 'text/plain')]) + [('Content-Type', 'text/plain')]) return ['Internal server error.\n'] def update_request(self, req): diff --git a/test/unit/common/middleware/test_tempauth.py b/test/unit/common/middleware/test_tempauth.py index 19979b077f..d9790074e5 100644 --- a/test/unit/common/middleware/test_tempauth.py +++ b/test/unit/common/middleware/test_tempauth.py @@ -13,13 +13,8 @@ # See the License for the specific language governing permissions and # limitations under the License. -try: - import simplejson as json -except ImportError: - import json import unittest from contextlib import contextmanager -from time import time from base64 import b64encode from swift.common.middleware import tempauth as auth @@ -181,7 +176,7 @@ class TestAuth(unittest.TestCase): def test_auth_deny_non_reseller_prefix(self): req = self._make_request('/v1/BLAH_account', - headers={'X-Auth-Token': 'BLAH_t'}) + headers={'X-Auth-Token': 'BLAH_t'}) resp = req.get_response(self.test_auth) self.assertEquals(resp.status_int, 401) self.assertEquals(req.environ['swift.authorize'], @@ -190,9 +185,9 @@ class TestAuth(unittest.TestCase): def test_auth_deny_non_reseller_prefix_no_override(self): fake_authorize = lambda x: Response(status='500 Fake') req = self._make_request('/v1/BLAH_account', - headers={'X-Auth-Token': 'BLAH_t'}, - environ={'swift.authorize': fake_authorize} - ) + headers={'X-Auth-Token': 'BLAH_t'}, + environ={'swift.authorize': fake_authorize} + ) resp = req.get_response(self.test_auth) self.assertEquals(resp.status_int, 500) self.assertEquals(req.environ['swift.authorize'], fake_authorize) @@ -204,7 +199,7 @@ class TestAuth(unittest.TestCase): local_app = FakeApp() local_auth = auth.filter_factory({'reseller_prefix': ''})(local_app) req = self._make_request('/v1/account', - headers={'X-Auth-Token': 't'}) + headers={'X-Auth-Token': 't'}) resp = req.get_response(local_auth) self.assertEquals(resp.status_int, 401) self.assertEquals(local_app.calls, 1) @@ -226,13 +221,14 @@ class TestAuth(unittest.TestCase): auth.filter_factory({'reseller_prefix': ''})(FakeApp()) local_authorize = lambda req: Response('test') req = self._make_request('/v1/account', environ={'swift.authorize': - local_authorize}) + local_authorize}) resp = req.get_response(local_auth) self.assertEquals(resp.status_int, 200) self.assertEquals(req.environ['swift.authorize'], local_authorize) def test_auth_fail(self): - resp = self._make_request('/v1/AUTH_cfa', + resp = self._make_request( + '/v1/AUTH_cfa', headers={'X-Auth-Token': 'AUTH_t'}).get_response(self.test_auth) self.assertEquals(resp.status_int, 401) @@ -331,26 +327,26 @@ class TestAuth(unittest.TestCase): def test_account_put_permissions(self): req = self._make_request('/v1/AUTH_new', - environ={'REQUEST_METHOD': 'PUT'}) + environ={'REQUEST_METHOD': 'PUT'}) req.remote_user = 'act:usr,act' resp = self.test_auth.authorize(req) self.assertEquals(resp.status_int, 403) req = self._make_request('/v1/AUTH_new', - environ={'REQUEST_METHOD': 'PUT'}) + environ={'REQUEST_METHOD': 'PUT'}) req.remote_user = 'act:usr,act,AUTH_other' resp = self.test_auth.authorize(req) self.assertEquals(resp.status_int, 403) # Even PUTs to your own account as account admin should fail req = self._make_request('/v1/AUTH_old', - environ={'REQUEST_METHOD': 'PUT'}) + environ={'REQUEST_METHOD': 'PUT'}) req.remote_user = 'act:usr,act,AUTH_old' resp = self.test_auth.authorize(req) self.assertEquals(resp.status_int, 403) req = self._make_request('/v1/AUTH_new', - environ={'REQUEST_METHOD': 'PUT'}) + environ={'REQUEST_METHOD': 'PUT'}) req.remote_user = 'act:usr,act,.reseller_admin' resp = self.test_auth.authorize(req) self.assertEquals(resp, None) @@ -358,33 +354,33 @@ class TestAuth(unittest.TestCase): # .super_admin is not something the middleware should ever see or care # about req = self._make_request('/v1/AUTH_new', - environ={'REQUEST_METHOD': 'PUT'}) + environ={'REQUEST_METHOD': 'PUT'}) req.remote_user = 'act:usr,act,.super_admin' resp = self.test_auth.authorize(req) self.assertEquals(resp.status_int, 403) def test_account_delete_permissions(self): req = self._make_request('/v1/AUTH_new', - environ={'REQUEST_METHOD': 'DELETE'}) + environ={'REQUEST_METHOD': 'DELETE'}) req.remote_user = 'act:usr,act' resp = self.test_auth.authorize(req) self.assertEquals(resp.status_int, 403) req = self._make_request('/v1/AUTH_new', - environ={'REQUEST_METHOD': 'DELETE'}) + environ={'REQUEST_METHOD': 'DELETE'}) req.remote_user = 'act:usr,act,AUTH_other' resp = self.test_auth.authorize(req) self.assertEquals(resp.status_int, 403) # Even DELETEs to your own account as account admin should fail req = self._make_request('/v1/AUTH_old', - environ={'REQUEST_METHOD': 'DELETE'}) + environ={'REQUEST_METHOD': 'DELETE'}) req.remote_user = 'act:usr,act,AUTH_old' resp = self.test_auth.authorize(req) self.assertEquals(resp.status_int, 403) req = self._make_request('/v1/AUTH_new', - environ={'REQUEST_METHOD': 'DELETE'}) + environ={'REQUEST_METHOD': 'DELETE'}) req.remote_user = 'act:usr,act,.reseller_admin' resp = self.test_auth.authorize(req) self.assertEquals(resp, None) @@ -392,7 +388,7 @@ class TestAuth(unittest.TestCase): # .super_admin is not something the middleware should ever see or care # about req = self._make_request('/v1/AUTH_new', - environ={'REQUEST_METHOD': 'DELETE'}) + environ={'REQUEST_METHOD': 'DELETE'}) req.remote_user = 'act:usr,act,.super_admin' resp = self.test_auth.authorize(req) self.assertEquals(resp.status_int, 403) @@ -400,41 +396,48 @@ class TestAuth(unittest.TestCase): def test_get_token_fail(self): resp = self._make_request('/auth/v1.0').get_response(self.test_auth) self.assertEquals(resp.status_int, 401) - resp = self._make_request('/auth/v1.0', + resp = self._make_request( + '/auth/v1.0', headers={'X-Auth-User': 'act:usr', 'X-Auth-Key': 'key'}).get_response(self.test_auth) self.assertEquals(resp.status_int, 401) def test_get_token_fail_invalid_x_auth_user_format(self): - resp = self._make_request('/auth/v1/act/auth', + resp = self._make_request( + '/auth/v1/act/auth', headers={'X-Auth-User': 'usr', 'X-Auth-Key': 'key'}).get_response(self.test_auth) self.assertEquals(resp.status_int, 401) def test_get_token_fail_non_matching_account_in_request(self): - resp = self._make_request('/auth/v1/act/auth', + resp = self._make_request( + '/auth/v1/act/auth', headers={'X-Auth-User': 'act2:usr', 'X-Auth-Key': 'key'}).get_response(self.test_auth) self.assertEquals(resp.status_int, 401) def test_get_token_fail_bad_path(self): - resp = self._make_request('/auth/v1/act/auth/invalid', + resp = self._make_request( + '/auth/v1/act/auth/invalid', headers={'X-Auth-User': 'act:usr', 'X-Auth-Key': 'key'}).get_response(self.test_auth) self.assertEquals(resp.status_int, 400) def test_get_token_fail_missing_key(self): - resp = self._make_request('/auth/v1/act/auth', + resp = self._make_request( + '/auth/v1/act/auth', headers={'X-Auth-User': 'act:usr'}).get_response(self.test_auth) self.assertEquals(resp.status_int, 401) def test_allowed_sync_hosts(self): a = auth.filter_factory({'super_admin_key': 'supertest'})(FakeApp()) self.assertEquals(a.allowed_sync_hosts, ['127.0.0.1']) - a = auth.filter_factory({'super_admin_key': 'supertest', - 'allowed_sync_hosts': + a = auth.filter_factory( + {'super_admin_key': 'supertest', + 'allowed_sync_hosts': '1.1.1.1,2.1.1.1, 3.1.1.1 , 4.1.1.1,, , 5.1.1.1'})(FakeApp()) - self.assertEquals(a.allowed_sync_hosts, + self.assertEquals( + a.allowed_sync_hosts, ['1.1.1.1', '2.1.1.1', '3.1.1.1', '4.1.1.1', '5.1.1.1']) def test_reseller_admin_is_owner(self): @@ -449,7 +452,7 @@ class TestAuth(unittest.TestCase): self.test_auth.authorize = mitm_authorize req = self._make_request('/v1/AUTH_cfa', - headers={'X-Auth-Token': 'AUTH_t'}) + headers={'X-Auth-Token': 'AUTH_t'}) req.remote_user = '.reseller_admin' self.test_auth.authorize(req) self.assertEquals(owner_values, [True]) @@ -465,8 +468,9 @@ class TestAuth(unittest.TestCase): self.test_auth.authorize = mitm_authorize - req = self._make_request('/v1/AUTH_cfa', - headers={'X-Auth-Token': 'AUTH_t'}) + req = self._make_request( + '/v1/AUTH_cfa', + headers={'X-Auth-Token': 'AUTH_t'}) req.remote_user = 'AUTH_cfa' self.test_auth.authorize(req) self.assertEquals(owner_values, [True]) @@ -482,8 +486,9 @@ class TestAuth(unittest.TestCase): self.test_auth.authorize = mitm_authorize - req = self._make_request('/v1/AUTH_cfa/c', - headers={'X-Auth-Token': 'AUTH_t'}) + req = self._make_request( + '/v1/AUTH_cfa/c', + headers={'X-Auth-Token': 'AUTH_t'}) req.remote_user = 'act:usr' self.test_auth.authorize(req) self.assertEquals(owner_values, [False]) @@ -491,7 +496,8 @@ class TestAuth(unittest.TestCase): def test_sync_request_success(self): self.test_auth.app = FakeApp(iter([('204 No Content', {}, '')]), sync_key='secret') - req = self._make_request('/v1/AUTH_cfa/c/o', + req = self._make_request( + '/v1/AUTH_cfa/c/o', environ={'REQUEST_METHOD': 'DELETE'}, headers={'x-container-sync-key': 'secret', 'x-timestamp': '123.456'}) @@ -502,7 +508,8 @@ class TestAuth(unittest.TestCase): def test_sync_request_fail_key(self): self.test_auth.app = FakeApp(iter([('204 No Content', {}, '')]), sync_key='secret') - req = self._make_request('/v1/AUTH_cfa/c/o', + req = self._make_request( + '/v1/AUTH_cfa/c/o', environ={'REQUEST_METHOD': 'DELETE'}, headers={'x-container-sync-key': 'wrongsecret', 'x-timestamp': '123.456'}) @@ -512,7 +519,8 @@ class TestAuth(unittest.TestCase): self.test_auth.app = FakeApp(iter([('204 No Content', {}, '')]), sync_key='othersecret') - req = self._make_request('/v1/AUTH_cfa/c/o', + req = self._make_request( + '/v1/AUTH_cfa/c/o', environ={'REQUEST_METHOD': 'DELETE'}, headers={'x-container-sync-key': 'secret', 'x-timestamp': '123.456'}) @@ -522,7 +530,8 @@ class TestAuth(unittest.TestCase): self.test_auth.app = FakeApp(iter([('204 No Content', {}, '')]), sync_key=None) - req = self._make_request('/v1/AUTH_cfa/c/o', + req = self._make_request( + '/v1/AUTH_cfa/c/o', environ={'REQUEST_METHOD': 'DELETE'}, headers={'x-container-sync-key': 'secret', 'x-timestamp': '123.456'}) @@ -533,7 +542,8 @@ class TestAuth(unittest.TestCase): def test_sync_request_fail_no_timestamp(self): self.test_auth.app = FakeApp(iter([('204 No Content', {}, '')]), sync_key='secret') - req = self._make_request('/v1/AUTH_cfa/c/o', + req = self._make_request( + '/v1/AUTH_cfa/c/o', environ={'REQUEST_METHOD': 'DELETE'}, headers={'x-container-sync-key': 'secret'}) req.remote_addr = '127.0.0.1' @@ -543,7 +553,8 @@ class TestAuth(unittest.TestCase): def test_sync_request_fail_sync_host(self): self.test_auth.app = FakeApp(iter([('204 No Content', {}, '')]), sync_key='secret') - req = self._make_request('/v1/AUTH_cfa/c/o', + req = self._make_request( + '/v1/AUTH_cfa/c/o', environ={'REQUEST_METHOD': 'DELETE'}, headers={'x-container-sync-key': 'secret', 'x-timestamp': '123.456'}) @@ -554,7 +565,8 @@ class TestAuth(unittest.TestCase): def test_sync_request_success_lb_sync_host(self): self.test_auth.app = FakeApp(iter([('204 No Content', {}, '')]), sync_key='secret') - req = self._make_request('/v1/AUTH_cfa/c/o', + req = self._make_request( + '/v1/AUTH_cfa/c/o', environ={'REQUEST_METHOD': 'DELETE'}, headers={'x-container-sync-key': 'secret', 'x-timestamp': '123.456', @@ -565,7 +577,8 @@ class TestAuth(unittest.TestCase): self.test_auth.app = FakeApp(iter([('204 No Content', {}, '')]), sync_key='secret') - req = self._make_request('/v1/AUTH_cfa/c/o', + req = self._make_request( + '/v1/AUTH_cfa/c/o', environ={'REQUEST_METHOD': 'DELETE'}, headers={'x-container-sync-key': 'secret', 'x-timestamp': '123.456', @@ -574,6 +587,12 @@ class TestAuth(unittest.TestCase): resp = req.get_response(self.test_auth) self.assertEquals(resp.status_int, 204) + def test_options_call(self): + req = self._make_request('/v1/AUTH_cfa/c/o', + environ={'REQUEST_METHOD': 'OPTIONS'}) + resp = self.test_auth.authorize(req) + self.assertEquals(resp, None) + class TestParseUserCreation(unittest.TestCase): def test_parse_user_creation(self): @@ -607,11 +626,11 @@ class TestParseUserCreation(unittest.TestCase): 'user64_%s_%s' % ( b64encode('test').rstrip('='), b64encode('tester3').rstrip('=')): - 'testing .reseller_admin', + 'testing .reseller_admin', 'user64_%s_%s' % ( b64encode('user_foo').rstrip('='), b64encode('ab').rstrip('=')): - 'urlly .admin http://a.b/v1/DEF_has', + 'urlly .admin http://a.b/v1/DEF_has', })(FakeApp()) self.assertEquals(auth_filter.users, { 'test:tester3': { diff --git a/test/unit/proxy/test_server.py b/test/unit/proxy/test_server.py index 62113c1789..8e4d95daf7 100644 --- a/test/unit/proxy/test_server.py +++ b/test/unit/proxy/test_server.py @@ -3403,6 +3403,75 @@ class TestObjectController(unittest.TestCase): sock.close() self.assertEquals(before_request_instances, _request_instances) + def test_OPTIONS(self): + with save_globals(): + controller = proxy_server.ObjectController(self.app, 'a', + 'c', 'o.jpg') + + def my_empty_container_info(*args): + return {} + controller.container_info = my_empty_container_info + req = Request.blank( + '/a/c/o.jpg', + {'REQUEST_METHOD': 'OPTIONS'}, + headers={'Origin': 'http://foo.com'}) + resp = controller.OPTIONS(req) + self.assertEquals(401, resp.status_int) + + def my_empty_origin_container_info(*args): + return {'cors': {'allow_origin': None}} + controller.container_info = my_empty_origin_container_info + req = Request.blank( + '/a/c/o.jpg', + {'REQUEST_METHOD': 'OPTIONS'}, + headers={'Origin': 'http://foo.com'}) + resp = controller.OPTIONS(req) + self.assertEquals(401, resp.status_int) + + def my_container_info(*args): + return { + 'cors': { + 'allow_origin': 'http://foo.bar:8080 https://foo.bar', + 'allow_headers': 'x-foo', + 'max_age': 999, + } + } + controller.container_info = my_container_info + req = Request.blank( + '/a/c/o.jpg', + {'REQUEST_METHOD': 'OPTIONS'}, + headers={'Origin': 'https://foo.bar'}) + req.content_length = 0 + resp = controller.OPTIONS(req) + self.assertEquals(200, resp.status_int) + self.assertEquals( + set(['http://foo.bar:8080', 'https://foo.bar']), + set(resp.headers['access-control-allow-origin'].split())) + self.assertEquals( + 'GET, POST, PUT, DELETE, HEAD', + resp.headers['access-control-allow-methods']) + self.assertEquals('999', resp.headers['access-control-max-age']) + self.assertEquals( + 'x-foo', + resp.headers['access-control-allow-headers']) + req = Request.blank('/a/c/o.jpg', {'REQUEST_METHOD': 'OPTIONS'}) + req.content_length = 0 + resp = controller.OPTIONS(req) + self.assertEquals(401, resp.status_int) + req = Request.blank( + '/a/c/o.jpg', + {'REQUEST_METHOD': 'OPTIONS'}, + headers={'Origin': 'http://foo.com'}) + resp = controller.OPTIONS(req) + self.assertEquals(401, resp.status_int) + req = Request.blank( + '/a/c/o.jpg', + {'REQUEST_METHOD': 'OPTIONS'}, + headers={'Origin': 'http://foo.bar'}) + controller.app.cors_allow_origin = ['http://foo.bar', ] + resp = controller.OPTIONS(req) + self.assertEquals(200, resp.status_int) + class TestContainerController(unittest.TestCase): "Test swift.proxy_server.ContainerController" @@ -3892,6 +3961,74 @@ class TestContainerController(unittest.TestCase): res = controller.HEAD(req) self.assert_(called[0]) + def test_OPTIONS(self): + with save_globals(): + controller = proxy_server.ContainerController(self.app, 'a', 'c') + + def my_empty_container_info(*args): + return {} + controller.container_info = my_empty_container_info + req = Request.blank( + '/a/c', + {'REQUEST_METHOD': 'OPTIONS'}, + headers={'Origin': 'http://foo.com'}) + resp = controller.OPTIONS(req) + self.assertEquals(401, resp.status_int) + + def my_empty_origin_container_info(*args): + return {'cors': {'allow_origin': None}} + controller.container_info = my_empty_origin_container_info + req = Request.blank( + '/a/c', + {'REQUEST_METHOD': 'OPTIONS'}, + headers={'Origin': 'http://foo.com'}) + resp = controller.OPTIONS(req) + self.assertEquals(401, resp.status_int) + + def my_container_info(*args): + return { + 'cors': { + 'allow_origin': 'http://foo.bar:8080 https://foo.bar', + 'allow_headers': 'x-foo', + 'max_age': 999, + } + } + controller.container_info = my_container_info + req = Request.blank( + '/a/c', + {'REQUEST_METHOD': 'OPTIONS'}, + headers={'Origin': 'https://foo.bar'}) + req.content_length = 0 + resp = controller.OPTIONS(req) + self.assertEquals(200, resp.status_int) + self.assertEquals( + set(['http://foo.bar:8080', 'https://foo.bar']), + set(resp.headers['access-control-allow-origin'].split())) + self.assertEquals( + 'GET, POST, PUT, DELETE, HEAD', + resp.headers['access-control-allow-methods']) + self.assertEquals('999', resp.headers['access-control-max-age']) + self.assertEquals( + 'x-foo', + resp.headers['access-control-allow-headers']) + req = Request.blank('/a/c', {'REQUEST_METHOD': 'OPTIONS'}) + req.content_length = 0 + resp = controller.OPTIONS(req) + self.assertEquals(401, resp.status_int) + req = Request.blank( + '/a/c', + {'REQUEST_METHOD': 'OPTIONS'}, + headers={'Origin': 'http://foo.bar'}) + resp = controller.OPTIONS(req) + self.assertEquals(401, resp.status_int) + req = Request.blank( + '/a/c', + {'REQUEST_METHOD': 'OPTIONS'}, + headers={'Origin': 'http://foo.bar'}) + controller.app.cors_allow_origin = ['http://foo.bar', ] + resp = controller.OPTIONS(req) + self.assertEquals(200, resp.status_int) + class TestAccountController(unittest.TestCase):