Merge lp:~jamestait/canonical-identity-provider/lp1648775-force-exhausted-paper-renewal into lp:canonical-identity-provider/release

Proposed by James Tait
Status: Merged
Approved by: James Tait
Approved revision: no longer in the source branch.
Merge reported by: Otto Co-Pilot
Merged at revision: not available
Proposed branch: lp:~jamestait/canonical-identity-provider/lp1648775-force-exhausted-paper-renewal
Merge into: lp:canonical-identity-provider/release
Diff against target: 341 lines (+109/-22)
9 files modified
src/identityprovider/tests/utils.py (+1/-1)
src/webui/templates/common/printed_codes_nearly_exhausted_warning.html (+2/-2)
src/webui/templates/device/print-codes.html (+6/-2)
src/webui/tests/test_views_account.py (+3/-3)
src/webui/tests/test_views_devices.py (+25/-3)
src/webui/tests/test_views_ui.py (+37/-1)
src/webui/urls.py (+11/-5)
src/webui/views/devices.py (+15/-5)
src/webui/views/ui.py (+9/-0)
To merge this branch: bzr merge lp:~jamestait/canonical-identity-provider/lp1648775-force-exhausted-paper-renewal
Reviewer Review Type Date Requested Status
Ricardo Kirkner (community) Approve
Review via email: mp+322778@code.launchpad.net

Commit message

Redirect the user to the device list page and remind them to generate and print new paper codes if their current and only list is near exhaustion.

Description of the change

Redirect the user to the device list page and remind them to generate and
print new paper codes if their current and only list is near exhaustion.

The TwoFactorLoginTestCase has been updated to include the login token in the
URLs - I believe this is valid because redirection to the twofactor view will
now always include that token. The token is then passed between views to
enable the "Continue" link from the device-print view to redirect the user to
their original location in the RP.

Note that if the user has two sets of printed codes and only one is close to exhaustion, they will not be forced to renew the codes. In a follow-up branch, they will be prompted to renew via email.

