From 9a107daf07859a60acb1876fe44cd0a487b7dd1d Mon Sep 17 00:00:00 2001 From: Kiall Mac Innes Date: Wed, 26 Jun 2013 15:23:07 +0100 Subject: [PATCH] Add simple Maintenance Mode WSGI middleware. Additionally, this commit moves the auth middware to a common middleware module. Change-Id: Ieddc8ebca25b99483232cb4b0203871454789ad4 --- designate/api/{auth.py => middleware.py} | 33 +++++- .../{test_auth.py => test_middleware.py} | 105 ++++++++++++++++-- designate/tests/test_api/test_v1/__init__.py | 5 +- etc/designate/api-paste.ini.sample | 13 ++- 4 files changed, 136 insertions(+), 20 deletions(-) rename designate/api/{auth.py => middleware.py} (69%) rename designate/tests/test_api/{test_auth.py => test_middleware.py} (50%) diff --git a/designate/api/auth.py b/designate/api/middleware.py similarity index 69% rename from designate/api/auth.py rename to designate/api/middleware.py index c3fcbbf14..ef17e5bcb 100644 --- a/designate/api/auth.py +++ b/designate/api/middleware.py @@ -13,6 +13,7 @@ # 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 flask from designate.openstack.common import cfg from designate.openstack.common import local from designate.openstack.common import log as logging @@ -22,8 +23,38 @@ from designate.context import DesignateContext LOG = logging.getLogger(__name__) +cfg.CONF.register_opts([ + cfg.BoolOpt('maintenance-mode', default=False, + help='Enable API Maintenance Mode'), + cfg.StrOpt('maintenance-mode-role', default='admin', + help='Role allowed to bypass maintaince mode'), +], group='service:api') -def pipeline_factory(loader, global_conf, **local_conf): + +class MaintenanceMiddleware(wsgi.Middleware): + def __init__(self, application): + super(MaintenanceMiddleware, self).__init__(application) + + self.enabled = cfg.CONF['service:api'].maintenance_mode + self.role = cfg.CONF['service:api'].maintenance_mode_role + + def process_request(self, request): + # If maintaince mode is not enabled, pass the request on as soon as + # possible + if not self.enabled: + return None + + # If the caller has the bypass role, let them through + if ('context' in request.environ + and self.role in request.environ['context'].roles): + LOG.warning('Request authorized to bypass maintenance mode') + return None + + # Otherwise, reject the request with a 503 Service Unavailable + return flask.Response(status=503, headers={'Retry-After': 60}) + + +def auth_pipeline_factory(loader, global_conf, **local_conf): """ A paste pipeline replica that keys off of auth_strategy. diff --git a/designate/tests/test_api/test_auth.py b/designate/tests/test_api/test_middleware.py similarity index 50% rename from designate/tests/test_api/test_auth.py rename to designate/tests/test_api/test_middleware.py index 5bbd8d4f5..f80b846b3 100644 --- a/designate/tests/test_api/test_auth.py +++ b/designate/tests/test_api/test_middleware.py @@ -1,4 +1,5 @@ # Copyright 2012 Managed I.T. +# Copyright 2013 Hewlett-Packard Development Company, L.P. # # Author: Kiall Mac Innes # @@ -14,20 +15,102 @@ # License for the specific language governing permissions and limitations # under the License. from designate.tests.test_api import ApiTestCase -from designate.api import auth +from designate.api import middleware + + +class FakeContext(object): + def __init__(self, roles=[]): + self.roles = roles class FakeRequest(object): - headers = {} - environ = {} + def __init__(self): + self.headers = {} + self.environ = {} + + def get_response(self, app): + return "FakeResponse" + + +class MaintenanceMiddlewareTest(ApiTestCase): + __test__ = True + + def test_process_request_disabled(self): + self.config(maintenance_mode=False, group='service:api') + + request = FakeRequest() + app = middleware.MaintenanceMiddleware({}) + + # Process the request + response = app(request) + + # Ensure request was not blocked + self.assertEqual(response, 'FakeResponse') + + def test_process_request_enabled_reject(self): + self.config(maintenance_mode=True, maintenance_mode_role='admin', + group='service:api') + + request = FakeRequest() + request.environ['context'] = FakeContext(roles=['user']) + + app = middleware.MaintenanceMiddleware({}) + + # Process the request + response = app(request) + + # Ensure request was blocked + self.assertEqual(response.status_code, 503) + + def test_process_request_enabled_reject_no_roles(self): + self.config(maintenance_mode=True, maintenance_mode_role='admin', + group='service:api') + + request = FakeRequest() + request.environ['context'] = FakeContext(roles=[]) + + app = middleware.MaintenanceMiddleware({}) + + # Process the request + response = app(request) + + # Ensure request was blocked + self.assertEqual(response.status_code, 503) + + def test_process_request_enabled_reject_no_context(self): + self.config(maintenance_mode=True, maintenance_mode_role='admin', + group='service:api') + + request = FakeRequest() + app = middleware.MaintenanceMiddleware({}) + + # Process the request + response = app(request) + + # Ensure request was blocked + self.assertEqual(response.status_code, 503) + + def test_process_request_enabled_bypass(self): + self.config(maintenance_mode=True, maintenance_mode_role='admin', + group='service:api') + + request = FakeRequest() + request.environ['context'] = FakeContext(roles=['admin']) + + app = middleware.MaintenanceMiddleware({}) + + # Process the request + response = app(request) + + # Ensure request was not blocked + self.assertEqual(response, 'FakeResponse') class KeystoneContextMiddlewareTest(ApiTestCase): __test__ = True def test_process_request(self): - app = {} - middleware = auth.KeystoneContextMiddleware(app) + app = middleware.KeystoneContextMiddleware({}) request = FakeRequest() @@ -39,7 +122,7 @@ class KeystoneContextMiddlewareTest(ApiTestCase): } # Process the request - middleware.process_request(request) + app.process_request(request) self.assertIn('context', request.environ) @@ -55,8 +138,7 @@ class KeystoneContextMiddlewareTest(ApiTestCase): # Set the policy to accept the authz self.policy({'use_sudo': '@'}) - app = {} - middleware = auth.KeystoneContextMiddleware(app) + app = middleware.KeystoneContextMiddleware({}) request = FakeRequest() @@ -70,7 +152,7 @@ class KeystoneContextMiddlewareTest(ApiTestCase): } # Process the request - middleware.process_request(request) + app.process_request(request) self.assertIn('context', request.environ) @@ -89,13 +171,12 @@ class NoAuthContextMiddlewareTest(ApiTestCase): __test__ = True def test_process_request(self): - app = {} - middleware = auth.NoAuthContextMiddleware(app) + app = middleware.NoAuthContextMiddleware({}) request = FakeRequest() # Process the request - middleware.process_request(request) + app.process_request(request) self.assertIn('context', request.environ) diff --git a/designate/tests/test_api/test_v1/__init__.py b/designate/tests/test_api/test_v1/__init__.py index 47b1be046..34cf74e98 100644 --- a/designate/tests/test_api/test_v1/__init__.py +++ b/designate/tests/test_api/test_v1/__init__.py @@ -15,7 +15,7 @@ # under the License. from designate.openstack.common import log as logging from designate.api import v1 as api_v1 -from designate.api import auth +from designate.api import middleware from designate.tests.test_api import ApiTestCase @@ -35,7 +35,8 @@ class ApiV1Test(ApiTestCase): self.app.wsgi_app = api_v1.FaultWrapperMiddleware(self.app.wsgi_app) # Inject the NoAuth middleware - self.app.wsgi_app = auth.NoAuthContextMiddleware(self.app.wsgi_app) + self.app.wsgi_app = middleware.NoAuthContextMiddleware( + self.app.wsgi_app) # Obtain a test client self.client = self.app.test_client() diff --git a/etc/designate/api-paste.ini.sample b/etc/designate/api-paste.ini.sample index 6b05067eb..26a0344c9 100644 --- a/etc/designate/api-paste.ini.sample +++ b/etc/designate/api-paste.ini.sample @@ -7,9 +7,9 @@ use = egg:Paste#urlmap paste.app_factory = designate.api.versions:factory [composite:osapi_dns_v1] -use = call:designate.api.auth:pipeline_factory -noauth = noauthcontext faultwrapper_v1 osapi_dns_app_v1 -keystone = authtoken keystonecontext faultwrapper_v1 osapi_dns_app_v1 +use = call:designate.api.middleware:auth_pipeline_factory +noauth = noauthcontext maintenance faultwrapper_v1 osapi_dns_app_v1 +keystone = authtoken keystonecontext maintenance faultwrapper_v1 osapi_dns_app_v1 [app:osapi_dns_app_v1] paste.app_factory = designate.api.v1:factory @@ -17,11 +17,14 @@ paste.app_factory = designate.api.v1:factory [filter:faultwrapper_v1] paste.filter_factory = designate.api.v1:FaultWrapperMiddleware.factory +[filter:maintenance] +paste.filter_factory = designate.api.middleware:MaintenanceMiddleware.factory + [filter:noauthcontext] -paste.filter_factory = designate.api.auth:NoAuthContextMiddleware.factory +paste.filter_factory = designate.api.middleware:NoAuthContextMiddleware.factory [filter:keystonecontext] -paste.filter_factory = designate.api.auth:KeystoneContextMiddleware.factory +paste.filter_factory = designate.api.middleware:KeystoneContextMiddleware.factory [filter:authtoken] paste.filter_factory = keystoneclient.middleware.auth_token:filter_factory