Merge branch 'feature/customer-users'

* feature/customer-users:
  add german translation
  add code documentation to docs
  document osusers code
  add customer field to osusers.models.User
  allow generated password for new osusers
This commit is contained in:
Jan Dittberner 2014-12-27 22:59:11 +01:00
commit 42b1b07cc4
13 changed files with 868 additions and 38 deletions

1
.gitignore vendored
View file

@ -43,3 +43,4 @@ Desktop.ini
htmlcov/ htmlcov/
tags tags
_build/ _build/
*.mo

View file

@ -1,6 +1,11 @@
Changelog Changelog
========= =========
* :feature:`-` add a `customer` field to :py:class:`osusers.models.User`
* :feature:`-` allow empty password input in
:py:class:`osusers.admin.UserCreationForm` to allow generated passwords for
new users
* :release:`0.3.0 <2014-12-27>` * :release:`0.3.0 <2014-12-27>`
* :feature:`-` call create/delete mailbox tasks when saving/deleting mailboxes * :feature:`-` call create/delete mailbox tasks when saving/deleting mailboxes
* :support:`-` use celery routers from gvacommon * :support:`-` use celery routers from gvacommon

150
docs/code.rst Normal file
View file

@ -0,0 +1,150 @@
******************
Code documentation
******************
.. index:: Django
gva is implemented as `Django`_ project and provides a frontend for
administrators and customers.
.. _Django: https://www.djangoproject.com/
The project module :py:mod:`gnuviechadmin`
==========================================
.. automodule:: gnuviechadmin
:py:mod:`celery <gnuviechadmin.celery>`
---------------------------------------
.. automodule:: gnuviechadmin.celery
:members:
:py:mod:`urls <gnuviechadmin.urls>`
-----------------------------------
.. automodule:: gnuviechadmin.urls
:py:mod:`wsgi <gnuviechadmin.wsgi>`
-----------------------------------
.. automodule:: gnuviechadmin.wsgi
:members:
:py:mod:`settings <gnuviechadmin.settings>`
-------------------------------------------
.. automodule:: gnuviechadmin.settings
:py:mod:`base <gnuviechadmin.settings.base>`
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.. automodule:: gnuviechadmin.settings.base
:members:
:py:mod:`local <gnuviechadmin.settings.local>`
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.. automodule:: gnuviechadmin.settings.local
:py:mod:`production <gnuviechadmin.settings.production>`
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.. automodule:: gnuviechadmin.settings.production
:py:mod:`test <gnuviechadmin.settings.test>`
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.. automodule:: gnuviechadmin.settings.test
:py:mod:`gvacommon`
===================
This module is imported from a separate git project via git subtree and
provides some functionality that is common to all gnuviechadmin subprojects.
.. automodule:: gvacommon
:py:mod:`celeryrouters <gvacommon.celeryrouters>`
-------------------------------------------------
.. automodule:: gvacommon.celeryrouters
:members:
:undoc-members:
:py:mod:`managemails` app
=========================
.. automodule:: managemails
:py:mod:`admin <managemails.admin>`
-----------------------------------
.. automodule:: managemails.admin
:members:
:py:mod:`models <managemails.models>`
-------------------------------------
.. automodule:: managemails.models
:members:
:py:mod:`osusers` app
=====================
.. automodule:: osusers
:py:mod:`admin <osusers.admin>`
-------------------------------
.. automodule:: osusers.admin
:members:
:py:mod:`apps <osusers.apps>`
-----------------------------
.. automodule:: osusers.apps
:members:
:py:mod:`models <osusers.models>`
---------------------------------
.. automodule:: osusers.models
:members:
:py:mod:`tasks <osusers.tasks>`
-------------------------------
.. automodule:: osusers.tasks
.. autotask:: osusers.tasks.add_ldap_user_to_group
.. autotask:: osusers.tasks.create_file_mailbox
.. autotask:: osusers.tasks.create_ldap_group
.. autotask:: osusers.tasks.create_ldap_user
.. autotask:: osusers.tasks.delete_file_mail_userdir
.. autotask:: osusers.tasks.delete_file_mailbox
.. autotask:: osusers.tasks.delete_file_sftp_userdir
.. autotask:: osusers.tasks.delete_ldap_group_if_empty
.. autotask:: osusers.tasks.delete_ldap_user
.. autotask:: osusers.tasks.remove_ldap_user_from_group
.. autotask:: osusers.tasks.setup_file_mail_userdir
.. autotask:: osusers.tasks.setup_file_sftp_userdir

View file

