launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #06918
[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" />
+ <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).