Merge branch 'release/0.10.0' into production

* release/0.10.0:
  update docs version, add release to changelog
  fix taskresults.tests
  add german translation for new strings
  add docstrings, restrict queryset of osusers.views
  implement caching for get_hosting_package
  add list, delete and edit comment of SSH public keys
  add view osusers.views.AddSshPublicKey
  repair osusers.tests.test_admin
  define readonly fields and own delete action for SSH key admin
  trigger tasks on SshPublicKey save and delete
  add administration form and admin class for SshPublicKey
  implement SshPublicKey model, manager and tests
  document HTML improvements
  use bootstrap alert classes for messages
  add api for set_file_ssh_authorized_keys task
  adapt comments to gvafile server side
  update Django version to 1.7.4
This commit is contained in:
Jan Dittberner 2015-02-01 03:03:58 +01:00
commit 630d46b595
27 changed files with 1461 additions and 294 deletions

View file

@ -1,6 +1,20 @@
Changelog
=========
* :release:`0.10.0 <2015-02-01>`
* :support:`-` move taskresults tests to tasksresults.tests and fix them
* :support:`-` cache result of get_hosting_package method of
gvawebcore.views.HostingPackageAndCustomerMixin to improve page loading
performance
* :feature:`-` add ability to add, list and delete SSH public keys assigned to
a hosting package's operating system user and change their comments
* :feature:`-` add ability to add SSH public keys for operating system users
* :support:`-` make tests in osusers.tests work again
* :support:`-` minor HTML improvements
* :support:`-` add API for gvafile task set_file_ssh_authorized_keys (requires
gvafile >= 0.5.0 on the fileserver side)
* :support:`-` update to Django 1.7.4
* :release:`0.9.0 <2015-01-27>`
* :feature:`-` setup nginx virtual host and PHP configuration for websites
(requires gvaweb >= 0.1.0 on web server)

View file

@ -60,9 +60,9 @@ copyright = u'2014, 2015 Jan Dittberner'
# built documents.
#
# The short X.Y version.
version = '0.9'
version = '0.10'
# The full version, including alpha/beta/rc tags.
release = '0.9.0'
release = '0.10.0'
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.

View file

@ -1,5 +1,5 @@
[run]
source = gnuviechadmin,managemails,osusers,domains
source = gnuviechadmin,managemails,osusers,domains,taskresults,gvawebcore,userdbs
[report]
omit = */migrations/*,*/tests/*.py,*/tests.py,gnuviechadmin/settings/local.py,gnuviechadmin/settings/production.py

View file

@ -73,7 +73,7 @@ def create_file_mailbox(username, mailboxname):
:param str username: the user name
:param str mailboxname: the mailbox name
:raises GVAFileException: if the mailbox directory cannot be created
:raises Exception: if the mailbox directory cannot be created
:return: the created mailbox directory name
:rtype: str
@ -87,7 +87,7 @@ def delete_file_mailbox(username, mailboxname):
:param str username: the user name
:param str mailboxname: the mailbox name
:raises GVAFileException: if the mailbox directory cannot be deleted
:raises Exception: if the mailbox directory cannot be deleted
:return: the deleted mailbox directory name
:rtype: str
@ -118,3 +118,18 @@ def delete_file_website_hierarchy(username, sitename):
:rtype: str
"""
@shared_task
def set_file_ssh_authorized_keys(username, ssh_keys):
"""
This task sets the authorized keys for ssh logins.
:param str username: the user name
:param list ssh_key: an ssh_key
:raises Exception: if the update of the creation or update of ssh
authorized_keys failed
:return: the name of the authorized_keys file
:rtype: str
"""

View file

@ -273,10 +273,10 @@ INSTALLED_APPS = DJANGO_APPS + ALLAUTH_APPS + LOCAL_APPS
MESSAGE_TAGS = {
messages.DEBUG: '',
messages.ERROR: 'text-danger',
messages.INFO: 'text-info',
messages.SUCCESS: 'text-success',
messages.WARNING: 'text-warning',
messages.ERROR: 'alert-danger',
messages.INFO: 'alert-info',
messages.SUCCESS: 'alert-success',
messages.WARNING: 'alert-warning',
}
########## END APP CONFIGURATION

View file

@ -17,10 +17,14 @@ class HostingPackageAndCustomerMixin(object):
hosting_package_kwarg = 'package'
"""Keyword argument used to find the hosting package in the URL."""
hostingpackage = None
def get_hosting_package(self):
return get_object_or_404(
CustomerHostingPackage,
pk=int(self.kwargs[self.hosting_package_kwarg]))
if self.hostingpackage is None:
self.hostingpackage = get_object_or_404(
CustomerHostingPackage,
pk=int(self.kwargs[self.hosting_package_kwarg]))
return self.hostingpackage
def get_customer_object(self):
return self.get_hosting_package().customer

View file

@ -121,6 +121,7 @@ class CustomerHostingPackageDetails(StaffOrSelfLoginRequiredMixin, DetailView):
'domains': context['hostingpackage'].domains.all(),
'mailboxes': context['hostingpackage'].mailboxes,
})
context['sshkeys'] = context['osuser'].sshpublickey_set.all()
return context

View file

@ -52,7 +52,7 @@ def create_ldap_user(username, uid, gid, gecos, homedir, shell, password):
@shared_task
def set_ldap_user_password(self, username, password):
def set_ldap_user_password(username, password):
"""
This task sets the password of an existing :py:class:`LDAP user
<ldapentities.models.LdapUser>`.

View file

@ -7,8 +7,8 @@ msgid ""
msgstr ""
"Project-Id-Version: gnuviechadmin\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2015-01-27 18:55+0100\n"
"PO-Revision-Date: 2015-01-27 19:06+0100\n"
"POT-Creation-Date: 2015-02-01 02:12+0100\n"
"PO-Revision-Date: 2015-02-01 02:29+0100\n"
"Last-Translator: Jan Dittberner <jan@dittberner.info>\n"
"Language-Team: Jan Dittberner <jan@dittberner.info>\n"
"Language: de\n"
@ -464,6 +464,10 @@ msgstr ""
"Angemeldet als <a href=\"%(profile_url)s\" class=\"navbar-link\" title="
"\"Mein Profil\">%(user_display)s</a>"
#: templates/base.html:87
msgid "Close"
msgstr "Schließen"
#: templates/dashboard/index.html:3
msgid "Welcome"
msgstr "Willkommen"
@ -524,14 +528,15 @@ msgid "Mailboxes"
msgstr "Postfächer"
#: templates/dashboard/user_dashboard.html:18
#: templates/hostingpackages/customerhostingpackage_detail.html:176
#: templates/hostingpackages/customerhostingpackage_detail.html:177
msgid "Databases"
msgstr "Datenbanken"
#: templates/dashboard/user_dashboard.html:19
#: templates/hostingpackages/customerhostingpackage_detail.html:86
#: templates/hostingpackages/customerhostingpackage_detail.html:149
#: templates/hostingpackages/customerhostingpackage_detail.html:184
#: templates/hostingpackages/customerhostingpackage_detail.html:87
#: templates/hostingpackages/customerhostingpackage_detail.html:150
#: templates/hostingpackages/customerhostingpackage_detail.html:185
#: templates/osusers/sshpublickey_list.html:27
msgid "Actions"
msgstr "Aktionen"
@ -666,6 +671,13 @@ msgstr "SFTP-Benutzername"
msgid "SSH/SFTP username"
msgstr "SSH/SFTP-Benutzername"
#: templates/hostingpackages/customerhostingpackage_detail.html:42
#, python-format
msgid "There is an SSH public key set for this user."
msgid_plural "There are %(counter)s SSH public keys set for this user."
msgstr[0] "Es wurde ein SSH-Schlüssel für diesen Nutzer hinterlegt."
msgstr[1] "Es wurden %(counter)s SSH-Schlüssel für diesen Nutzer hinterlegt."
#: templates/hostingpackages/customerhostingpackage_detail.html:43
msgid "Upload server"
msgstr "Uploadserver"
@ -706,126 +718,137 @@ msgstr "SFTP-Passwort setzen"
msgid "Set SSH/SFTP password"
msgstr "SSH/SFTP-Passwort setzen"
#: templates/hostingpackages/customerhostingpackage_detail.html:78
#: templates/hostingpackages/customerhostingpackage_detail.html:71
msgid "Add an SSH public key that can be used as an alternative for password"
msgstr ""
"Einen SSH-Schlüssel, der als Alternative zum Passwort genutzt werden kann, "
"hinzufügen"
#: templates/hostingpackages/customerhostingpackage_detail.html:71
#: templates/osusers/sshpublickey_list.html:46
msgid "Add SSH public key"
msgstr "SSH-Schlüssel hinzufügen"
#: templates/hostingpackages/customerhostingpackage_detail.html:79
msgid "Domains"
msgstr "Domains"
#: templates/hostingpackages/customerhostingpackage_detail.html:83
#: templates/hostingpackages/customerhostingpackage_detail.html:84
msgid "Domain name"
msgstr "Domainname"
#: templates/hostingpackages/customerhostingpackage_detail.html:84
#: templates/hostingpackages/customerhostingpackage_detail.html:147
#: templates/hostingpackages/customerhostingpackage_detail.html:85
#: templates/hostingpackages/customerhostingpackage_detail.html:148
msgid "Mail addresses"
msgstr "E-Mailadressen"
#: templates/hostingpackages/customerhostingpackage_detail.html:85
#: templates/hostingpackages/customerhostingpackage_detail.html:86
msgid "Websites"
msgstr "Webauftritte"
#: templates/hostingpackages/customerhostingpackage_detail.html:86
#: templates/hostingpackages/customerhostingpackage_detail.html:87
msgid "Domain actions"
msgstr "Domainaktionen"
#: templates/hostingpackages/customerhostingpackage_detail.html:97
#: templates/hostingpackages/customerhostingpackage_detail.html:98
msgid "Edit mail address targets"
msgstr "E-Mailadressziele bearbeiten"
#: templates/hostingpackages/customerhostingpackage_detail.html:98
#: templates/hostingpackages/customerhostingpackage_detail.html:99
msgid "Delete mail address"
msgstr "E-Mailadresse löschen"
#: templates/hostingpackages/customerhostingpackage_detail.html:103
#: templates/hostingpackages/customerhostingpackage_detail.html:115
#: templates/hostingpackages/customerhostingpackage_detail.html:104
#: templates/hostingpackages/customerhostingpackage_detail.html:116
msgid "None"
msgstr "Keine"
#: templates/hostingpackages/customerhostingpackage_detail.html:110
#: templates/hostingpackages/customerhostingpackage_detail.html:111
msgid "Delete website"
msgstr "Webauftritt löschen"
#: templates/hostingpackages/customerhostingpackage_detail.html:119
#: templates/hostingpackages/customerhostingpackage_detail.html:120
msgid "Add mail address"
msgstr "E-Mailadresse hinzufügen"
#: templates/hostingpackages/customerhostingpackage_detail.html:122
#: templates/hostingpackages/customerhostingpackage_detail.html:123
msgid "Add website"
msgstr "Webauftritt anlegen"
#: templates/hostingpackages/customerhostingpackage_detail.html:130
#: templates/hostingpackages/customerhostingpackage_detail.html:131
msgid "There are no domains assigned to this hosting package yet."
msgstr "Diesem Paket sind noch keine Domains zugeordnet."
#: templates/hostingpackages/customerhostingpackage_detail.html:133
#: templates/hostingpackages/customerhostingpackage_detail.html:134
msgid "Add domain"
msgstr "Domain hinzufügen"
#: templates/hostingpackages/customerhostingpackage_detail.html:141
#: templates/hostingpackages/customerhostingpackage_detail.html:142
msgid "E-Mail-Accounts"
msgstr "E-Mailkonten"
#: templates/hostingpackages/customerhostingpackage_detail.html:146
#: templates/hostingpackages/customerhostingpackage_detail.html:147
msgid "Mailbox"
msgstr "Postfach"
#: templates/hostingpackages/customerhostingpackage_detail.html:148
#: templates/hostingpackages/customerhostingpackage_detail.html:157
#: templates/hostingpackages/customerhostingpackage_detail.html:149
#: templates/hostingpackages/customerhostingpackage_detail.html:158
msgid "Active"
msgstr "Aktiv"
#: templates/hostingpackages/customerhostingpackage_detail.html:149
#: templates/hostingpackages/customerhostingpackage_detail.html:150
msgid "Mailbox actions"
msgstr "Postfachaktionen"
#: templates/hostingpackages/customerhostingpackage_detail.html:157
#: templates/hostingpackages/customerhostingpackage_detail.html:158
msgid "inactive"
msgstr "inaktiv"
#: templates/hostingpackages/customerhostingpackage_detail.html:159
#: templates/hostingpackages/customerhostingpackage_detail.html:160
msgid "Set mailbox password"
msgstr "Postfachpasswort setzen"
#: templates/hostingpackages/customerhostingpackage_detail.html:165
#: templates/hostingpackages/customerhostingpackage_detail.html:166
msgid "There are no mailboxes assigned to this hosting package yet."
msgstr "Diesem Hostingpaket sind noch keine Postfächer zugeordnet."
#: templates/hostingpackages/customerhostingpackage_detail.html:168
#: templates/hostingpackages/customerhostingpackage_detail.html:169
msgid "Add mailbox"
msgstr "Postfach hinzufügen"
#: templates/hostingpackages/customerhostingpackage_detail.html:181
#: templates/hostingpackages/customerhostingpackage_detail.html:182
msgid "Database name"
msgstr "Datenbankname"
#: templates/hostingpackages/customerhostingpackage_detail.html:182
#: templates/hostingpackages/customerhostingpackage_detail.html:183
msgid "Database user"
msgstr "Datenbanknutzer"
#: templates/hostingpackages/customerhostingpackage_detail.html:183
#: templates/hostingpackages/customerhostingpackage_detail.html:184
msgid "Database type"
msgstr "Datenbanktyp"
#: templates/hostingpackages/customerhostingpackage_detail.html:183
#: templates/hostingpackages/customerhostingpackage_detail.html:184
msgid "Type"
msgstr "Typ"
#: templates/hostingpackages/customerhostingpackage_detail.html:184
#: templates/hostingpackages/customerhostingpackage_detail.html:185
msgid "Database actions"
msgstr "Datenbankaktionen"
#: templates/hostingpackages/customerhostingpackage_detail.html:194
#: templates/hostingpackages/customerhostingpackage_detail.html:195
msgid "Set database user password"
msgstr "Datenbanknutzerpasswort setzen"
#: templates/hostingpackages/customerhostingpackage_detail.html:195
#: templates/hostingpackages/customerhostingpackage_detail.html:196
msgid "Delete database"
msgstr "Datenbank löschen"
#: templates/hostingpackages/customerhostingpackage_detail.html:202
#: templates/hostingpackages/customerhostingpackage_detail.html:203
msgid "There are no databases assigned to this hosting package yet."
msgstr "Diesem Hostingpaket sind noch keine Datenbanken zugeordnet."
#: templates/hostingpackages/customerhostingpackage_detail.html:205
#: templates/hostingpackages/customerhostingpackage_detail.html:206
msgid "Add database"
msgstr "Datenbank hinzufügen"
@ -856,12 +879,14 @@ msgid "Do you really want to delete the mail address %(mailaddress)s?"
msgstr "Wollen Sie die E-Mailadresse %(mailaddress)s wirklich löschen?"
#: templates/managemails/mailaddress_confirm_delete.html:28
#: templates/osusers/sshpublickey_confirm_delete.html:30
#: templates/userdbs/userdatabase_confirm_delete.html:29
#: templates/websites/website_confirm_delete.html:29
msgid "Yes, do it!"
msgstr "Ja, so soll es sein!"
#: templates/managemails/mailaddress_confirm_delete.html:29
#: templates/osusers/sshpublickey_confirm_delete.html:31
#: templates/userdbs/userdatabase_confirm_delete.html:30
#: templates/websites/website_confirm_delete.html:30
msgid "Cancel"
@ -938,6 +963,177 @@ msgstr "Bitte geben Sie das neue Passwort für Ihr Postfach ein."
msgid "Please specify the new password for the mailbox."
msgstr "Bitte geben Sie das neue Passwort für das Postfach ein."
#: templates/osusers/sshpublickey_confirm_delete.html:6
#, python-format
msgid "Delete SSH Public Key for Operating System User %(osuser)s"
msgstr "SSH-Schlüssel des Betriebssystemnutzers %(osuser)s löschen"
#: templates/osusers/sshpublickey_confirm_delete.html:8
#, python-format
msgid ""
"Delete SSH Public Key for Operating System User %(osuser)s of Customer "
"%(full_name)s"
msgstr ""
"SSH-Schlüssel des Betriebssystemnutzers %(osuser)s des Kunden %(full_name)s "
"löschen"
#: templates/osusers/sshpublickey_confirm_delete.html:14
#, python-format
msgid ""
"Delete SSH Public Key <small>for Operating System User %(osuser)s</small>"
msgstr ""
"SSH-Schlüssel löschen <small>für Betriebssystemnutzer %(osuser)s</small>"
#: templates/osusers/sshpublickey_confirm_delete.html:16
#, python-format
msgid ""
"Delete SSH Public Key <small>for Operating System User %(osuser)s of "
"Customer %(full_name)s</small>"
msgstr ""
"SSH-Schlüssel löschen <small>von Betriebssystemnutzer %(osuser)s des Kunden "
"%(full_name)s</small>"
#: templates/osusers/sshpublickey_confirm_delete.html:23
#, python-format
msgid "Do you really want to delete the %(algorithm)s SSH public key?"
msgstr "Wollen Sie den %(algorithm)s-SSH-Schlüssel wirklich löschen?"
#: templates/osusers/sshpublickey_confirm_delete.html:26
msgid ""
"When you confirm the deletion of this key you will no longer be able to use "
"the corresponding private key for authentication."
msgstr ""
"Wenn Sie die Löschung dieses Schlüssels bestätigen, werden Sie den "
"dazugehörigen privaten Schlüssel nicht weiter für die Anmeldung verwenden "
"können."
#: templates/osusers/sshpublickey_confirm_delete.html:31
msgid "Cancel and go back to the SSH key list"
msgstr "Abbrechen und zurückgehen zur Liste der SSH-Schlüssel"
#: templates/osusers/sshpublickey_create.html:6
#, python-format
msgid "Add new SSH Public Key for Operating System User %(osuser)s"
msgstr "Neuen SSH-Schlüssel für Betriebssystemnutzer %(osuser)s hinterlegen"
#: templates/osusers/sshpublickey_create.html:8
#, python-format
msgid ""
"Add a new SSH Public Key for Operating System User %(osuser)s of Customer "
"%(full_name)s"
msgstr ""
"Neuen SSH-Schlüssel für Betriebssystemnutzer %(osuser)s des Kunden "
"%(full_name)s hinterlegen"
#: templates/osusers/sshpublickey_create.html:14
#, python-format
msgid ""
"Add new SSH Public Key <small>for Operating System User %(osuser)s</small>"
msgstr ""
"Neuen SSH-Schlüssel hinterlegen <small>für Betriebssystemnutzer %(osuser)s</"
"small>"
#: templates/osusers/sshpublickey_create.html:16
#, python-format
msgid ""
"Add a new SSH Public Key <small>for Operating System User %(osuser)s of "
"Customer %(full_name)s</small>"
msgstr ""
"Neuen SSH-Schlüssel hinterlegen <small>für Betriebssystemnutzer %(osuser)s "
"der Kunden %(full_name)s</small>"
#: templates/osusers/sshpublickey_edit_comment.html:6
#, python-format
msgid "Edit Comment of SSH Public Key for Operating System User %(osuser)s"
msgstr ""
"Kommentar eines SSH-Schlüssels für Betriebssystemnutzer %(osuser)s ändern"
#: templates/osusers/sshpublickey_edit_comment.html:8
#, python-format
msgid ""
"Edit Comment of SSH Public Key for Operating System User %(osuser)s of "
"Customer %(full_name)s"
msgstr ""
"Kommentar des SSH-Schlüssels des Betriebssystemnutzers %(osuser)s des Kunden "
"%(full_name)s ändern"
#: templates/osusers/sshpublickey_edit_comment.html:14
#, python-format
msgid ""
"Edit Comment of Public Key <small>for Operating System User %(osuser)s</"
"small>"
msgstr ""
"Kommentar eines SSH-Schlüssels ändern <small>für Betriebssystemnutzer "
"%(osuser)s</small>"
#: templates/osusers/sshpublickey_edit_comment.html:16
#, python-format
msgid ""
"Edit Comment of SSH Public Key <small>for Operating System User %(osuser)s "
"of Customer %(full_name)s</small>"
msgstr ""
"Kommentar des SSH-Schlüssels ändern <small>für Betriebssystemnutzer "
"%(osuser)s des Kunden %(full_name)s</small>"
#: templates/osusers/sshpublickey_list.html:6
#, python-format
msgid "SSH Public Keys for Operating System User %(osuser)s"
msgstr "SSH-Schlüssel für Betriebssystemnutzer %(osuser)s"
#: templates/osusers/sshpublickey_list.html:8
#, python-format
msgid ""
"SSH Public Keys for Operating System User %(osuser)s of Customer "
"%(full_name)s"
msgstr ""
"SSH-Schlüssel des Betriebssystemnutzers %(osuser)s des Kunden %(full_name)s"
#: templates/osusers/sshpublickey_list.html:14
#, python-format
msgid "SSH Public Keys <small>for Operating System User %(osuser)s</small>"
msgstr "SSH-Schlüssel <small>für Betriebssystemnutzer %(osuser)s</small>"
#: templates/osusers/sshpublickey_list.html:16
#, python-format
msgid ""
"SSH Public Keys <small>for Operating System User %(osuser)s of Customer "
"%(full_name)s</small>"
msgstr ""
"SSH-Schlüssel <small>des Betriebssystemnutzers %(osuser)s des Kunden "
"%(full_name)s</small>"
#: templates/osusers/sshpublickey_list.html:25
msgid "Algorithm"
msgstr "Algorithmus"
#: templates/osusers/sshpublickey_list.html:26
msgid "Comment"
msgstr "Kommentar"
#: templates/osusers/sshpublickey_list.html:27
msgid "SSH public key actions"
msgstr "Aktionen für SSH-Schlüssel"
#: templates/osusers/sshpublickey_list.html:36
msgid "Delete this SSH public key"
msgstr "Diesen SSH-Schlüssel löschen"
#: templates/osusers/sshpublickey_list.html:36
msgid "Delete"
msgstr "Löschen"
#: templates/osusers/sshpublickey_list.html:37
msgid "Edit this SSH public key's comment"
msgstr "Den Kommentar dieses SSH-Schlüssels ändern"
#: templates/osusers/sshpublickey_list.html:37
msgid "Edit Comment"
msgstr "Kommentar ändern"
#: templates/osusers/sshpublickey_list.html:44
msgid "There are now SSH public keys set for this operating system user yet."
msgstr "Diesem Betriebssytemnutzer wurden noch keine SSH-Schlüssel zugeordnet."
#: templates/osusers/user_setpassword.html:5
#: templates/osusers/user_setpassword.html:13
#, python-format

View file

@ -3,16 +3,24 @@ This module contains the Django admin classes of the :py:mod:`osusers` app.
"""
from django import forms
from django.utils.translation import ugettext as _
from django.utils.translation import ugettext_lazy as _
from django.contrib import admin
from fileservertasks.tasks import set_file_ssh_authorized_keys
from gvawebcore.forms import (
PASSWORD_MISMATCH_ERROR
)
from taskresults.models import TaskResult
from .forms import (
INVALID_SSH_PUBLIC_KEY,
DUPLICATE_SSH_PUBLIC_KEY_FOR_USER,
)
from .models import (
AdditionalGroup,
Group,
Shadow,
SshPublicKey,
User,
)
@ -216,5 +224,158 @@ class GroupAdmin(admin.ModelAdmin):
return actions
class SshPublicKeyCreationForm(forms.ModelForm):
"""
A form for creating :py:class:`SSH public keys
<osusers.models.SshPublicKey>`.
"""
publickeytext = forms.CharField(
label=_('Key text'), widget=forms.Textarea,
help_text=_('A SSH public key in either OpenSSH or RFC 4716 format'))
class Meta:
model = SshPublicKey
fields = ['user']
def clean_publickeytext(self):
keytext = self.cleaned_data.get('publickeytext')
try:
SshPublicKey.objects.parse_keytext(keytext)
except:
raise forms.ValidationError(INVALID_SSH_PUBLIC_KEY)
return keytext
def clean(self):
user = self.cleaned_data.get('user')
keytext = self.cleaned_data.get('publickeytext')
if user and keytext:
alg, data, comment = SshPublicKey.objects.parse_keytext(keytext)
if SshPublicKey.objects.filter(
user=user, algorithm=alg, data=data
).exists():
self.add_error(
'publickeytext',
forms.ValidationError(DUPLICATE_SSH_PUBLIC_KEY_FOR_USER))
def save(self, commit=True):
"""
Save the provided ssh public key in properly split format.
:param boolean commit: whether to save the created public key
:return: ssh public key instance
:rtype: :py:class:`osusers.models.SshPublicKey`
"""
algorithm, keydata, comment = SshPublicKey.objects.parse_keytext(
self.cleaned_data.get('publickeytext'))
self.instance.algorithm = algorithm
self.instance.data = keydata
self.instance.comment = comment
return super(SshPublicKeyCreationForm, self).save(commit)
class SshPublicKeyAdmin(admin.ModelAdmin):
"""
Admin class for working with :py:class:`SSH public keys
<osusers.models.SshPublicKey>`.
"""
actions = ['perform_delete_selected']
add_form = SshPublicKeyCreationForm
list_display = ['user', 'algorithm', 'comment']
add_fieldsets = (
(None, {
'classes': ('wide',),
'fields': ('user', 'publickeytext')}),
)
def get_form(self, request, obj=None, **kwargs):
"""
Use special form for ssh public key creation."
:param request: the current HTTP request
:param obj: either a :py:class:`SshPublicKey
<osusers.models.SshPublicKey>` instance or None for a new SSH
public key
:param kwargs: keyword arguments to be passed to
:py:meth:`django.contrib.admin.ModelAdmin.get_form`
:return: form instance
"""
defaults = {}
if obj is None:
defaults.update({
'form': self.add_form,
'fields': admin.options.flatten_fieldsets(self.add_fieldsets),
})
defaults.update(kwargs)
return super(SshPublicKeyAdmin, self).get_form(
request, obj, **defaults)
def get_readonly_fields(self, request, obj=None):
"""
Make sure that algorithm and data of SSH public keys are not editable.
:param request: the current HTTP request
:param obj: either a :py:class:`SshPublicKey
<osusers.models.SshPublicKey>` instance or None for a new SSH
public key
:return: a list of fields
:rtype: list
"""
if obj:
return ['algorithm', 'data']
return []
def perform_delete_selected(self, request, queryset):
"""
Action to delete a list of selected ssh keys.
This action makes sure that the ssh keys of all users affected by the
current deletion are refreshed on the file server.
:param request: the current HTTP request
:param queryset: Django ORM queryset representing the selected ssh keys
"""
users = set([
item['user'] for item in
queryset.values('user').distinct()
])
queryset.delete()
for user in users:
TaskResult.objects.create_task_result(
set_file_ssh_authorized_keys.delay(
User.objects.get(uid=user).username, [
str(key) for key in SshPublicKey.objects.filter(
user_id=user)
]),
'set_file_ssh_authorized_keys'
)
perform_delete_selected.short_description = _(
'Delete selected SSH public keys')
def get_actions(self, request):
"""
Get the available actions for SSH public keys.
This overrides the default behavior to remove the default
`delete_selected` action.
:param request: the current HTTP request
:return: list of actions
:rtype: list
"""
actions = super(SshPublicKeyAdmin, self).get_actions(request)
if 'delete_selected' in actions:
del actions['delete_selected']
return actions
admin.site.register(Group, GroupAdmin)
admin.site.register(User, UserAdmin)
admin.site.register(SshPublicKey, SshPublicKeyAdmin)

View file

@ -13,7 +13,14 @@ from crispy_forms.layout import Submit
from gvawebcore.forms import PasswordModelFormMixin
from .models import User
from .models import (
SshPublicKey,
User,
)
INVALID_SSH_PUBLIC_KEY = _('Invalid SSH public key data format.')
DUPLICATE_SSH_PUBLIC_KEY_FOR_USER = _(
'This SSH public key is already assigned to this user.')
class ChangeOsUserPasswordForm(PasswordModelFormMixin, forms.ModelForm):
@ -43,3 +50,85 @@ class ChangeOsUserPasswordForm(PasswordModelFormMixin, forms.ModelForm):
"""
self.instance.set_password(self.cleaned_data['password1'])
return super(ChangeOsUserPasswordForm, self).save(commit=commit)
class AddSshPublicKeyForm(forms.ModelForm):
"""
A form for creating :py:class:`SSH public keys
<osusers.models.SshPublicKey>`.
"""
publickeytext = forms.CharField(
label=_('Key text'), widget=forms.Textarea,
help_text=_('A SSH public key in either OpenSSH or RFC 4716 format'))
class Meta:
model = SshPublicKey
fields = []
def __init__(self, *args, **kwargs):
hosting_package = kwargs.pop('hostingpackage')
self.osuser = hosting_package.osuser
super(AddSshPublicKeyForm, self).__init__(*args, **kwargs)
self.helper = FormHelper()
self.helper.form_action = reverse(
'add_ssh_key', kwargs={'package': hosting_package.id})
self.helper.add_input(Submit('submit', _('Add SSH public key')))
def clean_publickeytext(self):
keytext = self.cleaned_data.get('publickeytext')
try:
SshPublicKey.objects.parse_keytext(keytext)
except:
raise forms.ValidationError(INVALID_SSH_PUBLIC_KEY)
return keytext
def clean(self):
keytext = self.cleaned_data.get('publickeytext')
alg, data, comment = SshPublicKey.objects.parse_keytext(keytext)
if SshPublicKey.objects.filter(
user=self.osuser, algorithm=alg, data=data
).exists():
self.add_error(
'publickeytext',
forms.ValidationError(DUPLICATE_SSH_PUBLIC_KEY_FOR_USER)
)
def save(self, commit=True):
"""
Save the provided ssh public key in properly split format.
:param boolean commit: whether to save the created public key
:return: ssh public key instance
:rtype: :py:class:`osusers.models.SshPublicKey`
"""
algorithm, keydata, comment = SshPublicKey.objects.parse_keytext(
self.cleaned_data.get('publickeytext'))
self.instance.user = self.osuser
self.instance.algorithm = algorithm
self.instance.data = keydata
self.instance.comment = comment
return super(AddSshPublicKeyForm, self).save(commit)
class EditSshPublicKeyCommentForm(forms.ModelForm):
"""
A form for editing :py:class:`SSH public key
<osusers.models.SshPublicKey>` comment fields.
"""
class Meta:
model = SshPublicKey
fields = ['comment']
def __init__(self, *args, **kwargs):
hosting_package = kwargs.pop('hostingpackage')
self.osuser = hosting_package.osuser
super(EditSshPublicKeyCommentForm, self).__init__(*args, **kwargs)
self.fields['comment'].widget = forms.TextInput()
self.helper = FormHelper()
self.helper.form_action = reverse(
'edit_ssh_key_comment',
kwargs={'package': hosting_package.id, 'pk': self.instance.id})
self.helper.add_input(Submit('submit', _('Change Comment')))

View file

@ -7,8 +7,8 @@ msgid ""
msgstr ""
"Project-Id-Version: osusers\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2015-01-27 18:55+0100\n"
"PO-Revision-Date: 2015-01-24 18:25+0100\n"
"POT-Creation-Date: 2015-02-01 02:12+0100\n"
"PO-Revision-Date: 2015-02-01 02:17+0100\n"
"Last-Translator: Jan Dittberner <jan@dittberner.info>\n"
"Language-Team: Jan Dittberner <jan@dittberner.info>\n"
"Language: de\n"
@ -19,128 +19,158 @@ msgstr ""
"X-Generator: Poedit 1.6.10\n"
"X-Poedit-SourceCharset: UTF-8\n"
#: osusers/admin.py:45
#: osusers/admin.py:53
msgid "Password"
msgstr "Passwort"
#: osusers/admin.py:49
#: osusers/admin.py:57
msgid "Password (again)"
msgstr "Passwortwiederholung"
#: osusers/admin.py:158
#: osusers/admin.py:166
msgid "Delete selected users"
msgstr "Ausgewählte Nutzer löschen"
#: osusers/admin.py:199
#: osusers/admin.py:207
msgid "Delete selected groups"
msgstr "Ausgewählte Gruppen löschen"
#: osusers/admin.py:234 osusers/forms.py:62
msgid "Key text"
msgstr "Schlüsseltext"
#: osusers/admin.py:235 osusers/forms.py:63
msgid "A SSH public key in either OpenSSH or RFC 4716 format"
msgstr ""
"Öffentlicher Teil eines SSH-Schlüssels entweder im OpenSSH- oder im RFC-4716-"
"Format"
#: osusers/admin.py:359
msgid "Delete selected SSH public keys"
msgstr "Ausgewählte SSH-Schlüssel löschen"
#: osusers/apps.py:17
msgid "Operating System Users and Groups"
msgstr "Betriebssystemnutzer- und Gruppen"
#: osusers/forms.py:33
#: osusers/forms.py:21
msgid "Invalid SSH public key data format."
msgstr "Ungültiges Format für den öffentlichen Teil eines SSH-Schlüssels."
#: osusers/forms.py:23
msgid "This SSH public key is already assigned to this user."
msgstr "Dieser SSH-Schlüssel wurde diesem Nutzer bereits zugeordnet."
#: osusers/forms.py:40
msgid "Set password"
msgstr "Passwort setzen"
#: osusers/models.py:47
#: osusers/forms.py:76
msgid "Add SSH public key"
msgstr "SSH-Schlüssel hinzufügen"
#: osusers/forms.py:134
msgid "Change Comment"
msgstr "Kommentar ändern"
#: osusers/models.py:50
msgid "You can not use a user's primary group."
msgstr "Sie können nicht die primäre Gruppe des Nutzers verwenden."
#: osusers/models.py:77
#: osusers/models.py:80
msgid "Group name"
msgstr "Gruppenname"
#: osusers/models.py:79
#: osusers/models.py:82
msgid "Group ID"
msgstr "Gruppen-ID"
#: osusers/models.py:80
#: osusers/models.py:83
msgid "Description"
msgstr "Beschreibung"
#: osusers/models.py:82
#: osusers/models.py:85
msgid "Group password"
msgstr "Gruppenpasswort"
#: osusers/models.py:87 osusers/models.py:221
#: osusers/models.py:90 osusers/models.py:224
msgid "Group"
msgstr "Gruppe"
#: osusers/models.py:88
#: osusers/models.py:91
msgid "Groups"
msgstr "Gruppen"
#: osusers/models.py:218
#: osusers/models.py:221
msgid "User name"
msgstr "Nutzername"
#: osusers/models.py:220
#: osusers/models.py:223
msgid "User ID"
msgstr "Nutzer-ID"
#: osusers/models.py:222
#: osusers/models.py:225
msgid "Gecos field"
msgstr "GECOS-Feld"
#: osusers/models.py:223
#: osusers/models.py:226
msgid "Home directory"
msgstr "Home-Verzeichnis"
#: osusers/models.py:224
#: osusers/models.py:227
msgid "Login shell"
msgstr "Loginshell"
#: osusers/models.py:230 osusers/models.py:370
#: osusers/models.py:233 osusers/models.py:373 osusers/models.py:566
msgid "User"
msgstr "Nutzer"
#: osusers/models.py:231
#: osusers/models.py:234
msgid "Users"
msgstr "Nutzer"
#: osusers/models.py:371
#: osusers/models.py:374
msgid "Encrypted password"
msgstr "Verschlüsseltes Passwort"
#: osusers/models.py:373
#: osusers/models.py:376
msgid "Date of last change"
msgstr "Datum der letzten Änderung"
#: osusers/models.py:374
#: osusers/models.py:377
msgid "This is expressed in days since Jan 1, 1970"
msgstr "Ausgedrückt als Tage seit dem 1. Januar 1970"
#: osusers/models.py:377
#: osusers/models.py:380
msgid "Minimum age"
msgstr "Minimales Alter"
#: osusers/models.py:378
#: osusers/models.py:381
msgid "Minimum number of days before the password can be changed"
msgstr "Minmale Anzahl von Tagen bevor das Passwort geändert werden kann"
#: osusers/models.py:382
#: osusers/models.py:385
msgid "Maximum age"
msgstr "Maximales Alter"
#: osusers/models.py:383
#: osusers/models.py:386
msgid "Maximum number of days after which the password has to be changed"
msgstr ""
"Maximale Anzahl von Tagen, nach denen das Passwort geändert werden muss"
#: osusers/models.py:387
#: osusers/models.py:390
msgid "Grace period"
msgstr "Duldungsperiode"
#: osusers/models.py:388
#: osusers/models.py:391
msgid "The number of days before the password is going to expire"
msgstr "Anzahl von Tagen nach denen das Passwort verfällt"
#: osusers/models.py:392
#: osusers/models.py:395
msgid "Inactivity period"
msgstr "Inaktivitätsperiode"
#: osusers/models.py:393
#: osusers/models.py:396
msgid ""
"The number of days after the password has expired during which the password "
"should still be accepted"
@ -148,36 +178,65 @@ msgstr ""
"Die Anzahl von Tagen für die ein verfallenes Passwort noch akzeptiert werden "
"soll"
#: osusers/models.py:397
#: osusers/models.py:400
msgid "Account expiration date"
msgstr "Kontoverfallsdatum"
#: osusers/models.py:398
#: osusers/models.py:401
msgid ""
"The date of expiration of the account, expressed as number of days since Jan "
"1, 1970"
msgstr "Kontoverfallsdatum in Tagen seit dem 1. Januar 1970"
#: osusers/models.py:405
#: osusers/models.py:408
msgid "Shadow password"
msgstr "Shadow-Passwort"
#: osusers/models.py:406
#: osusers/models.py:409
msgid "Shadow passwords"
msgstr "Shadow-Passwörter"
#: osusers/models.py:432
#: osusers/models.py:435
msgid "Additional group"
msgstr "Weitere Gruppe"
#: osusers/models.py:433
#: osusers/models.py:436
msgid "Additional groups"
msgstr "Weitere Gruppen"
#: osusers/views.py:42
#: osusers/models.py:567
msgid "Algorithm"
msgstr "Algorithmus"
#: osusers/models.py:568
msgid "Key bytes"
msgstr "Schlüsselbytes"
#: osusers/models.py:569
msgid "Base64 encoded key bytes"
msgstr "Base64-kodierte Schlüsselbytes"
#: osusers/models.py:570
msgid "Comment"
msgstr "Kommentar"
#: osusers/models.py:575
msgid "SSH public key"
msgstr "Öffentlicher SSH-Schlüssel"
#: osusers/models.py:576
msgid "SSH public keys"
msgstr "Öffentliche SSH-Schlüssel"
#: osusers/views.py:56
#, python-brace-format
msgid "New password for {username} has been set successfully."
msgstr "Für {username} wurde erfolgreich ein neues Passwort gesetzt."
#: osusers/views.py:92
#, python-brace-format
msgid "Successfully added new {algorithm} SSH public key"
msgstr "Neuer {algorithm}-SSH-Schlüssel erfolgreich hinzugefügt"
#~ msgid "Passwords don't match"
#~ msgstr "Passwörter stimmen nicht überein"