@ -13,13 +13,18 @@
# All configuration values have a default; values that are commented out # All configuration values have a default; values that are commented out
# serve to show the default. # serve to show the default.
#import sys import sys
#import os import os
import django
# If extensions (or modules to document with autodoc) are in another directory, # If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the # add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here. # documentation root, use os.path.abspath to make it absolute, like shown here.
#sys.path.insert(0, os.path.abspath('.')) sys.path.insert(0, os.path.abspath(os.path.join('..', 'gnuviechadmin')))
os.environ['GVA_SITE_ADMINMAIL'] = 'admin@gva.example.org'
django.setup()
# -- General configuration ----------------------------------------------------- # -- General configuration -----------------------------------------------------

View file

@ -14,6 +14,7 @@ Contents:
install install
deploy deploy
tests tests
code
changelog changelog

View file

@ -0,0 +1,60 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
msgid ""
msgstr ""
"Project-Id-Version: managemails\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2014-12-27 22:45+0100\n"
"PO-Revision-Date: 2014-12-27 22:57+0100\n"
"Last-Translator: Jan Dittberner <jan@dittberner.info>\n"
"Language-Team: de <de@li.org>\n"
"Language: de\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
"X-Generator: Poedit 1.6.10\n"
"X-Poedit-SourceCharset: UTF-8\n"
#: admin.py:14
msgid "Passwords don't match"
msgstr "Passwörter stimmen nicht überein"
#: admin.py:21 tests/test_admin.py:37
msgid "Hash"
msgstr "Hash-Code"
#: admin.py:44
msgid "Password"
msgstr "Passwort"
#: admin.py:46
msgid "Password (again)"
msgstr "Passwortwiederholung"
#: admin.py:100
msgid "Activate"
msgstr "Aktivieren"
#: admin.py:101
msgid "Deactivate"
msgstr "Deaktivieren"
#: models.py:51
msgid "Mailbox"
msgstr "Postfach"
#: models.py:52
msgid "Mailboxes"
msgstr "Postfächer"
#: models.py:76
msgid "Mail address"
msgstr "E-Mailadresse"
#: models.py:77
msgid "Mail addresses"
msgstr "E-Mailadressen"

View file

@ -0,0 +1,5 @@
"""
This app is for managing operating system users and groups.
"""
default_app_config = 'osusers.apps.OsusersAppConfig'

View file

