Merge lp:~canonical-isd-hackers/canonical-identity-provider/fix952915 into lp:canonical-identity-provider/release

Proposed by Simon Davy
Status: Merged
Approved by: David Owen
Approved revision: no longer in the source branch.
Merged at revision: 370
Proposed branch: lp:~canonical-isd-hackers/canonical-identity-provider/fix952915
Merge into: lp:canonical-identity-provider/release
Diff against target: 201 lines (+63/-31)
4 files modified
identityprovider/decorators.py (+6/-4)
identityprovider/tests/unit/test_decorators.py (+34/-13)
identityprovider/tests/unit/test_views_devices.py (+15/-0)
identityprovider/views/devices.py (+8/-14)
To merge this branch: bzr merge lp:~canonical-isd-hackers/canonical-identity-provider/fix952915
Reviewer Review Type Date Requested Status
David Owen (community) Approve
Review via email: mp+97034@code.launchpad.net

Commit message

Fixes #953036 by adding a require_twofactor directive to sso_login_required, to allow views to be 2f protected in a standard manner, and then applying as appropriate.

Description of the change

Fixes #953036 by adding a require_twofactor directive to sso_login_required, to allow views to be 2f protected in a standard manner, and then applying as appropriate

Note: device_addition still does the check manually, as it needs to special case for when there are NO devices.

To post a comment you must log in.
Revision history for this message
David Owen (dsowen) wrote :

#19-22 would be clearer, imo, as:

required = require_twofactor or user_requires_twofactor...

#23: No, not a good place to check the site requirements. The decide view must handle that (and do).

review: Needs Fixing
Revision history for this message
David Owen (dsowen) wrote :

Looks great.

