Merge lp:~leonardr/lazr.restful/basic-auth into lp:lazr.restful

Proposed by Leonard Richardson
Status: Merged
Approved by: Aaron Bentley
Approved revision: 79
Merged at revision: not available
Proposed branch: lp:~leonardr/lazr.restful/basic-auth
Merge into: lp:lazr.restful
Diff against target: 168 lines
2 files modified
src/lazr/restful/docs/wsgi.txt (+92/-0)
src/lazr/restful/wsgi.py (+53/-0)
To merge this branch: bzr merge lp:~leonardr/lazr.restful/basic-auth
Reviewer Review Type Date Requested Status
Aaron Bentley (community) Approve
Review via email: mp+12538@code.launchpad.net
To post a comment you must log in.
Revision history for this message
Leonard Richardson (leonardr) wrote :

This branch gives lazr.restful a piece of WSGI middleware that handles HTTP Basic Auth. This is a generally useful piece of code, but I'm mainly putting it into lazr.restful so I can use it to test corresponding client-side functionality I'm going to add to lazr.restfulclient.

Revision history for this message
Aaron Bentley (abentley) wrote :

As discussed in IRC:

1. please rename "check_credentials" to "authenticate_user"
2. ensure the check_credentials docstring is valid ReST, i.e. add an indent on the second line of :return:.
3. please document either the __init__ parameters or the ivars
4. please rename BasicAuthMiddleware.regex to something more intuitive like secure_path_pattern.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== added file 'src/lazr/restful/docs/wsgi.txt'
--- src/lazr/restful/docs/wsgi.txt 1970-01-01 00:00:00 +0000
+++ src/lazr/restful/docs/wsgi.txt 2009-09-28 18:10:24 +0000
@@ -0,0 +1,92 @@
1WSGI Middleware
2===============
3
4lazr.restful defines some simple WSGI middleware for doing
5authentication. This is mainly for setting up testing scenarios for
6lazr.restfulclient, but you can use the middleware in real
7applications.
8
9BasicAuthMiddleware
10-------------------
11
12The BasicAuthMiddleware implements HTTP Basic Auth. You just need to
13subclass it and define the check_credentials() method. This method
14returns an object identifying the user who's trying to
15authenticate. If the authentication credentials are invalid, it
16returns None.
17
18 >>> from lazr.restful.wsgi import BasicAuthMiddleware
19 >>> class SimpleBasicAuthMiddleware(BasicAuthMiddleware):
20 ... """Accepts "user/password", rejects everything else."""
21 ...
22 ... def check_credentials(self, username, password):
23 ... if username == "user" and password == "password":
24 ... return username
25 ... return None
26
27First let's create a really simple WSGI application that responds to
28any request with a 200 response code.
29
30 >>> from lazr.restful.testing.webservice import WebServiceApplication
31 >>> from lazr.restful.example.base.tests.test_integration import (
32 ... CookbookWebServiceTestPublication)
33
34 >>> def dummy_application(environ, start_response):
35 ... start_response('200', [('Content-type','text/plain')])
36 ... return ['Success']
37
38Now let's protect part of that application with SimpleBasicAuthMiddleware.
39
40 >>> def protected_application():
41 ... return SimpleBasicAuthMiddleware(
42 ... dummy_application, realm="WSGI middleware test",
43 ... protect=".*protected.*")
44
45 >>> import wsgi_intercept
46 >>> from wsgi_intercept.httplib2_intercept import install
47 >>> install()
48 >>> wsgi_intercept.add_wsgi_intercept(
49 ... 'wsgitest', 80, protected_application)
50
51Most of the application's URLs are not protected by the
52middleware. You can access them without providing credentials.
53
54 >>> import httplib2
55 >>> client = httplib2.Http()
56 >>> response, body = client.request('http://wsgitest/')
57 >>> print response['status']
58 200
59 >>> print body
60 Success
61
62Any URL that includes the string "protected" is protected by the
63middleware, and cannot be accessed without credentials.
64
65 >>> response, body = client.request('http://wsgitest/protected/')
66 >>> print response['status']
67 401
68 >>> print response['www-authenticate']
69 Basic realm="WSGI middleware test"
70
71 >>> response, body = client.request(
72 ... 'http://wsgitest/this-is-protected-as-well/')
73 >>> print response['status']
74 401
75
76The check_credentials() implementation given at the beginning of the
77test will only accept the user/password combination "user"/"password".
78Provide a bad username or password and you'll get a 401.
79
80 >>> client.add_credentials("baduser", "baspassword")
81 >>> response, body = client.request('http://wsgitest/protected/')
82 >>> print response['status']
83 401
84
85Provide the correct credentials and you'll get a 200, even for the
86protected URIs.
87
88 >>> client.add_credentials("user", "password")
89 >>> response, body = client.request('http://wsgitest/protected/')
90 >>> print response['status']
91 200
92
093
=== modified file 'src/lazr/restful/wsgi.py'
--- src/lazr/restful/wsgi.py 2009-09-03 19:53:39 +0000
+++ src/lazr/restful/wsgi.py 2009-09-28 18:10:24 +0000
@@ -2,10 +2,12 @@
22
3__metaclass__ = type3__metaclass__ = type
4__all__ = [4__all__ = [
5 'BasicAuthMiddleware',
5 'WSGIApplication',6 'WSGIApplication',
6 ]7 ]
78
8from pkg_resources import resource_string9from pkg_resources import resource_string
10import re
9from wsgiref.simple_server import make_server as wsgi_make_server11from wsgiref.simple_server import make_server as wsgi_make_server
1012
11from zope.component import getUtility13from zope.component import getUtility
@@ -60,3 +62,54 @@
60 """Create a WSGI server object for a particular web service."""62 """Create a WSGI server object for a particular web service."""
61 cls.configure_server(host, port, config_package, config_file)63 cls.configure_server(host, port, config_package, config_file)
62 return wsgi_make_server(host, int(port), cls)64 return wsgi_make_server(host, int(port), cls)
65
66
67class BasicAuthMiddleware(object):
68 """WSGI middleware that implements HTTP Basic Auth.
69
70 To use this middleware, subclass this class and define
71 check_credentials().
72 """
73
74 def __init__(self, application, realm="Restricted area", protect='.*'):
75 self.regex = re.compile(protect)
76 self.application = application
77 self.realm = realm
78
79 def _unauthorized(self, start_response):
80 start_response("401 Unauthorized",
81 [('WWW-Authenticate',
82 'Basic realm="%s"' % self.realm)])
83 return ['401 Unauthorized']
84
85 def __call__(self, environ, start_response):
86 path_info = environ.get('PATH_INFO', '/')
87 if not self.regex.match(path_info):
88 environ['authenticated_user'] = None
89 return self.application(environ, start_response)
90
91 authorization = environ.get('HTTP_AUTHORIZATION')
92 if authorization is None:
93 return self._unauthorized(start_response)
94
95 method, auth = authorization.split(' ', 1)
96 if method.lower() != 'basic':
97 return self._unauthorized(start_response)
98
99 auth = auth.strip().decode('base64')
100 username, password = auth.split(':', 1)
101 authenticated_user = self.check_credentials(username, password)
102 if authenticated_user is None:
103 return self._unauthorized(start_response)
104
105 environ['authenticated_user'] = authenticated_user
106
107 return self.application(environ, start_response)
108
109 def check_credentials(self, username, password):
110 """Given a username and password, return an authenticated user.
111
112 :return: An object designating the authenticated user, or
113 None if the username/password combination is invalid.
114 """
115 raise NotImplementedError()

Subscribers

People subscribed via source and target branches