View file

@ -0,0 +1,37 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
import django.utils.timezone
import model_utils.fields
class Migration(migrations.Migration):
dependencies = [
('osusers', '0004_auto_20150104_1751'),
]
operations = [
migrations.CreateModel(
name='SshPublicKey',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, verbose_name='created', editable=False)),
('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, verbose_name='modified', editable=False)),
('algorithm', models.CharField(max_length=20, verbose_name='Algorithm')),
('data', models.TextField(help_text='Base64 encoded key bytes', verbose_name='Key bytes')),
('comment', models.TextField(verbose_name='Comment', blank=True)),
('user', models.ForeignKey(verbose_name='User', to='osusers.User')),
],
options={
'verbose_name': 'SSH public key',
'verbose_name_plural': 'SSH public keys',
},
bases=(models.Model,),
),
migrations.AlterUniqueTogether(
name='sshpublickey',
unique_together=set([('user', 'algorithm', 'data')]),
),
]

View file

@ -4,9 +4,11 @@ This module defines the database models of operating system users.
"""
from __future__ import unicode_literals
import base64
from datetime import date
import logging
import os
import six
from django.db import models, transaction
from django.conf import settings
@ -35,6 +37,7 @@ from ldaptasks.tasks import (
from fileservertasks.tasks import (
delete_file_mail_userdir,
delete_file_sftp_userdir,
set_file_ssh_authorized_keys,
setup_file_mail_userdir,
setup_file_sftp_userdir,
)
@ -477,3 +480,124 @@ class AdditionalGroup(TimeStampedModel, models.Model):
'remove_ldap_user_from_group'
)
super(AdditionalGroup, self).delete(*args, **kwargs)
class SshPublicKeyManager(models.Manager):
"""
Default manager for :py:class:`SSH public key
<osusers.models.SshPublicKey>` instances.
"""
def parse_keytext(self, keytext):
"""
Parse a SSH public key in OpenSSH or :rfc:`4716` format into its
components algorithm, key data and comment.
:param str keytext: key text
:return: triple of algorithm name, key data and comment
:rtype: triple of str
"""
if keytext.startswith('---- BEGIN SSH2 PUBLIC KEY ----'):
comment = ''
data = ''
continued = ''
headers = {}
for line in keytext.splitlines():
if line == '---- BEGIN SSH2 PUBLIC KEY ----':
continue
elif ':' in line: # a header line
header_tag, header_value = [
item.strip() for item in line.split(':', 1)]
if header_value.endswith('\\'):
continued = header_value[:-1]
else:
headers[header_tag.lower()] = header_value
elif continued:
if line.endswith('\\'):
continued += line[:-1]
continue
header_value = continued + line
headers[header_tag.lower()] = header_value
continued = ''
elif line == '---- END SSH2 PUBLIC KEY ----':
break
elif line: # ignore empty lines
data += line
if 'comment' in headers:
comment = headers['comment']
else:
parts = keytext.split()
if len(parts) > 3:
raise ValueError("unsupported key format")
data = parts[1]
comment = len(parts) == 3 and parts[2] or ""
keybytes = base64.b64decode(data)
parts = keybytes.split(b'\x00' * 3)
alglength = six.byte2int(parts[1])
algname = parts[1][1:1+alglength]
return algname, data, comment
def create_ssh_public_key(self, user, keytext):
"""
Create a new :py:class:`SSH public key <osusers.models.SshPublicKey>`
for a user from the given key text representation. The text can be
either in openssh format or :rfc:`4716` format.
:param user: :py:class:`operating system user <osusers.models.User>`
:param str keytext: key text
:return: public key
:retype: :py:class:`osusers.models.SshPublicKey`
"""
algorithm, data, comment = self.parse_keytext(keytext)
return self.create(
user=user, algorithm=algorithm, data=data, comment=comment)
@python_2_unicode_compatible
class SshPublicKey(TimeStampedModel):
"""
This entity class represents single SSH keys for an :py:class:`operating
system user <osusers.models.User>`.
"""
user = models.ForeignKey(User, verbose_name=_('User'))
algorithm = models.CharField(_('Algorithm'), max_length=20)
data = models.TextField(_('Key bytes'),
help_text=_('Base64 encoded key bytes'))
comment = models.TextField(_('Comment'), blank=True)
objects = SshPublicKeyManager()
class Meta:
verbose_name = _('SSH public key')
verbose_name_plural = _('SSH public keys')
unique_together = [('user', 'algorithm', 'data')]
def __str__(self):
return "{algorithm} {data} {comment}".format(
algorithm=self.algorithm, data=self.data, comment=self.comment
).strip()
def save(self, **kwargs):
key = super(SshPublicKey, self).save(**kwargs)
TaskResult.objects.create_task_result(
set_file_ssh_authorized_keys.delay(
self.user.username, [
str(key) for key in
SshPublicKey.objects.filter(user=self.user)]),
'set_file_ssh_authorized_keys'
)
return key
def delete(self, **kwargs):
super(SshPublicKey, self).delete(**kwargs)
TaskResult.objects.create_task_result(
set_file_ssh_authorized_keys.delay(
self.user.username, [
str(key) for key in
SshPublicKey.objects.filter(user=self.user)]),
'set_file_ssh_authorized_keys'
)

View file

@ -3,7 +3,9 @@ from django.contrib.admin import AdminSite
from django.test import TestCase
from django.test.utils import override_settings
from mock import patch, Mock
from django.contrib.auth import get_user_model
from mock import Mock
from osusers.models import (
Group,
@ -16,31 +18,24 @@ from osusers.admin import (
UserCreationForm,
)
Customer = get_user_model()
class TaskResultInlineTest(TestCase):
class CustomerTestCase(TestCase):
def setUp(self):
self.site = AdminSite()
super(TaskResultInlineTest, self).setUp()
def test_get_queryset_calls_update_taskstatus(self):
with patch('osusers.admin.admin.TabularInline.get_queryset') as mock:
entrymock = Mock(name='entry')
mock.return_value = [entrymock]
requestmock = Mock(name='request')
UserTaskResultInline(User, self.site).get_queryset(requestmock)
entrymock.update_taskstatus.assert_calledwith()
def test_has_add_permissions_returns_false(self):
self.assertFalse(
UserTaskResultInline(User, self.site).has_add_permission(
self, Mock(name='request'))
)
self.customer = Customer.objects.create_user('test')
super(CustomerTestCase, self).setUp()
class UserCreationFormTest(TestCase):
class UserCreationFormTest(CustomerTestCase):
def test_clean_password2_same(self):
form = UserCreationForm()
form.cleaned_data = {'password1': 'secret', 'password2': 'secret'}
form.cleaned_data = {
'customer': self.customer,
'password1': 'secret',
'password2': 'secret'
}
self.assertEqual(form.clean_password2(), 'secret')
def test_clean_password2_empty(self):
@ -50,7 +45,11 @@ class UserCreationFormTest(TestCase):
def test_clean_password2_mismatch(self):
form = UserCreationForm()
form.cleaned_data = {'password1': 'secretx', 'password2': 'secrety'}
form.cleaned_data = {
'customer': self.customer,
'password1': 'secretx',
'password2': 'secrety'
}
with self.assertRaises(forms.ValidationError) as cm:
form.clean_password2()
self.assertEqual(cm.exception.message, PASSWORD_MISMATCH_ERROR)
@ -62,7 +61,11 @@ class UserCreationFormTest(TestCase):
)
def test_save_commit(self):
form = UserCreationForm()
form.cleaned_data = {'password1': 'secret', 'password2': 'secret'}
form.cleaned_data = {
'customer': self.customer,
'password1': 'secret',
'password2': 'secret'
}
user = form.save()
self.assertIsNotNone(user)
self.assertEqual(User.objects.get(pk=user.uid), user)
@ -72,7 +75,7 @@ class UserCreationFormTest(TestCase):
self.assertIsNone(form.save_m2m())
class UserAdminTest(TestCase):
class UserAdminTest(CustomerTestCase):
def setUp(self):
site = AdminSite()
self.uadmin = UserAdmin(User, site)
@ -91,11 +94,12 @@ class UserAdminTest(TestCase):
BROKER_BACKEND='memory'
)
def test_get_form_with_object(self):
user = User.objects.create_user()
user = User.objects.create_user(customer=self.customer)
form = self.uadmin.get_form(Mock(name='request'), user)
self.assertEqual(
form.Meta.fields,
['username', 'group', 'gecos', 'homedir', 'shell', 'uid']
['username', 'group', 'gecos', 'homedir', 'shell', 'customer',
'uid']
)
def test_get_inline_instances_without_object(self):
@ -108,7 +112,7 @@ class UserAdminTest(TestCase):
BROKER_BACKEND='memory'
)
def test_get_inline_instances_with_object(self):
user = User.objects.create_user()
user = User.objects.create_user(customer=self.customer)
inlines = self.uadmin.get_inline_instances(
Mock(name='request'), user)
self.assertEqual(len(inlines), len(UserAdmin.inlines))
@ -138,27 +142,3 @@ class GroupAdminTest(TestCase):
self.assertEqual(len(inlines), len(GroupAdmin.inlines))
for index in range(len(inlines)):
self.assertIsInstance(inlines[index], GroupAdmin.inlines[index])
class DeleteTaskResultAdminTest(TestCase):
def setUp(self):
site = AdminSite()
self.dtradmin = DeleteTaskResultAdmin(DeleteTaskResult, site)
super(DeleteTaskResultAdminTest, self).setUp()
def test_has_add_permission_returns_false_without_object(self):
self.assertFalse(
self.dtradmin.has_add_permission(Mock(name='request')))
def test_has_add_permission_returns_false_with_object(self):
self.assertFalse(
self.dtradmin.has_add_permission(Mock(name='request'),
Mock(name='test')))
def test_get_queryset_calls_update_taskstatus(self):
with patch('osusers.admin.admin.ModelAdmin.get_queryset') as mock:
entrymock = Mock(name='entry')
mock.return_value = [entrymock]
requestmock = Mock(name='request')
self.dtradmin.get_queryset(requestmock)
entrymock.update_taskstatus.assert_calledwith()

View file

@ -1,11 +1,11 @@
from datetime import date
from django.conf import settings
from django.core.exceptions import ValidationError
from django.test import TestCase
from django.test.utils import override_settings
from django.utils import timezone
from mock import patch, MagicMock
from django.contrib.auth import get_user_model
from passlib.hash import sha512_crypt
@ -14,8 +14,84 @@ from osusers.models import (
AdditionalGroup,
Group,
Shadow,
SshPublicKey,
User,
)
from taskresults.models import TaskResult
EXAMPLE_KEY_1_RFC4716 = """---- BEGIN SSH2 PUBLIC KEY ----
Comment: "1024-bit RSA, converted from OpenSSH by me@example.com"
x-command: /home/me/bin/lock-in-guest.sh
AAAAB3NzaC1yc2EAAAABIwAAAIEA1on8gxCGJJWSRT4uOrR13mUaUk0hRf4RzxSZ1zRb
YYFw8pfGesIFoEuVth4HKyF8k1y4mRUnYHP1XNMNMJl1JcEArC2asV8sHf6zSPVffozZ
5TT4SfsUu/iKy9lUcCfXzwre4WWZSXXcPff+EHtWshahu3WzBdnGxm5Xoi89zcE=
---- END SSH2 PUBLIC KEY ----"""
EXAMPLE_KEY_2_RFC4716 = """---- BEGIN SSH2 PUBLIC KEY ----
Comment: This is my public key for use on \
servers which I don't like.
AAAAB3NzaC1kc3MAAACBAPY8ZOHY2yFSJA6XYC9HRwNHxaehvx5wOJ0rzZdzoSOXxbET
W6ToHv8D1UJ/z+zHo9Fiko5XybZnDIaBDHtblQ+Yp7StxyltHnXF1YLfKD1G4T6JYrdH
YI14Om1eg9e4NnCRleaqoZPF3UGfZia6bXrGTQf3gJq2e7Yisk/gF+1VAAAAFQDb8D5c
vwHWTZDPfX0D2s9Rd7NBvQAAAIEAlN92+Bb7D4KLYk3IwRbXblwXdkPggA4pfdtW9vGf
J0/RHd+NjB4eo1D+0dix6tXwYGN7PKS5R/FXPNwxHPapcj9uL1Jn2AWQ2dsknf+i/FAA
vioUPkmdMc0zuWoSOEsSNhVDtX3WdvVcGcBq9cetzrtOKWOocJmJ80qadxTRHtUAAACB
AN7CY+KKv1gHpRzFwdQm7HK9bb1LAo2KwaoXnadFgeptNBQeSXG1vO+JsvphVMBJc9HS
n24VYtYtsMu74qXviYjziVucWKjjKEb11juqnF0GDlB3VVmxHLmxnAz643WK42Z7dLM5
sY29ouezv4Xz2PuMch5VGPP+CDqzCM4loWgV
---- END SSH2 PUBLIC KEY ----"""
EXAMPLE_KEY_3_RFC4716 = """---- BEGIN SSH2 PUBLIC KEY ----
Comment: DSA Public Key for use with MyIsp
AAAAB3NzaC1kc3MAAACBAPY8ZOHY2yFSJA6XYC9HRwNHxaehvx5wOJ0rzZdzoSOXxbET
W6ToHv8D1UJ/z+zHo9Fiko5XybZnDIaBDHtblQ+Yp7StxyltHnXF1YLfKD1G4T6JYrdH
YI14Om1eg9e4NnCRleaqoZPF3UGfZia6bXrGTQf3gJq2e7Yisk/gF+1VAAAAFQDb8D5c
vwHWTZDPfX0D2s9Rd7NBvQAAAIEAlN92+Bb7D4KLYk3IwRbXblwXdkPggA4pfdtW9vGf
J0/RHd+NjB4eo1D+0dix6tXwYGN7PKS5R/FXPNwxHPapcj9uL1Jn2AWQ2dsknf+i/FAA
vioUPkmdMc0zuWoSOEsSNhVDtX3WdvVcGcBq9cetzrtOKWOocJmJ80qadxTRHtUAAACB
AN7CY+KKv1gHpRzFwdQm7HK9bb1LAo2KwaoXnadFgeptNBQeSXG1vO+JsvphVMBJc9HS
n24VYtYtsMu74qXviYjziVucWKjjKEb11juqnF0GDlB3VVmxHLmxnAz643WK42Z7dLM5
sY29ouezv4Xz2PuMch5VGPP+CDqzCM4loWgV
---- END SSH2 PUBLIC KEY ----"""
EXAMPLE_KEY_4_OPENSSH = "".join((
"ssh-rsa ",
"AAAAB3NzaC1yc2EAAAABIwAAAIEA1on8gxCGJJWSRT4uOrR13mUaUk0hRf4RzxSZ1zRb",
"YYFw8pfGesIFoEuVth4HKyF8k1y4mRUnYHP1XNMNMJl1JcEArC2asV8sHf6zSPVffozZ",
"5TT4SfsUu/iKy9lUcCfXzwre4WWZSXXcPff+EHtWshahu3WzBdnGxm5Xoi89zcE="
))
EXAMPLE_KEY_5_RFC4716_MULTILINE = """---- BEGIN SSH2 PUBLIC KEY ----
Comment: DSA Public Key \\
for use with \\
MyIsp
AAAAB3NzaC1kc3MAAACBAPY8ZOHY2yFSJA6XYC9HRwNHxaehvx5wOJ0rzZdzoSOXxbET
W6ToHv8D1UJ/z+zHo9Fiko5XybZnDIaBDHtblQ+Yp7StxyltHnXF1YLfKD1G4T6JYrdH
YI14Om1eg9e4NnCRleaqoZPF3UGfZia6bXrGTQf3gJq2e7Yisk/gF+1VAAAAFQDb8D5c
vwHWTZDPfX0D2s9Rd7NBvQAAAIEAlN92+Bb7D4KLYk3IwRbXblwXdkPggA4pfdtW9vGf
J0/RHd+NjB4eo1D+0dix6tXwYGN7PKS5R/FXPNwxHPapcj9uL1Jn2AWQ2dsknf+i/FAA
vioUPkmdMc0zuWoSOEsSNhVDtX3WdvVcGcBq9cetzrtOKWOocJmJ80qadxTRHtUAAACB
AN7CY+KKv1gHpRzFwdQm7HK9bb1LAo2KwaoXnadFgeptNBQeSXG1vO+JsvphVMBJc9HS
n24VYtYtsMu74qXviYjziVucWKjjKEb11juqnF0GDlB3VVmxHLmxnAz643WK42Z7dLM5
sY29ouezv4Xz2PuMch5VGPP+CDqzCM4loWgV
---- END SSH2 PUBLIC KEY ----"""
EXAMPLE_KEY_6_RFC4716_EMPTY_LINE = """---- BEGIN SSH2 PUBLIC KEY ----
Comment: DSA Public Key for use with MyIsp
AAAAB3NzaC1kc3MAAACBAPY8ZOHY2yFSJA6XYC9HRwNHxaehvx5wOJ0rzZdzoSOXxbET
W6ToHv8D1UJ/z+zHo9Fiko5XybZnDIaBDHtblQ+Yp7StxyltHnXF1YLfKD1G4T6JYrdH
YI14Om1eg9e4NnCRleaqoZPF3UGfZia6bXrGTQf3gJq2e7Yisk/gF+1VAAAAFQDb8D5c
vwHWTZDPfX0D2s9Rd7NBvQAAAIEAlN92+Bb7D4KLYk3IwRbXblwXdkPggA4pfdtW9vGf
J0/RHd+NjB4eo1D+0dix6tXwYGN7PKS5R/FXPNwxHPapcj9uL1Jn2AWQ2dsknf+i/FAA
vioUPkmdMc0zuWoSOEsSNhVDtX3WdvVcGcBq9cetzrtOKWOocJmJ80qadxTRHtUAAACB
AN7CY+KKv1gHpRzFwdQm7HK9bb1LAo2KwaoXnadFgeptNBQeSXG1vO+JsvphVMBJc9HS
n24VYtYtsMu74qXviYjziVucWKjjKEb11juqnF0GDlB3VVmxHLmxnAz643WK42Z7dLM5
sY29ouezv4Xz2PuMch5VGPP+CDqzCM4loWgV
---- END SSH2 PUBLIC KEY ----"""
Customer = get_user_model()
@override_settings(
@ -29,9 +105,10 @@ class TestCaseWithCeleryTasks(TestCase):
class AdditionalGroupTest(TestCaseWithCeleryTasks):
def setUp(self):
customer = Customer.objects.create(username='test')
self.group1 = Group.objects.create(groupname='test1', gid=1000)
self.user = User.objects.create(
username='test', uid=1000, group=self.group1,
customer=customer, username='test', uid=1000, group=self.group1,
homedir='/home/test', shell='/bin/bash')
def test_clean_primary_group(self):
@ -48,13 +125,11 @@ class AdditionalGroupTest(TestCaseWithCeleryTasks):
def test_save(self):
group2 = Group.objects.create(groupname='test2', gid=1001)
GroupTaskResult.objects.all().delete()
addgroup = AdditionalGroup(user=self.user, group=group2)
addgroup.save()
taskres = GroupTaskResult.objects.all()
taskres = TaskResult.objects.all()
self.assertTrue(len(taskres), 1)
self.assertEqual(taskres[0].task_name, 'add_ldap_user_to_group')
self.assertEqual(taskres[0].group, group2)
self.assertEqual(taskres[0].task_name, 'setup_file_sftp_userdir')
def test_delete(self):
group2 = Group.objects.create(groupname='test2', gid=1001)
@ -86,26 +161,27 @@ class GroupTest(TestCaseWithCeleryTasks):
def test_save(self):
group = Group(gid=10000, groupname='test')
self.assertIs(group.save(), group)
taskres = GroupTaskResult.objects.all()
self.assertEqual(len(taskres), 1)
self.assertEqual(taskres[0].group, group)
self.assertEqual(taskres[0].task_name, 'create_ldap_group')
def test_delete(self):
group = Group.objects.create(gid=10000, groupname='test')
self.assertEqual(len(Group.objects.all()), 1)
self.assertEqual(len(GroupTaskResult.objects.all()), 1)
group.delete()
self.assertEqual(len(Group.objects.all()), 0)
self.assertEqual(len(GroupTaskResult.objects.all()), 0)
self.assertEqual(len(TaskResult.objects.all()), 1)
tr = TaskResult.objects.first()
self.assertEqual(tr.task_name, 'delete_ldap_group')
class ShadowManagerTest(TestCaseWithCeleryTasks):
def setUp(self):
self.customer = Customer.objects.create(username='test')
super(ShadowManagerTest, self).setUp()
def test_create_shadow(self):
user = User(
username='test', uid=1000,
group=Group(gid=1000, groupname='test'),
homedir='/home/test', shell='/bin/fooshell')
customer=self.customer, username='test', uid=1000,
group=Group(gid=1000, groupname='test'), homedir='/home/test',
shell='/bin/fooshell')
shadow = Shadow.objects.create_shadow(user, 'test')
self.assertTrue(sha512_crypt.verify('test', shadow.passwd))
self.assertEqual(shadow.changedays,
@ -119,12 +195,16 @@ class ShadowManagerTest(TestCaseWithCeleryTasks):
class ShadowTest(TestCaseWithCeleryTasks):
def setUp(self):
self.customer = Customer.objects.create(username='test')
super(ShadowTest, self).setUp()
def test___str__(self):
group = Group.objects.create(
groupname='test', gid=1000)
user = User.objects.create(
username='test', uid=1000, group=group, homedir='/home/test',
shell='/bin/bash')
customer=self.customer, username='test', uid=1000, group=group,
homedir='/home/test', shell='/bin/bash')
shadow = Shadow(user=user)
self.assertEqual(str(shadow), 'for user test (1000)')
@ -132,90 +212,13 @@ class ShadowTest(TestCaseWithCeleryTasks):
group = Group.objects.create(
groupname='test', gid=1000)
user = User.objects.create(
username='test', uid=1000, group=group, homedir='/home/test',
shell='/bin/bash')
customer=self.customer, username='test', uid=1000, group=group,
homedir='/home/test', shell='/bin/bash')
shadow = Shadow(user=user)
shadow.set_password('test')
self.assertTrue(sha512_crypt.verify('test', shadow.passwd))
TEST_TASK_UUID = '3120f6a8-2665-4fa3-a785-79efd28bfe92'
TEST_TASK_NAME = 'test.task'
TEST_TASK_RESULT = '4ll y0ur b453 4r3 b3l0ng t0 u5'
class TaskResultTest(TestCase):
def test__set_result_fields_not_ready(self):
mock = MagicMock(task_id=TEST_TASK_UUID, task_name=TEST_TASK_NAME)
mock.ready.return_value = False
tr = DeleteTaskResult.objects.create(mock, TEST_TASK_NAME)
self.assertFalse(tr.is_finished)
self.assertFalse(tr.is_success)
self.assertEqual(tr.state, '')
self.assertEqual(tr.result_body, '')
def test__set_result_fields_ready(self):
mock = MagicMock(task_id=TEST_TASK_UUID, task_name=TEST_TASK_NAME,
state='SUCCESS', result=TEST_TASK_RESULT)
mock.ready.return_value = True
tr = DeleteTaskResult.objects.create(mock, TEST_TASK_NAME)
self.assertTrue(tr.is_finished)
self.assertTrue(tr.is_success)
self.assertEqual(tr.state, 'SUCCESS')
self.assertEqual(tr.result_body, TEST_TASK_RESULT)
def test__set_result_fields_exception(self):
mock = MagicMock(task_id=TEST_TASK_UUID, task_name=TEST_TASK_NAME,
state='FAILURE', result=Exception('Fail'))
mock.ready.return_value = True
tr = DeleteTaskResult.objects.create(mock, TEST_TASK_NAME)
self.assertTrue(tr.is_finished)
self.assertFalse(tr.is_success)
self.assertEqual(tr.state, 'FAILURE')
self.assertEqual(tr.result_body, 'Fail')
@patch('osusers.models.AsyncResult')
def test_update_taskstatus_unfinished(self, asyncres):
mock = MagicMock(task_id=TEST_TASK_UUID, task_name=TEST_TASK_NAME)
mock.ready.return_value = False
tr = DeleteTaskResult.objects.create(mock, TEST_TASK_NAME)
self.assertFalse(tr.is_finished)
mymock = asyncres(TEST_TASK_UUID)
mymock.ready.return_value = True
mymock.state = 'SUCCESS'
mymock.result = TEST_RESULT
tr.update_taskstatus()
mymock.ready.assert_called_with()
self.assertTrue(tr.is_finished)
@patch('osusers.models.AsyncResult')
def test_update_taskstatus_finished(self, asyncres):
mock = MagicMock(task_id=TEST_TASK_UUID, task_name=TEST_TASK_NAME)
mock.ready.return_value = True
mock.state = 'SUCCESS'
mock.result = TEST_RESULT
tr = DeleteTaskResult.objects.create(mock, TEST_TASK_NAME)
self.assertTrue(tr.is_finished)
mymock = asyncres(TEST_TASK_UUID)
tr.update_taskstatus()
self.assertFalse(mymock.ready.called)
self.assertTrue(tr.is_finished)
TEST_RESULT = MagicMock()
TEST_RESULT.task_id = TEST_TASK_UUID
TEST_RESULT.task_name = TEST_TASK_NAME
TEST_RESULT.ready.return_value = False
class TaskResultManagerTest(TestCase):
def test_create(self):
tr = DeleteTaskResult.objects.create(TEST_RESULT, TEST_TASK_NAME)
self.assertIsInstance(tr, DeleteTaskResult)
self.assertEqual(tr.task_uuid, TEST_TASK_UUID)
self.assertEqual(tr.task_name, TEST_TASK_NAME)
@override_settings(
OSUSER_MINUID=10000, OSUSER_MINGID=10000, OSUSER_USERNAME_PREFIX='test',
OSUSER_HOME_BASEPATH='/home', OSUSER_DEFAULT_SHELL='/bin/fooshell'
@ -224,13 +227,18 @@ class UserManagerTest(TestCaseWithCeleryTasks):
def _create_group(self):
return Group.objects.create(gid=10000, groupname='foo')
def setUp(self):
self.customer = Customer.objects.create(username='test')
super(UserManagerTest, self).setUp()
def test_get_next_uid_first(self):
self.assertEqual(User.objects.get_next_uid(), 10000)
def test_get_next_uid_second(self):
User.objects.create(
uid=10010, username='foo', group=self._create_group(),
homedir='/home/foo', shell='/bin/fooshell')
customer=self.customer, uid=10010, username='foo',
group=self._create_group(), homedir='/home/foo',
shell='/bin/fooshell')
self.assertEqual(User.objects.get_next_uid(), 10011)
def test_get_next_username_first(self):
@ -238,22 +246,23 @@ class UserManagerTest(TestCaseWithCeleryTasks):
def test_get_next_username_second(self):
User.objects.create(
uid=10000, username='test01', group=self._create_group(),
homedir='/home/foo', shell='/bin/fooshell')
customer=self.customer, uid=10000, username='test01',
group=self._create_group(), homedir='/home/foo',
shell='/bin/fooshell')
self.assertEqual(User.objects.get_next_username(), 'test02')
def test_get_next_username_gaps(self):
group = self._create_group()
User.objects.create(
uid=10000, username='test01', group=group,
customer=self.customer, uid=10000, username='test01', group=group,
homedir='/home/foo', shell='/bin/fooshell')
User.objects.create(
uid=10002, username='test03', group=group,
customer=self.customer, uid=10002, username='test03', group=group,
homedir='/home/foo', shell='/bin/fooshell')
self.assertEqual(User.objects.get_next_username(), 'test02')
def test_create_user_first(self):
user = User.objects.create_user()
user = User.objects.create_user(customer=self.customer)
self.assertIsInstance(user, User)
self.assertEqual(user.uid, 10000)
self.assertEqual(user.group.gid, 10000)
@ -264,15 +273,16 @@ class UserManagerTest(TestCaseWithCeleryTasks):
self.assertIsNotNone(user.shadow)
def test_create_user_tasks(self):
user = User.objects.create_user()
gtaskres = GroupTaskResult.objects.all()
self.assertEqual(len(gtaskres), 1)
self.assertEqual(gtaskres[0].task_name, 'create_ldap_group')
self.assertEqual(gtaskres[0].group, user.group)
User.objects.create_user(customer=self.customer)
taskres = TaskResult.objects.all()
self.assertEqual(len(taskres), 2)
tasknames = [r.task_name for r in taskres]
self.assertEqual(tasknames.count('setup_file_sftp_userdir'), 1)
self.assertEqual(tasknames.count('setup_file_mail_userdir'), 1)
def test_create_user_second(self):
User.objects.create_user()
user = User.objects.create_user()
User.objects.create_user(customer=self.customer)
user = User.objects.create_user(customer=self.customer)
self.assertIsInstance(user, User)
self.assertEqual(user.uid, 10001)
self.assertEqual(user.group.gid, 10001)
@ -284,7 +294,8 @@ class UserManagerTest(TestCaseWithCeleryTasks):
self.assertEqual(len(User.objects.all()), 2)
def test_create_user_known_password(self):
user = User.objects.create_user(password='foobar')
user = User.objects.create_user(
customer=self.customer, password='foobar')
self.assertIsInstance(user, User)
self.assertEqual(user.uid, 10000)
self.assertEqual(user.group.gid, 10000)
@ -296,7 +307,8 @@ class UserManagerTest(TestCaseWithCeleryTasks):
self.assertTrue(sha512_crypt.verify('foobar', user.shadow.passwd))
def test_create_user_predefined_username(self):
user = User.objects.create_user(username='tester')
user = User.objects.create_user(
customer=self.customer, username='tester')
self.assertIsInstance(user, User)
self.assertEqual(user.uid, 10000)
self.assertEqual(user.group.gid, 10000)
@ -307,7 +319,7 @@ class UserManagerTest(TestCaseWithCeleryTasks):
self.assertIsNotNone(user.shadow)
def test_create_user_commit(self):
user = User.objects.create_user(commit=True)
user = User.objects.create_user(customer=self.customer, commit=True)
self.assertIsInstance(user, User)
self.assertEqual(user.uid, 10000)
self.assertEqual(user.group.gid, 10000)
@ -323,55 +335,176 @@ class UserManagerTest(TestCaseWithCeleryTasks):
OSUSER_HOME_BASEPATH='/home', OSUSER_DEFAULT_SHELL='/bin/fooshell'
)
class UserTest(TestCaseWithCeleryTasks):
def setUp(self):
self.customer = Customer.objects.create_user('test')
super(UserTest, self).setUp()
def test___str__(self):
user = User.objects.create_user()
user = User.objects.create_user(self.customer)
self.assertEqual(str(user), 'test01 (10000)')
def test_set_password(self):
user = User.objects.create_user()
user = User.objects.create_user(self.customer)
self.assertFalse(sha512_crypt.verify('test', user.shadow.passwd))
UserTaskResult.objects.all().delete()
user.set_password('test')
self.assertTrue(sha512_crypt.verify('test', user.shadow.passwd))
taskres = UserTaskResult.objects.all()
self.assertEqual(len(taskres), 1)
self.assertEqual(taskres[0].user, user)
self.assertEqual(taskres[0].task_name, 'create_ldap_user')
def test_save(self):
user = User.objects.create_user()
UserTaskResult.objects.all().delete()
user = User.objects.create_user(self.customer)
TaskResult.objects.all().delete()
user.save()
taskres = UserTaskResult.objects.all()
self.assertEqual(len(taskres), 1)
self.assertEqual(taskres[0].user, user)
self.assertEqual(taskres[0].task_name, 'create_ldap_user')
taskres = TaskResult.objects.all()
self.assertEqual(len(taskres), 2)
task_names = [r.task_name for r in taskres]
self.assertIn('setup_file_sftp_userdir', task_names)
self.assertIn('setup_file_mail_userdir', task_names)
def test_delete_only_user(self):
user = User.objects.create_user()
user = User.objects.create_user(self.customer)
TaskResult.objects.all().delete()
user.delete()
taskres = DeleteTaskResult.objects.all()
self.assertEqual(len(taskres), 2)
self.assertIn('delete_ldap_user',
[r.task_name for r in taskres])
self.assertIn('delete_ldap_group_if_empty',
[r.task_name for r in taskres])
taskres = TaskResult.objects.all()
self.assertEqual(len(taskres), 3)
tasknames = [r.task_name for r in taskres]
self.assertEqual(tasknames.count('delete_file_mail_userdir'), 1)
self.assertEqual(tasknames.count('delete_file_sftp_userdir'), 1)
self.assertEqual(tasknames.count('delete_ldap_group'), 1)
self.assertEqual(len(User.objects.all()), 0)
def test_delete_additional_groups(self):
group1 = Group.objects.create(gid=2000, groupname='group1')
group2 = Group.objects.create(gid=2001, groupname='group2')
user = User.objects.create_user()
user = User.objects.create_user(self.customer)
for group in [group1, group2]:
user.additionalgroup_set.add(
AdditionalGroup.objects.create(user=user, group=group))
TaskResult.objects.all().delete()
user.delete()
taskres = DeleteTaskResult.objects.all()
self.assertEqual(len(taskres), 4)
taskres = TaskResult.objects.all()
self.assertEqual(len(taskres), 3)
tasknames = [t.task_name for t in taskres]
self.assertEqual(tasknames.count('remove_ldap_user_from_group'), 2)
self.assertEqual(tasknames.count('delete_ldap_user'), 1)
self.assertEqual(tasknames.count('delete_ldap_group_if_empty'), 1)
self.assertEqual(tasknames.count('delete_file_mail_userdir'), 1)
self.assertEqual(tasknames.count('delete_file_sftp_userdir'), 1)
self.assertEqual(tasknames.count('delete_ldap_group'), 1)
self.assertEqual(len(User.objects.all()), 0)
self.assertEqual(len(AdditionalGroup.objects.all()), 0)
def test_is_sftp_user(self):
user = User.objects.create_user(self.customer)
self.assertFalse(user.is_sftp_user())
sftp_group = Group.objects.create(
gid=2000, groupname=settings.OSUSER_SFTP_GROUP)
user.additionalgroup_set.add(
AdditionalGroup.objects.create(user=user, group=sftp_group))
self.assertTrue(user.is_sftp_user())
class SshPublicKeyManagerTest(TestCaseWithCeleryTasks):
def test_parse_keytext_rfc4716_1(self):
res = SshPublicKey.objects.parse_keytext(EXAMPLE_KEY_1_RFC4716)
self.assertEqual(len(res), 3)
self.assertGreater(len(res[1]), 40)
self.assertEqual(res[0], 'ssh-rsa')
self.assertEqual(
res[2], '"1024-bit RSA, converted from OpenSSH by me@example.com"')
def test_parse_keytext_rfc4716_2(self):
res = SshPublicKey.objects.parse_keytext(EXAMPLE_KEY_2_RFC4716)
self.assertEqual(len(res), 3)
self.assertEqual(res[0], 'ssh-dss')
self.assertGreater(len(res[1]), 40)
self.assertEqual(
res[2],
"This is my public key for use on servers which I don't like.")
def test_parse_keytext_rfc4716_3(self):
res = SshPublicKey.objects.parse_keytext(EXAMPLE_KEY_3_RFC4716)
self.assertEqual(len(res), 3)
self.assertEqual(res[0], 'ssh-dss')
self.assertGreater(len(res[1]), 40)
self.assertEqual(res[2], "DSA Public Key for use with MyIsp")
def test_parse_keytext_openssh(self):
res = SshPublicKey.objects.parse_keytext(EXAMPLE_KEY_4_OPENSSH)
self.assertEquals(len(res), 3)
self.assertEqual(res[0], 'ssh-rsa')
self.assertGreater(len(res[1]), 40)
self.assertEqual(res[2], '')
def test_parse_keytext_invalid(self):
with self.assertRaises(ValueError):
SshPublicKey.objects.parse_keytext("\r\n".join(["xx"]*10))
def test_parse_keytext_empty_line(self):
res = SshPublicKey.objects.parse_keytext(
EXAMPLE_KEY_6_RFC4716_EMPTY_LINE)
self.assertEqual(len(res), 3)
self.assertEqual(res[0], 'ssh-dss')
self.assertGreater(len(res[1]), 40)
self.assertEqual(res[2], "DSA Public Key for use with MyIsp")
def test_parse_keytext_multiline_comment(self):
res = SshPublicKey.objects.parse_keytext(
EXAMPLE_KEY_5_RFC4716_MULTILINE)
self.assertEqual(len(res), 3)
self.assertEqual(res[0], 'ssh-dss')
self.assertGreater(len(res[1]), 40)
self.assertEqual(res[2], "DSA Public Key for use with MyIsp")
def test_create_ssh_public_key(self):
customer = Customer.objects.create_user('test')
user = User.objects.create_user(customer)
key = SshPublicKey.objects.create_ssh_public_key(
user, EXAMPLE_KEY_4_OPENSSH)
self.assertIsInstance(key, SshPublicKey)
self.assertEqual(key.user, user)
self.assertEqual(key.algorithm, 'ssh-rsa')
self.assertEqual(key.data, EXAMPLE_KEY_4_OPENSSH.split()[1])
self.assertEqual(key.comment, '')
class SshPublicKeyTest(TestCaseWithCeleryTasks):
def setUp(self):
super(SshPublicKeyTest, self).setUp()
customer = Customer.objects.create_user('test')
self.user = User.objects.create_user(customer)
TaskResult.objects.all().delete()
def test__str__rfc4716(self):
res = SshPublicKey.objects.create_ssh_public_key(
self.user, EXAMPLE_KEY_3_RFC4716)
self.assertEqual(
str(res), 'ssh-dss AAAAB3NzaC1kc3MAAACBAPY8ZOHY2yFSJA6XYC9HRwNHxae'
'hvx5wOJ0rzZdzoSOXxbETW6ToHv8D1UJ/z+zHo9Fiko5XybZnDIaBDHtblQ+Yp7St'
'xyltHnXF1YLfKD1G4T6JYrdHYI14Om1eg9e4NnCRleaqoZPF3UGfZia6bXrGTQf3g'
'Jq2e7Yisk/gF+1VAAAAFQDb8D5cvwHWTZDPfX0D2s9Rd7NBvQAAAIEAlN92+Bb7D4'
'KLYk3IwRbXblwXdkPggA4pfdtW9vGfJ0/RHd+NjB4eo1D+0dix6tXwYGN7PKS5R/F'
'XPNwxHPapcj9uL1Jn2AWQ2dsknf+i/FAAvioUPkmdMc0zuWoSOEsSNhVDtX3WdvVc'
'GcBq9cetzrtOKWOocJmJ80qadxTRHtUAAACBAN7CY+KKv1gHpRzFwdQm7HK9bb1LA'
'o2KwaoXnadFgeptNBQeSXG1vO+JsvphVMBJc9HSn24VYtYtsMu74qXviYjziVucWK'
'jjKEb11juqnF0GDlB3VVmxHLmxnAz643WK42Z7dLM5sY29ouezv4Xz2PuMch5VGPP'
'+CDqzCM4loWgV DSA Public Key for use with MyIsp')
def test__str__openssh(self):
res = SshPublicKey.objects.create_ssh_public_key(
self.user, EXAMPLE_KEY_4_OPENSSH)
self.assertEqual(str(res), EXAMPLE_KEY_4_OPENSSH)
def test_call_tasks_on_save(self):
SshPublicKey.objects.create_ssh_public_key(
self.user, EXAMPLE_KEY_4_OPENSSH)
taskresults = TaskResult.objects.all()
self.assertEqual(len(taskresults), 1)
self.assertEqual(
taskresults[0].task_name, 'set_file_ssh_authorized_keys')
def test_call_tasks_on_delete(self):
key = SshPublicKey.objects.create_ssh_public_key(
self.user, EXAMPLE_KEY_4_OPENSSH)
TaskResult.objects.all().delete()
key.delete()
taskresults = TaskResult.objects.all()
self.assertEqual(len(taskresults), 1)
self.assertEqual(
taskresults[0].task_name, 'set_file_ssh_authorized_keys')

View file

@ -6,11 +6,25 @@ from __future__ import absolute_import, unicode_literals
from django.conf.urls import patterns, url
from .views import SetOsUserPassword
from .views import (
AddSshPublicKey,
DeleteSshPublicKey,
EditSshPublicKeyComment,
ListSshPublicKeys,
SetOsUserPassword,
)
urlpatterns = patterns(
'',
url(r'^(?P<slug>[\w0-9@.+-_]+)/setpassword$', SetOsUserPassword.as_view(),
name='set_osuser_password'),
url(r'^(?P<package>\d+)/ssh-keys/$', ListSshPublicKeys.as_view(),
name='list_ssh_keys'),
url(r'^(?P<package>\d+)/ssh-keys/add$', AddSshPublicKey.as_view(),
name='add_ssh_key'),
url(r'^(?P<package>\d+)/ssh-keys/(?P<pk>\d+)/edit-comment$',
EditSshPublicKeyComment.as_view(), name='edit_ssh_key_comment'),
url(r'^(?P<package>\d+)/ssh-keys/(?P<pk>\d+)/delete$',
DeleteSshPublicKey.as_view(), name='delete_ssh_key'),
)

View file

@ -4,15 +4,29 @@ This module defines the views for gnuviechadmin operating system user handling.
"""
from __future__ import unicode_literals, absolute_import
from django.core.urlresolvers import reverse
from django.shortcuts import redirect
from django.views.generic import UpdateView
from django.views.generic import (
CreateView,
DeleteView,
ListView,
UpdateView,
)
from django.utils.translation import ugettext as _
from django.contrib import messages
from gvacommon.viewmixins import StaffOrSelfLoginRequiredMixin
from gvawebcore.views import HostingPackageAndCustomerMixin
from .forms import ChangeOsUserPasswordForm
from .models import User
from .forms import (
AddSshPublicKeyForm,
ChangeOsUserPasswordForm,
EditSshPublicKeyCommentForm,
)
from .models import (
SshPublicKey,
User,
)
class SetOsUserPassword(StaffOrSelfLoginRequiredMixin, UpdateView):
@ -43,3 +57,131 @@ class SetOsUserPassword(StaffOrSelfLoginRequiredMixin, UpdateView):
username=osuser.username
))
return redirect(osuser.customerhostingpackage)
class AddSshPublicKey(
HostingPackageAndCustomerMixin, StaffOrSelfLoginRequiredMixin, CreateView
):
"""
This view is used to add an SSH key for an existing hosting account's
operating system user.
"""
model = SshPublicKey
context_object_name = 'key'
template_name_suffix = '_create'
form_class = AddSshPublicKeyForm
def get_form_kwargs(self):
kwargs = super(AddSshPublicKey, self).get_form_kwargs()
kwargs['hostingpackage'] = self.get_hosting_package()
return kwargs
def get_context_data(self, **kwargs):
context = super(AddSshPublicKey, self).get_context_data(**kwargs)
context.update({
'customer': self.get_customer_object(),
'osuser': self.get_hosting_package().osuser.username,
})
return context
def form_valid(self, form):
key = form.save()
messages.success(
self.request,
_('Successfully added new {algorithm} SSH public key').format(
algorithm=key.algorithm)
)
return redirect(self.get_hosting_package())
class ListSshPublicKeys(
HostingPackageAndCustomerMixin, StaffOrSelfLoginRequiredMixin, ListView
):
"""
This view is used for showing the list of :py:class:`SSH public keys
<osusers.models.SshPublicKey>` assigned to the hosting package specified
via URL parameter 'pattern'.
"""
model = SshPublicKey
context_object_name = 'keys'
def get_context_data(self, **kwargs):
context = super(ListSshPublicKeys, self).get_context_data(**kwargs)
context.update({
'hostingpackage': self.get_hosting_package(),
'customer': self.get_customer_object(),
'osuser': self.get_hosting_package().osuser.username,
})
return context
class DeleteSshPublicKey(
HostingPackageAndCustomerMixin, StaffOrSelfLoginRequiredMixin, DeleteView
):
"""
This view is used for delete confirmation of a :py:class:`SSH public key
<osusers.models.SshPublicKey>`.
"""
model = SshPublicKey
context_object_name = 'key'
def get_queryset(self):
return super(DeleteSshPublicKey, self).get_queryset().filter(
user=self.get_hosting_package().osuser)
def get_context_data(self, **kwargs):
context = super(DeleteSshPublicKey, self).get_context_data(**kwargs)
context.update({
'hostingpackage': self.get_hosting_package(),
'customer': self.get_customer_object(),
'osuser': self.get_hosting_package().osuser.username,
})
return context
def get_success_url(self):
return reverse(
'list_ssh_keys', kwargs={'package': self.get_hosting_package().id}
)
class EditSshPublicKeyComment(
HostingPackageAndCustomerMixin, StaffOrSelfLoginRequiredMixin, UpdateView
):
"""
This view is used for editing the comment field of a :py:class:`SSH public
key <osusers.models.SshPublicKey>`.
"""
model = SshPublicKey
context_object_name = 'key'
fields = ['comment']
template_name_suffix = '_edit_comment'
form_class = EditSshPublicKeyCommentForm
def get_queryset(self):
return super(EditSshPublicKeyComment, self).get_queryset().filter(
user=self.get_hosting_package().osuser)
def get_form_kwargs(self):
kwargs = super(EditSshPublicKeyComment, self).get_form_kwargs()
kwargs['hostingpackage'] = self.get_hosting_package()
return kwargs
def get_context_data(self, **kwargs):
context = super(EditSshPublicKeyComment, self).get_context_data(
**kwargs)
context.update({
'hostingpackage': self.get_hosting_package(),
'customer': self.get_customer_object(),
'osuser': self.get_hosting_package().osuser.username,
})
return context
def get_success_url(self):
return reverse(
'list_ssh_keys', kwargs={'package': self.get_hosting_package().id}
)

