Merge lp:~canonical-isd-hackers/canonical-identity-provider/web_api-authentications into lp:~canonical-isd-hackers/canonical-identity-provider/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
Reviewer Review Type Date Requested Status
Canonical ISD hackers Pending
Review via email: mp+48362@code.launchpad.net
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)

Subscribers

People subscribed via source and target branches

to all changes: