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

Subscribers

People subscribed via source and target branches