View file

@ -0,0 +1,54 @@
from __future__ import absolute_import, unicode_literals
from django.test import TestCase
from mock import patch, MagicMock
from taskresults.models import TaskResult
TEST_TASK_UUID = '3120f6a8-2665-4fa3-a785-79efd28bfe92'
TEST_TASK_NAME = 'test.task'
TEST_TASK_RESULT = '4ll y0ur b453 4r3 b3l0ng t0 u5'
class TaskResultTest(TestCase):
@patch('taskresults.models.app')
def test_update_taskstatus_unfinished(self, app):
mock = MagicMock(id=TEST_TASK_UUID, task_name=TEST_TASK_NAME)
mock.ready.return_value = False
tr = TaskResult.objects.create_task_result(mock, TEST_TASK_NAME)
self.assertFalse(tr.finished)
mymock = app.AsyncResult(TEST_TASK_UUID)
mymock.state = 'SUCCESS'
mymock.get.return_value = TEST_RESULT
tr.fetch_result()
mymock.get.assert_called_with(no_ack=True, timeout=1)
self.assertTrue(tr.finished)
@patch('taskresults.models.app')
def test_update_taskstatus_finished(self, app):
mock = MagicMock(id=TEST_TASK_UUID, task_name=TEST_TASK_NAME)
mock.ready.return_value = True
mock.state = 'SUCCESS'
mock.result = TEST_RESULT
tr = TaskResult.objects.create_task_result(mock, TEST_TASK_NAME)
tr.fetch_result()
self.assertTrue(tr.finished)
mymock = app.AsyncResult(TEST_TASK_UUID)
tr.fetch_result()
self.assertEqual(mymock.get.call_count, 1)
self.assertTrue(tr.finished)
TEST_RESULT = MagicMock()
TEST_RESULT.task_id = TEST_TASK_UUID
TEST_RESULT.task_name = TEST_TASK_NAME
TEST_RESULT.ready.return_value = False
class TaskResultManagerTest(TestCase):
def test_create_task_result(self):
mock = MagicMock(id=TEST_TASK_UUID)
tr = TaskResult.objects.create_task_result(mock, TEST_TASK_NAME)
self.assertIsInstance(tr, TaskResult)
self.assertEqual(tr.task_id, TEST_TASK_UUID)
self.assertEqual(tr.task_name, TEST_TASK_NAME)