@ -1,3 +1,7 @@
"""
This module contains the Django admin classes of the :py:mod:`osusers` app.
"""
from django import forms from django import forms
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from django.contrib import admin from django.contrib import admin
@ -10,13 +14,24 @@ from .models import (
) )
PASSWORD_MISMATCH_ERROR = _("Passwords don't match") PASSWORD_MISMATCH_ERROR = _("Passwords don't match")
"""
Error message for non matching passwords.
"""
class AdditionalGroupInline(admin.TabularInline): class AdditionalGroupInline(admin.TabularInline):
"""
Inline for :py:class:`osusers.models.AdditionalGroup` instances.
"""
model = AdditionalGroup model = AdditionalGroup
class ShadowInline(admin.TabularInline): class ShadowInline(admin.TabularInline):
"""
Inline for :py:class:`osusers.models.ShadowInline` instances.
"""
model = Shadow model = Shadow
readonly_fields = ['passwd'] readonly_fields = ['passwd']
can_delete = False can_delete = False
@ -24,22 +39,30 @@ class ShadowInline(admin.TabularInline):
class UserCreationForm(forms.ModelForm): class UserCreationForm(forms.ModelForm):
""" """
A form for creating system users. A form for creating :py:class:`operating system users
<osusers.models.User>`.
""" """
password1 = forms.CharField(label=_('Password'), password1 = forms.CharField(
widget=forms.PasswordInput) label=_('Password'), widget=forms.PasswordInput,
password2 = forms.CharField(label=_('Password (again)'), required=False,
widget=forms.PasswordInput) )
password2 = forms.CharField(
label=_('Password (again)'), widget=forms.PasswordInput,
required=False,
)
class Meta: class Meta:
model = User model = User
fields = [] fields = ['customer']
def clean_password2(self): def clean_password2(self):
""" """
Check that the two password entries match. Check that the two password entries match.
:return: the validated password
:rtype: str or None
""" """
password1 = self.cleaned_data.get('password1') password1 = self.cleaned_data.get('password1')
password2 = self.cleaned_data.get('password2') password2 = self.cleaned_data.get('password2')
@ -51,8 +74,13 @@ class UserCreationForm(forms.ModelForm):
""" """
Save the provided password in hashed format. Save the provided password in hashed format.
:param boolean commit: whether to save the created user
:return: user instance
:rtype: :py:class:`osusers.models.User`
""" """
user = User.objects.create_user( user = User.objects.create_user(
customer=self.cleaned_data['customer'],
password=self.cleaned_data['password1'], commit=commit) password=self.cleaned_data['password1'], commit=commit)
return user return user
@ -60,10 +88,16 @@ class UserCreationForm(forms.ModelForm):
""" """
No additional groups are created when this form is saved, so this No additional groups are created when this form is saved, so this
method just does nothing. method just does nothing.
""" """
class UserAdmin(admin.ModelAdmin): class UserAdmin(admin.ModelAdmin):
"""
Admin class for working with :py:class:`operating system users
<osusers.models.User>`.
"""
actions = ['perform_delete_selected'] actions = ['perform_delete_selected']
add_form = UserCreationForm add_form = UserCreationForm
inlines = [AdditionalGroupInline, ShadowInline] inlines = [AdditionalGroupInline, ShadowInline]
@ -71,13 +105,20 @@ class UserAdmin(admin.ModelAdmin):
add_fieldsets = ( add_fieldsets = (
(None, { (None, {
'classes': ('wide',), 'classes': ('wide',),
'fields': ('password1', 'password2')}), 'fields': ('customer', 'password1', 'password2')}),
) )
def get_form(self, request, obj=None, **kwargs): def get_form(self, request, obj=None, **kwargs):
""" """
Use special form during user creation. Use special form during user creation.
:param request: the current HTTP request
:param obj: either a :py:class:`User <osusers.models.User>` instance or
None for a new user
:param kwargs: keyword arguments to be passed to
:py:meth:`django.contrib.admin.ModelAdmin.get_form`
:return: form instance
""" """
defaults = {} defaults = {}
if obj is None: if obj is None:
@ -89,16 +130,47 @@ class UserAdmin(admin.ModelAdmin):
return super(UserAdmin, self).get_form(request, obj, **defaults) return super(UserAdmin, self).get_form(request, obj, **defaults)
def get_readonly_fields(self, request, obj=None): def get_readonly_fields(self, request, obj=None):
"""
Make sure that uid is not editable for existing users.
:param request: the current HTTP request
:param obj: either a :py:class:`User <osusers.models.User>` instance or
None for a new user
:return: a list of fields
:rtype: list
"""
if obj: if obj:
return ['uid'] return ['uid']
return [] return []
def perform_delete_selected(self, request, queryset): def perform_delete_selected(self, request, queryset):
"""
Action to delete a list of selected users.
This action calls the delete method of each selected user in contrast
to the default `delete_selected`.
:param request: the current HTTP request
:param queryset: Django ORM queryset representing the selected users
"""
for user in queryset.all(): for user in queryset.all():
user.delete() user.delete()
perform_delete_selected.short_description = _('Delete selected users') perform_delete_selected.short_description = _('Delete selected users')
def get_actions(self, request): def get_actions(self, request):
"""
Get the available actions for users.
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(UserAdmin, self).get_actions(request) actions = super(UserAdmin, self).get_actions(request)
if 'delete_selected' in actions: if 'delete_selected' in actions:
del actions['delete_selected'] del actions['delete_selected']
@ -106,19 +178,40 @@ class UserAdmin(admin.ModelAdmin):
class GroupAdmin(admin.ModelAdmin): class GroupAdmin(admin.ModelAdmin):
"""
Admin class for workint with :py:class:`operating system groups
<osusers.models.Group>`.
"""
actions = ['perform_delete_selected'] actions = ['perform_delete_selected']
def get_inline_instances(self, request, obj=None):
if obj is None:
return []
return super(GroupAdmin, self).get_inline_instances(request, obj)
def perform_delete_selected(self, request, queryset): def perform_delete_selected(self, request, queryset):
"""
Action to delete a list of selected groups.
This action calls the delete method of each selected group in contrast
to the default `delete_selected`.
:param request: the current HTTP request
:param queryset: Django ORM queryset representing the selected groups
"""
for group in queryset.all(): for group in queryset.all():
group.delete() group.delete()
perform_delete_selected.short_description = _('Delete selected groups') perform_delete_selected.short_description = _('Delete selected groups')
def get_actions(self, request): def get_actions(self, request):
"""
Get the available actions for groups.
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(GroupAdmin, self).get_actions(request) actions = super(GroupAdmin, self).get_actions(request)
if 'delete_selected' in actions: if 'delete_selected' in actions:
del actions['delete_selected'] del actions['delete_selected']

View file

@ -0,0 +1,17 @@
"""
This module contains the :py:class:`django.apps.AppConfig` instance for the
:py:module:`osusers` app.
"""
from __future__ import unicode_literals
from django.apps import AppConfig
from django.utils.translation import ugettext_lazy as _
class OsusersAppConfig(AppConfig):
"""
AppConfig for the :py:mod:`osusers` app.
"""
name = 'osusers'
verbose_name = _('Operating System Users and Groups')

View file

@ -0,0 +1,175 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
msgid ""
msgstr ""
"Project-Id-Version: osusers\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2014-12-27 22:46+0100\n"
"PO-Revision-Date: 2014-12-27 22:54+0100\n"
"Last-Translator: Jan Dittberner <jan@dittberner.info>\n"
"Language-Team: de <de@li.org>\n"
"Language: de\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
"X-Generator: Poedit 1.6.10\n"
"X-Poedit-SourceCharset: UTF-8\n"
#: admin.py:16
msgid "Passwords don't match"
msgstr "Passwörter stimmen nicht überein"
#: admin.py:47
msgid "Password"
msgstr "Passwort"
#: admin.py:51
msgid "Password (again)"
msgstr "Passwortwiederholung"
#: admin.py:160
msgid "Delete selected users"
msgstr "Ausgewählte Nutzer löschen"
#: admin.py:201
msgid "Delete selected groups"
msgstr "Ausgewählte Gruppen löschen"
#: apps.py:17
msgid "Operating System Users and Groups"
msgstr "Betriebssystemnutzer- und Gruppen"
#: models.py:41
msgid "You can not use a user's primary group."
msgstr "Sie können nicht die primäre Gruppe des Nutzers verwenden."
#: models.py:71
msgid "Group name"
msgstr "Gruppenname"
#: models.py:73
msgid "Group ID"
msgstr "Gruppen-ID"
#: models.py:74
msgid "Description"
msgstr "Beschreibung"
#: models.py:76
msgid "Group password"
msgstr "Gruppenpasswort"
#: models.py:81 models.py:212
msgid "Group"
msgstr "Gruppe"
#: models.py:82
msgid "Groups"
msgstr "Gruppen"
#: models.py:209
msgid "User name"
msgstr "Nutzername"
#: models.py:211
msgid "User ID"
msgstr "Nutzer-ID"
#: models.py:213
msgid "Gecos field"
msgstr "GECOS-Feld"
#: models.py:214
msgid "Home directory"
msgstr "Home-Verzeichnis"
#: models.py:215
msgid "Login shell"
msgstr "Loginshell"
#: models.py:221 models.py:335
msgid "User"
msgstr "Nutzer"
#: models.py:222
msgid "Users"
msgstr "Nutzer"
#: models.py:336
msgid "Encrypted password"
msgstr "Verschlüsseltes Passwort"
#: models.py:338
msgid "Date of last change"
msgstr "Datum der letzten Änderung"
#: models.py:339
msgid "This is expressed in days since Jan 1, 1970"
msgstr "Ausgedrückt als Tage seit dem 1. Januar 1970"
#: models.py:342
msgid "Minimum age"
msgstr "Minimales Alter"
#: models.py:343
msgid "Minimum number of days before the password can be changed"
msgstr "Minmale Anzahl von Tagen bevor das Passwort geändert werden kann"
#: models.py:347
msgid "Maximum age"
msgstr "Maximales Alter"
#: models.py:348
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"
#: models.py:352
msgid "Grace period"
msgstr "Duldungsperiode"
#: models.py:353
msgid "The number of days before the password is going to expire"
msgstr "Anzahl von Tagen nach denen das Passwort verfällt"
#: models.py:357
msgid "Inactivity period"
msgstr "Inaktivitätsperiode"
#: models.py:358
msgid ""
"The number of days after the password has expired during which the password "
"should still be accepted"
msgstr ""
"Die Anzahl von Tagen für die ein verfallenes Passwort noch akzeptiert werden "
"soll"
#: models.py:362
msgid "Account expiration date"
msgstr "Kontoverfallsdatum"
#: models.py:363
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"
#: models.py:370
msgid "Shadow password"
msgstr "Shadow-Passwort"
#: models.py:371
msgid "Shadow passwords"
msgstr "Shadow-Passwörter"
#: models.py:397
msgid "Additional group"
msgstr "Weitere Gruppe"
#: models.py:398
msgid "Additional groups"
msgstr "Weitere Gruppen"

View file

@ -0,0 +1,22 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
from django.conf import settings
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('osusers', '0002_auto_20141226_1456'),
]
operations = [
migrations.AddField(
model_name='user',
name='customer',
field=models.ForeignKey(default=1, to=settings.AUTH_USER_MODEL),
preserve_default=False,
),
]

View file

