Merge lp:~thisfred/u1db/add-basic-auth into lp:u1db

Proposed by Eric Casteleijn
Status: Merged
Approved by: Samuele Pedroni
Approved revision: 398
Merged at revision: 395
Proposed branch: lp:~thisfred/u1db/add-basic-auth
Merge into: lp:u1db
Diff against target: 395 lines (+231/-27)
5 files modified
u1db/remote/basic_auth_middleware.py (+63/-0)
u1db/remote/oauth_middleware.py (+3/-2)
u1db/tests/test_auth_middleware.py (+157/-15)
u1db/tests/test_https.py (+7/-9)
u1db/tests/test_remote_sync_target.py (+1/-1)
To merge this branch: bzr merge lp:~thisfred/u1db/add-basic-auth
Reviewer Review Type Date Requested Status
Samuele Pedroni Approve
Review via email: mp+122323@code.launchpad.net

Commit message

Added basic authentication middleware.

Description of the change

Added basic authentication middleware.

To post a comment you must log in.
lp:~thisfred/u1db/add-basic-auth updated
396. By Eric Casteleijn

prefix configurable

397. By Eric Casteleijn

improved testing, removed copy paste detritus

398. By Eric Casteleijn

more prefix tests

Revision history for this message
Samuele Pedroni (pedronis) wrote :