View file

@ -82,11 +82,12 @@
<h1>{% block page_title %}Example Base Template{% endblock page_title %}</h1>
{% if messages %}
<ul class="list-unstyled">
{% for message in messages %}
<li{% if message.tags %} class="{{ message.tags }}"{% endif %}>{{ message }}</li>
{% endfor %}
</ul>
{% for message in messages %}
<div class="alert alert-dismissible {{ message.tags }}" role="alert">
<button type="close" class="close" data-dismiss="alert" aria-label="{% trans "Close" %}"><span aria-hidden="true">&times;</span></button>
{{ message }}
</div>
{% endfor %}
{% endif %}
{% block content %}

View file

@ -39,7 +39,7 @@
<dd>{% blocktrans with num=hostingpackage.used_mailbox_count total=hostingpackage.mailbox_count %}{{ num }} of {{ total }} in use{% endblocktrans %} <span class="glyphicon
glyphicon-info-sign" title="{% blocktrans with mailboxcount=hostingpackage.mailboxcount %}The package provides {{ mailboxcount }} mailboxes the difference comes from mailbox options.{% endblocktrans %}"></span></dd>
<dt>{% if osuser.is_sftp_user %}{% trans "SFTP username" %}{% else %}{% trans "SSH/SFTP username" %}{% endif %}</dt>
<dd>{{ osuser.username }}</dd>
<dd>{{ osuser.username }}{% if sshkeys %} <a href="{% url 'list_ssh_keys' package=hostingpackage.id %}" class="badge" title="{% blocktrans count counter=sshkeys|length %}There is an SSH public key set for this user.{% plural %}There are {{ counter }} SSH public keys set for this user.{% endblocktrans %}"><i class="fa fa-key"></i> {{ sshkeys|length }}</a>{% endif %}</dd>
<dt>{% trans "Upload server" %}</dt>
<dd>{{ uploadserver }}</dd>
</dl>
@ -68,6 +68,7 @@
<ul class="list-group">
<li class="list-group-item"><a href="#" title="{% trans "Edit Hosting Package Description" %}">{% trans "Edit description" %}</a></li>
<li class="list-group-item"><a href="{% url "set_osuser_password" slug=osuser.username %}">{% if osuser.is_sftp_user %}{% trans "Set SFTP password" %}{% else %}{% trans "Set SSH/SFTP password" %}{% endif %}</a></li>
<li class="list-group-item"><a href="{% url "add_ssh_key" package=hostingpackage.id %}" title="{% blocktrans %}Add an SSH public key that can be used as an alternative for password{% endblocktrans %}">{% trans "Add SSH public key" %}</a></li>
</ul>
</div>
</div>