@ -1,3 +1,7 @@
"""
This module defines the database models of operating system users.
"""
from __future__ import unicode_literals from __future__ import unicode_literals
from datetime import date from datetime import date
@ -30,7 +34,7 @@ from .tasks import (
) )
logger = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
CANNOT_USE_PRIMARY_GROUP_AS_ADDITIONAL = _( CANNOT_USE_PRIMARY_GROUP_AS_ADDITIONAL = _(
@ -38,8 +42,19 @@ CANNOT_USE_PRIMARY_GROUP_AS_ADDITIONAL = _(
class GroupManager(models.Manager): class GroupManager(models.Manager):
"""
Manager class for :py:class:`osusers.models.Group`.
"""
def get_next_gid(self): def get_next_gid(self):
"""
Get the next available group id.
:returns: group id
:rtype: int
"""
q = self.aggregate(models.Max('gid')) q = self.aggregate(models.Max('gid'))
if q['gid__max'] is None: if q['gid__max'] is None:
return settings.OSUSER_MINGID return settings.OSUSER_MINGID
@ -48,6 +63,10 @@ class GroupManager(models.Manager):
@python_2_unicode_compatible @python_2_unicode_compatible
class Group(TimeStampedModel, models.Model): class Group(TimeStampedModel, models.Model):
"""
This entity class corresponds to an operating system group.
"""
groupname = models.CharField( groupname = models.CharField(
_('Group name'), max_length=16, unique=True) _('Group name'), max_length=16, unique=True)
gid = models.PositiveSmallIntegerField( gid = models.PositiveSmallIntegerField(
@ -67,34 +86,73 @@ class Group(TimeStampedModel, models.Model):
@transaction.atomic @transaction.atomic
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
"""
Save the group to the database and synchronizes group information to
LDAP.
:param args: positional arguments to be passed on to
:py:meth:`django.db.Model.save`
:param kwargs: keyword arguments to be passed on to
:py:meth:`django.db.Model.save`
:return: self
:rtype: :py:class:`osusers.models.Group`
"""
super(Group, self).save(*args, **kwargs) super(Group, self).save(*args, **kwargs)
dn = create_ldap_group.delay( dn = create_ldap_group.delay(
self.groupname, self.gid, self.descr).get() self.groupname, self.gid, self.descr).get()
logger.info("created LDAP group with dn %s", dn) _LOGGER.info("created LDAP group with dn %s", dn)
return self return self
@transaction.atomic @transaction.atomic
def delete(self, *args, **kwargs): def delete(self, *args, **kwargs):
"""
Delete the group from LDAP and the database.
:param args: positional arguments to be passed on to
:py:meth:`django.db.Model.delete`
:param kwargs: keyword arguments to be passed on to
:py:meth:`django.db.Model.delete`
"""
delete_ldap_group_if_empty.delay(self.groupname).get() delete_ldap_group_if_empty.delay(self.groupname).get()
super(Group, self).delete(*args, **kwargs) super(Group, self).delete(*args, **kwargs)
class UserManager(models.Manager): class UserManager(models.Manager):
"""
Manager class for :py:class:`osusers.models.User`.
"""
def get_next_uid(self): def get_next_uid(self):
"""
Get the next available user id.
:return: user id
:rtype: int
"""
q = self.aggregate(models.Max('uid')) q = self.aggregate(models.Max('uid'))
if q['uid__max'] is None: if q['uid__max'] is None:
return settings.OSUSER_MINUID return settings.OSUSER_MINUID
return max(settings.OSUSER_MINUID, q['uid__max'] + 1) return max(settings.OSUSER_MINUID, q['uid__max'] + 1)
def get_next_username(self): def get_next_username(self):
"""
Get the next available user name.
:return: user name
:rtype: str
"""
count = 1 count = 1
usernameformat = "{0}{1:02d}" usernameformat = "{0}{1:02d}"
nextuser = usernameformat.format(settings.OSUSER_USERNAME_PREFIX, nextuser = usernameformat.format(settings.OSUSER_USERNAME_PREFIX,
count) count)
for user in self.values('username').filter( for user in self.values('username').filter(
username__startswith=settings.OSUSER_USERNAME_PREFIX).order_by( username__startswith=settings.OSUSER_USERNAME_PREFIX
'username'): ).order_by('username'):
if user['username'] == nextuser: if user['username'] == nextuser:
count += 1 count += 1
nextuser = usernameformat.format( nextuser = usernameformat.format(
@ -104,7 +162,26 @@ class UserManager(models.Manager):
return nextuser return nextuser
@transaction.atomic @transaction.atomic
def create_user(self, username=None, password=None, commit=False): def create_user(
self, customer, username=None, password=None, commit=False
):
"""
Create a new user with a primary group named the same as the user and
an initial password.
If username is None the result of :py:meth:`get_next_username` is used.
If password is None a new password will be generated using passlib's
:py:func:`generate_password`.
:param customer: Django User instance this user is associated to
:param str username: the username or None
:param str password: the password or None
:param boolean commit: whether to commit the user data to the database
or not
:return: new user
:rtype: :py:class:`osusers.models.User`
"""
uid = self.get_next_uid() uid = self.get_next_uid()
gid = Group.objects.get_next_gid() gid = Group.objects.get_next_gid()
if username is None: if username is None:
@ -114,7 +191,7 @@ class UserManager(models.Manager):
homedir = os.path.join(settings.OSUSER_HOME_BASEPATH, username) homedir = os.path.join(settings.OSUSER_HOME_BASEPATH, username)
group = Group.objects.create(groupname=username, gid=gid) group = Group.objects.create(groupname=username, gid=gid)
user = self.create(username=username, group=group, uid=uid, user = self.create(username=username, group=group, uid=uid,
homedir=homedir, homedir=homedir, customer=customer,
shell=settings.OSUSER_DEFAULT_SHELL) shell=settings.OSUSER_DEFAULT_SHELL)
user.set_password(password) user.set_password(password)
if commit: if commit:
@ -124,6 +201,10 @@ class UserManager(models.Manager):
@python_2_unicode_compatible @python_2_unicode_compatible
class User(TimeStampedModel, models.Model): class User(TimeStampedModel, models.Model):
"""
This entity class corresponds to an operating system user.
"""
username = models.CharField( username = models.CharField(
_('User name'), max_length=64, unique=True) _('User name'), max_length=64, unique=True)
uid = models.PositiveSmallIntegerField( uid = models.PositiveSmallIntegerField(
@ -132,6 +213,7 @@ class User(TimeStampedModel, models.Model):
gecos = models.CharField(_('Gecos field'), max_length=128, blank=True) gecos = models.CharField(_('Gecos field'), max_length=128, blank=True)
homedir = models.CharField(_('Home directory'), max_length=256) homedir = models.CharField(_('Home directory'), max_length=256)
shell = models.CharField(_('Login shell'), max_length=64) shell = models.CharField(_('Login shell'), max_length=64)
customer = models.ForeignKey(settings.AUTH_USER_MODEL)
objects = UserManager() objects = UserManager()
@ -144,6 +226,15 @@ class User(TimeStampedModel, models.Model):
@transaction.atomic @transaction.atomic
def set_password(self, password): def set_password(self, password):
"""
Set the password of the user.
The password is set to the user's
:py:class:`Shadow <osusers.models.Shadow>` instance and to LDAP.
:param str password: the new password
"""
if hasattr(self, 'shadow'): if hasattr(self, 'shadow'):
self.shadow.set_password(password) self.shadow.set_password(password)
else: else:
@ -158,12 +249,24 @@ class User(TimeStampedModel, models.Model):
@transaction.atomic @transaction.atomic
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
"""
Save the user to the database, create user directories and synchronize
user information to LDAP.
:param args: positional arguments to be passed on to
:py:meth:`django.db.Model.save`
:param kwargs: keyword arguments to be passed on to
:py:meth:`django.db.Model.save`
:return: self
:rtype: :py:class:`osusers.models.User`
"""
dn = create_ldap_user.delay( dn = create_ldap_user.delay(
self.username, self.uid, self.group.gid, self.gecos, self.username, self.uid, self.group.gid, self.gecos,
self.homedir, self.shell, password=None).get() self.homedir, self.shell, password=None).get()
sftp_dir = setup_file_sftp_userdir.delay(self.username).get() sftp_dir = setup_file_sftp_userdir.delay(self.username).get()
mail_dir = setup_file_mail_userdir.delay(self.username).get() mail_dir = setup_file_mail_userdir.delay(self.username).get()
logger.info( _LOGGER.info(
"created user %(user)s with LDAP dn %(dn)s, home directory " "created user %(user)s with LDAP dn %(dn)s, home directory "
"%(homedir)s and mail base directory %(maildir)s.", { "%(homedir)s and mail base directory %(maildir)s.", {
'user': self, 'dn': dn, 'user': self, 'dn': dn,
@ -173,6 +276,16 @@ class User(TimeStampedModel, models.Model):
@transaction.atomic @transaction.atomic
def delete(self, *args, **kwargs): def delete(self, *args, **kwargs):
"""
Delete the user and its groups from LDAP and the database and remove
the user's directories.
:param args: positional arguments to be passed on to
:py:meth:`django.db.Model.delete`
:param kwargs: keyword arguments to be passed on to
:py:meth:`django.db.Model.delete`
"""
delete_file_mail_userdir.delay(self.username).get() delete_file_mail_userdir.delay(self.username).get()
delete_file_sftp_userdir.delay(self.username).get() delete_file_sftp_userdir.delay(self.username).get()
for group in [ag.group for ag in self.additionalgroup_set.all()]: for group in [ag.group for ag in self.additionalgroup_set.all()]:
@ -184,9 +297,23 @@ class User(TimeStampedModel, models.Model):
class ShadowManager(models.Manager): class ShadowManager(models.Manager):
"""
Manager class for :py:class:`osusers.models.Shadow`.
"""
@transaction.atomic @transaction.atomic
def create_shadow(self, user, password): def create_shadow(self, user, password):
"""
Create a new shadow instance with typical Linux settings for the given
user with the given password.
:param user: :py:class:`User <osusers.models.User>` instance
:param str password: the password
:return: new Shadow instance
:rtype: :py:class:`osusers.models.Shadow` instance
"""
changedays = (timezone.now().date() - date(1970, 1, 1)).days changedays = (timezone.now().date() - date(1970, 1, 1)).days
shadow = self.create( shadow = self.create(
user=user, changedays=changedays, user=user, changedays=changedays,
@ -200,6 +327,11 @@ class ShadowManager(models.Manager):
@python_2_unicode_compatible @python_2_unicode_compatible
class Shadow(TimeStampedModel, models.Model): class Shadow(TimeStampedModel, models.Model):
"""
This entity class corresponds to an operating system user's shadow file
entry.
"""
user = models.OneToOneField(User, primary_key=True, verbose_name=_('User')) user = models.OneToOneField(User, primary_key=True, verbose_name=_('User'))
passwd = models.CharField(_('Encrypted password'), max_length=128) passwd = models.CharField(_('Encrypted password'), max_length=128)
changedays = models.PositiveSmallIntegerField( changedays = models.PositiveSmallIntegerField(
@ -242,11 +374,21 @@ class Shadow(TimeStampedModel, models.Model):
return 'for user {0}'.format(self.user) return 'for user {0}'.format(self.user)
def set_password(self, password): def set_password(self, password):
"""
Set and encrypt the password.
:param str password: the password
"""
self.passwd = sha512_crypt.encrypt(password) self.passwd = sha512_crypt.encrypt(password)
@python_2_unicode_compatible @python_2_unicode_compatible
class AdditionalGroup(TimeStampedModel, models.Model): class AdditionalGroup(TimeStampedModel, models.Model):
"""
This entity class corresponds to additional group assignments for an
:py:class:`operating system user <osusers.models.User>`.
"""
user = models.ForeignKey(User) user = models.ForeignKey(User)
group = models.ForeignKey(Group) group = models.ForeignKey(Group)
@ -255,21 +397,45 @@ class AdditionalGroup(TimeStampedModel, models.Model):
verbose_name = _('Additional group') verbose_name = _('Additional group')
verbose_name_plural = _('Additional groups') verbose_name_plural = _('Additional groups')
def __str__(self):
return '{0} in {1}'.format(self.user, self.group)
def clean(self): def clean(self):
"""
Ensure that the assigned group is different from the user's primary
group.
"""
if self.user.group == self.group: if self.user.group == self.group:
raise ValidationError(CANNOT_USE_PRIMARY_GROUP_AS_ADDITIONAL) raise ValidationError(CANNOT_USE_PRIMARY_GROUP_AS_ADDITIONAL)
@transaction.atomic @transaction.atomic
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
"""
Persists the group assignment to LDAP and the database.
:param args: positional arguments to be passed on to
:py:meth:`django.db.Model.save`
:param kwargs: keyword arguments to be passed on to
:py:meth:`django.db.Model.save`
:return: this instance
:rtype: :py:class:`AdditionalGroup <osusers.models.AdditionalGroup>`
"""
add_ldap_user_to_group.delay( add_ldap_user_to_group.delay(
self.user.username, self.group.groupname).get() self.user.username, self.group.groupname).get()
super(AdditionalGroup, self).save(*args, **kwargs) return super(AdditionalGroup, self).save(*args, **kwargs)
@transaction.atomic @transaction.atomic
def delete(self, *args, **kwargs): def delete(self, *args, **kwargs):
"""
Delete the group assignment from LDAP and the database.
:param args: positional arguments to be passed on to
:py:meth:`django.db.Model.delete`
:param kwargs: keyword arguments to be passed on to
:py:meth:`django.db.Model.delete`
"""
remove_ldap_user_from_group.delay( remove_ldap_user_from_group.delay(
self.user.username, self.group.groupname).get() self.user.username, self.group.groupname).get()
super(AdditionalGroup, self).delete(*args, **kwargs) super(AdditionalGroup, self).delete(*args, **kwargs)
def __str__(self):
return '{0} in {1}'.format(self.user, self.group)

View file

@ -1,3 +1,10 @@
"""
This module defines task stubs for the tasks implemented on the `Celery`_
workers.
.. _Celery: http://www.celeryproject.org/
"""
from __future__ import absolute_import from __future__ import absolute_import
from celery import shared_task from celery import shared_task
@ -5,59 +12,182 @@ from celery import shared_task
@shared_task @shared_task
def create_ldap_group(groupname, gid, descr): def create_ldap_group(groupname, gid, descr):
pass """
This task creates an :py:class:`LDAP group <ldapentities.models.LdapGroup>`
if it does not exist yet.
If a group with the given name exists its group id and description
attributes are updated.
:param str groupname: the group name
:param int gid: the group id
:param str descr: description text for the group
:return: the distinguished name of the group
:rtype: str
"""
@shared_task @shared_task
def create_ldap_user(username, uid, gid, gecos, homedir, shell, password): def create_ldap_user(username, uid, gid, gecos, homedir, shell, password):
pass """
This task creates an :py:class:`LDAP user <ldapentities.models.LdapUser>`
if it does not exist yet.
The task is rejected if the primary group of the user is not defined.
The user's fields are updated if the user already exists.
:param str username: the user name
:param int uid: the user id
:param int gid: the user's primary group's id
:param str gecos: the text for the GECOS field
:param str homedir: the user's home directory
:param str shell: the user's login shell
:param str or None password: the clear text password, if :py:const:`None`
is passed the password is not touched
:raises celery.exceptions.Reject: if the specified primary group does not
exist
:return: the distinguished name of the user
:rtype: str
"""
@shared_task @shared_task
def add_ldap_user_to_group(username, groupname): def add_ldap_user_to_group(username, groupname):
pass """
This task adds the specified user to the given group.
This task does nothing if the user is already member of the group.
:param str username: the user name
:param str groupname: the group name
:raises celery.exceptions.Retry: if the user does not exist yet,
:py:func:`create_ldap_user` should be called before
:return: True if the user has been added to the group otherwise False
:rtype: boolean
"""
@shared_task @shared_task
def remove_ldap_user_from_group(username, groupname): def remove_ldap_user_from_group(username, groupname):
pass """
This task removes the given user from the given group.
:param str username: the user name
:param str groupname: the group name
:return: True if the user has been removed, False otherwise
:rtype: boolean
"""
@shared_task @shared_task
def delete_ldap_user(username): def delete_ldap_user(username):
pass """
This task deletes the given user.
:param str username: the user name
:return: True if the user has been deleted, False otherwise
:rtype: boolean
"""
@shared_task @shared_task
def delete_ldap_group_if_empty(groupname): def delete_ldap_group_if_empty(groupname):
pass """
This task deletes the given group.
:param str groupname: the group name
:return: True if the user has been deleted, False otherwise
:rtype: boolean
"""
@shared_task @shared_task
def setup_file_sftp_userdir(username): def setup_file_sftp_userdir(username):
pass """
This task creates the home directory for an SFTP user if it does not exist
yet.
:param str username: the user name
:raises Exception: if the SFTP directory of the user cannot be created
:return: the created directory name
:rtype: str
"""
@shared_task @shared_task
def delete_file_sftp_userdir(username): def delete_file_sftp_userdir(username):
pass """
This task recursively deletes the home directory of an SFTP user if it
does not exist yet.
:param str username: the user name
:raises Exception: if the SFTP directory of the user cannot be removed
:return: the removed directory name
:rtype: str
"""
@shared_task @shared_task
def setup_file_mail_userdir(username): def setup_file_mail_userdir(username):
pass """
This task creates the mail base directory for a user if it does not exist
yet.
:param str username: the user name
:raises Exception: if the mail base directory for the user cannot be
created
:return: the created directory name
:rtype: str
"""
@shared_task @shared_task
def delete_file_mail_userdir(username): def delete_file_mail_userdir(username):
pass """
This task recursively deletes the mail base directory for a user if it
does not exist yet.
:param str username: the user name
:raises Exception: if the mail base directory of the user cannot be removed
:return: the removed directory name
:rtype: str
"""
@shared_task @shared_task
def create_file_mailbox(username, mailboxname): def create_file_mailbox(username, mailboxname):
pass """
This task creates a new mailbox directory for the given user and mailbox
name.
:param str username: the user name
:param str mailboxname: the mailbox name
:raises GVAFileException: if the mailbox directory cannot be created
:return: the created mailbox directory name
:rtype: str
"""
@shared_task @shared_task
def delete_file_mailbox(username, mailboxname): def delete_file_mailbox(username, mailboxname):
pass """
This task deletes the given mailbox of the given user.
:param str username: the user name
:param str mailboxname: the mailbox name
:raises GVAFileException: if the mailbox directory cannot be deleted
:return: the deleted mailbox directory name
:rtype: str
"""