Merge lp:~james-w/lava-dashboard/oauth into lp:lava-dashboard

Proposed by James Westby
Status: Rejected
Rejected by: Zygmunt Krynicki
Proposed branch: lp:~james-w/lava-dashboard/oauth
Merge into: lp:lava-dashboard
Diff against target: 431 lines (+400/-0)
3 files modified
dashboard_server/default_settings.py (+3/-0)
oauth_middleware/__init__.py (+90/-0)
oauth_middleware/tests.py (+307/-0)
To merge this branch: bzr merge lp:~james-w/lava-dashboard/oauth
Reviewer Review Type Date Requested Status
Linaro Infrastructure Pending
Review via email: mp+38013@code.launchpad.net

Description of the change

Hi,

Another inital code push for discussion, this time adding oauth support.

I looked at the existing django apps for doing this, and didn't really
like the approach of any of them. django-oauth requires separate decorators
on views that can and should accept oauth signed requests. django-piston
is closer I think, but implies lots of other things that don't really
fit in to the current architecture. If we want to move towards something
like django-piston provides then we should drop this and go that way I think.

The approach I took is to provide a middleware that sets request.user
if the request is oauth signed. This means that django's existing auth
framework continues to work, and you can just use it's existing decorators
etc. to do access control, and use request.user to know who is making
a request.

There are some issues though:

1. This relies on an external project that is unpackaged at this time.
2. That external project ships a patched embedded copy of python-oauth, though
I don't know what the patches are for.
3. That project requires consumers to be pre-registered, and I'm not sure
we want that. It would be possible to work around it, but would require
some work.
4. I'm not sure I have the Resource stuff correct in this branch.
5. I'm not convinced that I have thought through all the corners and so there
may be security holes.
6. There is nothing so far for the view to know if the request is oauth,
which consumer it is etc., and no support for differing token access levels.
We won't need any of that right away, but if we want that then django-piston
may be the way to go rather than adding all of that.

Thanks,

James

To post a comment you must log in.
Revision history for this message
James Westby (james-w) wrote :

Oh, and django-oauth's tests are not isolated, so running all the tests
in the project currently fails.

Thanks,

James

Revision history for this message
Zygmunt Krynicki (zyga) wrote :

This branch could be re-targeted at linaro-django-xmlrpc. If we are going to do oauth (eventually we might) then that's the logical place it should go to.

Unmerged revisions

73. By James Westby

Allow request.user to be missing.

72. By James Westby

