← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~rvb/maas/maas-ssh-keys-ui into lp:maas

 

Raphaël Badin has proposed merging lp:~rvb/maas/maas-ssh-keys-ui into lp:maas with lp:~rvb/maas/maas-ssh-keys-models as a prerequisite.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)

For more details, see:
https://code.launchpad.net/~rvb/maas/maas-ssh-keys-ui/+merge/99769

This branch adds the UI to display and manage one's SSH public keys.  I've followed Huw's mockups for the UI.
-- 
https://code.launchpad.net/~rvb/maas/maas-ssh-keys-ui/+merge/99769
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~rvb/maas/maas-ssh-keys-ui into lp:maas.
=== modified file 'src/maasserver/forms.py'
--- src/maasserver/forms.py	2012-03-28 06:21:37 +0000
+++ src/maasserver/forms.py	2012-03-28 16:16:22 +0000
@@ -15,6 +15,7 @@
     "NodeForm",
     "MACAddressForm",
     "MAASAndNetworkForm",
+    "SSHKeyForm",
     "UbuntuForm",
     "UIAdminNodeEditForm",
     "UINodeEditForm",
@@ -42,6 +43,7 @@
     Node,
     NODE_AFTER_COMMISSIONING_ACTION,
     NODE_AFTER_COMMISSIONING_ACTION_CHOICES,
+    SSHKey,
     UserProfile,
     )
 
@@ -112,7 +114,27 @@
         model = MACAddress
 
 
+class SSHKeyForm(ModelForm):
+    key = forms.CharField(
+        label="Public key",
+        widget=forms.Textarea(attrs={'rows': '5', 'cols': '30'}),
+        required=True)
+
+    class Meta:
+        model = SSHKey
+
+    def __init__(self, user, *args, **kwargs):
+        super(SSHKeyForm, self).__init__(*args, **kwargs)
+        self.user = user
+
+    def save(self):
+        key = super(SSHKeyForm, self).save(commit=False)
+        key.user = self.user
+        return key.save()
+
+
 class MultipleMACAddressField(forms.MultiValueField):
+
     def __init__(self, nb_macs=1, *args, **kwargs):
         fields = [MACAddressFormField() for i in range(nb_macs)]
         super(MultipleMACAddressField, self).__init__(fields, *args, **kwargs)

=== modified file 'src/maasserver/templates/maasserver/prefs.html'
--- src/maasserver/templates/maasserver/prefs.html	2012-03-15 13:58:32 +0000
+++ src/maasserver/templates/maasserver/prefs.html	2012-03-28 16:16:22 +0000
@@ -18,9 +18,10 @@
 
 {% block content %}
   <div id="prefs">
-    <div id="api" class="block size7 first">
+    <div id="keys" class="block size7 first">
       <h2>Keys</h2>
       <h3>MAAS keys</h3>
+      <div id="api">
       <ul class="list">
         {% for token in user.get_profile.get_authorisation_tokens %}
           <li class="bundle">
@@ -28,15 +29,36 @@
               <img title="Delete MAAS key"
                    src="{{ STATIC_URL }}img/delete.png" />
             </a>
-            <input type="text" 
-                   value="{{ token.consumer.key }}:{{ token.key }}:{{ token.secret }}" 
+            <input type="text"
+                   value="{{ token.consumer.key }}:{{ token.key }}:{{ token.secret }}"
                    id="{{ token.key }}"
                    class="disabled" />
           </li>
         {% endfor %}
       </ul>
       <p id="token_creation_placeholder" />
+      </div>
+      <div class="clear"></div>
+      <div id="ssh-keys">
+      <h3>SSH keys</h3>
+      <ul class="list">
+        {% for key in user.sshkey_set.all %}
+          <li>
+          <a href="{% url 'prefs-delete-sshkey' key.id %}" class="icon right">
+              <img title="Delete SSH key"
+                   src="{{ STATIC_URL }}img/delete.png" />
+            </a>
+            {{ key.display_html }}
+          </li>
+	{% empty %}
+	  No SSH key configured.
+        {% endfor %}
+      </ul>
+      <a href="{% url 'prefs-add-sshkey' %}"
+         id="add_ssh_key"
+         class="button right">+ Add SSH key</a>
     </div>
+    <div class="clear"></div>
     <div id="profile" class="block size7 first">
       <h2>User details</h2>
       <form action="." method="post">

