Merge lp:~michael.nelson/canonical-identity-provider/demo-test into lp:canonical-identity-provider/release

Proposed by Michael Nelson
Status: Rejected
Rejected by: Daniel Manrique
Proposed branch: lp:~michael.nelson/canonical-identity-provider/demo-test
Merge into: lp:canonical-identity-provider/release
Diff against target: 300 lines (+230/-3)
4 files modified
src/webui/forms.py (+72/-0)
src/webui/templates/account/ssh_keys.html (+23/-2)
src/webui/tests/test_forms.py (+113/-0)
src/webui/views/account.py (+22/-1)
To merge this branch: bzr merge lp:~michael.nelson/canonical-identity-provider/demo-test
Reviewer Review Type Date Requested Status
Ubuntu One hackers Pending
Review via email: mp+296275@code.launchpad.net
To post a comment you must log in.

Unmerged revisions

1476. By Michael Nelson

Demo test

1475. By Michael Nelson

Thomi's changes.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'src/webui/forms.py'
--- src/webui/forms.py 2016-05-30 21:15:35 +0000
+++ src/webui/forms.py 2016-06-02 01:29:45 +0000
@@ -2,6 +2,8 @@
2# the GNU Affero General Public License version 3 (see the file LICENSE).2# the GNU Affero General Public License version 3 (see the file LICENSE).
33
4from django import forms4from django import forms
5from django.utils.html import escape
6from django.utils.safestring import mark_safe
5from django.utils.translation import ugettext_lazy as _7from django.utils.translation import ugettext_lazy as _
6from gpgservice_client import (8from gpgservice_client import (
7 GPGServiceException,9 GPGServiceException,
@@ -13,6 +15,8 @@
13from webservices.launchpad import (15from webservices.launchpad import (
14 LaunchpadAPIError,16 LaunchpadAPIError,
15 add_lp_ssh_key,17 add_lp_ssh_key,
18 delete_lp_ssh_key,
19 get_lp_ssh_keys,
16)20)
17from webui.gpg import (21from webui.gpg import (
18 SSOGPGClient,22 SSOGPGClient,
@@ -252,3 +256,71 @@
252 def add_key_to_launchpad(self):256 def add_key_to_launchpad(self):
253 keytext = self.cleaned_data['ssh_key']257 keytext = self.cleaned_data['ssh_key']
254 add_lp_ssh_key(self.account.openid_identifier, keytext, dry_run=False)258 add_lp_ssh_key(self.account.openid_identifier, keytext, dry_run=False)
259
260
261def format_ssh_key(ssh_key):
262 """Decide how to display 'ssh_key'.
263
264 SSH keys are freeform text, so users absolutely *will* try and add keys
265 that contain javascript, so we need to make sure they're escaped when
266 being displayed to the user.
267 """
268 try:
269 key_type, key_text, comment = ssh_key.split(' ', 2)
270 except ValueError:
271 # This should never happen, as launchpad is supposed to guarantee a
272 # valid ssh key format. However, we need to exercise this code path
273 # in tests to ensure that users malicously POST'ing bad key formats
274 # will get a ValidationError.
275 # Just return the escaped data - this will never be run in production:
276 return mark_safe_lazy(
277 '<label>Bad Key Data: <span>{key}</span></label>').format(
278 key=escape(ssh_key))
279 else:
280 #return mark_safe_lazy(
281 # format_html isn't being used here because it's not lazy?
282 return mark_safe(
283 '<label>{key_type_label}<span>{key_type}</span></label> '
284 '<label>{key_text_label}<span>{key_text}</span></label> '
285 '<label>{comment_label}<span>{comment}</span></label> '.format(
286 key_type_label=_("Key Type: "),
287 key_type=escape(key_type),
288 key_text_label=_("Key Text: "),
289 key_text=escape(key_text),
290 comment_label=_("Comment: "),
291 comment=escape(comment)
292 ))
293
294
295from django.forms.widgets import CheckboxChoiceInput
296
297class SSHChoiceInput(CheckboxChoiceInput):
298
299 def __init__(self, *args, **kwargs):
300 super(SSHChoiceInput, self).__init__(*args, **kwargs)
301 self.choice_label = format_ssh_key(self.choice_label)
302
303
304class SSHFieldRenderer(forms.widgets.CheckboxFieldRenderer):
305
306 choice_input_class = SSHChoiceInput
307
308
309class DeleteSSHKeyForm(forms.Form):
310
311 ssh_keys = forms.MultipleChoiceField(
312 required=True,
313 widget=forms.CheckboxSelectMultiple(renderer=SSHFieldRenderer),
314 )
315
316 def __init__(self, account, *args, **kwargs):
317 super(DeleteSSHKeyForm, self).__init__(*args, **kwargs)
318 self.account = account
319 keys = get_lp_ssh_keys(self.account.openid_identifier)
320 choices = [(key, key) for key in keys]
321 self.fields['ssh_keys'].choices = choices
322
323 def delete_keys_from_launchpad(self):
324 for key in self.cleaned_data['ssh_keys']:
325 delete_lp_ssh_key(self.account.openid_identifier, key,
326 dry_run=False)
255327
=== modified file 'src/webui/templates/account/ssh_keys.html'
--- src/webui/templates/account/ssh_keys.html 2016-05-26 00:06:10 +0000
+++ src/webui/templates/account/ssh_keys.html 2016-06-02 01:29:45 +0000
@@ -8,9 +8,30 @@
8{% block title %}{% trans "SSH Keys" %}{% endblock %}8{% block title %}{% trans "SSH Keys" %}{% endblock %}
99
10{% block text_title %}10{% block text_title %}
11 <h1 class="u1-h-main">{% trans "SSH Keys" %}</h1>11 <h1 class="u1-h-main">{% trans "Change your SSH keys" %}</h1>
12{% endblock %}12{% endblock %}
1313
14{% block content %}14{% block content %}
15<p>Coming soon!</p>15{% if delete_key_form.fields.ssh_keys.choices %}
16 <b>Delete SSH Keys:</b>
17 <form action="" method="POST">
18 {% csrf_token %}
19 {{ delete_key_form.non_field_errors }}
20 {{ delete_key_form.fields.ssh_keys.errors }}
21 <ul>
22 {% for key_choice_id, key_choice_label in delete_key_form.fields.ssh_keys.choices %}
23 <li>{{key_choice_id}}</li>
24 {% endfor %}
25 </ul>
26 {{ delete_key_form }}
27 <input class="cta" type="submit" name="delete_key" value="{% trans "Delete Selected Keys" %}"/>
28 </form>
29{% endif %}
30
31<b>Import new SSH key</b>
32<form action="" method="POST">
33 {% csrf_token %}
34 {{ import_key_form }}
35 <input class="cta" type="submit" name="import_key" value="{% trans "Import SSH key" %}"/>
36</form>
16{% endblock %}37{% endblock %}
1738
=== modified file 'src/webui/tests/test_forms.py'
--- src/webui/tests/test_forms.py 2016-05-30 21:15:35 +0000
+++ src/webui/tests/test_forms.py 2016-06-02 01:29:45 +0000
@@ -20,10 +20,12 @@
20from webui.forms import (20from webui.forms import (
21 ClaimOpenPGPKeyForm,21 ClaimOpenPGPKeyForm,
22 ClaimSSHKeyForm,22 ClaimSSHKeyForm,
23 DeleteSSHKeyForm,
23 DisabledGPGKeysForm,24 DisabledGPGKeysForm,
24 EnabledGPGKeysForm,25 EnabledGPGKeysForm,
25 PendingGPGValidationTokenForm,26 PendingGPGValidationTokenForm,
26 VerifySignedTextForm,27 VerifySignedTextForm,
28 format_ssh_key,
27)29)
28from webui.tests.gpg_fixture import SSOGPGServiceFixture30from webui.tests.gpg_fixture import SSOGPGServiceFixture
29from webui.views import gpg_messages31from webui.views import gpg_messages
@@ -672,3 +674,114 @@
672 account.openid_identifier,674 account.openid_identifier,
673 'ssh-rsa foo bar',675 'ssh-rsa foo bar',
674 dry_run=False)676 dry_run=False)
677
678
679class FormatSSHKeyTestCase(SSOBaseTestCase):
680
681 def test_escapes_xss(self):
682 bad_content = '<script>window.alert("boo!");</script>'
683 bad_key = ' '.join((bad_content,) * 3)
684 formatted_key = format_ssh_key(bad_key)
685 self.assertNotIn(bad_content, formatted_key)
686 self.assertIn(
687 '&lt;script&gt;window.alert(&quot;boo!&quot;);&lt;/script&gt;',
688 formatted_key
689 )
690
691
692class DeleteSSHKeyFormTestCase(SSOBaseTestCase):
693
694 SAMPLE_RSA_KEY = 'ssh-rsa keytext_goes_here comment goes here'
695 SAMPLE_DSA_KEY = 'ssh-dss more_keytext_here this is a comment'
696
697 def setUp(self):
698 super(DeleteSSHKeyFormTestCase, self).setUp()
699 self.account = self.factory.make_account()
700
701 def patch_lp_delete_key(self, raise_exception=None):
702 """Patch the 'delete_lp_ssh_key', creating realistic side effects.
703
704 :param raise_exception: If specified, must be one of NoSuchAccount,
705 SSHKeyAdditionTypeError or SSHKeyAdditionDataError Exceptions will
706 be raised with the same message as if they'd been returned from
707 launchpad.
708 """
709 exception_messages = {
710 NoSuchAccount: "No account found for openid identifier '{openid}'",
711 SSHKeyAdditionTypeError: "Invalid SSH key type: '{sshkeytype}'",
712 SSHKeyAdditionDataError: "Invalid SSH key data: '{sshkey}'",
713 }
714
715 def _side_effect(openid_id, key_text, dry_run):
716 key_type = key_text.split(' ')[0]
717 message = exception_messages[raise_exception].format(
718 openid=openid_id,
719 sshkeytype=key_type,
720 sshkey=key_text,
721 )
722 raise raise_exception(400, message)
723
724 mock = self.patch('webui.forms.delete_lp_ssh_key')
725 if raise_exception is not None:
726 mock.side_effect = _side_effect
727 return mock
728
729 def patch_get_keys(self, returned_keys=[]):
730 mock = self.patch('webui.forms.get_lp_ssh_keys')
731 mock.return_value = returned_keys
732 return mock
733
734 def assert_form_field_has_error(self, form, field, expected_error):
735 self.assertIn(field, form.errors)
736 self.assertEqual([expected_error], form.errors[field])
737
738 def test_form_widget_with_labels(self):
739 self.patch_get_keys([self.SAMPLE_RSA_KEY, self.SAMPLE_DSA_KEY])
740 form = DeleteSSHKeyForm(self.account)
741
742
743 html = form.as_ul()
744
745 self.assertIn('<label>Key Type', html)
746
747 def test_form_passes_account_id_to_get_lp_keys(self):
748 mock_get_keys = self.patch_get_keys()
749 DeleteSSHKeyForm(self.account)
750
751 mock_get_keys.assert_called_once_with(self.account.openid_identifier)
752
753 def test_form_with_no_keys(self):
754 self.patch_get_keys()
755 form = DeleteSSHKeyForm(self.account)
756
757 self.assertEqual(0, len(form.fields['ssh_keys'].choices))
758
759 def test_form_multiple_keys(self):
760 self.patch_get_keys([self.SAMPLE_RSA_KEY, self.SAMPLE_DSA_KEY])
761 form = DeleteSSHKeyForm(self.account)
762
763 self.assertEqual(2, len(form.fields['ssh_keys'].choices))
764 rsa_choice, dsa_choice = form.fields['ssh_keys'].choices
765 self.assertEqual(
766 (self.SAMPLE_RSA_KEY, format_ssh_key(self.SAMPLE_RSA_KEY)),
767 rsa_choice,
768 )
769 self.assertEqual(
770 (self.SAMPLE_DSA_KEY, format_ssh_key(self.SAMPLE_DSA_KEY)),
771 dsa_choice,
772 )
773
774 def test_form_deletes_ssh_key(self):
775 self.patch_get_keys([self.SAMPLE_RSA_KEY])
776 form = DeleteSSHKeyForm(
777 self.account, {'ssh_keys': [self.SAMPLE_RSA_KEY]})
778
779 mock_lp_delete = self.patch_lp_delete_key()
780 self.assertTrue(form.is_valid())
781 mock_lp_delete.reset_mock()
782 form.delete_keys_from_launchpad()
783
784 mock_lp_delete.assert_called_once_with(
785 self.account.openid_identifier,
786 self.SAMPLE_RSA_KEY,
787 dry_run=False)
675788
=== modified file 'src/webui/views/account.py'
--- src/webui/views/account.py 2016-05-26 00:06:10 +0000
+++ src/webui/views/account.py 2016-06-02 01:29:45 +0000
@@ -64,6 +64,8 @@
64from webui.decorators import check_readonly, sso_login_required64from webui.decorators import check_readonly, sso_login_required
65from webui.forms import (65from webui.forms import (
66 ClaimOpenPGPKeyForm,66 ClaimOpenPGPKeyForm,
67 ClaimSSHKeyForm,
68 DeleteSSHKeyForm,
67 DisabledGPGKeysForm,69 DisabledGPGKeysForm,
68 EnabledGPGKeysForm,70 EnabledGPGKeysForm,
69 PendingGPGValidationTokenForm,71 PendingGPGValidationTokenForm,
@@ -636,4 +638,23 @@
636@sso_login_required638@sso_login_required
637@check_readonly639@check_readonly
638def ssh_keys(request):640def ssh_keys(request):
639 return render(request, 'account/ssh_keys.html', {})641
642 if 'delete_keys' in request.POST:
643 delete_keys_form = DeleteSSHKeyForm(request.user, request.POST)
644 if delete_keys_form.is_valid():
645 delete_keys_form.delete_keys_from_launchpad()
646 else:
647 delete_keys_form = DeleteSSHKeyForm(request.user)
648
649 if 'import_key' in request.POST:
650 import_key_form = ClaimSSHKeyForm(request.user, request.POST)
651 if import_key_form.is_valid():
652 import_key_form.add_key_to_launchpad()
653 else:
654 import_key_form = ClaimSSHKeyForm(request.user)
655
656 context = {
657 'delete_key_form': delete_keys_form,
658 'import_key_form': import_key_form,
659 }
660 return render(request, 'account/ssh_keys.html', context)