View file

@ -0,0 +1,35 @@
{% extends "osusers/base.html" %}
{% load i18n crispy_forms_tags %}
{% block title %}{{ block.super }} - {% spaceless %}
{% if user == customer %}
{% blocktrans %}Delete SSH Public Key for Operating System User {{ osuser }}{% endblocktrans %}
{% else %}
{% blocktrans with full_name=customer.get_full_name %}Delete SSH Public Key for Operating System User {{ osuser }} of Customer {{ full_name }}{% endblocktrans %}
{% endif %}
{% endspaceless %}{% endblock title %}
{% block page_title %}{% spaceless %}
{% if user == customer %}
{% blocktrans %}Delete SSH Public Key <small>for Operating System User {{ osuser }}</small>{% endblocktrans %}
{% else %}
{% blocktrans with full_name=customer.get_full_name %}Delete SSH Public Key <small>for Operating System User {{ osuser }} of Customer {{ full_name }}</small>{% endblocktrans %}
{% endif %}
{% endspaceless %}{% endblock page_title %}
{% block content %}
<div class="panel panel-warning">
<div class="panel-heading">
{% blocktrans with algorithm=key.algorithm %}Do you really want to delete the {{ algorithm }} SSH public key?{% endblocktrans %}
</div>
<div class="panel-body form">
<p >{% blocktrans %}When you confirm the deletion of this key you will no longer be able to use the corresponding private key for authentication.{% endblocktrans %}</p>
<pre>{{ key }}</pre>
<form action="{% url 'delete_ssh_key' package=hostingpackage.id pk=key.id %}" method="post">
{% csrf_token %}
<input class="btn btn-warning" type="submit" value="{% trans "Yes, do it!" %}" />
<a class="btn btn-default" href="{% url 'list_ssh_keys' package=hostingpackage.id %}" title="{% trans "Cancel and go back to the SSH key list" %}">{% trans "Cancel" %}</a>
</form>
</div>
</div>
{% endblock content %}