=== added file 'src/maasserver/templates/maasserver/prefs_add_sshkey.html'
--- src/maasserver/templates/maasserver/prefs_add_sshkey.html	1970-01-01 00:00:00 +0000
+++ src/maasserver/templates/maasserver/prefs_add_sshkey.html	2012-03-28 16:16:22 +0000
@@ -0,0 +1,16 @@
+{% extends "maasserver/base.html" %}
+
+{% block title %}Add SSH key{% endblock %}
+{% block page-title %}Add SSH key{% endblock %}
+
+{% block content %}
+  <form action="." method="post">
+    <ul>
+    {% for field in form %}
+      {% include "maasserver/form_field.html" %}
+    {% endfor %}
+    </ul>
+    <input type="submit" value="Add key" />
+      &nbsp;&nbsp;<a class="link-button" href="{% url 'prefs' %}">Cancel</a>
+  </form>
+{% endblock %}

=== added file 'src/maasserver/templates/maasserver/prefs_confirm_delete_sshkey.html'
--- src/maasserver/templates/maasserver/prefs_confirm_delete_sshkey.html	1970-01-01 00:00:00 +0000
+++ src/maasserver/templates/maasserver/prefs_confirm_delete_sshkey.html	2012-03-28 16:16:22 +0000
@@ -0,0 +1,22 @@
+{% extends "maasserver/base.html" %}
+
+{% block title %}Delete SSH key{% endblock %}
+{% block page-title %}Delete SSH key{% endblock %}
+
+{% block content %}
+    <div class="block auto-width">
+        <h2>
+          Are you sure you want to delete the following key?<br />
+        </h2>
+	<p style="word-wrap: break-word; width: 700px;">{{ key }}</p>
+        <p>This action is permanent and can not be undone.</p>
+        <p>
+          <form action="." method="post">
+            <input type="hidden" name="post" value="yes" />
+            <input type="submit" value="Delete key" class="right" />
+            <a href="{% url 'prefs' %}">Cancel</a>
+          </form>
+        </p>
+    </div>
+{% endblock %}
+

=== modified file 'src/maasserver/testing/__init__.py'
--- src/maasserver/testing/__init__.py	2012-03-21 05:38:18 +0000
+++ src/maasserver/testing/__init__.py	2012-03-28 16:16:22 +0000
@@ -10,11 +10,13 @@
 
 __metaclass__ = type
 __all__ = [
+    "get_data",
     "get_fake_provisioning_api_proxy",
     "reload_object",
     "reload_objects",
     ]
 
+import os
 from uuid import uuid1
 
 from provisioningserver.testing import fakeapi
@@ -93,3 +95,13 @@
     assert all(isinstance(obj, model_class) for obj in model_objects)
     return model_class.objects.filter(
         id__in=[obj.id for obj in model_objects])
+
+
+def get_data(filename):
+    """Utility method to read the content of files in
+    src/maasserver/tests.
+
+    Usually used to read files in src/maasserver/tests/data."""
+    path = os.path.join(
+        os.path.dirname(os.path.abspath(__file__)), '..', 'tests', filename)
+    return file(path).read()

=== modified file 'src/maasserver/testing/factory.py'
--- src/maasserver/testing/factory.py	2012-03-24 01:07:19 +0000
+++ src/maasserver/testing/factory.py	2012-03-28 16:16:22 +0000
@@ -26,6 +26,7 @@
     NODE_STATUS,
     SSHKey,
     )
+from maasserver.testing import get_data
 from maasserver.testing.enum import map_enum
 import maastesting.factory
 
@@ -83,6 +84,12 @@
         return User.objects.create_user(
             username=username, password=password, email=email)
 