Add a middleware on top of django-oauth that puts the user on the request as normal.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'dashboard_server/default_settings.py'
--- dashboard_server/default_settings.py 2010-10-02 21:25:06 +0000
+++ dashboard_server/default_settings.py 2010-10-08 19:37:59 +0000
@@ -103,6 +103,7 @@
103 'django.middleware.common.CommonMiddleware',103 'django.middleware.common.CommonMiddleware',
104 'django.contrib.sessions.middleware.SessionMiddleware',104 'django.contrib.sessions.middleware.SessionMiddleware',
105 'django.contrib.auth.middleware.AuthenticationMiddleware',105 'django.contrib.auth.middleware.AuthenticationMiddleware',
106 'oauth_middleware.OAuthMiddleware',
106 'django.middleware.transaction.TransactionMiddleware',107 'django.middleware.transaction.TransactionMiddleware',
107)108)
108109
@@ -129,6 +130,8 @@
129 'django.contrib.sites',130 'django.contrib.sites',
130 'django.contrib.databrowse',131 'django.contrib.databrowse',
131 'django.contrib.humanize',132 'django.contrib.humanize',
133 'oauth_provider',
134 'oauth_middleware',
132 'dashboard_app',135 'dashboard_app',
133)136)
134137
135138
=== added directory 'oauth_middleware'
=== added file 'oauth_middleware/__init__.py'
--- oauth_middleware/__init__.py 1970-01-01 00:00:00 +0000
+++ oauth_middleware/__init__.py 2010-10-08 19:37:59 +0000
@@ -0,0 +1,90 @@
1from django.conf import settings
2from django.http import HttpResponseForbidden
3
4from oauth.oauth import (
5 OAuthError,
6 OAuthRequest,
7 OAuthServer,
8 OAuthSignatureMethod_PLAINTEXT,
9 OAuthSignatureMethod_HMAC_SHA1,
10 )
11
12from oauth_provider.decorators import CheckOAuth
13from oauth_provider.stores import DataStore as _DataStore
14
15
16def get_oauth_auth_params(request):
17 # Django converts Authorization header in HTTP_AUTHORIZATION
18 auth_header = {}
19 if 'Authorization' in request.META:
20 auth_header = {'Authorization': request.META['Authorization']}
21 elif 'HTTP_AUTHORIZATION' in request.META:
22 auth_header = {'Authorization': request.META['HTTP_AUTHORIZATION']}
23 # Don't include extra parameters when request.method is POST and
24 # request.MIME['CONTENT_TYPE'] is "application/x-www-form-urlencoded"
25 # (See http://oauth.net/core/1.0a/#consumer_req_param).
26 # But there is an issue with Django's test Client and custom content types
27 # so an ugly test is made here, if you find a better solution...
28 parameters = {}
29 if request.method == "POST" and \
30 (request.META.get('CONTENT_TYPE') == "application/x-www-form-urlencoded" \
31 or request.META.get('SERVER_NAME') == 'testserver'):
32 parameters = dict(request.REQUEST.items())
33 return {"method": request.method,
34 "uri": request.build_absolute_uri(),
35 "auth_header": auth_header,
36 "parameters": parameters,
37 "query_string": request.META.get('QUERY_STRING', ''),
38 }
39
40
41class DataStore(_DataStore):
42
43 def lookup_consumer(self, key):
44 # Do something here to allow arbitrary consumers based on a
45 # settings key. Also need to override other methods in that case
46 # to avoid key checks
47 return super(DataStore, self).lookup_consumer(key)
48
49
50def authenticate(method=None, uri=None, auth_header=None,
51 parameters=None, query_string=None):
52 oauth_request = OAuthRequest.from_request(method,
53 uri,
54 headers=auth_header,
55 parameters=parameters,
56 query_string=query_string)
57 if oauth_request is None:
58 # Do we want to 403 here, it's half an oauth signed request?
59 return None
60 OAUTH_SIGNATURE_METHODS = getattr(settings, 'OAUTH_SIGNATURE_METHODS', ['plaintext', 'hmac-sha1'])
61 oauth_server = OAuthServer(DataStore(oauth_request))
62 if 'plaintext' in OAUTH_SIGNATURE_METHODS:
63 oauth_server.add_signature_method(OAuthSignatureMethod_PLAINTEXT())
64 if 'hmac-sha1' in OAUTH_SIGNATURE_METHODS:
65 oauth_server.add_signature_method(OAuthSignatureMethod_HMAC_SHA1())
66 consumer, token, parameters = oauth_server.verify_request(oauth_request)
67 # Handle resource names?
68 return token.user
69
70
71class OAuthMiddleware(object):
72
73 def process_request(self, request):
74 oauth_auth_params = get_oauth_auth_params(request)
75 try:
76 user = authenticate(**oauth_auth_params)
77 except OAuthError, e:
78 return HttpResponseForbidden(content=str(e))
79 if user:
80 # If the user is already authenticated and that user doesn't
81 # match the one that the headers are for then something
82 # is wrong, so give an Unauthenticated response
83 if getattr(request, "user", None) and request.user.is_authenticated():
84 if request.user != user:
85 return HttpResponseForbidden()
86 # User is valid. Overwrite request.user with the user that
87 # signed the request, which will prevent any
88 # django.contrib.auth checks, and so sessions will be
89 # invalid.
90 request.user = user
091
=== added file 'oauth_middleware/models.py'
=== added file 'oauth_middleware/tests.py'
--- oauth_middleware/tests.py 1970-01-01 00:00:00 +0000
+++ oauth_middleware/tests.py 2010-10-08 19:37:59 +0000
@@ -0,0 +1,307 @@
1import time
2
3from django.conf import settings
4from django.contrib.auth.models import AnonymousUser, User
5from django.http import HttpRequest
6from django.test import TestCase
7
8from oauth.oauth import (
9 OAuthError,
10 OAuthRequest,
11 OAuthSignatureMethod_PLAINTEXT,
12 )
13from oauth_provider.models import (
14 Consumer,
15 Resource,
16 Token,
17 )
18
19from oauth_middleware import (
20 authenticate,
21 get_oauth_auth_params,
22 OAuthMiddleware,
23 )
24
25
26class TestRequest(HttpRequest):
27
28 def __init__(self, params=None):
29 super(TestRequest, self).__init__()
30 self.META["HTTP_HOST"] = "foo"
31 self._params = params or {}
32
33 def _get_request(self):
34 return self._params
35
36 REQUEST = property(_get_request)
37
38
39class GetOAuthAuthParamsTests(TestCase):
40
41 def get_request(self, params=None):
42 request = TestRequest(params=params)
43 return request
44
45 def test_empty_auth_header_by_default(self):
46 request = self.get_request()
47 params = get_oauth_auth_params(request)
48 self.assertEqual({}, params["auth_header"])
49
50 def test_auth_header_from_Authorization(self):
51 request = self.get_request()
52 request.META["Authorization"] = "foo"
53 params = get_oauth_auth_params(request)
54 self.assertEqual({"Authorization": "foo"}, params["auth_header"])
55
56 def test_auth_header_from_Http_Authorization(self):
57 request = self.get_request()
58 request.META["HTTP_AUTHORIZATION"] = "bar"
59 params = get_oauth_auth_params(request)
60 self.assertEqual({"Authorization": "bar"}, params["auth_header"])
61
62 def test_auth_header_from_Authorization_not_Http_Auth(self):
63 request = self.get_request()
64 request.META["Authorization"] = "foo"
65 request.META["HTTP_AUTHORIZATION"] = "bar"
66 params = get_oauth_auth_params(request)
67 self.assertEqual({"Authorization": "foo"}, params["auth_header"])
68
69 def test_parameters_empty_by_default(self):
70 request = self.get_request()
71 params = get_oauth_auth_params(request)
72 self.assertEqual({}, params["parameters"])
73
74 def test_parameters_with_form_post(self):
75 params = {"foo": "bar"}
76 request = self.get_request(params=params)
77 request.method = "POST"
78 request.META["CONTENT_TYPE"] = "application/x-www-form-urlencoded"
79 oauth_params = get_oauth_auth_params(request)
80 self.assertEqual(params, oauth_params["parameters"])
81
82 def test_parameters_with_post_to_test_server(self):
83 params = {"foo": "bar"}
84 request = self.get_request(params=params)
85 request.method = "POST"
86 request.META["SERVER_NAME"] = "testserver"
87 oauth_params = get_oauth_auth_params(request)
88 self.assertEqual(params, oauth_params["parameters"])
89
90 def test_parameters_includes_method(self):
91 request = self.get_request()
92 request.method = "PATCH"
93 oauth_params = get_oauth_auth_params(request)
94 self.assertEqual("PATCH", oauth_params["method"])
95
96 def test_parameters_includes_absolute_uri(self):
97 request = self.get_request()
98 oauth_params = get_oauth_auth_params(request)
99 self.assertEqual(request.build_absolute_uri(), oauth_params["uri"])
100
101 def test_parameters_includes_empty_query_string_by_default(self):
102 request = self.get_request()
103 oauth_params = get_oauth_auth_params(request)
104 self.assertEqual('', oauth_params["query_string"])
105
106 def test_parameters_includes_query_string(self):
107 query_string = "?foo=bar"
108 request = self.get_request()
109 request.META["QUERY_STRING"] = query_string
110 oauth_params = get_oauth_auth_params(request)
111 self.assertEqual(query_string, oauth_params["query_string"])
112
113
114class OAuthTestCase(TestCase):
115
116 def setUp(self):
117 self._original_signature_methods = getattr(
118 settings, 'OAUTH_SIGNATURE_METHODS', None)
119 super(OAuthTestCase, self).setUp()
120
121 def tearDown(self):
122 super(OAuthTestCase, self).tearDown()
123 if self._original_signature_methods is not None:
124 setattr(
125 settings, 'OAUTH_SIGNATURE_METHODS',
126 self._original_signature_methods)
127 elif getattr(settings, 'OAUTH_SIGNATURE_METHODS', None) is not None:
128 delattr(settings, 'OAUTH_SIGNATURE_METHODS')
129
130 def get_user(self, username=None):
131 if username is None:
132 username = "test-user"
133 user = User.objects.create(username=username)
134 user.save()
135 return user
136
137 def get_consumer(self):
138 return Consumer.objects.create_consumer("consumer")
139
140 def get_resource(self):
141 return Resource.objects.create(name="foo", url="bar")
142
143 def get_token(self, user, consumer, resource):
144 return Token.objects.create_token(
145 consumer, Token.ACCESS, time.time(), resource,
146 user=user)
147
148 def get_request(self, consumer, token, method, uri):
149 request = OAuthRequest.from_consumer_and_token(
150 consumer, token=token, http_method=method, http_url=uri)
151 request.sign_request(
152 OAuthSignatureMethod_PLAINTEXT(), consumer, token)
153 return request
154
155 def get_signed_request(self, user, method, uri):
156 consumer = self.get_consumer()
157 resource = self.get_resource()
158 token = self.get_token(user, consumer, resource)
159 request = OAuthRequest.from_consumer_and_token(
160 consumer, token=token, http_method=method, http_url=uri)
161 request.sign_request(
162 OAuthSignatureMethod_PLAINTEXT(), consumer, token)
163 return request
164
165
166class AuthenticateTests(OAuthTestCase):
167
168 def test_invalid_oauth_request_returns_None(self):
169 user = authenticate(uri="http://example.com")
170 self.assertEqual(None, user)
171
172 def test_invalid_oauth_params_raise_error(self):
173 self.assertRaises(
174 OAuthError, authenticate, uri="http://example.com",
175 auth_header={"Authorization": "OAuth foo"})
176
177 def test_authenticate_returns_user(self):
178 user = self.get_user()
179 method = "POST"
180 uri = "http://example.com"
181 request = self.get_signed_request(user, method, uri)
182 self.assertEqual(
183 user,
184 authenticate(
185 method=method, uri=uri,
186 auth_header=request.to_header()))
187
188 def test_authenticate_refuses_unwanted_methods(self):
189 settings.OAUTH_SIGNATURE_METHODS = ["hmac-sha1"]
190 user = self.get_user()
191 method = "POST"
192 uri = "http://example.com"
193 request = self.get_signed_request(user, method, uri)
194 self.assertRaises(
195 OAuthError, authenticate, method=method, uri=uri,
196 auth_header=request.to_header())
197
198 def test_authenticate_refuses_bad_signature(self):
199 user = self.get_user()
200 method = "POST"
201 uri = "http://example.com"
202 request = self.get_signed_request(user, method, uri)
203 request.set_parameter('oauth_signature', '000')
204 self.assertRaises(
205 OAuthError, authenticate, method=method, uri=uri,
206 auth_header=request.to_header())
207
208 def test_authenticate_refuses_unknown_token(self):
209 user = self.get_user()
210 consumer = self.get_consumer()
211 resource = self.get_resource()
212 token = self.get_token(user, consumer, resource)
213 method = "POST"
214 uri = "http://example.com"
215 request = self.get_request(consumer, token, method, uri)
216 request.set_parameter('oauth_signature', '000')
217 token.delete()
218 self.assertRaises(
219 OAuthError, authenticate, method=method, uri=uri,
220 auth_header=request.to_header())
221
222 def test_authenticate_refuses_unknown_user(self):
223 user = self.get_user()
224 consumer = self.get_consumer()
225 resource = self.get_resource()
226 token = self.get_token(user, consumer, resource)
227 method = "POST"
228 uri = "http://example.com"
229 request = self.get_request(consumer, token, method, uri)
230 request.set_parameter('oauth_signature', '000')
231 user.delete()
232 self.assertRaises(
233 OAuthError, authenticate, method=method, uri=uri,
234 auth_header=request.to_header())
235
236
237class OAuthMiddlewareTests(OAuthTestCase):
238
239 def test_nothing_happens_on_non_oauth_requests(self):
240 request = TestRequest()
241 request.user = "user"
242 middleware = OAuthMiddleware()
243 response = middleware.process_request(request)
244 self.assertEqual("user", request.user)
245 self.assertEqual(None, response)
246
247 def test_forbidden_on_bad_oauth_request(self):
248 request = TestRequest()
249 request.user = "user"
250 request.META["HTTP_AUTHORIZATION"] = (
251 "OAuth oauth_consumer_key=foo oauth_token=bar "
252 "oauth_signature=baz oauth_signature_method=zap "
253 "oauth_timestamp=zabg oauth_nonce=zot")
254 middleware = OAuthMiddleware()
255 response = middleware.process_request(request)
256 self.assertEqual(403, response.status_code)
257
258 def test_authenticated_user_set_on_request(self):
259 user = self.get_user()
260 request = TestRequest()
261 request.user = AnonymousUser()
262 method = "POST"
263 uri = "http://example.com"
264 oauth_request = self.get_signed_request(user, method, uri)
265 request.META["HTTP_AUTHORIZATION"] = oauth_request.to_header().values()[0]
266 middleware = OAuthMiddleware()
267 response = middleware.process_request(request)
268 self.assertEqual(None, response)
269 self.assertEqual(user, request.user)
270
271 def test_forbidden_if_request_already_authenticated(self):
272 user = self.get_user(username="test-user")
273 other_user = self.get_user(username="other-user")
274 request = TestRequest()
275 request.user = other_user
276 method = "POST"
277 uri = "http://example.com"
278 oauth_request = self.get_signed_request(user, method, uri)
279 request.META["HTTP_AUTHORIZATION"] = oauth_request.to_header().values()[0]
280 middleware = OAuthMiddleware()
281 response = middleware.process_request(request)
282 self.assertEqual(403, response.status_code)
283
284 def test_not_forbidden_if_same_user_authenticated(self):
285 user = self.get_user()
286 request = TestRequest()
287 request.user = user
288 method = "POST"
289 uri = "http://example.com"
290 oauth_request = self.get_signed_request(user, method, uri)
291 request.META["HTTP_AUTHORIZATION"] = oauth_request.to_header().values()[0]
292 middleware = OAuthMiddleware()
293 response = middleware.process_request(request)
294 self.assertEqual(None, response)
295 self.assertEqual(user, request.user)
296
297 def test_allows_no_user_on_request(self):
298 user = self.get_user()
299 request = TestRequest()
300 method = "POST"
301 uri = "http://example.com"
302 oauth_request = self.get_signed_request(user, method, uri)
303 request.META["HTTP_AUTHORIZATION"] = oauth_request.to_header().values()[0]
304 middleware = OAuthMiddleware()
305 response = middleware.process_request(request)
306 self.assertEqual(None, response)
307 self.assertEqual(user, request.user)
0308
=== added file 'oauth_middleware/views.py'

Subscribers

People subscribed via source and target branches