Merge lp:~canonical-isd-hackers/canonical-identity-provider/enforce-user-requirestwo-factor into lp:canonical-identity-provider/release
- enforce-user-requirestwo-factor
- Merge into trunk
Proposed by
Michael Foord
Status: | Merged |
---|---|
Merged at revision: | 324 |
Proposed branch: | lp:~canonical-isd-hackers/canonical-identity-provider/enforce-user-requirestwo-factor |
Merge into: | lp:canonical-identity-provider/release |
Diff against target: |
798 lines (+264/-76) 15 files modified
identityprovider/decorators.py (+37/-0) identityprovider/models/twofactor.py (+36/-0) identityprovider/tests/acceptance/devices/add_device.py (+1/-1) identityprovider/tests/acceptance/devices/login.py (+1/-1) identityprovider/tests/acceptance/devices/prefs.py (+12/-2) identityprovider/tests/acceptance/devices/remove_device.py (+1/-1) identityprovider/tests/acceptance/devices/rename_device.py (+1/-1) identityprovider/tests/acceptance/views_protected.csv (+8/-0) identityprovider/tests/acceptance/views_protected.py (+44/-0) identityprovider/tests/unit/test_decorators.py (+74/-2) identityprovider/tests/unit/test_views_ui.py (+12/-13) identityprovider/views/account.py (+8/-9) identityprovider/views/devices.py (+10/-10) identityprovider/views/server.py (+2/-2) identityprovider/views/ui.py (+17/-34) |
To merge this branch: | bzr merge lp:~canonical-isd-hackers/canonical-identity-provider/enforce-user-requirestwo-factor |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Canonical ISD hackers | Pending | ||
Review via email: mp+91824@code.launchpad.net |
Commit message
Adds new sso_login_required decorator to wrap up django's login_required and add twofactor checks
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 'identityprovider/decorators.py' |
2 | --- identityprovider/decorators.py 2012-01-24 12:23:06 +0000 |
3 | +++ identityprovider/decorators.py 2012-02-09 22:30:07 +0000 |
4 | @@ -2,9 +2,17 @@ |
5 | # GNU Affero General Public License version 3 (see the file LICENSE). |
6 | |
7 | import logging |
8 | +from functools import wraps |
9 | |
10 | from django.template import RequestContext |
11 | from django.http import Http404, HttpResponseRedirect, HttpResponseForbidden |
12 | +from django.contrib.auth import REDIRECT_FIELD_NAME |
13 | +from django.utils.decorators import available_attrs |
14 | +from django.contrib.auth.decorators import ( |
15 | + login_required as django_login_required, |
16 | +) |
17 | +from django.contrib.auth.views import redirect_to_login |
18 | +from django.core.urlresolvers import reverse |
19 | from django.core.cache import cache |
20 | from django.conf import settings |
21 | from django.template.loader import render_to_string |
22 | @@ -16,6 +24,8 @@ |
23 | |
24 | from identityprovider import auth |
25 | from identityprovider.cookies import set_test_cookie, test_cookie_worked |
26 | +from identityprovider.models import twofactor |
27 | + |
28 | |
29 | def guest_required(func): |
30 | @functools.wraps(func) |
31 | @@ -38,6 +48,33 @@ |
32 | return _dont_cache_decorator |
33 | |
34 | |
35 | +def sso_login_required(function=None, redirect_field_name=REDIRECT_FIELD_NAME, login_url=None): |
36 | + """ |
37 | + Decorator the wraps up django's login_required, and also checks for 2f |
38 | + """ |
39 | + def decorator(view_func): |
40 | + @wraps(view_func, assigned=available_attrs(view_func)) |
41 | + def _wrapped_view(request, *args, **kwargs): |
42 | + if (twofactor.user_requires_twofactor_auth(request, request.user) and |
43 | + not twofactor.is_authenticated(request)): |
44 | + # some views send token as None, which breaks reverse |
45 | + if 'token' in kwargs and kwargs['token'] == None: |
46 | + del kwargs['token'] |
47 | + return redirect_to_login( |
48 | + request.get_full_path(), |
49 | + reverse('twofactor', kwargs=kwargs), |
50 | + redirect_field_name) |
51 | + return view_func(request, *args, **kwargs) |
52 | + return django_login_required( |
53 | + _wrapped_view, |
54 | + redirect_field_name, |
55 | + login_url) |
56 | + |
57 | + if function: |
58 | + return decorator(function) |
59 | + return decorator |
60 | + |
61 | + |
62 | def check_readonly(func): |
63 | """ A readonly aware decorator. |
64 | |
65 | |
66 | === modified file 'identityprovider/models/twofactor.py' |
67 | --- identityprovider/models/twofactor.py 2012-01-30 14:42:31 +0000 |
68 | +++ identityprovider/models/twofactor.py 2012-02-09 22:30:07 +0000 |
69 | @@ -1,10 +1,15 @@ |
70 | # Copyright 2010-2012 Canonical Ltd. This software is licensed under the |
71 | # GNU Affero General Public License version 3 (see the file LICENSE). |
72 | + |
73 | +import logging |
74 | + |
75 | from oath.hotp import accept_hotp |
76 | from django.db import models |
77 | from django.conf import settings |
78 | from django.utils.translation import ugettext as _ |
79 | |
80 | +from gargoyle import gargoyle |
81 | + |
82 | from identityprovider.models import Account |
83 | from identityprovider.utils import AuthenticationError |
84 | |
85 | @@ -20,6 +25,37 @@ |
86 | else: |
87 | return None |
88 | |
89 | +TWOFACTOR_KEY = 'logged_in_via_two_factor' |
90 | + |
91 | +def is_twofactor_enabled(request): |
92 | + return gargoyle.is_active('TWOFACTOR', request) |
93 | + |
94 | +def login(request): |
95 | + request.session[TWOFACTOR_KEY] = True |
96 | + |
97 | +def logout(request): |
98 | + if TWOFACTOR_KEY in request.session: |
99 | + del request.session[TWOFACTOR_KEY] |
100 | + |
101 | +def is_authenticated(request): |
102 | + return request.session.get(TWOFACTOR_KEY, False) |
103 | + |
104 | +def site_requires_twofactor_auth(request, token): |
105 | + if not is_twofactor_enabled(request): |
106 | + return False |
107 | + # TODO check if site requires 2f (pape?) |
108 | + return False |
109 | + |
110 | +def user_requires_twofactor_auth(request, account): |
111 | + if not is_twofactor_enabled(request): |
112 | + return False |
113 | + if account.twofactor_required: |
114 | + if account.has_twofactor_devices(): |
115 | + return True |
116 | + logging.warning('Account %s requires two-factor but has no devices' % account.openid_identifier) |
117 | + return False |
118 | + |
119 | + |
120 | class AuthenticationDevice(models.Model): |
121 | account = models.ForeignKey(Account, related_name='devices') |
122 | key = models.TextField() |
123 | |
124 | === added file 'identityprovider/tests/acceptance/devices/__init__.py' |
125 | === modified file 'identityprovider/tests/acceptance/devices/add_device.py' |
126 | --- identityprovider/tests/acceptance/devices/add_device.py 2012-01-27 16:28:56 +0000 |
127 | +++ identityprovider/tests/acceptance/devices/add_device.py 2012-02-09 22:30:07 +0000 |
128 | @@ -4,7 +4,7 @@ |
129 | from sst.actions import * |
130 | |
131 | from identityprovider.tests.acceptance import _helpers as helpers |
132 | -from _helpers import click_add_device_button, click_add_new_device_link |
133 | +from identityprovider.tests.acceptance.devices._helpers import click_add_device_button, click_add_new_device_link |
134 | |
135 | |
136 | config.set_base_url_from_env() |
137 | |
138 | === modified file 'identityprovider/tests/acceptance/devices/login.py' |
139 | --- identityprovider/tests/acceptance/devices/login.py 2012-02-03 13:57:54 +0000 |
140 | +++ identityprovider/tests/acceptance/devices/login.py 2012-02-09 22:30:07 +0000 |
141 | @@ -4,7 +4,7 @@ |
142 | from canonical.isd.tests.sst import config |
143 | from sst.actions import * |
144 | |
145 | -from _helpers import add_device |
146 | +from identityprovider.tests.acceptance.devices._helpers import add_device |
147 | from identityprovider.tests.acceptance import _helpers as helpers |
148 | |
149 | def enter_otp(otp): |
150 | |
151 | === modified file 'identityprovider/tests/acceptance/devices/prefs.py' |
152 | --- identityprovider/tests/acceptance/devices/prefs.py 2012-01-27 16:28:56 +0000 |
153 | +++ identityprovider/tests/acceptance/devices/prefs.py 2012-02-09 22:30:07 +0000 |
154 | @@ -1,8 +1,9 @@ |
155 | # Test user two factor preferences |
156 | +from oath import hotp |
157 | from canonical.isd.tests.sst import config |
158 | from sst.actions import * |
159 | |
160 | -from _helpers import add_device |
161 | +from identityprovider.tests.acceptance.devices._helpers import add_device |
162 | from identityprovider.tests.acceptance import _helpers as helpers |
163 | |
164 | |
165 | @@ -26,7 +27,7 @@ |
166 | disabled="disabled") |
167 | |
168 | # Add an authentication device |
169 | -add_device() |
170 | +aes_key = add_device() |
171 | |
172 | # Test preferences form is now enabled |
173 | assert_attribute(get_element(tag="input", name="two-factor", value="always"), |
174 | @@ -45,7 +46,16 @@ |
175 | set_radio_value(get_element(tag="input", name="two-factor", value="always")) |
176 | click_button("authentication-prefs") |
177 | |
178 | +# so we now should now have the twofactor auth form |
179 | +assert_url('/two_factor_auth?next=/device-list') |
180 | + |
181 | +# 2 factor login |
182 | +otp = hotp.hotp(aes_key, 1) |
183 | +write_textfield('id_oath_token', otp) |
184 | +click_button(get_element(type='submit')) |
185 | + |
186 | # Check that "always" is now shown in the user preferences |
187 | +assert_url('/device-list') |
188 | assert_attribute(get_element(tag="input", name="two-factor", value="always"), |
189 | 'checked', 'true') |
190 | assert_attribute(get_element(tag="input", name="two-factor", value="as-needed"), |
191 | |
192 | === modified file 'identityprovider/tests/acceptance/devices/remove_device.py' |
193 | --- identityprovider/tests/acceptance/devices/remove_device.py 2012-01-27 16:29:24 +0000 |
194 | +++ identityprovider/tests/acceptance/devices/remove_device.py 2012-02-09 22:30:07 +0000 |
195 | @@ -3,7 +3,7 @@ |
196 | from sst.actions import * |
197 | |
198 | from identityprovider.tests.acceptance import _helpers as helpers |
199 | -from _helpers import add_device |
200 | +from identityprovider.tests.acceptance.devices._helpers import add_device |
201 | |
202 | |
203 | def click_remove_button(): |
204 | |
205 | === modified file 'identityprovider/tests/acceptance/devices/rename_device.py' |
206 | --- identityprovider/tests/acceptance/devices/rename_device.py 2012-01-27 16:28:56 +0000 |
207 | +++ identityprovider/tests/acceptance/devices/rename_device.py 2012-02-09 22:30:07 +0000 |
208 | @@ -2,7 +2,7 @@ |
209 | from sst.actions import * |
210 | |
211 | from identityprovider.tests.acceptance import _helpers as helpers |
212 | -from _helpers import ( |
213 | +from identityprovider.tests.acceptance.devices._helpers import ( |
214 | add_device, |
215 | assert_device, |
216 | assert_no_device, |
217 | |
218 | === added file 'identityprovider/tests/acceptance/views_protected.csv' |
219 | --- identityprovider/tests/acceptance/views_protected.csv 1970-01-01 00:00:00 +0000 |
220 | +++ identityprovider/tests/acceptance/views_protected.csv 2012-02-09 22:30:07 +0000 |
221 | @@ -0,0 +1,8 @@ |
222 | +url |
223 | +'/+edit' |
224 | +'/+emails' |
225 | +'/+index' |
226 | +'/+applications' |
227 | +'/+new-email' |
228 | +'/+verify-email' |
229 | +'/+remove-email' |
230 | |
231 | === added file 'identityprovider/tests/acceptance/views_protected.py' |
232 | --- identityprovider/tests/acceptance/views_protected.py 1970-01-01 00:00:00 +0000 |
233 | +++ identityprovider/tests/acceptance/views_protected.py 2012-02-09 22:30:07 +0000 |
234 | @@ -0,0 +1,44 @@ |
235 | +import sys |
236 | +from urllib import quote |
237 | + |
238 | +from canonical.isd.tests.sst import config |
239 | +from sst.actions import * |
240 | + |
241 | +import _helpers as helpers |
242 | +from devices import _helpers as dev_helpers |
243 | + |
244 | +config.set_base_url_from_env() |
245 | + |
246 | +try: |
247 | + import _views_protected_shared as shared |
248 | +except ImportError: |
249 | + class module(object): |
250 | + pass |
251 | + m = module() |
252 | + |
253 | + m.twofactor_password = 'Admin009' |
254 | + m.twofactor_email = helpers.register_account(password=m.twofactor_password) |
255 | + m.aes_key = dev_helpers.add_device() |
256 | + # Change the preference to always require 2 factor authentication and save |
257 | + set_radio_value(get_element(tag="input", name="two-factor", value="always")) |
258 | + click_button("authentication-prefs") |
259 | + assert_url('/two_factor_auth?next=/device-list') |
260 | + helpers.logout() |
261 | + |
262 | + sys.modules['_views_protected_shared'] = m |
263 | + import _views_protected_shared as shared |
264 | + |
265 | +# are we redirected? |
266 | +go_to(url) |
267 | +wait_for(assert_title, 'Log in') |
268 | + |
269 | +helpers.login(shared.twofactor_email, shared.twofactor_password) |
270 | + |
271 | +go_to(url) |
272 | +# should be redirected |
273 | +assert_url('/two_factor_auth?next=%s' % quote(url)) |
274 | + |
275 | +helpers.logout() |
276 | + |
277 | + |
278 | + |
279 | |
280 | === modified file 'identityprovider/tests/unit/test_decorators.py' |
281 | --- identityprovider/tests/unit/test_decorators.py 2012-01-10 21:17:33 +0000 |
282 | +++ identityprovider/tests/unit/test_decorators.py 2012-02-09 22:30:07 +0000 |
283 | @@ -1,9 +1,9 @@ |
284 | from datetime import datetime |
285 | from django.test import TestCase |
286 | |
287 | -from mock import Mock, patch |
288 | +from mock import Mock, patch, MagicMock |
289 | |
290 | -from identityprovider.decorators import ratelimit |
291 | +from identityprovider.decorators import ratelimit, sso_login_required |
292 | |
293 | |
294 | class RatelimitTestCase(TestCase): |
295 | @@ -32,3 +32,75 @@ |
296 | limiter.current_key(request) |
297 | |
298 | self.assertEqual(mock_datetime.utcnow.called, True) |
299 | + |
300 | + |
301 | +class SSOLoginRequiredTestCase(TestCase): |
302 | + |
303 | + @staticmethod |
304 | + def fake_view(request, *args, **kwargs): |
305 | + return "SUCCESS" |
306 | + |
307 | + def mock_request(self, authed, path='/'): |
308 | + mock_request = MagicMock() |
309 | + mock_request.method = 'GET' |
310 | + mock_request.user.is_authenticated.return_value = authed |
311 | + mock_request.POST = {} |
312 | + mock_request.META = {} |
313 | + mock_request.build_absolute_uri.return_value = path |
314 | + mock_request.get_full_path.return_value = path |
315 | + return mock_request |
316 | + |
317 | + def test_django_login_check_still_works(self): |
318 | + view = sso_login_required(self.fake_view, login_url='/login') |
319 | + response = view(self.mock_request(False, '/target')) |
320 | + self.assertEqual(response.status_code, 302) |
321 | + self.assertEqual(response['Location'], '/login?next=/target') |
322 | + |
323 | + @patch('identityprovider.decorators.twofactor.user_requires_twofactor_auth') |
324 | + def test_allowed_2f_not_required(self, mock_user): |
325 | + mock_user.return_value = False |
326 | + view = sso_login_required(self.fake_view, login_url='/login') |
327 | + response = view(self.mock_request(True)) |
328 | + self.assertEqual(response, 'SUCCESS') |
329 | + |
330 | + @patch('identityprovider.decorators.twofactor.is_authenticated') |
331 | + @patch('identityprovider.decorators.twofactor.user_requires_twofactor_auth') |
332 | + def test_allowed_when_2f_required_and_2f_authed(self, mock_user, mock_auth): |
333 | + mock_user.return_value = True |
334 | + mock_auth.return_value = True |
335 | + view = sso_login_required(self.fake_view, login_url='/login') |
336 | + response = view(self.mock_request(True)) |
337 | + self.assertEqual(response, 'SUCCESS') |
338 | + |
339 | + @patch('identityprovider.decorators.twofactor.is_authenticated') |
340 | + @patch('identityprovider.decorators.twofactor.user_requires_twofactor_auth') |
341 | + def test_redirected_when_2f_required_and_not_2f_authed(self, mock_user, mock_auth): |
342 | + mock_user.return_value = True |
343 | + mock_auth.return_value = False |
344 | + view = sso_login_required(self.fake_view, login_url='/login') |
345 | + response = view(self.mock_request(True, '/target')) |
346 | + self.assertEqual(response.status_code, 302) |
347 | + self.assertEqual(response['Location'], '/two_factor_auth?next=/target') |
348 | + |
349 | + def test_decoration_no_params(self): |
350 | + @sso_login_required |
351 | + def view(request, *args, **kwargs): |
352 | + return "SUCCESS" |
353 | + response = view(self.mock_request(True, '/target')) |
354 | + # if this works, the decorator handles this invocation properly |
355 | + self.assertEqual(response, 'SUCCESS') |
356 | + |
357 | + def test_decoration_with_paramss(self): |
358 | + @sso_login_required(login_url='/alt_login') |
359 | + def view(request, *args, **kwargs): |
360 | + return "SUCCESS" |
361 | + response = view(self.mock_request(True, '/target')) |
362 | + # if this works, the decorator handles this invocation properly |
363 | + self.assertEqual(response, 'SUCCESS') |
364 | + |
365 | + |
366 | + |
367 | + |
368 | + |
369 | + |
370 | + |
371 | |
372 | === modified file 'identityprovider/tests/unit/test_views_ui.py' |
373 | --- identityprovider/tests/unit/test_views_ui.py 2012-02-09 11:47:25 +0000 |
374 | +++ identityprovider/tests/unit/test_views_ui.py 2012-02-09 22:30:07 +0000 |
375 | @@ -30,7 +30,7 @@ |
376 | from identityprovider.models import EmailAddress, Person, OpenIDRPConfig |
377 | from identityprovider.models.const import EmailStatus |
378 | from identityprovider.views import ui, server |
379 | -from identityprovider.models import Account, AuthToken |
380 | +from identityprovider.models import Account, AuthToken, twofactor |
381 | from identityprovider.models.authtoken import ( |
382 | InvalidEmailException, |
383 | create_token, |
384 | @@ -1180,14 +1180,14 @@ |
385 | data['next'] = next |
386 | return self.client.post('/two_factor_auth', data) |
387 | |
388 | - @patch('identityprovider.views.ui.site_requires_twofactor_auth') |
389 | + @patch('identityprovider.views.ui.twofactor.site_requires_twofactor_auth') |
390 | def test_when_site_does_not_require_twofactor_no_oath_field(self, mock_site): |
391 | mock_site.return_value = False |
392 | response = self.client.get('/+login') |
393 | tree = PyQuery(fromstring(response.content)) |
394 | self.assertEqual(len(tree.find('input[name="oath_token"]')), 0) |
395 | |
396 | - @patch('identityprovider.views.ui.site_requires_twofactor_auth') |
397 | + @patch('identityprovider.views.ui.twofactor.site_requires_twofactor_auth') |
398 | def test_when_site_requires_twofactor_oath_field_is_present(self, mock_site): |
399 | mock_site.return_value = True |
400 | response = self.client.get('/+login') |
401 | @@ -1197,7 +1197,7 @@ |
402 | oath = inputs[0] |
403 | self.assertEqual(oath.attrib['autocomplete'], 'off') |
404 | |
405 | - @patch('identityprovider.views.ui.site_requires_twofactor_auth') |
406 | + @patch('identityprovider.views.ui.twofactor.site_requires_twofactor_auth') |
407 | def test_when_site_requires_twofactor_oath_field_is_present_lp(self, mock_site): |
408 | mock_site.return_value = True |
409 | response = self.client.get('/+login') |
410 | @@ -1207,27 +1207,26 @@ |
411 | oath = inputs[0] |
412 | self.assertEqual(oath.attrib['autocomplete'], 'off') |
413 | |
414 | - |
415 | - @patch('identityprovider.views.ui.user_requires_twofactor_auth') |
416 | + @patch('identityprovider.views.ui.twofactor.user_requires_twofactor_auth') |
417 | def test_when_user_requires_twofactor_redirected(self, mock_user): |
418 | mock_user.return_value = True |
419 | r = self.do_login() |
420 | self.assertRedirects(r, '/two_factor_auth') |
421 | |
422 | - @patch('identityprovider.views.ui.user_requires_twofactor_auth') |
423 | + @patch('identityprovider.views.ui.twofactor.user_requires_twofactor_auth') |
424 | def test_when_user_requires_twofactor_redirected_with_token(self, mock_user): |
425 | mock_user.return_value = True |
426 | token = 'a' * 16 |
427 | r = self.do_login(token=token) |
428 | self.assertRedirects(r, '/%s/two_factor_auth' % token) |
429 | |
430 | - @patch('identityprovider.views.ui.user_requires_twofactor_auth') |
431 | + @patch('identityprovider.views.ui.twofactor.user_requires_twofactor_auth') |
432 | def test_when_user_requires_twofactor_redirected_with_next(self, mock_user): |
433 | mock_user.return_value = True |
434 | r = self.do_login(next='/+edit') |
435 | self.assertRedirects(r, '/two_factor_auth?next=/%2Bedit') |
436 | |
437 | - @patch('identityprovider.views.ui.site_requires_twofactor_auth') |
438 | + @patch('identityprovider.views.ui.twofactor.site_requires_twofactor_auth') |
439 | @patch('identityprovider.views.ui.authenticate_device') |
440 | def test_full_twofactor_login_redirected_to_next(self, mock_auth, mock_site): |
441 | mock_auth.return_value = True |
442 | @@ -1236,7 +1235,7 @@ |
443 | mock_auth.assert_called_once_with(self.account, '123456') |
444 | self.assertRedirects(r, '/+edit') |
445 | |
446 | - @patch('identityprovider.views.ui.site_requires_twofactor_auth') |
447 | + @patch('identityprovider.views.ui.twofactor.site_requires_twofactor_auth') |
448 | def test_full_twofactor_login_illformed_token_displays_error(self, mock_site): |
449 | mock_site.return_value = True |
450 | response = self.do_login(next='/+edit', oath_token='XXX') |
451 | @@ -1246,7 +1245,7 @@ |
452 | self.assertEqual(len(error), 1) |
453 | self.assertEqual(error[0].text.strip(), "Please enter a 6-digit or 8-digit one-time password.") |
454 | |
455 | - @patch('identityprovider.views.ui.site_requires_twofactor_auth') |
456 | + @patch('identityprovider.views.ui.twofactor.site_requires_twofactor_auth') |
457 | @patch('identityprovider.views.ui.authenticate_device') |
458 | def test_full_twofactor_login_invalid_code_displays_error(self, mock_auth, mock_site): |
459 | mock_auth.side_effect = AuthenticationError("ERRORMSG") |
460 | @@ -1314,12 +1313,12 @@ |
461 | def test_user_requires_twofactor_auth(self): |
462 | def f(require2f, has_devices, returns, logs): |
463 | with patch('logging.warning') as mock_warning: |
464 | - with patch('identityprovider.views.ui.is_twofactor_enabled') as mock_flag: |
465 | + with patch('identityprovider.views.ui.twofactor.is_twofactor_enabled') as mock_flag: |
466 | mock_flag.return_value = True |
467 | account = Mock() |
468 | account.twofactor_required = require2f |
469 | account.has_twofactor_devices.return_value = has_devices |
470 | - r = ui.user_requires_twofactor_auth(None, account) |
471 | + r = twofactor.user_requires_twofactor_auth(None, account) |
472 | self.assertEqual(returns, r) |
473 | self.assertEqual(logs, mock_warning.called) |
474 | f(require2f=False, has_devices=False, returns=False, logs=False) |
475 | |
476 | === modified file 'identityprovider/views/account.py' |
477 | --- identityprovider/views/account.py 2012-01-13 14:49:47 +0000 |
478 | +++ identityprovider/views/account.py 2012-02-09 22:30:07 +0000 |
479 | @@ -4,7 +4,6 @@ |
480 | from openid.yadis.constants import YADIS_HEADER_NAME, YADIS_CONTENT_TYPE |
481 | |
482 | from django.conf import settings |
483 | -from django.contrib.auth.decorators import login_required |
484 | from django.http import HttpResponseRedirect |
485 | from django.shortcuts import render_to_response, get_object_or_404 |
486 | from django.template import RequestContext |
487 | @@ -12,7 +11,7 @@ |
488 | from django.views.decorators.vary import vary_on_headers |
489 | |
490 | from identityprovider.cookies import set_test_cookie, test_cookie_worked |
491 | -from identityprovider.decorators import check_readonly |
492 | +from identityprovider.decorators import check_readonly, sso_login_required |
493 | from identityprovider.forms import EditAccountForm, LoginForm, NewEmailForm |
494 | from identityprovider.models import (send_validation_email_request, |
495 | EmailAddress) |
496 | @@ -58,7 +57,7 @@ |
497 | return set_test_cookie(render_to_response('cookies.html')) |
498 | |
499 | |
500 | -@login_required |
501 | +@sso_login_required |
502 | def account_index(request, token=None): |
503 | request.token = token |
504 | account = request.user |
505 | @@ -86,7 +85,7 @@ |
506 | return render_to_response('account/edit.html', context) |
507 | |
508 | |
509 | -@login_required |
510 | +@sso_login_required |
511 | def account_emails(request): |
512 | account = request.user |
513 | form = NewEmailForm() |
514 | @@ -103,7 +102,7 @@ |
515 | return render_to_response('account/emails.html', context) |
516 | |
517 | |
518 | -@login_required |
519 | +@sso_login_required |
520 | def account_deactivate(request, token=None): |
521 | request.token = token |
522 | import django.contrib.auth as auth |
523 | @@ -152,7 +151,7 @@ |
524 | ) |
525 | |
526 | |
527 | -@login_required |
528 | +@sso_login_required |
529 | @check_readonly |
530 | def new_email(request, token=None, emailid=None): |
531 | request.token = token |
532 | @@ -171,7 +170,7 @@ |
533 | return render_to_response('account/new_email.html', context) |
534 | |
535 | |
536 | -@login_required |
537 | +@sso_login_required |
538 | def verify_email(request, token=None): |
539 | request.token = token |
540 | emailid = request.GET.get('id', 0) |
541 | @@ -180,7 +179,7 @@ |
542 | return _send_verification_email(request, email.email, token) |
543 | |
544 | |
545 | -@login_required |
546 | +@sso_login_required |
547 | def delete_email(request, token=None): |
548 | request.token = token |
549 | emailid = request.GET.get('id', 0) |
550 | @@ -201,7 +200,7 @@ |
551 | return render_to_response('account/delete_email.html', context) |
552 | |
553 | |
554 | -@login_required |
555 | +@sso_login_required |
556 | @check_readonly |
557 | def applications(request): |
558 | if request.method == 'POST': |
559 | |
560 | === modified file 'identityprovider/views/devices.py' |
561 | --- identityprovider/views/devices.py 2012-02-01 22:34:09 +0000 |
562 | +++ identityprovider/views/devices.py 2012-02-09 22:30:07 +0000 |
563 | @@ -4,7 +4,6 @@ |
564 | import re |
565 | from base64 import b16encode |
566 | |
567 | -from django.contrib.auth.decorators import login_required |
568 | from django.core.urlresolvers import reverse |
569 | from django.conf import settings |
570 | from django.shortcuts import get_object_or_404, render_to_response |
571 | @@ -18,6 +17,7 @@ |
572 | from identityprovider.models import AuthenticationDevice |
573 | from identityprovider.models.twofactor import get_otp_type |
574 | from identityprovider.forms import HOTPDeviceForm, DeviceRenameForm |
575 | +from identityprovider.decorators import sso_login_required |
576 | |
577 | from .device_urldata import * |
578 | from .utils import HttpResponseRedirect, HttpResponseSeeOther, allow_only |
579 | @@ -52,7 +52,7 @@ |
580 | return rand_bytes(n) |
581 | |
582 | |
583 | -@login_required |
584 | +@sso_login_required |
585 | @require_twofactor |
586 | @allow_only('GET', 'POST') |
587 | def device_list(request): |
588 | @@ -88,7 +88,7 @@ |
589 | return render_to_response('device/list.html', context) |
590 | |
591 | |
592 | -@login_required |
593 | +@sso_login_required |
594 | @require_twofactor |
595 | @allow_only('GET', 'POST') |
596 | def device_addition(request): |
597 | @@ -173,7 +173,7 @@ |
598 | |
599 | |
600 | |
601 | -@login_required |
602 | +@sso_login_required |
603 | @require_twofactor |
604 | @allow_only('GET', 'POST') |
605 | def device_verification(request, device_id): |
606 | @@ -187,7 +187,7 @@ |
607 | pass |
608 | |
609 | |
610 | -@login_required |
611 | +@sso_login_required |
612 | @require_twofactor |
613 | @allow_only('GET', 'POST') |
614 | def device_removal(request, device_id): |
615 | @@ -235,10 +235,10 @@ |
616 | device_list_path=reverse(DEVICE_LIST), |
617 | form=form)) |
618 | |
619 | -device_rename = login_required(DeviceRenameView.as_view()) |
620 | - |
621 | - |
622 | -@login_required |
623 | +device_rename = sso_login_required(DeviceRenameView.as_view()) |
624 | + |
625 | + |
626 | +@sso_login_required |
627 | @require_twofactor |
628 | @allow_only('GET', 'POST') |
629 | def pad_removal(request): |
630 | @@ -248,7 +248,7 @@ |
631 | return HttpResponseSeeOther('/device-list') |
632 | |
633 | |
634 | -@login_required |
635 | +@sso_login_required |
636 | @require_twofactor |
637 | @allow_only('GET', 'POST') |
638 | def device_lost(request): |
639 | |
640 | === modified file 'identityprovider/views/server.py' |
641 | --- identityprovider/views/server.py 2012-01-24 12:23:06 +0000 |
642 | +++ identityprovider/views/server.py 2012-02-09 22:30:07 +0000 |
643 | @@ -33,7 +33,6 @@ |
644 | import django.contrib.auth as auth |
645 | |
646 | from django.conf import settings |
647 | -from django.contrib.auth.decorators import login_required |
648 | from django.http import ( |
649 | Http404, |
650 | HttpResponse, |
651 | @@ -55,6 +54,7 @@ |
652 | import identityprovider.signed as signed |
653 | |
654 | from identityprovider.const import LAUNCHPAD_TEAMS_NS |
655 | +from identityprovider.decorators import sso_login_required |
656 | from identityprovider.forms import ( |
657 | PreAuthorizeForm, |
658 | SRegRequestForm, |
659 | @@ -532,7 +532,7 @@ |
660 | openid_response.addExtension(sreg_response) |
661 | |
662 | |
663 | -@login_required |
664 | +@sso_login_required |
665 | def _process_decide(request, orequest, decision): |
666 | oresponse = orequest.answer(decision, |
667 | identity=request.user.openid_identity_url) |
668 | |
669 | === modified file 'identityprovider/views/ui.py' |
670 | --- identityprovider/views/ui.py 2012-02-06 16:54:13 +0000 |
671 | +++ identityprovider/views/ui.py 2012-02-09 22:30:07 +0000 |
672 | @@ -24,12 +24,16 @@ |
673 | from django.utils.decorators import method_decorator |
674 | from django import forms |
675 | |
676 | -from gargoyle import gargoyle |
677 | from gargoyle.decorators import switch_is_active |
678 | |
679 | from identityprovider.branding import current_brand |
680 | -from identityprovider.decorators import (check_readonly, dont_cache, |
681 | - guest_required, limitlogin, requires_cookies) |
682 | +from identityprovider.decorators import ( |
683 | + check_readonly, |
684 | + dont_cache, |
685 | + guest_required, |
686 | + limitlogin, |
687 | + requires_cookies, |
688 | + ) |
689 | from identityprovider.forms import ( |
690 | ConfirmNewAccountForm, |
691 | ForgotPasswordForm, |
692 | @@ -48,6 +52,7 @@ |
693 | from identityprovider.models.const import (AccountStatus, EmailStatus, |
694 | LoginTokenType) |
695 | from identityprovider.models.openidmodels import OpenIDRPConfig |
696 | +from identityprovider.models import twofactor |
697 | import identityprovider.signed as signed |
698 | from identityprovider.utils import ( |
699 | AuthenticationError, |
700 | @@ -62,7 +67,6 @@ |
701 | from identityprovider.views.utils import get_rpconfig, set_session_token_info |
702 | |
703 | |
704 | - |
705 | logger = logging.getLogger('sso') |
706 | |
707 | |
708 | @@ -91,30 +95,6 @@ |
709 | errors.append(str(exc)) |
710 | |
711 | |
712 | -def is_twofactor_enabled(request): |
713 | - return gargoyle.is_active('TWOFACTOR', request) |
714 | - |
715 | - |
716 | -# these may need to live elsewhere, but putting here for now |
717 | -def site_requires_twofactor_auth(request, token): |
718 | - if not is_twofactor_enabled(request): |
719 | - return False |
720 | - # TODO check if site requires 2f (pape?) |
721 | - return False |
722 | - |
723 | - |
724 | -def user_requires_twofactor_auth(request, account): |
725 | - if not is_twofactor_enabled(request): |
726 | - return False |
727 | - if account.twofactor_required: |
728 | - if account.has_twofactor_devices(): |
729 | - return True |
730 | - logging.warning('Account %s requires two-factor but has no devices' % account.openid_identifier) |
731 | - return False |
732 | - |
733 | - |
734 | - |
735 | - |
736 | def authenticate_user(email, password): |
737 | """Attempts to authenticate a user. Returns account on success, or throws |
738 | AuthenticationErrors on failure""" |
739 | @@ -191,12 +171,12 @@ |
740 | template_name = 'registration/login.html' |
741 | |
742 | def get_login_type(self, request, token): |
743 | - twofactor = site_requires_twofactor_auth(request, token) |
744 | - return twofactor, TwoFactorLoginForm if twofactor else LoginForm |
745 | + required = twofactor.site_requires_twofactor_auth(request, token) |
746 | + return required, TwoFactorLoginForm if required else LoginForm |
747 | |
748 | def get(self, request, token=None, rpconfig=None): |
749 | rpconfig = self.setup(request, token, rpconfig) |
750 | - twofactor, form_cls = self.get_login_type(request, token) |
751 | + required2f, form_cls = self.get_login_type(request, token) |
752 | # track login attempts |
753 | stats.increment('flows.login', key='requested', rpconfig=rpconfig) |
754 | return self.render(request, token, rpconfig, form_cls()) |
755 | @@ -223,7 +203,8 @@ |
756 | # handle case where the user's account requires two factor but we |
757 | # didn't know that until we authenticated them |
758 | oath_token = form.cleaned_data.get('oath_token', None) |
759 | - if not oath_token and user_requires_twofactor_auth(request, account): |
760 | + if (not oath_token and |
761 | + twofactor.user_requires_twofactor_auth(request, account)): |
762 | kwargs = {'token':token} if token else {} |
763 | url = reverse('twofactor', kwargs=kwargs) |
764 | if next and _is_safe_redirect_url(next): |
765 | @@ -234,7 +215,7 @@ |
766 | if site_twofactor or oath_token: |
767 | try: |
768 | authenticate_device(account, oath_token) |
769 | - request.session['logged_in_via_two_factor'] = True |
770 | + twofactor.login(request) |
771 | except AuthenticationError as e: |
772 | return self.display_errors(request, token, rpconfig, form, e) |
773 | |
774 | @@ -279,7 +260,7 @@ |
775 | |
776 | try: |
777 | authenticate_device(account, form.cleaned_data['oath_token']) |
778 | - request.session['logged_in_via_two_step'] = True |
779 | + twofactor.login(request) |
780 | stats.increment('flows.login', key='success', rpconfig=rpconfig) |
781 | except AuthenticationError as e: |
782 | return self.display_errors(request, token, rpconfig, form, e) |
783 | @@ -297,6 +278,7 @@ |
784 | |
785 | @method_decorator(dont_cache) |
786 | @method_decorator(limitlogin()) |
787 | + # for this page, we need to be logged in but NOT twofactored |
788 | @method_decorator(login_required) |
789 | @method_decorator(switch_is_active('TWOFACTOR')) |
790 | def dispatch(self, *args, **kwargs): |
791 | @@ -380,6 +362,7 @@ |
792 | # We don't want to lose session[token] when we log the user out |
793 | raw_orequest = request.session.get(token, None) |
794 | auth.logout(request) |
795 | + twofactor.logout(request) |
796 | self.set_orequest(request.session, token, raw_orequest) |
797 | |
798 | template_file = ('%s/registration/logout.html' % |