+    def make_sshkey(self, user):
+        key_string = get_data('data/test_rsa.pub')
+        key = SSHKey(key=key_string, user=user)
+        key.save()
+        return key
+
     def make_user_with_keys(self, n_keys=2, **kwargs):
         """Create a user with n `SSHKey`.
 

=== modified file 'src/maasserver/tests/test_models.py'
--- src/maasserver/tests/test_models.py	2012-03-28 16:16:22 +0000
+++ src/maasserver/tests/test_models.py	2012-03-28 16:16:22 +0000
@@ -43,6 +43,7 @@
     validate_ssh_public_key,
     )
 from maasserver.provisioning import get_provisioning_api_proxy
+from maasserver.testing import get_data
 from maasserver.testing.enum import map_enum
 from maasserver.testing.factory import factory
 from maasserver.testing.testcase import TestCase
@@ -521,16 +522,6 @@
         self.assertTrue(set(SYSTEM_USERS).isdisjoint(usernames))
 
 
-def get_data(filename):
-    """Utility method to read the content of files in
-    src/maasserver/tests.
-
-    Usually used to read files in src/maasserver/tests/data."""
-    path = os.path.join(
-        os.path.dirname(os.path.abspath(__file__)), filename)
-    return file(path).read()
-
-
 class SSHKeyValidatorTest(TestCase):
 
     def test_validates_rsa_public_key(self):

=== modified file 'src/maasserver/tests/test_views.py'
--- src/maasserver/tests/test_views.py	2012-03-28 06:21:37 +0000
+++ src/maasserver/tests/test_views.py	2012-03-28 16:16:22 +0000
@@ -31,9 +31,13 @@
     Config,
     NODE_AFTER_COMMISSIONING_ACTION,
     POWER_TYPE_CHOICES,
+    SSHKey,
     UserProfile,
     )
-from maasserver.testing import reload_object
+from maasserver.testing import (
+    get_data,
+    reload_object,
+    )
 from maasserver.testing.factory import factory
 from maasserver.testing.testcase import (
     LoggedInTestCase,
@@ -363,6 +367,87 @@
         # The password is SHA1ized, we just make sure that it has changed.
         self.assertNotEqual(old_pw, user.password)
 
+    def test_prefs_displays_message_when_no_public_keys_are_configured(self):
+        response = self.client.get('/account/prefs/')
+        self.assertIn("No SSH key configured.", response.content)
+
+    def test_prefs_displays_add_ssh_key_button(self):
+        response = self.client.get('/account/prefs/')
+        add_key_link = reverse('prefs-add-sshkey')
+        self.assertIn(add_key_link, get_content_links(response))
+
+    def create_keys_for_user(self, user):
+        return [factory.make_sshkey(self.logged_in_user) for i in range(3)]
+
+    def test_prefs_displays_compact_representation_of_users_keys(self):
+        keys = self.create_keys_for_user(self.logged_in_user)
+        response = self.client.get('/account/prefs/')
+        for key in keys:
+            self.assertIn(key.display_html(), response.content)
+
+    def test_prefs_displays_link_to_delete_ssh_keys(self):
+        keys = self.create_keys_for_user(self.logged_in_user)
+        response = self.client.get('/account/prefs/')
+        links = get_content_links(response)
+        for key in keys:
+            del_key_link = reverse('prefs-delete-sshkey', args=[key.id])
+            self.assertIn(del_key_link, links)
+
+
+class KeyManagementTest(LoggedInTestCase):
+
+    def test_add_key_GET(self):
+        # The 'Add key' page displays a form to add a key.
+        response = self.client.get(reverse('prefs-add-sshkey'))
+        doc = fromstring(response.content)
+
+        self.assertEqual(1, len(doc.cssselect('textarea#id_key')))
+        # The page features a form that submits to itself.
+        self.assertSequenceEqual(
+            ['.'],
+            [elem.get('action').strip() for elem in doc.cssselect(
+                '#content form')])
+
+    def test_add_key_POST_adds_key(self):
+        key_string = get_data('data/test_rsa.pub')
+        response = self.client.post(
+            reverse('prefs-add-sshkey'), {'key': key_string})
+
+        self.assertEqual(httplib.FOUND, response.status_code)
+        self.assertTrue(SSHKey.objects.filter(key=key_string).exists())
+
+    def test_delete_key_GET(self):
+        # The 'Delete key' page displays a confirmation page with a form.
+        key = factory.make_sshkey(self.logged_in_user)
+        del_link = reverse('prefs-delete-sshkey', args=[key.id])
+        response = self.client.get(del_link)
+        doc = fromstring(response.content)
+
+        self.assertIn(
+            "Are you sure you want to delete the following key?",
+            response.content)
+        # The page features a form that submits to itself.
+        self.assertSequenceEqual(
+            ['.'],
+            [elem.get('action').strip() for elem in doc.cssselect(
+                '#content form')])
+
+    def test_delete_key_GET_cannot_access_someoneelses_key(self):
+        key = factory.make_sshkey(factory.make_user())
+        del_link = reverse('prefs-delete-sshkey', args=[key.id])
+        response = self.client.get(del_link)
+
+        self.assertEqual(httplib.NOT_FOUND, response.status_code)
+
+    def test_delete_key_POST(self):
+        # A POST request deletes the key.
+        key = factory.make_sshkey(self.logged_in_user)
+        del_link = reverse('prefs-delete-sshkey', args=[key.id])
+        response = self.client.post(del_link, {'post': 'yes'})
+
+        self.assertEqual(httplib.FOUND, response.status_code)
+        self.assertFalse(SSHKey.objects.filter(id=key.id).exists())
+
 
 class AdminLoggedInTestCase(LoggedInTestCase):
 

=== modified file 'src/maasserver/urls.py'
--- src/maasserver/urls.py	2012-03-24 00:11:31 +0000
+++ src/maasserver/urls.py	2012-03-28 16:16:22 +0000
@@ -41,6 +41,8 @@
     proxy_to_longpoll,
     settings,
     settings_add_archive,
+    SSHKeyCreateView,
+    SSHKeyDeleteView,
     userprefsview,
     )
 
@@ -68,6 +70,12 @@
 # URLs for logged-in users.
 urlpatterns += patterns('maasserver.views',
     url(r'^account/prefs/$', userprefsview, name='prefs'),
+    url(
+        r'^account/prefs/sshkey/add/$', SSHKeyCreateView.as_view(),
+        name='prefs-add-sshkey'),
+    url(
+        r'^account/prefs/sshkey/delete/(?P<keyid>\d*)/$',
+        SSHKeyDeleteView.as_view(), name='prefs-delete-sshkey'),
     url(r'^accounts/logout/$', logout, name='logout'),
     url(
         r'^$',

=== modified file 'src/maasserver/views.py'
--- src/maasserver/views.py	2012-03-23 19:20:59 +0000
+++ src/maasserver/views.py	2012-03-28 16:16:22 +0000
@@ -10,10 +10,21 @@
 
 __metaclass__ = type
 __all__ = [
+    "AccountsAdd",
+    "AccountsDelete",
+    "AccountsEdit",
+    "AccountsView",
+    "combo_view",
+    "login",
     "logout",
     "NodeListView",
     "NodesCreateView",
     "NodeView",
+    "NodeEdit",
+    "settings",
+    "settings_add_archive",
+    "SSHKeyCreateView",
+    "SSHKeyDeleteView",
     ]
 
 from logging import getLogger
@@ -64,6 +75,7 @@
     MAASAndNetworkForm,
     NewUserCreationForm,
     ProfileForm,
+    SSHKeyForm,
     UbuntuForm,
     UIAdminNodeEditForm,
     UINodeEditForm,
@@ -71,6 +83,7 @@
 from maasserver.messages import messaging
 from maasserver.models import (
     Node,
+    SSHKey,
     UserProfile,
     )
 
@@ -162,6 +175,41 @@
         return reverse('index')
 
 
+class SSHKeyCreateView(CreateView):
+
+    form_class = SSHKeyForm
+    template_name = 'maasserver/prefs_add_sshkey.html'
+
+    def get_form_kwargs(self):
+        kwargs = super(SSHKeyCreateView, self).get_form_kwargs()
+        kwargs['user'] = self.request.user
+        return kwargs
+
+    def form_valid(self, form):
+        messages.info(self.request, "SSH key added.")
+        return super(SSHKeyCreateView, self).form_valid(form)
+
+    def get_success_url(self):
+        return reverse('prefs')
+
+
+class SSHKeyDeleteView(DeleteView):
+
+    template_name = 'maasserver/prefs_confirm_delete_sshkey.html'
+    context_object_name = 'key'
+
+    def get_object(self):
+        keyid = self.kwargs.get('keyid', None)
+        return get_object_or_404(SSHKey, user=self.request.user, id=keyid)
+
+    def form_valid(self, form):
+        messages.info(self.request, "SSH key deleted.")
+        return super(SSHKeyDeleteView, self).form_valid(form)
+
+    def get_success_url(self):
+        return reverse('prefs')
+
+
 def process_form(request, form_class, redirect_url, prefix,
                  success_message=None, form_kwargs=None):
     """Utility method to process subforms (i.e. forms with a prefix).