1. Victor Ng
  2. server-core-metrics

Commits

Ryan Kelly  committed 186175c

Bug 692658 - add acknowledgement phase to auth workflow; r=rmiller

  • Participants
  • Parent commits 5fe4fc1
  • Branches default

Comments (0)

Files changed (3)

File services/baseapp.py

View file
 from routes import Mapper
 
 from webob.dec import wsgify
-from webob.exc import HTTPNotFound, HTTPServiceUnavailable
+from webob.exc import HTTPNotFound, HTTPServiceUnavailable, HTTPException
 from webob import Response
 
 from services.util import (CatchErrorMiddleware, round_time, BackendError,
     @wsgify
     @_notified
     def __call__(self, request):
+        """Entry point for the WSGI app."""
         # the app is being killed, no more requests please
         if self.killing:
             raise HTTPServiceUnavailable()
         # pre-hook
         before_headers = self._before_call(request)
 
+        try:
+            response = self._dispatch_request(request)
+        except HTTPException, response:
+            # set before-call headers on all responses
+            response.headers.update(before_headers)
+            raise
+        else:
+            # set X-Weave-Timestamp on success responses
+            response.headers['X-Weave-Timestamp'] = str(request.server_time)
+            response.headers.update(before_headers)
+            return response
+
+    def _dispatch_request(self, request):
+        """Dispatch the request.
+
+        This will dispatch the request either to a special internal handler
+        or to one of the configured controller methods.
+        """ 
         # XXX
         # removing the trailing slash - ambiguity on client side
         url = request.path_info.rstrip('/')
                 request.method in ('HEAD', 'GET')):
                 return self._heartbeat(request)
 
+        # the debug page is called
         if self.debug_page is not None and url == '/%s' % self.debug_page:
             return self._debug(request)
 
+        # the request must be going to a controller method
         match = self.mapper.routematch(environ=request.environ)
 
         if match is None:
 
         match, __ = match
 
-        # authentication control
-        if self.auth is not None:
+        # if auth is enabled, wrap it around the call to the controller
+        if self.auth is None:
+            return self._dispatch_request_with_match(request, match)
+        else:
             self.auth.check(request, match)
+            try:
+                response = self._dispatch_request_with_match(request, match)
+            except HTTPException, response:
+                self.auth.acknowledge(request, response)
+                raise
+            else:
+                self.auth.acknowledge(request, response)
+                return response
 
+    def _dispatch_request_with_match(self, request, match):
+        """Dispatch a request according to a URL routing match."""
         function = self._get_function(match['controller'], match['action'])
         if function is None:
             raise HTTPNotFound('Unknown URL %r' % request.path_info)
 
         # create the response object in case we get back a string
         response = self._create_response(request, result, function)
-
-        # setting up the X-Weave-Timestamp
-        response.headers['X-Weave-Timestamp'] = str(request.server_time)
-        response.headers.update(before_headers)
         return response
 
     def _get_params(self, request):

File services/tests/test_baseapp.py

View file
 from services.wsgiauth import Authentication
 from services.tests.support import make_request
 
-from webob.exc import HTTPUnauthorized, HTTPServiceUnavailable
+from webob.exc import HTTPUnauthorized, HTTPServiceUnavailable, HTTPNotFound
 
 
 class _Foo(object):
     def user(self, request):
         return '|%s|' % request.user.get('username', None)
 
+    def missing(self, request):
+        raise HTTPNotFound(request)
+
 
 class Mod1(object):
     pass
     urls = [('POST', '/', 'foo', 'index'),
             ('GET', '/secret', 'foo', 'secret', {'auth': True}),
             ('GET', '/user/{username:[a-zA-Z0-9._-]+}', 'foo', 'user'),
+            ('GET', '/missing', 'foo', 'missing'),
             ('GET', '/boom', 'foo', 'boom'),
             ('GET', '/boom2', 'foo', 'boom2'),
             ('GET', '/boom3', 'foo', 'boom3')]
               'mod1.backend': 'services.tests.test_baseapp.Mod1',
               'mod2.backend': 'services.tests.test_baseapp.Mod2',
               }
+    auth_class = None
 
     def setUp(self):
-        self.app = SyncServerApp(self.urls, self.controllers, self.config)
+        self.app = SyncServerApp(self.urls, self.controllers, self.config,
+                                 auth_class=self.auth_class)
 
     def test_host_config(self):
         request = make_request("/", method='POST', host='localhost')
         self.assertEqual(res.body, '1')
 
     def test_auth(self):
-        # we don't have any auth by default
+        """Test authentication using the specific auth class."""
+        # we don't have any auth, this should just work
         request = make_request("/secret", method='GET')
         res = self.app(request)
         self.assertEqual(res.body, 'here')
 
-        # now let's add an auth
-        app = SyncServerApp(self.urls, self.controllers,
-                            self.config, auth_class=Authentication)
-        request = make_request("/secret", method='GET')
-
-        try:
-            app(request)
-        except HTTPUnauthorized, error:
-            self.assertEqual(error.headers['WWW-Authenticate'],
-                             'Basic realm="Sync"')
-        else:
-            raise AssertionError('Excepted a failure here')
-
-        auth = 'Basic %s' % base64.b64encode('tarek:tarek')
-        request.environ['HTTP_AUTHORIZATION'] = auth
-        res = app(request)
-        self.assertEqual(res.body, 'here')
-
     def test_retry_after(self):
         config = {'global.retry_after': 60,
                   'auth.backend': 'services.auth.dummy.DummyAuth'}
                 ('GET', '/boom3', 'foo', 'boom3')]
 
         controllers = {'foo': _Foo}
-        app = SyncServerApp(urls, controllers, config)
+        app = SyncServerApp(urls, controllers, config,
+                            auth_class=self.auth_class)
 
         request = make_request("/boom", method="GET", host="localhost")
         try:
         controllers = {}
 
         # testing the default configuration
-        app = SyncServerApp(urls, controllers, config)
+        app = SyncServerApp(urls, controllers, config,
+                            auth_class=self.auth_class)
 
         # a heartbeat returns a 200 / empty body
         request = make_request("/__heartbeat__")
                 raise HTTPServiceUnavailable()
 
         # testing that new app
-        app = MyCoolApp(urls, controllers, config)
+        app = MyCoolApp(urls, controllers, config, auth_class=self.auth_class)
 
         # a heartbeat returns a 503 / empty body
         request = make_request("/__heartbeat__")
         self.assertEqual(res.status_int, 200)
         self.assertTrue("|testuser|" in res.body)
 
+    def test_notfound(self):
+        # test that non-existent pages raise a 404.
+        # this one has no match, so it will return early
+        request = make_request("/nonexistent")
+        self.assertEquals(self.app(request).status, "404 Not Found")
+        # this one has a match, will raise from below the auth handler
+        request = make_request("/missing")
+        self.assertRaises(HTTPNotFound, self.app, request)
+
     def test_events(self):
 
         pings = []
                       'auth.backend': 'services.auth.dummy.DummyAuth'}
             urls = []
             controllers = {}
-            app = SyncServerApp(urls, controllers, config)
+            app = SyncServerApp(urls, controllers, config,
+                                auth_class=self.auth_class)
             request = make_request("/user/__hearbeat__")
             app(request)
         finally:
 
             urls = []
             controllers = {}
-            app = SyncServerApp(urls, controllers, config)
+            app = SyncServerApp(urls, controllers, config,
+                                auth_class=self.auth_class)
 
             # heartbeat should work
             request = make_request("/__heartbeat__")
 
             urls = []
             controllers = {}
-            app = SyncServerApp(urls, controllers, config)
+            app = SyncServerApp(urls, controllers, config,
+                                auth_class=self.auth_class)
 
             # heartbeat should work
             request = make_request("/__heartbeat__")
         self.assertEqual(mod2.__class__, Mod2)
 
 
+class TestBaseApp_Auth(TestBaseApp):
+
+    auth_class = Authentication
+
+    def test_auth(self):
+        # it should ask us to authenticate using HTTP-Basic-Auth
+        request = make_request("/secret", method='GET')
+        try:
+            self.app(request)
+        except HTTPUnauthorized, error:
+            self.assertEqual(error.headers['WWW-Authenticate'],
+                             'Basic realm="Sync"')
+        else:
+            raise AssertionError('Excepted a failure here')
+
+        # it should accept credentials using HTTP-Basic-Auth
+        auth = 'Basic %s' % base64.b64encode('tarek:tarek')
+        request.environ['HTTP_AUTHORIZATION'] = auth
+        res = self.app(request)
+        self.assertEqual(res.body, 'here')
+
+
 def test_suite():
     suite = unittest.TestSuite()
     suite.addTest(unittest.makeSuite(TestBaseApp))
+    suite.addTest(unittest.makeSuite(TestBaseApp_Auth))
     return suite
 
 

File services/wsgiauth.py

View file
 
 
 class Authentication(object):
-    """Authentication tool. defines the authentication strategy"""
+    """Authentication tool. Defines the authentication strategy.
+
+    Each services application can use a subclass of "Authentication" to define
+    its own authentication strategy.  The class must provide two methods:
+
+        * check(request, match):  check whether auth is required, extract and
+                                  verify credentials, and possibly raise an
+                                  error if auth fails.
+
+        * acknowledge(request, response):  add headers to the response to
+                                           acknowledge successful auth.
+
+    The base class implementation uses HTTP-Basic-Auth to authenticate
+    users.  New applications should consider using the "WhoAuthentication"
+    class from services.whoauth, which uses repoze.who to provide a pluggable
+    authentication stack.
+    """
     def __init__(self, config):
         self.config = config
         self.backend = load_and_configure(self.config, 'auth')
     def check(self, request, match):
         """Checks if the current request/match can be viewed.
 
-        This function can raise an UnauthorizedError, or
-        redirect to a login page.
-
+        This function can raise HTTPUnauthorized, or redirect to a
+        login page.
         """
         if match.get('auth') != 'True':
             return
 
         match['user_id'] = user_id
 
+    def acknowledge(self, request, response):
+        """Acknowledges successful auth back to the user.
+
+        This method might send a HTTP Authentication-Info header or set a
+        cookie allowing the client to remember its login session.
+        """
+        pass
+
     def authenticate_user(self, request, config, username=None):
         """Authenticates a user and returns his id.