review: Approve

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-02-10 15:27:42 +0000
3+++ identityprovider/decorators.py 2012-03-12 15:15:33 +0000
4@@ -48,15 +48,17 @@
5 return _dont_cache_decorator
6
7
8-def sso_login_required(function=None, redirect_field_name=REDIRECT_FIELD_NAME, login_url=None):
9+def sso_login_required(function=None, redirect_field_name=REDIRECT_FIELD_NAME,
10+ login_url=None, require_twofactor=False):
11 """
12 Decorator the wraps up django's login_required, and also checks for 2f
13 """
14 def decorator(view_func):
15 @wraps(view_func, assigned=available_attrs(view_func))
16 def _wrapped_view(request, *args, **kwargs):
17- if (twofactor.user_requires_twofactor_auth(request, request.user) and
18- not twofactor.is_authenticated(request)):
19+ required = (require_twofactor or
20+ twofactor.user_requires_twofactor_auth(request, request.user))
21+ if required and not twofactor.is_authenticated(request):
22 # only valid reverse arg is token
23 reverse_args = {}
24 if 'token' in kwargs and kwargs['token'] != None:
25@@ -67,7 +69,7 @@
26 redirect_field_name)
27 return view_func(request, *args, **kwargs)
28 return django_login_required(
29- _wrapped_view,
30+ _wrapped_view,
31 redirect_field_name,
32 login_url)
33
34
35=== modified file 'identityprovider/tests/unit/test_decorators.py'
36--- identityprovider/tests/unit/test_decorators.py 2012-02-07 16:45:12 +0000
37+++ identityprovider/tests/unit/test_decorators.py 2012-03-12 15:15:33 +0000
38@@ -65,19 +65,40 @@
39
40 @patch('identityprovider.decorators.twofactor.is_authenticated')
41 @patch('identityprovider.decorators.twofactor.user_requires_twofactor_auth')
42- def test_allowed_when_2f_required_and_2f_authed(self, mock_user, mock_auth):
43- mock_user.return_value = True
44- mock_auth.return_value = True
45- view = sso_login_required(self.fake_view, login_url='/login')
46- response = view(self.mock_request(True))
47- self.assertEqual(response, 'SUCCESS')
48-
49- @patch('identityprovider.decorators.twofactor.is_authenticated')
50- @patch('identityprovider.decorators.twofactor.user_requires_twofactor_auth')
51- def test_redirected_when_2f_required_and_not_2f_authed(self, mock_user, mock_auth):
52- mock_user.return_value = True
53- mock_auth.return_value = False
54- view = sso_login_required(self.fake_view, login_url='/login')
55+ def test_allowed_when_2f_required_by_user_and_2f_authed(self, mock_user, mock_auth):
56+ mock_user.return_value = True
57+ mock_auth.return_value = True
58+ view = sso_login_required(self.fake_view, login_url='/login')
59+ response = view(self.mock_request(True))
60+ self.assertEqual(response, 'SUCCESS')
61+
62+ @patch('identityprovider.decorators.twofactor.is_authenticated')
63+ @patch('identityprovider.decorators.twofactor.user_requires_twofactor_auth')
64+ def test_allowed_when_2f_required_by_keyword_and_2f_authed(self, mock_user, mock_auth):
65+ mock_user.return_value = False
66+ mock_auth.return_value = True
67+ deco = sso_login_required(require_twofactor=True, login_url='/login')
68+ view = deco(self.fake_view)
69+ response = view(self.mock_request(True))
70+ self.assertEqual(response, 'SUCCESS')
71+
72+ @patch('identityprovider.decorators.twofactor.is_authenticated')
73+ @patch('identityprovider.decorators.twofactor.user_requires_twofactor_auth')
74+ def test_redirected_when_2f_required_by_user_and_not_2f_authed(self, mock_user, mock_auth):
75+ mock_user.return_value = True
76+ mock_auth.return_value = False
77+ view = sso_login_required(self.fake_view, login_url='/login')
78+ response = view(self.mock_request(True, '/target'))
79+ self.assertEqual(response.status_code, 302)
80+ self.assertEqual(response['Location'], '/two_factor_auth?next=/target')
81+
82+ @patch('identityprovider.decorators.twofactor.is_authenticated')
83+ @patch('identityprovider.decorators.twofactor.user_requires_twofactor_auth')
84+ def test_redirected_when_2f_required_by_keyword_and_not_2f_authed(self, mock_user, mock_auth):
85+ mock_user.return_value = False
86+ mock_auth.return_value = False
87+ deco = sso_login_required(require_twofactor=True, login_url='/login')
88+ view = deco(self.fake_view)
89 response = view(self.mock_request(True, '/target'))
90 self.assertEqual(response.status_code, 302)
91 self.assertEqual(response['Location'], '/two_factor_auth?next=/target')
92
93=== modified file 'identityprovider/tests/unit/test_views_devices.py'
94--- identityprovider/tests/unit/test_views_devices.py 2012-03-12 11:47:08 +0000
95+++ identityprovider/tests/unit/test_views_devices.py 2012-03-12 15:15:33 +0000
96@@ -5,6 +5,7 @@
97 from django.test import TestCase
98 from lxml.html import fromstring
99 from pyquery import PyQuery
100+import mock
101
102 from identityprovider.models import AuthenticationDevice
103 from identityprovider.views.device_urldata import DEVICE_LIST
104@@ -32,6 +33,14 @@
105 password='test')
106
107 self.rename_view = reverse('device-rename', args=[self.device.id])
108+ self.patch = mock.patch(
109+ 'identityprovider.models.twofactor.is_authenticated')
110+ self.mock_auth = self.patch.__enter__()
111+ self.mock_auth.return_value = True
112+
113+ def tearDown(self):
114+ self.patch.__exit__(None, None, None)
115+
116
117 def test_device_rename_get(self):
118 response = self.client.get(self.rename_view)
119@@ -66,3 +75,9 @@
120 # post as new user
121 response = self.client.post(self.rename_view, {'name': 'bar'})
122 self.assertEqual(response.status_code, 404)
123+
124+ def test_device_rename_post_not_authed(self):
125+ self.mock_auth.return_value = False
126+ response = self.client.post(self.rename_view, {'name': 'bar'})
127+ self.assertEqual(response.status_code, 302)
128+ self.assertIn(reverse('twofactor'), response['Location'])
129
130=== modified file 'identityprovider/views/devices.py'
131--- identityprovider/views/devices.py 2012-03-12 11:09:42 +0000
132+++ identityprovider/views/devices.py 2012-03-12 15:15:33 +0000
133@@ -31,7 +31,7 @@
134
135
136 # A decorator
137-require_twofactor = switch_is_active('TWOFACTOR')
138+require_twofactor_enabled = switch_is_active('TWOFACTOR')
139
140
141 device_types = {
142@@ -74,7 +74,7 @@
143
144
145 @sso_login_required
146-@require_twofactor
147+@require_twofactor_enabled
148 @allow_only('GET', 'POST')
149 def device_list(request):
150 if request.method == 'POST':
151@@ -113,7 +113,7 @@
152
153
154 @sso_login_required
155-@require_twofactor
156+@require_twofactor_enabled
157 @allow_only('GET', 'POST')
158 def device_addition(request):
159
160@@ -211,7 +211,7 @@
161
162
163 @sso_login_required
164-@require_twofactor
165+@require_twofactor_enabled
166 @allow_only('GET', 'POST')
167 def device_verification(request, device_id):
168 # TODO: Verify that the logged-in user actually owns the device
169@@ -229,16 +229,10 @@
170 return get_object_or_404(AuthenticationDevice, id=device_id, account=user)
171
172
173-@sso_login_required
174-@require_twofactor
175+@sso_login_required(require_twofactor=True)
176+@require_twofactor_enabled
177 @allow_only('GET', 'POST')
178 def device_removal(request, device_id):
179- if not twofactor.is_authenticated(request):
180- return redirect_to_login(
181- request.get_full_path(),
182- reverse('twofactor')
183- )
184-
185 device = _get_device_or_404(device_id, request.user)
186
187 if request.method != 'POST':
188@@ -289,11 +283,11 @@
189 device_list_path=reverse(DEVICE_LIST),
190 form=form))
191
192-device_rename = sso_login_required(DeviceRenameView.as_view())
193+device_rename = sso_login_required(require_twofactor=True)(DeviceRenameView.as_view())
194
195
196 @sso_login_required
197-@require_twofactor
198+@require_twofactor_enabled
199 @allow_only('GET', 'POST')
200 def pad_removal(request):
201 if request.method == 'GET':