Mercurial > hg > savane-forge
changeset 209:34de8b88da36
Rework/clean-up 'my'
author | Sylvain Beucler <beuc@beuc.net> |
---|---|
date | Sat, 31 Jul 2010 13:22:48 +0200 |
parents | cc3105877a0e |
children | 064c3d182f7f |
files | savane/my/forms.py savane/my/tests.py savane/my/urls.py savane/my/views.py savane/svmain/models.py savane/utils/__init__.py templates/my/index.html templates/my/ssh.html templates/my/ssh_gpg.html |
diffstat | 9 files changed, 220 insertions(+), 181 deletions(-) [+] |
line wrap: on
line diff
new file mode 100644 --- /dev/null +++ b/savane/my/forms.py @@ -0,0 +1,69 @@ +# Manage user attributes +# Copyright (C) 2009 Sylvain Beucler +# Copyright (C) 2009 Jonathan Gonzalez V. +# +# This file is part of Savane. +# +# Savane is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# Savane is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +from django import forms +from django.utils.translation import ugettext, ugettext_lazy as _ +from savane.utils import * + +class MailForm( forms.Form ): + email = forms.EmailField(required=True) + action = forms.CharField( widget=forms.HiddenInput, required=True, initial='update_mail' ) + +class IdentityForm(forms.Form): + first_name = forms.CharField(required = True) + last_name = forms.CharField(required = False) + gpg_key = forms.CharField(widget=forms.Textarea(attrs={'cols':'70','rows':'15'}), required=False, + help_text=_("You can write down here your (ASCII) public key (gpg --export --armor keyid)")) + action = forms.CharField(widget=forms.HiddenInput, required=True, initial='update_identity') + +class SSHForm(forms.Form): + key_file = forms.FileField(required=False, help_text=_("Be sure to upload the file ending with .pub")) + key = forms.CharField(widget=forms.TextInput(attrs={'size':'60'}), required=False) + + def clean_key(self): + ssh_key = self.cleaned_data['key'] + + # String is not mandatory + if len(ssh_key) == 0: + return None + + try: + ssh_key_fingerprint(ssh_key) + except Exception as e: + raise forms.ValidationError(_("The uploaded string is not a public key file: %s") % e) + return ssh_key + + def clean_key_file(self): + ssh_key_file = self.cleaned_data['key_file'] + + # File is not mandatory + if ssh_key_file is None: + return None + + # Avoid large file attacks + if ssh_key_file.size > 100*1024: + return None + + ssh_key = ssh_key_file.read() + try: + ssh_key_fingerprint(ssh_key) + except Exception as e: + raise forms.ValidationError(_("The uploaded file is not a public key file: %s") % e) + + return ssh_key_file
--- a/savane/my/tests.py +++ b/savane/my/tests.py @@ -21,6 +21,13 @@ from django.core.urlresolvers import reverse import django.contrib.auth.models as auth_models import re +import tempfile +from savane.utils import ssh_key_fingerprint + +__test__ = {"doctest": """ +>>> ssh_key_fingerprint("ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDbYV67YG54OX3/c7GNIG7zS1sSF3ddhCwhodEGpcbkQs4QOi7gcZHopyjBqvhyJB5fu76odOqI9KngW5IfpPX4lK/3kZZ8QISiF6nekB8wbi49hlB9K8j7NZ7rBTIsKApVNFqd4vriE9m7842soBOc6/sYSEemHxjA7+d+qbkV8j5wuo1QH0ynA5jPMI8RHhTtUBEZIJK2AFUB42bx2XFakhSh5K2DAfZyZ2dKeRkKRRbFzr0eAvbyCPKT93seWAFypETiomKbjMBRvMJyfpTcx4legzs9oGfeLHIb3V0oyM3ysXdqkwoOwO43qCcG/lDFvonzBGlDKh/T07kVXdLh") +'2048 6e:57:73:c6:92:16:62:b8:cc:ed:01:3f:17:95:24:51 (RSA)\n' +"""} class SimpleTest(TestCase): #fixtures = [ @@ -36,12 +43,32 @@ auth_models.User.objects.create_user(username='test', email='test@test.tld', password='test') self.assertTrue(self.client.login(username='test', password='test')) + # Contact info response = self.client.get(reverse('savane:my:conf')) self.assertEqual(response.status_code, 200) response = self.client.post(reverse('savane:my:conf'), - {'action': 'update_identity', 'name': 'Lambda', 'last_name': 'Visitor'}) - self.assertEqual(response.status_code, 200) + {'action': 'update_identity', 'first_name': 'Lambda', 'last_name': 'Visitor'}) + self.assertEqual(response.status_code, 302) response = self.client.post(reverse('savane:my:conf'), - {'action': 'update_identity', 'name': '', 'last_name': 'Visitor'}) - self.assertFormError(response, 'form_identity', 'name', u'This field is required.') + {'action': 'update_identity', 'first_name': '', 'last_name': 'Visitor'}) + self.assertFormError(response, 'form_identity', 'first_name', u'This field is required.') + + # SSH keys + # - string + response = self.client.get(reverse('savane:my:ssh')) + self.assertEqual(response.status_code, 200) + response = self.client.post(reverse('savane:my:ssh'), + {'key': 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDbYV67YG54OX3/c7GNIG7zS1sSF3ddhCwhodEGpcbkQs4QOi7gcZHopyjBqvhyJB5fu76odOqI9KngW5IfpPX4lK/3kZZ8QISiF6nekB8wbi49hlB9K8j7NZ7rBTIsKApVNFqd4vriE9m7842soBOc6/sYSEemHxjA7+d+qbkV8j5wuo1QH0ynA5jPMI8RHhTtUBEZIJK2AFUB42bx2XFakhSh5K2DAfZyZ2dKeRkKRRbFzr0eAvbyCPKT93seWAFypETiomKbjMBRvMJyfpTcx4legzs9oGfeLHIb3V0oyM3ysXdqkwoOwO43qCcG/lDFvonzBGlDKh/T07kVXdLh'}) + self.assertEqual(response.status_code, 302) + response = self.client.post(reverse('savane:my:ssh'), {'key': 'ssh-rsa AAAABBM= me@myhost'}) + self.assertFormError(response, 'form', 'key', u'The uploaded string is not a public key file: ' + + 'SSH error: is not a public key file.\n') + + # - file upload + f = tempfile.TemporaryFile() + f.write('ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDbYV67YG54OX3/c7GNIG7zS1sSF3ddhCwhodEGpcbkQs4QOi7gcZHopyjBqvhyJB5fu76odOqI9KngW5IfpPX4lK/3kZZ8QISiF6nekB8wbi49hlB9K8j7NZ7rBTIsKApVNFqd4vriE9m7842soBOc6/sYSEemHxjA7+d+qbkV8j5wuo1QH0ynA5jPMI8RHhTtUBEZIJK2AFUB42bx2XFakhSh5K2DAfZyZ2dKeRkKRRbFzr0eAvbyCPKT93seWAFypETiomKbjMBRvMJyfpTcx4legzs9oGfeLHIb3V0oyM3ysXdqkwoOwO43qCcG/lDFvonzBGlDKh/T07kVXdLh') + f.flush() + f.seek(0) + response = self.client.post(reverse('savane:my:ssh'), {'key_file': f}) + self.assertEqual(response.status_code, 302)
--- a/savane/my/urls.py +++ b/savane/my/urls.py @@ -26,39 +26,40 @@ import django.contrib.auth.models as auth_models from savane.my.filters import * from savane.django_utils import decorated_patterns +from django.utils.translation import ugettext, ugettext_lazy as _ urlpatterns = patterns ('',) urlpatterns += decorated_patterns ('', login_required, url(r'^$', direct_to_template, { 'template' : 'my/index.html', - 'extra_context' : { 'title' : 'My account', }, }, + 'extra_context' : { 'title' : _("My account configuration"), }, }, name='index'), url('^conf/$', views.conf, - { 'extra_context' : {'title' : 'Contact info', }, }, + { 'extra_context' : {'title' : _("Contact info"), }, }, name='conf'), url('^conf/resume_skills/$', views.resume_skills, - { 'extra_context' : {'title' : 'Resume & skills', } }, + { 'extra_context' : {'title' : _("Edit your resume & skills"), } }, name='resume_skills'), - url('^conf/ssh_gpg/$', views.ssh_gpg, - { 'extra_context' : {'title' : 'SSH & GPG', } }, - name='ssh_gpg'), - url('^conf/ssh_gpg/delete/$', views.ssh_delete, + url('^conf/ssh/$', views.ssh, + { 'extra_context' : {'title' : _("Change authorized keys"), } }, + name='ssh'), + url('^conf/ssh/delete/$', views.ssh_delete, name='ssh_delete'), url('^i18n/$', direct_to_template, { 'template' : 'my/i18n.html', - 'extra_context' : {'title' : 'Language', } }, + 'extra_context' : {'title' : _("Language"), } }, name='i18n'), # TODO: set_lang only lasts for the user's session url('^i18n/', include('django.conf.urls.i18n')), url(r'^groups/$', only_mine(object_list), { 'queryset' : auth_models.Group.objects.all(), - 'extra_context' : { 'title' : "My groups", }, + 'extra_context' : { 'title' : _("My groups"), }, 'template_name' : 'svmain/group_list.html', }, name='group_list'), url(r'^memberships/$', only_mine(object_list), { 'queryset' : svmain_models.Membership.objects.all(), - 'extra_context' : { 'title' : "My memberships", }, + 'extra_context' : { 'title' : _("My memberships"), }, }, name='membership_list'), )
--- a/savane/my/views.py +++ b/savane/my/views.py @@ -22,17 +22,16 @@ from django.http import HttpResponseRedirect from django.contrib.auth import authenticate, login, logout from django.contrib.auth.decorators import login_required -from django import forms from django.contrib import messages -from django.utils.translation import ugettext as _ +from django.utils.translation import ugettext as _, ungettext from savane.svmain.models import SvUserInfo, SshKey -from savane.utils import * +from savane.my.forms import * from annoying.decorators import render_to @login_required() def conf(request, extra_context={}): form_mail = MailForm(initial={'email' : request.user.email}) - form_identity = IdentityForm(initial={'name' : request.user.first_name, + form_identity = IdentityForm(initial={'first_name' : request.user.first_name, 'last_name' : request.user.last_name}) form = None @@ -50,12 +49,14 @@ new_email = request.POST['email'] request.user.email = new_email request.user.save() - messages.success(request, u"The E-Mail address was succesfully updated. New E-Mail address is <%s>" % new_email) + messages.success(request, _("The e-mail address was successfully updated. New e-mail address is <%s>") % new_email) + return HttpResponseRedirect("") # reload elif action == 'update_identity': - request.user.first_name = request.POST['name'] + request.user.first_name = request.POST['first_name'] request.user.last_name = request.POST['last_name'] request.user.save() - messages.success(request, u"Personal information changed.") + messages.success(request, _("Personal information changed.")) + return HttpResponseRedirect("") # reload context = { 'form_mail' : form_mail, 'form_identity' : form_identity, @@ -72,73 +73,62 @@ context_instance=RequestContext(request)) @login_required() -def ssh_gpg(request, extra_context={}): +def ssh(request, extra_context={}): info = request.user.svuserinfo error_msg = None success_msg = None - form_ssh = SSHForm() - form_gpg = GPGForm(initial={'gpg_key' : info.gpg_key}) + form = SSHForm() ssh_keys = None if request.method == 'POST': form = None - action = request.POST['action'] - if action == 'add_ssh': - form_ssh = SSHForm(request.POST, request.FILES) - form = form_ssh - elif action == 'update_gpg': - form_gpg = GPGForm(request.POST) - form = form_gpg + form = SSHForm(request.POST, request.FILES) if form is not None and form.is_valid(): - if action == 'add_ssh': - if 'key' in request.POST: - key = request.POST['key'].strip() - if len(key) > 0: - ssh_key = SshKey(ssh_key=key) - request.user.sshkey_set.add(ssh_key) - success_msg = 'Authorized keys stored.' + keys_saved = 0 - if 'key_file' in request.FILES: - ssh_key_file = request.FILES['key_file'] - if ssh_key_file is not None: - key = '' - for chunk in ssh_key_file.chunks(): - key = key + chunk + if 'key' in request.POST: + key = request.POST['key'].strip() + if len(key) > 0: + ssh_key = SshKey(ssh_key=key) + request.user.sshkey_set.add(ssh_key) + keys_saved += 1 - if len(key) > 0: - ssh_key = SshKey(ssh_key=key) - request.user.sshkey_set.add(ssh_key) - success_msg = 'Authorized keys stored.' - - form_ssh = SSHForm() + if 'key_file' in request.FILES: + ssh_key_file = request.FILES['key_file'] + if ssh_key_file is not None: + key = '' + for chunk in ssh_key_file.chunks(): + key = key + chunk + if len(key) > 0: + ssh_key = SshKey(ssh_key=key) + request.user.sshkey_set.add(ssh_key) + keys_saved += 1 - if len( success_msg ) == 0: - error_msg = 'Cannot added the public key' - - elif action == 'update_gpg': - if 'gpg_key' in request.POST: - info.gpg_key = request.POST['gpg_key'] - info.save() - messages.success(request, _("GPG Key updated.")) + if keys_saved > 0: + messages.success(request, ungettext('Key registered', '%(count)d keys registered', keys_saved) % { + 'count': keys_saved}) + return HttpResponseRedirect("") # reload + else: + error_msg = _("Error while registering keys") + else: + form_ssh = SSHForm() keys = request.user.sshkey_set.all() if keys is not None: ssh_keys = dict() for key in keys: - ssh_keys[key.pk] = ssh_key_fingerprint( key.ssh_key ) - + ssh_keys[key.pk] = ssh_key_fingerprint(key.ssh_key) - context = { 'form_gpg' : form_gpg, - 'form_ssh' : form_ssh, + context = { 'form' : form, 'ssh_keys' : ssh_keys, 'error_msg' : error_msg, 'success_msg' : success_msg, } context.update(extra_context) - return render_to_response('my/ssh_gpg.html', + return render_to_response('my/ssh.html', context, context_instance=RequestContext(request)) @@ -150,54 +140,7 @@ ssh_key = request.user.sshkey_set.get(pk=request.POST.get('key_pk', 0)) ssh_key.delete() except SshKey.DoesNotExist: - messages.error(request, u"Cannot remove the selected key") + messages.error(request, _("Cannot remove the selected key")) return HttpResponseRedirect("../") else: return {} - - -class MailForm( forms.Form ): - email = forms.EmailField(required=True) - action = forms.CharField( widget=forms.HiddenInput, required=True, initial='update_mail' ) - -class IdentityForm( forms.Form ): - name = forms.CharField( required = True ) - last_name = forms.CharField( required = False ) - action = forms.CharField( widget=forms.HiddenInput, required=True, initial='update_identity' ) - -class GPGForm( forms.Form ): - gpg_key = forms.CharField( widget=forms.Textarea( attrs={'cols':'70','rows':'15'} ), required=False ) - action = forms.CharField( widget=forms.HiddenInput, required=True, initial='update_gpg' ) - -class SSHForm( forms.Form ): - key_file = forms.FileField(required=False, help_text="Be sure to upload the file ending with .pub") - key = forms.CharField(widget=forms.TextInput(attrs={'size':'60'}), required=False) - - action = forms.CharField(widget=forms.HiddenInput, required=True, initial='add_ssh') - - def clean_key( self ): - ssh_key = self.cleaned_data['key'] - - try: - ssh_key_fingerprint(ssh_key) - except: - raise forms.ValidationError("The uploaded string is not a public key file") - - return ssh_key - - def clean_key_file( self ): - ssh_key_file = self.cleaned_data['key_file'] - - if ssh_key_file is None: - return ssh_key_file - - ssh_key = str() - for chunk in ssh_key_file.chunks(): - ssh_key = ssh_key + chunk - - try: - ssh_key_fingerprint(ssh_key) - except: - raise forms.ValidationError("The uploaded file is not a public key file") - - return ssh_key_file
--- a/savane/svmain/models.py +++ b/savane/svmain/models.py @@ -127,7 +127,8 @@ #confirm_hash = models.CharField(max_length=96, blank=True, null=True) # Keys - gpg_key = models.TextField(blank=True) + gpg_key = models.TextField(blank=True, + help_text="You can write down here your (ASCII) public key (gpg --export --armor keyid)") gpg_key_count = models.IntegerField(null=True, blank=True) # SSH keys: cf. SshKey above
--- a/savane/utils/__init__.py +++ b/savane/utils/__init__.py @@ -16,32 +16,31 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. -import os +import os, tempfile import random import time import re -from subprocess import Popen, PIPE - -def ssh_key_fingerprint( ssh_key ): - if ssh_key is None or len(ssh_key) == 0: - return None +import subprocess +from django.utils.translation import ugettext as _ - file_name = '/tmp/%d' % random.randint(0, int(time.time())) - - tmp_file = open( file_name, 'wb+' ) - tmp_file.write( ssh_key ) - tmp_file.close() +def ssh_key_fingerprint(ssh_key): + """ + Check if the public key is valid using command-line 'ssh-keygen' + """ + tmp_file = tempfile.NamedTemporaryFile() # auto-removed + tmp_file.write(ssh_key) + tmp_file.flush() - cmd = 'ssh-keygen -l -f %s' % file_name - pipe = Popen( cmd, shell=True, stdout=PIPE).stdout - ssh_fp = pipe.readline() - - cmd = 'rm %s' % file_name - piep = Popen( cmd, shell=True, stdout=PIPE) + args = ['ssh-keygen', '-l', '-f', tmp_file.name] + try: + process = subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + except OSError as e: + # Error running ssh-keygen, probably not installed + return 'Failed to run ssh-keygen, contact the site administrator' + out_err = process.communicate() + out = out_err[0] - res = re.search("not a public key file", ssh_fp) - if res is not None: - raise 'Not a public key' + if process.returncode != 0: + raise Exception(_("SSH error: %s") % out.replace(tmp_file.name+' ', '')) - return ssh_fp - + return out.replace(tmp_file.name+' ', '')
--- a/templates/my/index.html +++ b/templates/my/index.html @@ -1,25 +1,26 @@ {% extends "base.html" %} +{% load i18n %} {% block content %} <div style="float: left; margin: 10px;"> - <p>Configure</p> + <p>{% trans "Account configuration" %}</p> <p> - <a href="{% url savane:my:conf %}">Contact info</a><br /> - <a href="{% url savane:my:ssh_gpg %}">SSH & GPG</a><br /> - <a href="{% url savane:my:resume_skills %}">My resume & skills</a><br /> - <a href="{% url savane:my:group_list %}">My groups</a><br /> + <a href="{% url savane:my:conf %}">{% trans "Contact info" %}</a><br /> + <a href="{% url savane:my:ssh %}">{% trans "SSH public keys" %}</a><br /> + <a href="{% url savane:my:resume_skills %}">{% trans "Edit resume and skills" %}</a><br /> + <a href="{% url savane:my:group_list %}">{% trans "My groups" %}</a><br /> <a href="{% url savane:my:membership_list %}">My memberships</a><br /> - <a href="{% url django.contrib.auth.views.password_change %}">Change my password</a><br /> - <a href="{% url savane:my:i18n %}">Set language</a> ({{LANGUAGE_CODE}})<br /> + <a href="{% url django.contrib.auth.views.password_change %}">{% trans "Change password" %}</a><br /> + <a href="{% url savane:my:i18n %}">{% trans "Set language" %}</a> ({{LANGUAGE_CODE}})<br /> </p> </div> <div style="float: left; margin: 10px;"> - <p>Public information</p> + <p>{% trans "Public information" %}</p> <p> - <a href="{% url savane:svmain:user_detail request.user.username %}">My public page</a><br /> + <a href="{% url savane:svmain:user_detail request.user.username %}">{% trans "View your public profile" %}</a><br /> </p> </div>
new file mode 100644 --- /dev/null +++ b/templates/my/ssh.html @@ -0,0 +1,36 @@ +{% extends "base.html" %} +{% load i18n %} + +{% block content %} +<h2>Working SSH Keys</h2> + +{% if ssh_keys.items %} +<p><code>$ ssh-keygen -l -f key.pub</code></p> +{% endif %} +{% for key_pk,key in ssh_keys.items %} +<ul> +<li> + {{ key }} + <form action="{% url savane:my:ssh_delete %}" method="post">{% csrf_token %} + <input type="hidden" name="key_pk" value="{{key_pk}}" /><input type="submit" value="Delete"</input> + </form> +</li> +</ul> +{% empty %} + No SSH keys yet. +{% endfor %} + +<h2>SSH Keys</h2> +<form action="" method="post" enctype="multipart/form-data">{% csrf_token %} + {{ form.as_p }} + <br /><input type="submit" value="Add" name="Add" /> +</form> + +{% endblock %} +{% comment %} +Local Variables: ** +mode: django-html ** +tab-width: 4 ** +indent-tabs-mode: nil ** +End: ** +{% endcomment %}
deleted file mode 100644 --- a/templates/my/ssh_gpg.html +++ /dev/null @@ -1,38 +0,0 @@ -{% extends "base.html" %} -{% load i18n %} - -{% block content %} -<h2>Working SSH Keys</h2> -{% for key_pk,key in ssh_keys.items %} -<li> - {{ key }} - <form action="delete/" method="post">{% csrf_token %} - <input type="hidden" name="key_pk" value="{{key_pk}}" /><input type="submit" value="Delete"</input> - </form> -</li> -{% empty %} - No SSH keys yet. -{% endfor %} - -<h2>SSH Keys</h2> -<form action="" method="post" enctype="multipart/form-data">{% csrf_token %} - {{ form_ssh.as_p }} - <br /><input type="submit" value="Add" name="Add" /> -</form> - -<h2>GPG Key</h2> -{% trans "You can write down here your (ASCII) public key (gpg --export --armor keyid):" %} -<form action="" method="post">{% csrf_token %} - {{ form_gpg.as_p }} - <br /><input type="submit" value="Update" name="Update" /> -</form> - - -{% endblock %} -{% comment %} -Local Variables: ** -mode: django-html ** -tab-width: 4 ** -indent-tabs-mode: nil ** -End: ** -{% endcomment %}