To post a comment you must log in.
Revision history for this message
Ricardo Kirkner (ricardokirkner) :
Revision history for this message
James Tait (jamestait) :
Revision history for this message
Ricardo Kirkner (ricardokirkner) :
review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'src/identityprovider/tests/utils.py'
2--- src/identityprovider/tests/utils.py 2016-12-22 17:43:18 +0000
3+++ src/identityprovider/tests/utils.py 2017-04-24 17:17:28 +0000
4@@ -66,7 +66,7 @@
5 Please print a new list for the following devices."""
6
7
8-def assert_exausted_warning(testcase, devices, response):
9+def assert_exhausted_warning(testcase, devices, response):
10 """Helper to assert warning is correctly displayed"""
11 dom = PyQuery(response.content)
12 warning = dom.find('#exhausted_warning')
13
14=== modified file 'src/webui/templates/common/printed_codes_nearly_exhausted_warning.html'
15--- src/webui/templates/common/printed_codes_nearly_exhausted_warning.html 2013-04-17 13:23:35 +0000
16+++ src/webui/templates/common/printed_codes_nearly_exhausted_warning.html 2017-04-24 17:17:28 +0000
17@@ -1,11 +1,11 @@
18-{% load i18n %}
19+{% load i18n url_with_token %}
20
21 {% if paper_devices_needing_renewal %}
22 <div id="exhausted_warning" class="message warning">
23 <p>{% blocktrans %}Your printed list of backup codes is nearly used up.
24 Please print a new list for the following devices.{% endblocktrans %}</p>
25 {% for device in paper_devices_needing_renewal %}
26- <p>{{ device.name }} <a href="{% url 'device-print' device.id %}" class="btn-sm"><span>{% trans 'Generate New Codes' %}</span></a></p>
27+ <p>{{ device.name }} <a href="{% url_with_token 'device-print' device_id=device.id %}" class="btn-sm"><span>{% trans 'Generate New Codes' %}</span></a></p>
28 {% endfor %}
29 </div>
30 {% endif %}
31
32=== modified file 'src/webui/templates/device/print-codes.html'
33--- src/webui/templates/device/print-codes.html 2014-12-09 21:41:56 +0000
34+++ src/webui/templates/device/print-codes.html 2017-04-24 17:17:28 +0000
35@@ -1,5 +1,5 @@
36 {% extends "base.html" %}
37-{% load i18n %}
38+{% load i18n url_with_token %}
39
40 {% comment %}
41 Copyright 2012 Canonical Ltd. This software is licensed under the GNU
42@@ -30,9 +30,13 @@
43 <button class="btn cta" id="printbtn" onclick="window.print()" data-qa-id="print_codes">
44 <span>{% trans "Print Codes" %}</span>
45 </button>
46+ {% if token and not needs_renewal %}
47+ <a class="cta secondary" href="{% url_with_token 'server-decide' %}" data-qa-id="continue_to_rp">{% trans "Continue" %}</a>
48+ {% else %}
49 <a class="cta secondary" href="{% url 'device-list' %}" data-qa-id="cancel_print_codes">{% trans "Go back" %}</a>
50+ {% endif %}
51 {% if generation_enabled %}
52- <a class="btn cta secondary print-new-codes" href="{% url 'device-generate' device_id %}" data-qa-id="generate_codes">
53+ <a class="btn cta secondary print-new-codes" href="{% url_with_token 'device-generate' device_id=device_id %}" data-qa-id="generate_codes">
54 <span>{% trans "Generate new codes" %}</span>
55 </a>
56 {% endif %}
57
58=== modified file 'src/webui/tests/test_views_account.py'
59--- src/webui/tests/test_views_account.py 2017-02-26 13:52:43 +0000
60+++ src/webui/tests/test_views_account.py 2017-04-24 17:17:28 +0000
61@@ -38,7 +38,7 @@
62 MISSING_BACKUP_DEVICE,
63 AuthenticatedTestCase,
64 SSOBaseTestCase,
65- assert_exausted_warning,
66+ assert_exhausted_warning,
67 )
68 from identityprovider.views.testing import MockLaunchpad
69 from webui.constants import EMAIL_EXISTS_ERROR
70@@ -639,10 +639,10 @@
71
72 if (self.twofactor_enabled and self.devices_count > 0 and
73 self.paper_device_exhausted):
74- assert_exausted_warning(self, devices, response)
75+ assert_exhausted_warning(self, devices, response)
76 else:
77 with self.assertRaises(AssertionError):
78- assert_exausted_warning(self, devices, response)
79+ assert_exhausted_warning(self, devices, response)
80
81 def test_device_preferences_with_twofactor_disabled(self):
82 name = 'webui.views.account.is_twofactor_enabled'
83
84=== modified file 'src/webui/tests/test_views_devices.py'
85--- src/webui/tests/test_views_devices.py 2017-01-26 17:26:43 +0000
86+++ src/webui/tests/test_views_devices.py 2017-04-24 17:17:28 +0000
87@@ -25,7 +25,7 @@
88 MISSING_BACKUP_DEVICE,
89 AuthenticatedTestCase,
90 SSOBaseTransactionTestCase,
91- assert_exausted_warning,
92+ assert_exhausted_warning,
93 test_concurrently,
94 )
95
96@@ -152,7 +152,7 @@
97
98 response = self.client.get(self.url)
99 with self.assertRaises(AssertionError):
100- assert_exausted_warning(self, [device], response)
101+ assert_exhausted_warning(self, [device], response)
102
103 def test_codes_exhausted_warning_exhausted(self):
104 counter = (settings.TWOFACTOR_PAPER_CODES -
105@@ -161,7 +161,17 @@
106 account=self.account, device_type='paper', counter=counter)
107
108 response = self.client.get(self.url)
109- assert_exausted_warning(self, [device], response)
110+ assert_exhausted_warning(self, [device], response)
111+
112+ def test_codes_exhausted_warning_exhausted_with_login_token(self):
113+ counter = (settings.TWOFACTOR_PAPER_CODES -
114+ settings.TWOFACTOR_PAPER_CODES_WARN_RENEWAL + 1)
115+ device = self.factory.make_device(
116+ account=self.account, device_type='paper', counter=counter)
117+
118+ url = reverse('device-list', kwargs=dict(token='a' * 16))
119+ response = self.client.get(url)
120+ assert_exhausted_warning(self, [device], response)
121
122 def test_device_list_no_devices(self):
123 assert self.account.devices.count() == 0
124@@ -582,6 +592,18 @@
125 self.assertEqual(len(strike), 0)
126 self.assertEqual(elem.text.strip(), code)
127
128+ def test_device_print_with_login_token(self):
129+ counter = (settings.TWOFACTOR_PAPER_CODES -
130+ settings.TWOFACTOR_PAPER_CODES_WARN_RENEWAL + 1)
131+ device = self.factory.make_device(
132+ account=self.account, device_type='paper', counter=counter)
133+
134+ url = reverse('device-print', kwargs=dict(device_id=device.id,
135+ token='a' * 16))
136+ response = self.client.get(url)
137+
138+ self.assertContains(response, 'Go back')
139+
140 def test_twofactor_disabled(self):
141 self.assert_twofactor_disabled(self.print_url)
142 self.assert_twofactor_disabled(self.generate_url)
143
144=== modified file 'src/webui/tests/test_views_ui.py'
145--- src/webui/tests/test_views_ui.py 2017-04-13 21:09:42 +0000
146+++ src/webui/tests/test_views_ui.py 2017-04-24 17:17:28 +0000
147@@ -1594,6 +1594,9 @@
148
149 class TwoFactorLoginTestCase(BaseTwoFactorLoginTestCase):
150
151+ url = reverse('twofactor', kwargs=dict(token='a' * 16))
152+ logout_link = 'a[href="/aaaaaaaaaaaaaaaa/+logout"]'
153+
154 def setUp(self):
155 super(TwoFactorLoginTestCase, self).setUp()
156 self.mock_site = self.patch(
157@@ -1640,7 +1643,7 @@
158
159 def test_when_user_requires_twofactor_redirected(self):
160 self.mock_site.return_value = False
161- r = self.do_login()
162+ r = self.do_login(token='a' * 16)
163 self.assertRedirects(r, self.url)
164
165 def test_when_user_requires_twofactor_redirected_with_token(self):
166@@ -1705,6 +1708,39 @@
167 self.assertRedirects(r, reverse('account-edit'))
168 mock_auth.assert_called_once_with(self.account, '123456')
169
170+ def test_twofactor_step_redirected_to_device_list_near_exhaustion(self):
171+ counter = (settings.TWOFACTOR_PAPER_CODES -
172+ settings.TWOFACTOR_PAPER_CODES_WARN_RENEWAL + 1)
173+ self.factory.make_device(
174+ account=self.account, device_type='paper', counter=counter)
175+ mock_auth = self.patch(
176+ 'webui.views.ui.twofactor.authenticate', return_value=True)
177+
178+ self.client.login(username=self.email, password=self.password)
179+ r = self.do_twofactor(oath_token='123456')
180+
181+ mock_auth.assert_called_once_with(self.account, '123456')
182+ url = reverse('device-list', kwargs=dict(token='a' * 16))
183+ self.assertRedirects(r, url)
184+
185+ def test_twofactor_step_redirected_to_next_near_exhaustion_of_one(self):
186+ """If we have paper devices not near exhaustion, don't force refresh"""
187+ counter = (settings.TWOFACTOR_PAPER_CODES -
188+ settings.TWOFACTOR_PAPER_CODES_WARN_RENEWAL + 1)
189+ self.factory.make_device(
190+ account=self.account, device_type='paper', counter=counter)
191+ self.factory.make_device(
192+ account=self.account, device_type='paper')
193+ mock_auth = self.patch(
194+ 'webui.views.ui.twofactor.authenticate', return_value=True)
195+
196+ self.client.login(username=self.email, password=self.password)
197+ r = self.do_twofactor(next_url=reverse('account-edit'),
198+ oath_token='123456')
199+
200+ self.assertRedirects(r, reverse('account-edit'))
201+ mock_auth.assert_called_once_with(self.account, '123456')
202+
203 def test_twofactor_step_illformed_token_displays_error(self):
204 self.client.login(username=self.email, password=self.password)
205 response = self.do_twofactor(next_url=reverse('account-edit'),
206
207=== modified file 'src/webui/urls.py'
208--- src/webui/urls.py 2017-02-26 01:27:07 +0000
209+++ src/webui/urls.py 2017-04-24 17:17:28 +0000
210@@ -12,6 +12,7 @@
211 'authtoken': '(?P<authtoken>%s)' % AUTHTOKEN_PATTERN,
212 'email_address': '(?P<email_address>.+)',
213 'fingerprint': '(?P<fingerprint>.+)',
214+ 'device_id': '(?P<device_id>.+)',
215 }
216
217 urlpatterns = patterns(
218@@ -135,16 +136,21 @@
219 urlpatterns += patterns(
220 'webui.views.devices',
221 url(r'^device-list$', 'device_list', name='device-list'),
222+ url(r'^%(token)s/device-list$' % repls, 'device_list', name='device-list'),
223 url(r'^device-addition$', 'device_addition', name='device-addition'),
224- url(r'^device-removal/(?P<device_id>.+)$', 'device_removal',
225+ url(r'^device-removal/%(device_id)s$' % repls, 'device_removal',
226 name='device-removal'),
227- url(r'^device-rename/(?P<device_id>.+)$', 'device_rename',
228+ url(r'^device-rename/%(device_id)s$' % repls, 'device_rename',
229 name='device-rename'),
230 url(r'^\+device-help$', 'device_help', name='device-help'),
231- url(r'^device-print/(?P<device_id>.+)$', 'device_print',
232- name='device-print'),
233- url(r'^device-generate/(?P<device_id>.+)$', 'device_generate',
234+ url(r'^device-print/%(device_id)s$' % repls, 'device_print',
235+ name='device-print'),
236+ url(r'^%(token)s/device-print/%(device_id)s$' % repls, 'device_print',
237+ name='device-print'),
238+ url(r'^device-generate/%(device_id)s$' % repls, 'device_generate',
239 name='device-generate'),
240+ url(r'^%(token)s/device-generate/%(device_id)s$' % repls,
241+ 'device_generate', name='device-generate'),
242 )
243
244 urlpatterns += patterns(
245
246=== modified file 'src/webui/views/devices.py'
247--- src/webui/views/devices.py 2017-01-26 17:26:43 +0000
248+++ src/webui/views/devices.py 2017-04-24 17:17:28 +0000
249@@ -95,13 +95,14 @@
250 @sso_login_required
251 @require_twofactor_enabled
252 @allow_only('GET')
253-def device_list(request):
254+def device_list(request, token=None):
255 paper_renewals = list(request.user.paper_devices_needing_renewal)
256 context = context_with_current_section(
257 request, device_addition_path=reverse(DEVICE_ADDITION),
258 devices=request.user.devices.all(),
259 need_backup_device_warning=request.user.need_backup_device_warning,
260- paper_devices_needing_renewal=paper_renewals
261+ paper_devices_needing_renewal=paper_renewals,
262+ token=token
263 )
264 return render(request, 'device/list.html', context)
265
266@@ -220,7 +221,7 @@
267 @sso_login_required(require_twofactor=True, require_twofactor_freshness=True)
268 @require_twofactor_enabled
269 @allow_only('GET')
270-def device_print(request, device_id):
271+def device_print(request, device_id, token=None):
272 device = get_object_or_404(
273 AuthenticationDevice, id=device_id, account=request.user,
274 device_type='paper')
275@@ -229,6 +230,8 @@
276 remaining_codes = settings.TWOFACTOR_PAPER_CODES - details.position
277 generation_enabled = (
278 remaining_codes <= settings.TWOFACTOR_PAPER_CODES_ALLOW_GENERATION)
279+ needs_renewal = (
280+ remaining_codes < settings.TWOFACTOR_PAPER_CODES_WARN_RENEWAL)
281
282 if generation_enabled:
283 messages.warning(request, DEVICE_GENERATION_WARNING)
284@@ -239,7 +242,10 @@
285 counter=details.position,
286 device_id=device.id,
287 generation_enabled=generation_enabled,
288+ needs_renewal=needs_renewal,
289 )
290+ if token is not None:
291+ context['token'] = token
292 return TemplateResponse(request, 'device/print-codes.html', context)
293
294
295@@ -258,7 +264,7 @@
296 @sso_login_required(require_twofactor=True, require_twofactor_freshness=True)
297 @require_twofactor_enabled
298 @allow_only('GET', 'POST')
299-def device_generate(request, device_id):
300+def device_generate(request, device_id, token=None):
301 device = get_object_or_404(
302 AuthenticationDevice, id=device_id, account=request.user,
303 device_type='paper')
304@@ -271,12 +277,16 @@
305 request,
306 codes=details.codes,
307 device_id=device.id,
308+ token=token,
309 )
310 return TemplateResponse(request, 'device/generate-codes.html', context)
311
312 device.counter = details.start
313 device.save()
314- return HttpResponseRedirect(reverse('device-print', args=(device.id,)))
315+ kwargs = {'device_id': device.id}
316+ if token:
317+ kwargs['token'] = token
318+ return HttpResponseRedirect(reverse('device-print', kwargs=kwargs))
319
320
321 @sso_login_required(require_twofactor=True, require_twofactor_freshness=True)
322
323=== modified file 'src/webui/views/ui.py'
324--- src/webui/views/ui.py 2017-04-18 20:14:37 +0000
325+++ src/webui/views/ui.py 2017-04-24 17:17:28 +0000
326@@ -347,6 +347,15 @@
327 limitlogin().reset_count(request)
328
329 next_url = request.POST.get('next')
330+ paper_devices = request.user.devices.filter(device_type='paper')
331+ # We only force renewal if all paper devices are at or near exhaustion
332+ needs_renewal = (paper_devices and len(paper_devices) ==
333+ len(list(request.user.paper_devices_needing_renewal)))
334+ if token and needs_renewal:
335+ url = reverse('device-list', kwargs=dict(token=token))
336+ if next_url:
337+ url += "?next=" + urlquote(next_url)
338+ return HttpResponseRedirect(url)
339 if next_url and is_safe_redirect_url(next_url):
340 return HttpResponseRedirect(next_url)
341 elif token: