Merge lp:~canonical-isd-hackers/canonical-identity-provider/web_api-authentications into lp:~canonical-isd-hackers/canonical-identity-provider/web_api
- web_api-authentications
- Merge into web_api
Proposed by
David Owen
Status: | Work in progress |
---|---|
Proposed branch: | lp:~canonical-isd-hackers/canonical-identity-provider/web_api-authentications |
Merge into: | lp:~canonical-isd-hackers/canonical-identity-provider/web_api |
Prerequisite: | lp:~canonical-isd-hackers/canonical-identity-provider/web_api-captchas |
Diff against target: |
409 lines (+207/-75) 6 files modified
doctests/stories/api-authentications.txt (+4/-4) identityprovider/webservice/models.py (+0/-56) identityprovider/webservice/urlinfo.py (+1/-0) identityprovider/webservice/urls.py (+3/-0) identityprovider/webservice/views.py (+198/-15) identityprovider/wsgi.py (+1/-0) |
To merge this branch: | bzr merge lp:~canonical-isd-hackers/canonical-identity-provider/web_api-authentications |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Canonical ISD hackers | Pending | ||
Review via email: mp+48362@code.launchpad.net |
Commit message
Description of the change
To post a comment you must log in.
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === modified file 'doctests/stories/api-authentications.txt' |
2 | --- doctests/stories/api-authentications.txt 2011-01-24 18:28:24 +0000 |
3 | +++ doctests/stories/api-authentications.txt 2011-02-02 20:13:30 +0000 |
4 | @@ -120,19 +120,19 @@ |
5 | >>> api.authentications.list_tokens(consumer_key='myopenid') |
6 | Traceback (most recent call last): |
7 | ... |
8 | - HTTPError: HTTP Error 403: Forbidden |
9 | + HTTPError: HTTP Error 403: FORBIDDEN |
10 | ... |
11 | >>> api.authentications.invalidate_token(consumer_key='myopenid', |
12 | ... token='mytoken') |
13 | Traceback (most recent call last): |
14 | ... |
15 | - HTTPError: HTTP Error 403: Forbidden |
16 | + HTTPError: HTTP Error 403: FORBIDDEN |
17 | ... |
18 | >>> api.authentications.validate_token(consumer_key='myopenid', |
19 | ... token='mytoken') |
20 | Traceback (most recent call last): |
21 | ... |
22 | - HTTPError: HTTP Error 403: Forbidden |
23 | + HTTPError: HTTP Error 403: FORBIDDEN |
24 | ... |
25 | |
26 | Test When OAuth Auth Credentials For Normal Users Are Supplied |
27 | @@ -218,5 +218,5 @@ |
28 | >>> api.authentications.authenticate(token_name='this-machine') |
29 | Traceback (most recent call last): |
30 | ... |
31 | - HTTPError: HTTP Error 403: Forbidden |
32 | + HTTPError: HTTP Error 403: FORBIDDEN |
33 | ... |
34 | |
35 | === modified file 'identityprovider/webservice/models.py' |
36 | --- identityprovider/webservice/models.py 2011-02-02 20:13:30 +0000 |
37 | +++ identityprovider/webservice/models.py 2011-02-02 20:13:30 +0000 |
38 | @@ -156,33 +156,6 @@ |
39 | |
40 | class AuthenticationSet(make_set('BaseAuthenticationSet', 'authentications', |
41 | IAuthenticationSet, Account, 'id')): |
42 | - """ All these methods assume that they're run behind Basic Auth """ |
43 | - @plain_user_required |
44 | - def authenticate(self, token_name): |
45 | - request = get_current_browser_request() |
46 | - account = request.environment['authenticated_user'] |
47 | - token = account.create_oauth_token(token_name) |
48 | - application_token_created.send( |
49 | - sender=self, openid_identifier=account.openid_identifier) |
50 | - return token.serialize() |
51 | - |
52 | - @api_user_required |
53 | - def list_tokens(self, consumer_key): |
54 | - tokens = Token.objects.filter( |
55 | - consumer__user__username=consumer_key) |
56 | - result = [{'token': t.token, 'name': t.name} for t in tokens] |
57 | - return result |
58 | - |
59 | - @api_user_required |
60 | - def validate_token(self, token, consumer_key): |
61 | - try: |
62 | - token = Token.objects.get( |
63 | - consumer__user__username=consumer_key, |
64 | - token=token) |
65 | - return token.serialize() |
66 | - except Token.DoesNotExist: |
67 | - return False |
68 | - |
69 | @api_user_required |
70 | def invalidate_token(self, token, consumer_key): |
71 | tokens = Token.objects.filter(token=token, |
72 | @@ -192,35 +165,6 @@ |
73 | sender=self, openid_identifier=consumer_key) |
74 | |
75 | |
76 | - @api_user_required |
77 | - def team_memberships(self, team_names, openid_identifier): |
78 | - accounts = Account.objects.filter(openid_identifier=openid_identifier) |
79 | - accounts = list(accounts) |
80 | - |
81 | - if len(accounts) == 1: |
82 | - account = accounts[0] |
83 | - memberships = get_team_memberships(team_names, account, False) |
84 | - return memberships |
85 | - else: |
86 | - return [] |
87 | - |
88 | - @api_user_required |
89 | - def account_by_email(self, email): |
90 | - account = Account.objects.get_by_email(email) |
91 | - if account: |
92 | - return _serialize_account(account) |
93 | - else: |
94 | - return None |
95 | - |
96 | - @api_user_required |
97 | - def account_by_openid(self, openid): |
98 | - try: |
99 | - account = Account.objects.get(openid_identifier=openid) |
100 | - except Account.DoesNotExist: |
101 | - return None |
102 | - else: |
103 | - return _serialize_account(account) |
104 | - |
105 | class CanNotResetPasswordError(Exception): |
106 | webservice_error(403) |
107 | |
108 | |
109 | === modified file 'identityprovider/webservice/urlinfo.py' |
110 | --- identityprovider/webservice/urlinfo.py 2011-02-02 20:13:30 +0000 |
111 | +++ identityprovider/webservice/urlinfo.py 2011-02-02 20:13:30 +0000 |
112 | @@ -1,4 +1,5 @@ |
113 | # Copyright 2010 Canonical Ltd. This software is licensed under the |
114 | # GNU Affero General Public License version 3 (see the file LICENSE). |
115 | |
116 | +AUTHENTICATIONS_VIEW = "api_authentications" |
117 | CAPTCHAS_VIEW = "api_captchas" |
118 | |
119 | === modified file 'identityprovider/webservice/urls.py' |
120 | --- identityprovider/webservice/urls.py 2011-02-02 20:13:30 +0000 |
121 | +++ identityprovider/webservice/urls.py 2011-02-02 20:13:30 +0000 |
122 | @@ -12,5 +12,8 @@ |
123 | |
124 | urlpatterns = patterns('identityprovider.webservice.views', |
125 | (r'^$', views.root), |
126 | + url(r'^authentications', |
127 | + views.AuthenticationsHandler(), |
128 | + name=AUTHENTICATIONS_VIEW), |
129 | url(r'^captchas', views.CaptchasHandler(), name=CAPTCHAS_VIEW), |
130 | ) |
131 | |
132 | === modified file 'identityprovider/webservice/views.py' |
133 | --- identityprovider/webservice/views.py 2011-02-02 20:13:30 +0000 |
134 | +++ identityprovider/webservice/views.py 2011-02-02 20:13:30 +0000 |
135 | @@ -1,3 +1,4 @@ |
136 | +from functools import wraps |
137 | from urlparse import urljoin |
138 | |
139 | from django.conf import settings |
140 | @@ -5,19 +6,29 @@ |
141 | from django.core.urlresolvers import reverse |
142 | from django.http import ( |
143 | HttpResponse, |
144 | + HttpResponseForbidden, |
145 | + HttpResponseNotAllowed, |
146 | HttpResponseRedirect, |
147 | - HttpResponseNotAllowed, |
148 | ) |
149 | from django.shortcuts import render_to_response, get_object_or_404 |
150 | from django.template import RequestContext |
151 | from django.utils.translation import ugettext as _ |
152 | |
153 | +from oauth_backend.models import Token |
154 | + |
155 | from piston.handler import BaseHandler |
156 | |
157 | import simplejson as json |
158 | |
159 | from identityprovider.auth import basic_authenticate, oauth_authenticate |
160 | +from identityprovider.models import APIUser, Account |
161 | from identityprovider.models.captcha import Captcha, NewCaptchaError |
162 | +from identityprovider.signals import ( |
163 | + application_token_invalidated, |
164 | + application_token_created, |
165 | + account_email_validated, |
166 | + account_created, |
167 | +) |
168 | from urlinfo import * |
169 | |
170 | |
171 | @@ -44,7 +55,7 @@ |
172 | def root(request): |
173 | if is_wadl_request(request): |
174 | from wadl import wadl |
175 | - return HttpResponse(wadl.replace('@@SERVICE_ROOT@@', api_reverse(request, root)), mimetype='application/vnd.sun.wadl+xml') |
176 | + return HttpResponse(wadl.replace('@@SERVICE_ROOT@@/', api_reverse(request, root)), mimetype='application/vnd.sun.wadl+xml') |
177 | |
178 | return JsonResponse({ |
179 | "registrations_collection_link": "http://openid.launchpad.dev/api/1.0/registration", |
180 | @@ -63,32 +74,204 @@ |
181 | # Captcha, 'id')): |
182 | |
183 | |
184 | -class NotAllowed(Exception): |
185 | - pass |
186 | +class BasicUnauthorized(HttpResponse): |
187 | + def __init__(self): |
188 | + super(BasicUnauthorized, self).__init__("401 Unauthorized", status=401) |
189 | + self["WWW-Authenticate"] = 'Basic realm="Restricted area"' |
190 | + |
191 | + |
192 | +class OauthUnauthorized(HttpResponse): |
193 | + def __init__(self): |
194 | + super(OauthUnauthorized, self).__init__("401 Unauthorized", status=401) |
195 | + self["WWW-Authenticate"] = 'OAuth realm="Restricted area"' |
196 | + |
197 | + |
198 | +class Forbidden(HttpResponse): |
199 | + """ |
200 | + This class exists solely to satisfy the tests, and maybe |
201 | + lazr.restfulclient, which wants the forbidden response to be |
202 | + title-cases, not upper-cased. |
203 | + """ |
204 | + def __init__(self): |
205 | + super(Forbidden, self).__init__("403 Forbidden", status=403) |
206 | |
207 | |
208 | class JsonHandler(object): |
209 | |
210 | + verbs = ["delete", "get", "post", "put"] |
211 | + |
212 | def _not_allowed(self, request): |
213 | - raise NotAllowed() |
214 | + allowed = [v.upper() for v in self.verbs |
215 | + if getattr(self, v) != self._not_allowed] |
216 | + return HttpResponseNotAllowed(allowed) |
217 | |
218 | delete = _not_allowed |
219 | get = _not_allowed |
220 | post = _not_allowed |
221 | put = _not_allowed |
222 | |
223 | - verbs = ["delete", "get", "post", "put"] |
224 | - |
225 | def __call__(self, request): |
226 | method = request.method.lower() |
227 | - if method in self.verbs: |
228 | - try: |
229 | - return JsonResponse(getattr(self, method)(request)) |
230 | - except NotAllowed: |
231 | - pass |
232 | - allowed = [v.upper() for v in self.verbs |
233 | - if getattr(self, v) != self._not_allowed] |
234 | - return HttpResponseNotAllowed(allowed) |
235 | + if method not in self.verbs: |
236 | + return self._not_allowed(request) |
237 | + r = getattr(self, method)(request) |
238 | + return r if isinstance(r, HttpResponse) else JsonResponse(r) |
239 | + |
240 | + |
241 | +def basic_auth_required(func): |
242 | + """ |
243 | + Requires the user to successfully authenticate (as any type of |
244 | + user) using HTTP Basic authentication. Sets request.basic_user. |
245 | + """ |
246 | + @wraps(func) |
247 | + def wrapper(self, request, *args, **kwargs): |
248 | + authorization = request.META.get('HTTP_AUTHORIZATION') |
249 | + if authorization is not None: |
250 | + method, auth = authorization.split(' ', 1) |
251 | + if method.lower() == 'basic': |
252 | + auth = auth.strip().decode('base64') |
253 | + username, password = auth.split(':', 1) |
254 | + credentials = username, password |
255 | + request.basic_user = basic_authenticate(username, password) |
256 | + if request.basic_user is not None: |
257 | + return func(self, request, *args, **kwargs) |
258 | + return BasicUnauthorized() |
259 | + return wrapper |
260 | + |
261 | + |
262 | +def plain_user_required(func): |
263 | + """ |
264 | + Requires that the basic-authenticated user is an Account. Assumes |
265 | + that @basic_auth_required has already been applied. |
266 | + """ |
267 | + @wraps(func) |
268 | + def wrapper(self, request, *args, **kwargs): |
269 | + if isinstance(request.basic_user, Account): |
270 | + return func(self, request, *args, **kwargs) |
271 | + return HttpResponseForbidden() |
272 | + return wrapper |
273 | + |
274 | + |
275 | +def api_user_required(func): |
276 | + """ |
277 | + Requires that the basic-authenticated user is an APIUser. Assumes |
278 | + that @basic_auth_required has already been applied. |
279 | + """ |
280 | + @wraps(func) |
281 | + def wrapper(self, request, *args, **kwargs): |
282 | + if isinstance(request.basic_user, APIUser): |
283 | + return func(self, request, *args, **kwargs) |
284 | + return HttpResponseForbidden() |
285 | + return wrapper |
286 | + |
287 | + |
288 | +class AuthenticationsHandler(JsonHandler): |
289 | + |
290 | + @plain_user_required |
291 | + def authenticate(self, request, token_name=None): |
292 | + account = request.basic_user |
293 | + token = account.create_oauth_token(token_name) |
294 | + application_token_created.send( |
295 | + sender=self, openid_identifier=account.openid_identifier) |
296 | + return token.serialize() |
297 | + |
298 | + @api_user_required |
299 | + def list_tokens(self, request, consumer_key=None): |
300 | + tokens = Token.objects.filter( |
301 | + consumer__user__username=consumer_key) |
302 | + result = [{'token': t.token, 'name': t.name} for t in tokens] |
303 | + return result |
304 | + |
305 | + @api_user_required |
306 | + def validate_token(self, request, token=None, consumer_key=None): |
307 | + try: |
308 | + token = Token.objects.get( |
309 | + consumer__user__username=consumer_key, |
310 | + token=token) |
311 | + return token.serialize() |
312 | + except Token.DoesNotExist: |
313 | + return False |
314 | + |
315 | + @api_user_required |
316 | + def invalidate_token(self, request, token=None, consumer_key=None): |
317 | + tokens = Token.objects.filter(token=token, |
318 | + consumer__user__username=consumer_key) |
319 | + tokens.delete() |
320 | + application_token_invalidated.send( |
321 | + sender=self, openid_identifier=consumer_key) |
322 | + |
323 | + @api_user_required |
324 | + def team_memberships(self, request, team_names=None, |
325 | + openid_identifier=None): |
326 | + accounts = Account.objects.filter(openid_identifier=openid_identifier) |
327 | + accounts = list(accounts) |
328 | + |
329 | + if len(accounts) == 1: |
330 | + account = accounts[0] |
331 | + memberships = get_team_memberships(team_names, account, False) |
332 | + return memberships |
333 | + else: |
334 | + return [] |
335 | + |
336 | + @api_user_required |
337 | + def account_by_email(self, request, email=None): |
338 | + account = Account.objects.get_by_email(email) |
339 | + if account: |
340 | + return _serialize_account(account) |
341 | + else: |
342 | + return None |
343 | + |
344 | + @api_user_required |
345 | + def account_by_openid(self, request, openid=None): |
346 | + try: |
347 | + account = Account.objects.get(openid_identifier=openid) |
348 | + except Account.DoesNotExist: |
349 | + return None |
350 | + else: |
351 | + return _serialize_account(account) |
352 | + |
353 | + read_ops = ["authenticate", "list_tokens", "validate_token", |
354 | + "team_memberships", "account_by_email", "account_by_openid"] |
355 | + |
356 | + @api_user_required |
357 | + def invalidate_token(self, request, token=None, consumer_key=None): |
358 | + tokens = Token.objects.filter( |
359 | + token=token, consumer__user__username=consumer_key) |
360 | + tokens.delete() |
361 | + application_token_invalidated.send( |
362 | + sender=self, openid_identifier=consumer_key) |
363 | + |
364 | + write_ops = ["invalidate_token"] |
365 | + |
366 | + def _dispatch(self, request, allowed, noop_fn=lambda: "fail"): |
367 | + ws_op = request.REQUEST.get("ws.op") |
368 | + |
369 | + if ws_op is None: |
370 | + return noop_fn() |
371 | + |
372 | + if ws_op not in allowed: |
373 | + return "fail" |
374 | + |
375 | + args = dict(request.REQUEST.items()) |
376 | + del args["ws.op"] |
377 | + fn = getattr(self, ws_op) |
378 | + import pdb |
379 | + if len(allowed)==1: pdb.set_trace() |
380 | + return fn(request, **args) |
381 | + |
382 | + @basic_auth_required |
383 | + def get(self, request): |
384 | + def noop(): |
385 | + return {"total_size": 0, |
386 | + "start": None, |
387 | + "resource_type_link": api_reverse( |
388 | + request, root, fragment="#authentications"), |
389 | + "entries": []} |
390 | + return self._dispatch(request, self.read_ops, noop) |
391 | + |
392 | + @basic_auth_required |
393 | + def post(self, request): |
394 | + return self._dispatch(request, self.write_ops) |
395 | |
396 | |
397 | class CaptchasHandler(JsonHandler): |
398 | |
399 | === modified file 'identityprovider/wsgi.py' |
400 | --- identityprovider/wsgi.py 2011-02-02 20:13:30 +0000 |
401 | +++ identityprovider/wsgi.py 2011-02-02 20:13:30 +0000 |
402 | @@ -90,6 +90,7 @@ |
403 | |
404 | dispatch = WSGIDispatch( |
405 | [(r'^/api/1.0/?$', django), |
406 | + (r'^/api/1.0/authentications', django), |
407 | (r'^/api/1.0/captchas', django), |
408 | (r'^/api/1.0', api)], |
409 | django) |