View file

@ -0,0 +1,30 @@
{% extends "osusers/base.html" %}
{% load i18n crispy_forms_tags %}
{% block title %}{{ block.super }} - {% spaceless %}
{% if user == customer %}
{% blocktrans %}Add new SSH Public Key for Operating System User {{ osuser }}{% endblocktrans %}
{% else %}
{% blocktrans with full_name=customer.get_full_name %}Add a new SSH Public Key for Operating System User {{ osuser }} of Customer {{ full_name }}{% endblocktrans %}
{% endif %}
{% endspaceless %}{% endblock title %}
{% block page_title %}{% spaceless %}
{% if user == customer %}
{% blocktrans %}Add new SSH Public Key <small>for Operating System User {{ osuser }}</small>{% endblocktrans %}
{% else %}
{% blocktrans with full_name=customer.get_full_name %}Add a new SSH Public Key <small>for Operating System User {{ osuser }} of Customer {{ full_name }}</small>{% endblocktrans %}
{% endif %}
{% endspaceless %}{% endblock page_title %}
{% block content %}
{% crispy form %}
{% endblock content %}
{% block extra_js %}
<script type="text/javascript">
$(document).ready(function() {
$('textarea').first().focus();
});
</script>
{% endblock extra_js %}

View file

@ -0,0 +1,30 @@
{% extends "osusers/base.html" %}
{% load i18n crispy_forms_tags %}
{% block title %}{{ block.super }} - {% spaceless %}
{% if user == customer %}
{% blocktrans %}Edit Comment of SSH Public Key for Operating System User {{ osuser }}{% endblocktrans %}
{% else %}
{% blocktrans with full_name=customer.get_full_name %}Edit Comment of SSH Public Key for Operating System User {{ osuser }} of Customer {{ full_name }}{% endblocktrans %}
{% endif %}
{% endspaceless %}{% endblock title %}
{% block page_title %}{% spaceless %}
{% if user == customer %}
{% blocktrans %}Edit Comment of Public Key <small>for Operating System User {{ osuser }}</small>{% endblocktrans %}
{% else %}
{% blocktrans with full_name=customer.get_full_name %}Edit Comment of SSH Public Key <small>for Operating System User {{ osuser }} of Customer {{ full_name }}</small>{% endblocktrans %}
{% endif %}
{% endspaceless %}{% endblock page_title %}
{% block content %}
{% crispy form %}
{% endblock content %}
{% block extra_js %}
<script type="text/javascript">
$(document).ready(function() {
$('input[type=text]').first().focus().select();
});
</script>
{% endblock extra_js %}

View file

@ -0,0 +1,47 @@
{% extends "osusers/base.html" %}
{% load i18n crispy_forms_tags %}
{% block title %}{{ block.super }} - {% spaceless %}
{% if user == customer %}
{% blocktrans %}SSH Public Keys for Operating System User {{ osuser }}{% endblocktrans %}
{% else %}
{% blocktrans with full_name=customer.get_full_name %}SSH Public Keys for Operating System User {{ osuser }} of Customer {{ full_name }}{% endblocktrans %}
{% endif %}
{% endspaceless %}{% endblock title %}
{% block page_title %}{% spaceless %}
{% if user == customer %}
{% blocktrans %}SSH Public Keys <small>for Operating System User {{ osuser }}</small>{% endblocktrans %}
{% else %}
{% blocktrans with full_name=customer.get_full_name %}SSH Public Keys <small>for Operating System User {{ osuser }} of Customer {{ full_name }}</small>{% endblocktrans %}
{% endif %}
{% endspaceless %}{% endblock page_title %}
{% block content %}
{% if keys %}
<table class="table">
<thead>
<tr>
<th class="name-column">{% trans "Algorithm" %}</th>
<th>{% trans "Comment" %}</th>
<th title="{% trans "SSH public key actions" %}" class="actions-column"><span class="sr-only">{% trans "Actions" %}</span></th>
</tr>
</thead>
<tbody>
{% for key in keys %}
<tr>
<td>{{ key.algorithm }}</td>
<td>{{ key.comment }}</td>
<td>
<a href="{% url 'delete_ssh_key' package=hostingpackage.id pk=key.id %}" title="{% trans "Delete this SSH public key" %}"><i class="glyphicon glyphicon-trash"></i><span class="sr-only"> {% trans "Delete" %}</span></a>
<a href="{% url 'edit_ssh_key_comment' package=hostingpackage.id pk=key.id %}" title="{% trans "Edit this SSH public key's comment" %}"><i class="glyphicon glyphicon-pencil"></i><span class="sr-only"> {% trans "Edit Comment" %}</span></a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<p class="bg-warning">{% trans "There are now SSH public keys set for this operating system user yet." %}</p>
{% endif %}
<p><a href="{% url 'add_ssh_key' package=hostingpackage.id %}" class="btn btn-primary">{% trans "Add SSH public key" %}</a></p>
{% endblock content %}

View file

@ -1,4 +1,4 @@
Django==1.7.3
Django==1.7.4
bpython==0.13.1
django-braces==1.4.0
django-model-utils==2.2