good

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== added file 'u1db/remote/basic_auth_middleware.py'
--- u1db/remote/basic_auth_middleware.py 1970-01-01 00:00:00 +0000
+++ u1db/remote/basic_auth_middleware.py 2012-09-05 16:09:18 +0000
@@ -0,0 +1,63 @@
1# Copyright 2012 Canonical Ltd.
2#
3# This file is part of u1db.
4#
5# u1db is free software: you can redistribute it and/or modify
6# it under the terms of the GNU Lesser General Public License version 3
7# as published by the Free Software Foundation.
8#
9# u1db is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU Lesser General Public License for more details.
13#
14# You should have received a copy of the GNU Lesser General Public License
15# along with u1db. If not, see <http://www.gnu.org/licenses/>.
16"""U1DB Basic Auth authorisation WSGI middleware."""
17import httplib
18try:
19 import simplejson as json
20except ImportError:
21 import json # noqa
22from wsgiref.util import shift_path_info
23
24
25class BasicAuthMiddleware(object):
26 """U1DB Basic Auth Authorisation WSGI middleware."""
27
28 def __init__(self, app, prefix):
29 self.app = app
30 self.prefix = prefix
31
32 def _error(self, start_response, status, description, message=None):
33 start_response("%d %s" % (status, httplib.responses[status]),
34 [('content-type', 'application/json')])
35 err = {"error": description}
36 if message:
37 err['message'] = message
38 return [json.dumps(err)]
39
40 def __call__(self, environ, start_response):
41 if self.prefix and not environ['PATH_INFO'].startswith(self.prefix):
42 return self._error(start_response, 400, "bad request")
43 auth = environ.get('HTTP_AUTHORIZATION')
44 if not auth:
45 return self._error(start_response, 401, "unauthorized",
46 "Missing Basic Authentication.")
47 scheme, encoded = auth.split(None, 1)
48 if scheme.lower() != 'basic':
49 return self._error(
50 start_response, 401, "unauthorized",
51 "Missing Basic Authentication")
52 user, password = encoded.decode('base64').split(':', 1)
53 if not self.verify_user(user, password):
54 return self._error(
55 start_response, 401, "unauthorized",
56 "Incorrect password or login.")
57 del environ['HTTP_AUTHORIZATION']
58 environ['user_id'] = user
59 shift_path_info(environ)
60 return self.app(environ, start_response)
61
62 def verify_user(self, username, password):
63 raise NotImplementedError(self.verify_user)
064
=== modified file 'u1db/remote/oauth_middleware.py'
--- u1db/remote/oauth_middleware.py 2012-08-14 14:31:45 +0000
+++ u1db/remote/oauth_middleware.py 2012-09-05 16:09:18 +0000
@@ -35,9 +35,10 @@
35 # from arrival time35 # from arrival time
36 timestamp_threshold = 30036 timestamp_threshold = 300
3737
38 def __init__(self, app, base_url):38 def __init__(self, app, base_url, prefix='/~/'):
39 self.app = app39 self.app = app
40 self.base_url = base_url40 self.base_url = base_url
41 self.prefix = prefix
4142
42 def get_oauth_data_store(self):43 def get_oauth_data_store(self):
43 """Provide a oauth.OAuthDataStore."""44 """Provide a oauth.OAuthDataStore."""
@@ -52,7 +53,7 @@
52 return [json.dumps(err)]53 return [json.dumps(err)]
5354
54 def __call__(self, environ, start_response):55 def __call__(self, environ, start_response):
55 if not environ['PATH_INFO'].startswith('/~/'):56 if self.prefix and not environ['PATH_INFO'].startswith(self.prefix):
56 return self._error(start_response, 400, "bad request")57 return self._error(start_response, 400, "bad request")
57 headers = {}58 headers = {}
58 if 'HTTP_AUTHORIZATION' in environ:59 if 'HTTP_AUTHORIZATION' in environ:
5960
=== renamed file 'u1db/tests/test_oauth_middleware.py' => 'u1db/tests/test_auth_middleware.py'
--- u1db/tests/test_oauth_middleware.py 2012-08-14 14:31:45 +0000
+++ u1db/tests/test_auth_middleware.py 2012-09-05 16:09:18 +0000
@@ -26,15 +26,89 @@
26from u1db import tests26from u1db import tests
2727
28from u1db.remote.oauth_middleware import OAuthMiddleware28from u1db.remote.oauth_middleware import OAuthMiddleware
2929from u1db.remote.basic_auth_middleware import BasicAuthMiddleware
3030
31BASE_URL = 'https://u1db.net'31
3232BASE_URL = 'https://example.net'
3333
34class TestAuthMiddleware(tests.TestCase):34
3535class TestBasicAuthMiddleware(tests.TestCase):
36 def setUp(self):36
37 super(TestAuthMiddleware, self).setUp()37 def setUp(self):
38 super(TestBasicAuthMiddleware, self).setUp()
39 self.got = []
40
41 def witness_app(environ, start_response):
42 start_response("200 OK", [("content-type", "text/plain")])
43 self.got.append((environ['user_id'], environ['PATH_INFO'],
44 environ['QUERY_STRING']))
45 return ["ok"]
46
47 class MyAuthMiddleware(BasicAuthMiddleware):
48
49 def verify_user(self, user, password):
50 if user != "correct_user":
51 return False
52 if password != "correct_password":
53 return False
54 return True
55
56 self.auth_midw = MyAuthMiddleware(witness_app, prefix="/pfx/")
57 self.app = paste.fixture.TestApp(self.auth_midw)
58
59 def test_expect_prefix(self):
60 url = BASE_URL + '/foo/doc/doc-id'
61 resp = self.app.delete(url, expect_errors=True)
62 self.assertEqual(400, resp.status)
63 self.assertEqual('application/json', resp.header('content-type'))
64 self.assertEqual('{"error": "bad request"}', resp.body)
65
66 def test_missing_auth(self):
67 url = BASE_URL + '/pfx/foo/doc/doc-id'
68 resp = self.app.delete(url, expect_errors=True)
69 self.assertEqual(401, resp.status)
70 self.assertEqual('application/json', resp.header('content-type'))
71 self.assertEqual(
72 {"error": "unauthorized",
73 "message": "Missing Basic Authentication."},
74 json.loads(resp.body))
75
76 def test_correct_auth(self):
77 user = "correct_user"
78 password = "correct_password"
79 params = {'old_rev': 'old-rev'}
80 url = BASE_URL + '/pfx/foo/doc/doc-id?%s' % (
81 '&'.join("%s=%s" % (k, v) for k, v in params.items()))
82 auth = '%s:%s' % (user, password)
83 headers = {
84 'Authorization': 'Basic %s' % (auth.encode('base64'),)}
85 resp = self.app.delete(url, headers=headers)
86 self.assertEqual(200, resp.status)
87 self.assertEqual(
88 [('correct_user', '/foo/doc/doc-id', 'old_rev=old-rev')], self.got)
89
90 def test_incorrect_auth(self):
91 user = "correct_user"
92 password = "incorrect_password"
93 params = {'old_rev': 'old-rev'}
94 url = BASE_URL + '/pfx/foo/doc/doc-id?%s' % (
95 '&'.join("%s=%s" % (k, v) for k, v in params.items()))
96 auth = '%s:%s' % (user, password)
97 headers = {
98 'Authorization': 'Basic %s' % (auth.encode('base64'),)}
99 resp = self.app.delete(url, headers=headers, expect_errors=True)
100 self.assertEqual(401, resp.status)
101 self.assertEqual('application/json', resp.header('content-type'))
102 self.assertEqual(
103 {"error": "unauthorized",
104 "message": "Incorrect password or login."},
105 json.loads(resp.body))
106
107
108class TestOAuthMiddlewareDefaultPrefix(tests.TestCase):
109 def setUp(self):
110
111 super(TestOAuthMiddlewareDefaultPrefix, self).setUp()
38 self.got = []112 self.got = []
39113
40 def witness_app(environ, start_response):114 def witness_app(environ, start_response):
@@ -61,8 +135,76 @@
61 self.assertEqual('application/json', resp.header('content-type'))135 self.assertEqual('application/json', resp.header('content-type'))
62 self.assertEqual('{"error": "bad request"}', resp.body)136 self.assertEqual('{"error": "bad request"}', resp.body)
63137
138 def test_oauth_in_header(self):
139 url = BASE_URL + '/~/foo/doc/doc-id'
140 params = {'old_rev': 'old-rev'}
141 oauth_req = oauth.OAuthRequest.from_consumer_and_token(
142 tests.consumer2,
143 tests.token2,
144 parameters=params,
145 http_url=url,
146 http_method='DELETE'
147 )
148 url = oauth_req.get_normalized_http_url() + '?' + (
149 '&'.join("%s=%s" % (k, v) for k, v in params.items()))
150 oauth_req.sign_request(tests.sign_meth_HMAC_SHA1,
151 tests.consumer2, tests.token2)
152 resp = self.app.delete(url, headers=oauth_req.to_header())
153 self.assertEqual(200, resp.status)
154 self.assertEqual([(tests.token2.key,
155 '/foo/doc/doc-id', 'old_rev=old-rev')], self.got)
156
157 def test_oauth_in_query_string(self):
158 url = BASE_URL + '/~/foo/doc/doc-id'
159 params = {'old_rev': 'old-rev'}
160 oauth_req = oauth.OAuthRequest.from_consumer_and_token(
161 tests.consumer1,
162 tests.token1,
163 parameters=params,
164 http_url=url,
165 http_method='DELETE'
166 )
167 oauth_req.sign_request(tests.sign_meth_HMAC_SHA1,
168 tests.consumer1, tests.token1)
169 resp = self.app.delete(oauth_req.to_url())
170 self.assertEqual(200, resp.status)
171 self.assertEqual([(tests.token1.key,
172 '/foo/doc/doc-id', 'old_rev=old-rev')], self.got)
173
174
175class TestOAuthMiddleware(tests.TestCase):
176
177 def setUp(self):
178 super(TestOAuthMiddleware, self).setUp()
179 self.got = []
180
181 def witness_app(environ, start_response):
182 start_response("200 OK", [("content-type", "text/plain")])
183 self.got.append((environ['token_key'], environ['PATH_INFO'],
184 environ['QUERY_STRING']))
185 return ["ok"]
186
187 class MyOAuthMiddleware(OAuthMiddleware):
188 get_oauth_data_store = lambda self: tests.testingOAuthStore
189
190 def verify(self, environ, oauth_req):
191 consumer, token = super(MyOAuthMiddleware, self).verify(
192 environ, oauth_req)
193 environ['token_key'] = token.key
194
195 self.oauth_midw = MyOAuthMiddleware(
196 witness_app, BASE_URL, prefix='/pfx/')
197 self.app = paste.fixture.TestApp(self.oauth_midw)
198
199 def test_expect_prefix(self):
200 url = BASE_URL + '/foo/doc/doc-id'
201 resp = self.app.delete(url, expect_errors=True)
202 self.assertEqual(400, resp.status)
203 self.assertEqual('application/json', resp.header('content-type'))
204 self.assertEqual('{"error": "bad request"}', resp.body)
205
64 def test_missing_oauth(self):206 def test_missing_oauth(self):
65 url = BASE_URL + '/~/foo/doc/doc-id'207 url = BASE_URL + '/pfx/foo/doc/doc-id'
66 resp = self.app.delete(url, expect_errors=True)208 resp = self.app.delete(url, expect_errors=True)
67 self.assertEqual(401, resp.status)209 self.assertEqual(401, resp.status)
68 self.assertEqual('application/json', resp.header('content-type'))210 self.assertEqual('application/json', resp.header('content-type'))
@@ -71,7 +213,7 @@
71 json.loads(resp.body))213 json.loads(resp.body))
72214
73 def test_oauth_in_query_string(self):215 def test_oauth_in_query_string(self):
74 url = BASE_URL + '/~/foo/doc/doc-id'216 url = BASE_URL + '/pfx/foo/doc/doc-id'
75 params = {'old_rev': 'old-rev'}217 params = {'old_rev': 'old-rev'}
76 oauth_req = oauth.OAuthRequest.from_consumer_and_token(218 oauth_req = oauth.OAuthRequest.from_consumer_and_token(
77 tests.consumer1,219 tests.consumer1,
@@ -88,7 +230,7 @@
88 '/foo/doc/doc-id', 'old_rev=old-rev')], self.got)230 '/foo/doc/doc-id', 'old_rev=old-rev')], self.got)
89231
90 def test_oauth_invalid(self):232 def test_oauth_invalid(self):
91 url = BASE_URL + '/~/foo/doc/doc-id'233 url = BASE_URL + '/pfx/foo/doc/doc-id'
92 params = {'old_rev': 'old-rev'}234 params = {'old_rev': 'old-rev'}
93 oauth_req = oauth.OAuthRequest.from_consumer_and_token(235 oauth_req = oauth.OAuthRequest.from_consumer_and_token(
94 tests.consumer1,236 tests.consumer1,
@@ -109,7 +251,7 @@
109 err)251 err)
110252
111 def test_oauth_in_header(self):253 def test_oauth_in_header(self):
112 url = BASE_URL + '/~/foo/doc/doc-id'254 url = BASE_URL + '/pfx/foo/doc/doc-id'
113 params = {'old_rev': 'old-rev'}255 params = {'old_rev': 'old-rev'}
114 oauth_req = oauth.OAuthRequest.from_consumer_and_token(256 oauth_req = oauth.OAuthRequest.from_consumer_and_token(
115 tests.consumer2,257 tests.consumer2,
@@ -128,7 +270,7 @@
128 '/foo/doc/doc-id', 'old_rev=old-rev')], self.got)270 '/foo/doc/doc-id', 'old_rev=old-rev')], self.got)
129271
130 def test_oauth_plain_text(self):272 def test_oauth_plain_text(self):
131 url = BASE_URL + '/~/foo/doc/doc-id'273 url = BASE_URL + '/pfx/foo/doc/doc-id'
132 params = {'old_rev': 'old-rev'}274 params = {'old_rev': 'old-rev'}
133 oauth_req = oauth.OAuthRequest.from_consumer_and_token(275 oauth_req = oauth.OAuthRequest.from_consumer_and_token(
134 tests.consumer1,276 tests.consumer1,
@@ -145,7 +287,7 @@
145 '/foo/doc/doc-id', 'old_rev=old-rev')], self.got)287 '/foo/doc/doc-id', 'old_rev=old-rev')], self.got)
146288
147 def test_oauth_timestamp_threshold(self):289 def test_oauth_timestamp_threshold(self):
148 url = BASE_URL + '/~/foo/doc/doc-id'290 url = BASE_URL + '/pfx/foo/doc/doc-id'
149 params = {'old_rev': 'old-rev'}291 params = {'old_rev': 'old-rev'}
150 oauth_req = oauth.OAuthRequest.from_consumer_and_token(292 oauth_req = oauth.OAuthRequest.from_consumer_and_token(
151 tests.consumer1,293 tests.consumer1,
152294
=== modified file 'u1db/tests/test_https.py'
--- u1db/tests/test_https.py 2012-07-05 15:26:52 +0000
+++ u1db/tests/test_https.py 2012-09-05 16:09:18 +0000
@@ -7,7 +7,6 @@
7from paste import httpserver7from paste import httpserver
88
9from u1db import (9from u1db import (
10 errors,
11 tests,10 tests,
12 )11 )
13from u1db.remote import (12from u1db.remote import (
@@ -21,7 +20,7 @@
21def oauth_https_server_def():20def oauth_https_server_def():
22 def make_server(host_port, handler, state):21 def make_server(host_port, handler, state):
23 app = http_app.HTTPApp(state)22 app = http_app.HTTPApp(state)
24 application = oauth_middleware.OAuthMiddleware(app, None)23 application = oauth_middleware.OAuthMiddleware(app, None, prefix='/~/')
25 application.get_oauth_data_store = lambda: tests.testingOAuthStore24 application.get_oauth_data_store = lambda: tests.testingOAuthStore
26 from OpenSSL import SSL25 from OpenSSL import SSL
27 cert_file = os.path.join(os.path.dirname(__file__), 'testing-certs',26 cert_file = os.path.join(os.path.dirname(__file__), 'testing-certs',
@@ -36,8 +35,6 @@
36 handler,35 handler,
37 ssl_context=ssl_context36 ssl_context=ssl_context
38 )37 )
39 # workaround apparent interface mismatch
40 orig_shutdown_request = srv.shutdown_request
4138
42 def shutdown_request(req):39 def shutdown_request(req):
43 req.shutdown()40 req.shutdown()
@@ -68,7 +65,7 @@
6865
69 def setUp(self):66 def setUp(self):
70 try:67 try:
71 import OpenSSL68 import OpenSSL # noqa
72 except ImportError:69 except ImportError:
73 self.skipTest("Requires pyOpenSSL")70 self.skipTest("Requires pyOpenSSL")
74 self.cacert_pem = os.path.join(os.path.dirname(__file__),71 self.cacert_pem = os.path.join(os.path.dirname(__file__),
@@ -96,7 +93,7 @@
96 self.startServer()93 self.startServer()
97 # don't print expected traceback server-side94 # don't print expected traceback server-side
98 self.server.handle_error = lambda req, cli_addr: None95 self.server.handle_error = lambda req, cli_addr: None
99 db = self.request_state._create_database('test')96 self.request_state._create_database('test')
100 remote_target = self.getSyncTarget('localhost', 'test')97 remote_target = self.getSyncTarget('localhost', 'test')
101 try:98 try:
102 remote_target.record_sync_info('other-id', 2, 'T-id')99 remote_target.record_sync_info('other-id', 2, 'T-id')
@@ -110,11 +107,12 @@
110 self.skipTest(107 self.skipTest(
111 "XXX certificate verification happens on linux only for now")108 "XXX certificate verification happens on linux only for now")
112 self.startServer()109 self.startServer()
113 db = self.request_state._create_database('test')110 self.request_state._create_database('test')
114 self.patch(http_client, 'CA_CERTS', self.cacert_pem)111 self.patch(http_client, 'CA_CERTS', self.cacert_pem)
115 remote_target = self.getSyncTarget('127.0.0.1', 'test')112 remote_target = self.getSyncTarget('127.0.0.1', 'test')
116 self.assertRaises(http_client.CertificateError,113 self.assertRaises(
117 remote_target.record_sync_info, 'other-id', 2, 'T-id')114 http_client.CertificateError, remote_target.record_sync_info,
115 'other-id', 2, 'T-id')
118116
119117
120load_tests = tests.load_with_scenarios118load_tests = tests.load_with_scenarios
121119
=== modified file 'u1db/tests/test_remote_sync_target.py'
--- u1db/tests/test_remote_sync_target.py 2012-07-19 19:50:58 +0000
+++ u1db/tests/test_remote_sync_target.py 2012-09-05 16:09:18 +0000
@@ -130,7 +130,7 @@
130130
131 def make_server(host_port, handler, state):131 def make_server(host_port, handler, state):
132 app = http_app.HTTPApp(state)132 app = http_app.HTTPApp(state)
133 application = oauth_middleware.OAuthMiddleware(app, None)133 application = oauth_middleware.OAuthMiddleware(app, None, prefix='/~/')
134 application.get_oauth_data_store = lambda: tests.testingOAuthStore134 application.get_oauth_data_store = lambda: tests.testingOAuthStore
135 srv = simple_server.WSGIServer(host_port, handler)135 srv = simple_server.WSGIServer(host_port, handler)
136 # patch the value in136 # patch the value in

Subscribers

People subscribed via source and target branches