Merge branch 'release/0.4.0' into production

* release/0.4.0: (25 commits)
  define version number, update changelog
  set database password at the appropriate place
  remove username argument from delete_pgsql_database call
  remove username argument of pgsqltasks.tasks.delete_pgsql_database
  fix documentation issues
  add autogenerated documentation for module members
  make userdbs admin work properly
  add initial migration for userdbs
  add admin and a bit of documentation
  add new incomplete userdbs app
  document addition of mysqltasks and pgsqltasks
  add mysqltasks and pgsqltasks with placeholders for the real tasks
  set default locale to en-us to avoid translated migrations
  add migration for verbose_name and verbose_name_plural in osusers.models.User
  switch to gvacommon.celeryrouters.GvaRouter
  unify routers, add support for mysql and pgsql tasks
  use taskresults app and delete_ldap_group task
  add taskresults app to handle celery task results
  add new task delete_ldap_group
  define celery timezone, restrict celery content to json
  ...
This commit is contained in:
Jan Dittberner 2015-01-11 15:33:18 +01:00
commit 0ef151f780
39 changed files with 1962 additions and 74 deletions

1
.gitignore vendored
View file

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

View file

@ -1,6 +1,19 @@
Changelog Changelog
========= =========
* :release:`0.4.0 <2015-01-11>`
* :feature:`-` add mysqltasks and pgsqltasks
* :feature:`-` add :py:mod:`userdbs` app to allow management of user databases
via :py:mod:`mysqltasks` and :py:mod:`pgsqltasks`
* :feature:`-` add new app :py:mod:`taskresults` that takes care of handling
asynchronous `Celery`_ results
* :feature:`-` add new task :py:func:`osusers.tasks.delete_ldap_group` (needs
gvaldap >= 0.2.0 on the LDAP side)
* :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
@ -23,10 +36,11 @@ Changelog
* :feature:`-` full test suite for osusers * :feature:`-` full test suite for osusers
* :feature:`-` full test suite for managemails app * :feature:`-` full test suite for managemails app
* :feature:`-` full test suite for domains app * :feature:`-` full test suite for domains app
* :feature:`-` `Celery <http://www.celeryproject.com/>`_ integration for ldap * :feature:`-` `Celery`_ integration for ldap synchronization
synchronization
* :release:`0.1 <2014-05-25>` * :release:`0.1 <2014-05-25>`
* :feature:`-` initial model code for os users * :feature:`-` initial model code for os users
* :feature:`-` initial model code for mail address and mailbox management * :feature:`-` initial model code for mail address and mailbox management
* :feature:`-` initial model code for domains * :feature:`-` initial model code for domains
.. _Celery: http://www.celeryproject.org/

235
docs/code.rst Normal file
View file

@ -0,0 +1,235 @@
******************
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:`mysqltasks` app
========================
.. automodule:: mysqltasks
:py:mod:`tasks <mysqltasks.tasks>`
----------------------------------
.. automodule:: mysqltasks.tasks
:members:
.. autotask:: mysqltasks.tasks.create_mysql_database
.. autotask:: mysqltasks.tasks.create_mysql_user
.. autotask:: mysqltasks.tasks.delete_mysql_database
.. autotask:: mysqltasks.tasks.delete_mysql_user
.. autotask:: mysqltasks.tasks.set_mysql_userpassword
: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
.. 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
:py:mod:`pgsqltasks` app
========================
.. automodule:: pgsqltasks
:members:
:py:mod:`tasks <pgsqltasks.tasks>`
----------------------------------
.. automodule:: pgsqltasks.tasks
.. autotask:: pgsqltasks.tasks.create_pgsql_database
.. autotask:: pgsqltasks.tasks.create_pgsql_user
.. autotask:: pgsqltasks.tasks.delete_pgsql_database
.. autotask:: pgsqltasks.tasks.delete_pgsql_user
.. autotask:: pgsqltasks.tasks.set_pgsql_userpassword
:py:mod:`taskresults` app
=========================
.. automodule:: taskresults
:py:mod:`admin <taskresults.admin>`
-----------------------------------
.. automodule:: taskresults.admin
:py:mod:`management.commands <taskresults.management.commands>`
---------------------------------------------------------------
.. automodule:: taskresults.management.commands
:py:mod:`fetch_taskresults <taskresult.management.commands.fetch_taskresults>`
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.. automodule:: taskresults.management.commands.fetch_taskresults
:py:mod:`models <taskresults.models>`
-------------------------------------
.. automodule:: taskresults.models
:py:mod:`userdbs` app
=====================
.. automodule:: userdbs
:py:mod:`admin <userdbs.admin>`
-------------------------------
.. automodule:: userdbs.admin
:members:
:py:mod:`models <userdbs.models>`
---------------------------------
.. automodule:: userdbs.models
:members:

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 -----------------------------------------------------
@ -48,16 +53,16 @@ master_doc = 'index'
# General information about the project. # General information about the project.
project = u'gnuviechadmin' project = u'gnuviechadmin'
copyright = u'2014, Jan Dittberner' copyright = u'2014, 2015 Jan Dittberner'
# The version info for the project you're documenting, acts as replacement for # The version info for the project you're documenting, acts as replacement for
# |version| and |release|, also used in various other places throughout the # |version| and |release|, also used in various other places throughout the
# built documents. # built documents.
# #
# The short X.Y version. # The short X.Y version.
version = '0.3' version = '0.4'
# The full version, including alpha/beta/rc tags. # The full version, including alpha/beta/rc tags.
release = '0.3.0' release = '0.4.0'
# The language for content autogenerated by Sphinx. Refer to documentation # The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages. # for a list of supported languages.

View file

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

View file

@ -82,7 +82,7 @@ DATABASES = {
TIME_ZONE = 'Europe/Berlin' TIME_ZONE = 'Europe/Berlin'
# See: https://docs.djangoproject.com/en/dev/ref/settings/#language-code # See: https://docs.djangoproject.com/en/dev/ref/settings/#language-code
LANGUAGE_CODE = 'de-de' LANGUAGE_CODE = 'en-us'
# See: https://docs.djangoproject.com/en/dev/ref/settings/#site-id # See: https://docs.djangoproject.com/en/dev/ref/settings/#site-id
SITE_ID = 1 SITE_ID = 1
@ -224,9 +224,13 @@ DJANGO_APPS = (
# Apps specific for this project go here. # Apps specific for this project go here.
LOCAL_APPS = ( LOCAL_APPS = (
'taskresults',
'mysqltasks',
'pgsqltasks',
'domains', 'domains',
'osusers', 'osusers',
'managemails', 'managemails',
'userdbs',
) )
# See: https://docs.djangoproject.com/en/dev/ref/settings/#installed-apps # See: https://docs.djangoproject.com/en/dev/ref/settings/#installed-apps
@ -279,10 +283,11 @@ CELERY_RESULT_BACKEND = 'amqp'
CELERY_RESULT_PERSISTENT = True CELERY_RESULT_PERSISTENT = True
CELERY_TASK_RESULT_EXPIRES = None CELERY_TASK_RESULT_EXPIRES = None
CELERY_ROUTES = ( CELERY_ROUTES = (
'gvacommon.celeryrouters.LdapRouter', 'gvacommon.celeryrouters.GvaRouter',
'gvacommon.celeryrouters.FileRouter',
) )
CELERY_ACCEPT_CONTENT = ['pickle', 'yaml', 'json'] CELERY_TIMEZONE = 'Europe/Berlin'
CELERY_ENABLE_UTC = True
CELERY_ACCEPT_CONTENT = ['json']
CELERY_TASK_SERIALIZER = 'json' CELERY_TASK_SERIALIZER = 'json'
CELERY_RESULT_SERIALIZER = 'json' CELERY_RESULT_SERIALIZER = 'json'
########## END CELERY CONFIGURATION ########## END CELERY CONFIGURATION

View file

@ -1,2 +1,3 @@
.*.swp .*.swp
*.pyc *.pyc
.ropeproject/

View file

@ -2,23 +2,14 @@
from __future__ import unicode_literals from __future__ import unicode_literals
class LdapRouter(object): class GvaRouter(object):
def route_for_task(self, task, args=None, kwargs=None): def route_for_task(self, task, args=None, kwargs=None):
if 'ldap' in task: for route in ['ldap', 'file', 'mysql', 'pgsql']:
return {'exchange': 'ldap', if route in task:
return {
'exchange': route,
'exchange_type': 'direct', 'exchange_type': 'direct',
'queue': 'ldap'} 'queue': route,
}
return None return None
class FileRouter(object):
def route_for_task(self, task, args=None, kwargs=None):
if 'file' in task:
return {'exchange': 'file',
'exchange_type': 'direct',
'queue': 'file'}
return None

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,4 @@
"""
This module contains :py:mod:`mysqltasks.tasks`.
"""

View file

@ -0,0 +1,4 @@
"""
Empty models to make Django accept mysqltasks as an app.
"""

View file

@ -0,0 +1,72 @@
"""
This module defines Celery_ tasks to manage MySQL users and databases.
"""
from __future__ import absolute_import
from celery import shared_task
@shared_task
def create_mysql_user(username, password):
"""
This task creates a new MySQL user.
:param str username: the user name
:param str password: the password
:return: the created user's name
:rtype: str
"""
@shared_task
def set_mysql_userpassword(username, password):
"""
Set a new password for an existing MySQL user.
:param str username: the user name
:param str password: the password
:return: True if the password could be set, False otherwise
:rtype: boolean
"""
@shared_task
def delete_mysql_user(username):
"""
This task deletes an existing MySQL user.
:param str username: the user name
:return: True if the user has been deleted, False otherwise
:rtype: boolean
"""
@shared_task
def create_mysql_database(dbname, username):
"""
This task creates a new MySQL database for the given MySQL user.
:param str dbname: database name
:param str username: the user name of an existing MySQL user
:return: the database name
:rtype: str
"""
@shared_task
def delete_mysql_database(dbname, username):
"""
This task deletes an existing MySQL database and revokes privileges of the
given user on that database.
:param str dbname: database name
:param str username: the user name of an existing MySQL user
:return: True if the database has been deleted, False otherwise
:rtype: boolean
"""

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:mod:`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

@ -0,0 +1,24 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
class Migration(migrations.Migration):
dependencies = [
('osusers', '0003_user_customer'),
]
operations = [
migrations.AlterModelOptions(
name='user',
options={'verbose_name': 'User', 'verbose_name_plural': 'Users'},
),
migrations.AlterField(
model_name='shadow',
name='user',
field=models.OneToOneField(primary_key=True, serialize=False, to='osusers.User', verbose_name='User'),
preserve_default=True,
),
]

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
@ -16,13 +20,15 @@ from model_utils.models import TimeStampedModel
from passlib.hash import sha512_crypt from passlib.hash import sha512_crypt
from passlib.utils import generate_password from passlib.utils import generate_password
from taskresults.models import TaskResult
from .tasks import ( from .tasks import (
add_ldap_user_to_group, add_ldap_user_to_group,
create_ldap_group, create_ldap_group,
create_ldap_user, create_ldap_user,
delete_file_mail_userdir, delete_file_mail_userdir,
delete_file_sftp_userdir, delete_file_sftp_userdir,
delete_ldap_group_if_empty, delete_ldap_group,
delete_ldap_user, delete_ldap_user,
remove_ldap_user_from_group, remove_ldap_user_from_group,
setup_file_mail_userdir, setup_file_mail_userdir,
@ -30,7 +36,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 +44,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 +65,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 +88,76 @@ 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_ldap_group_if_empty.delay(self.groupname).get() """
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`
"""
TaskResult.objects.create_task_result(
delete_ldap_group.delay(self.groupname),
'delete_ldap_group'
)
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 +167,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 +196,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 +206,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 +218,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 +231,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,23 +254,56 @@ 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() TaskResult.objects.create_task_result(
mail_dir = setup_file_mail_userdir.delay(self.username).get() setup_file_sftp_userdir.delay(self.username),
logger.info( 'setup_file_sftp_userdir'
"created user %(user)s with LDAP dn %(dn)s, home directory " )
"%(homedir)s and mail base directory %(maildir)s.", { TaskResult.objects.create_task_result(
setup_file_mail_userdir.delay(self.username),
'setup_file_mail_userdir'
)
_LOGGER.info(
"created user %(user)s with LDAP dn %(dn)s, scheduled home "
"directory and mail base directory creation.", {
'user': self, 'dn': dn, 'user': self, 'dn': dn,
'homedir': sftp_dir, 'maildir': mail_dir
}) })
return super(User, self).save(*args, **kwargs) return super(User, self).save(*args, **kwargs)
@transaction.atomic @transaction.atomic
def delete(self, *args, **kwargs): def delete(self, *args, **kwargs):
delete_file_mail_userdir.delay(self.username).get() """
delete_file_sftp_userdir.delay(self.username).get() 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`
"""
TaskResult.objects.create_task_result(
delete_file_mail_userdir.delay(self.username),
'delete_file_mail_userdir'
)
TaskResult.objects.create_task_result(
delete_file_sftp_userdir.delay(self.username),
'delete_file_sftp_userdir'
)
for group in [ag.group for ag in self.additionalgroup_set.all()]: for group in [ag.group for ag in self.additionalgroup_set.all()]:
remove_ldap_user_from_group.delay( remove_ldap_user_from_group.delay(
self.username, group.groupname).get() self.username, group.groupname).get()
@ -184,9 +313,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 +343,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 +390,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 +413,48 @@ 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):
remove_ldap_user_from_group.delay( """
self.user.username, self.group.groupname).get() Delete the group assignment from LDAP and the database.
super(AdditionalGroup, self).delete(*args, **kwargs)
def __str__(self): :param args: positional arguments to be passed on to
return '{0} in {1}'.format(self.user, self.group) :py:meth:`django.db.Model.delete`
:param kwargs: keyword arguments to be passed on to
:py:meth:`django.db.Model.delete`
"""
TaskResult.objects.create_task_result(
remove_ldap_user_from_group.delay(
self.user.username, self.group.groupname),
'remove_ldap_user_from_group'
)
super(AdditionalGroup, self).delete(*args, **kwargs)

View file

@ -1,3 +1,8 @@
"""
This module defines task stubs for the tasks implemented on the Celery
workers.
"""
from __future__ import absolute_import from __future__ import absolute_import
from celery import shared_task from celery import shared_task
@ -5,59 +10,194 @@ 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 if it is empty.
:param str groupname: the group name
:return: True if the user has been deleted, False otherwise
:rtype: boolean
"""
@shared_task
def delete_ldap_group(groupname):
"""
This taks 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
"""

View file

@ -0,0 +1,4 @@
"""
This module contains :py:mod:`pgsqltasks.tasks`.
"""

View file

@ -0,0 +1,4 @@
"""
Empty models to make Django accept pgsqltasks as an app.
"""

View file

@ -0,0 +1,70 @@
"""
This module defines Celery_ tasks to manage PostgreSQL users and databases.
"""
from __future__ import absolute_import
from celery import shared_task
@shared_task
def create_pgsql_user(username, password):
"""
This task creates a new PostgreSQL user.
:param str username: the user name
:param str password: the password
:return: the created user's name
:rtype: str
"""
@shared_task
def set_pgsql_userpassword(username, password):
"""
Set a new password for an existing PostgreSQL user.
:param str username: the user name
:param str password: the password
:return: True if the password could be set, False otherwise
:rtype: boolean
"""
@shared_task
def delete_pgsql_user(username):
"""
This task deletes an existing PostgreSQL user.
:param str username: the user name
:return: True if the user has been deleted, False otherwise
:rtype: boolean
"""
@shared_task
def create_pgsql_database(dbname, username):
"""
This task creates a new PostgreSQL database for the given PostgreSQL user.
:param str dbname: database name
:param str username: the user name of an existing PostgreSQL user
:return: the database name
:rtype: str
"""
@shared_task
def delete_pgsql_database(dbname):
"""
This task deletes an existing PostgreSQL database.
:param str dbname: database name
:return: True if the database has been deleted, False otherwise
:rtype: boolean
"""

View file

@ -0,0 +1,5 @@
"""
This is the taskresults app that is used for storing the results from
asynchronous Celery_ tasks.
"""

View file

@ -0,0 +1,12 @@
"""
This module defines the admin interface for the taskresults app.
"""
from __future__ import absolute_import
from django.contrib import admin
from .models import TaskResult
admin.site.register(TaskResult)

View file

@ -0,0 +1,4 @@
"""
This module defines management commands for the taskresults app.
"""

View file

@ -0,0 +1,20 @@
"""
This model contains the implementation of a management command to fetch the
results of all `Celery <http://www.celeryproject.org/>`_ tasks that are not
marked as finished yet.
"""
from __future__ import unicode_literals
from django.core.management.base import BaseCommand
from taskresults.models import TaskResult
class Command(BaseCommand):
help = "fetch task results"
def handle(self, *args, **options):
for taskresult in TaskResult.objects.filter(finished=False):
taskresult.fetch_result()
taskresult.save()

View file

@ -0,0 +1,29 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
class Migration(migrations.Migration):
dependencies = [
]
operations = [
migrations.CreateModel(
name='TaskResult',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('task_id', models.CharField(max_length=36, verbose_name='Task id')),
('task_name', models.CharField(max_length=64, verbose_name='Task name')),
('result', models.TextField(verbose_name='Task result')),
('finished', models.BooleanField(default=False)),
('state', models.CharField(max_length=16, verbose_name='Task state')),
],
options={
'verbose_name': 'Task result',
'verbose_name_plural': 'Task results',
},
bases=(models.Model,),
),
]

View file

@ -0,0 +1,47 @@
"""
This model defines the database models to handle Celery AsyncResults.
"""
from __future__ import unicode_literals
from django.db import models
from django.utils.encoding import python_2_unicode_compatible
from django.utils.translation import ugettext as _
from gnuviechadmin.celery import app
class TaskResultManager(models.Manager):
def create_task_result(self, asyncresult, name):
taskresult = self.create(task_id=asyncresult.id, task_name=name)
return taskresult
@python_2_unicode_compatible
class TaskResult(models.Model):
task_id = models.CharField(_('Task id'), max_length=36)
task_name = models.CharField(_('Task name'), max_length=64)
result = models.TextField(_('Task result'))
finished = models.BooleanField(default=False)
state = models.CharField(_('Task state'), max_length=16)
objects = TaskResultManager()
class Meta:
verbose_name = _('Task result')
verbose_name_plural = _('Task results')
def __str__(self):
return "{task_name} ({task_id}): {finished}".format(
task_name=self.task_name,
task_id=self.task_id,
finished=_('yes') if self.finished else _('no')
)
def fetch_result(self):
if not self.finished:
ar = app.AsyncResult(self.task_id)
res = ar.get(no_ack=True, timeout=1)
self.result = str(res)
self.state = ar.state
self.finished = True

View file

@ -0,0 +1,5 @@
"""
This app is for managing database users and user databases.
"""
default_app_config = 'userdbs.apps.UserdbsAppConfig'

View file

@ -0,0 +1,279 @@
"""
Admin functionality for the :py:mod:`userdbs.models` models.
"""
from __future__ import absolute_import
from django import forms
from django.contrib import admin
from django.utils.translation import ugettext_lazy as _
from .models import (
DatabaseUser,
UserDatabase,
)
class DatabaseUserCreationForm(forms.ModelForm):
"""
A form for creating :py:class:`database users
<userdbs.models.DatabaseUser>`
"""
class Meta:
model = DatabaseUser
fields = ['osuser', 'db_type']
def save(self, commit=True):
"""
Save the database user.
:param boolean commit: whether to save the created database user
:return: database user instance
:rtype: :py:class:`userdbs.models.DatabaseUser`
"""
dbuser = DatabaseUser.objects.create_database_user(
osuser=self.cleaned_data['osuser'],
db_type=self.cleaned_data['db_type'], commit=commit)
return dbuser
def save_m2m(self):
"""
Noop.
"""
class UserDatabaseCreationForm(forms.ModelForm):
"""
A form for creating :py:class:`user databases
<userdbs.models.UserDatabase>`
"""
class Meta:
model = UserDatabase
fields = ['db_user']
def save(self, commit=True):
"""
Save the user database.
:param boolean commit: whether to save the created user database
:return: user database instance
:rtype: :py:class:`userdbs.models.UserDatabase`
"""
database = UserDatabase.objects.create_userdatabase(
db_user=self.cleaned_data['db_user'], commit=commit)
return database
def save_m2m(self):
"""
Noop.
"""
class DatabaseUserAdmin(admin.ModelAdmin):
"""
Admin class for working with :py:class:`database users
<userdbs.models.DatabaseUser>`
"""
actions = ['perform_delete_selected']
add_form = DatabaseUserCreationForm
def get_form(self, request, obj=None, **kwargs):
"""
Use special form for database user creation.
:param request: the current HTTP request
:param obj: either a :py:class:`Database user
<userdbs.models.DatabaseUser>` instance or None for a new database
user
: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,
})
defaults.update(kwargs)
return super(DatabaseUserAdmin, self).get_form(
request, obj, **defaults)
def get_readonly_fields(self, request, obj=None):
"""
Make sure that osuser, name and db_type are not editable for existing
database users.
:param request: the current HTTP request
:param obj: either a :py:class:`Database user
<userdbs.models.DatabaseUser>` instance or None for a new database
user
:return: a list of fields
:rtype: list
"""
if obj:
return ['osuser', 'name', 'db_type']
return []
def save_model(self, request, obj, form, change):
"""
Make sure that the user is created in the target database.
:param request: the current HTTP request
:param obj: a :py:class:`Database user <userdbs.models.DatabaseUser>`
instance
:param form: the form instance
:param boolean change: whether this is a change operation or not
"""
if not change:
obj.create_in_database()
super(DatabaseUserAdmin, self).save_model(request, obj, form, change)
def perform_delete_selected(self, request, queryset):
"""
Action to delete a list of selected database users.
This action calls the delete method of each selected database user in
contrast to the default `delete_selected`
:param request: the current HTTP request
:param queryset: Django ORM queryset representing the selected database
users
"""
for dbuser in queryset.all():
dbuser.delete()
perform_delete_selected.short_description = _(
'Delete selected database users')
def get_actions(self, request):
"""
Get the available actions for database 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(DatabaseUserAdmin, self).get_actions(request)
if 'delete_selected' in actions:
del actions['delete_selected']
return actions
class UserDatabaseAdmin(admin.ModelAdmin):
"""
Admin class for working with :py:class:`user databases
<userdbs.models.UserDatabase>`
"""
actions = ['perform_delete_selected']
add_form = UserDatabaseCreationForm
def get_form(self, request, obj=None, **kwargs):
"""
Use special form for user database creation.
:param request: the current HTTP request
:param obj: either a :py:class:`User database
<userdbs.models.UserDatabase>` instance or None for a new user
database
: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,
})
defaults.update(kwargs)
return super(UserDatabaseAdmin, self).get_form(
request, obj, **defaults)
def get_readonly_fields(self, request, obj=None):
"""
Make sure that db_name and db_user are not editable for existing user
databases.
:param request: the current HTTP request
:param obj: either a :py:class:`User database
<userdbs.models.UserDatabase>` instance or None for a new user
database
:return: a list of fields
:rtype: list
"""
if obj:
return ['db_name', 'db_user']
return []
def save_model(self, request, obj, form, change):
"""
Make sure that the database is created in the target database server.
:param request: the current HTTP request
:param obj: a :py:class:`Database user <userdbs.models.DatabaseUser>`
instance
:param form: the form instance
:param boolean change: whether this is a change operation or not
"""
if not change:
obj.create_in_database()
super(UserDatabaseAdmin, self).save_model(request, obj, form, change)
def perform_delete_selected(self, request, queryset):
"""
Action to delete a list of selected user databases.
This action calls the delete method of each selected user database in
contrast to the default `delete_selected`
:param request: the current HTTP request
:param queryset: Django ORM queryset representing the selected user
databases
"""
for dbuser in queryset.all():
dbuser.delete()
for database in queryset.all():
database.delete()
perform_delete_selected.short_description = _(
'Delete selected user databases')
def get_actions(self, request):
"""
Get the available actions for user databases.
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(UserDatabaseAdmin, self).get_actions(request)
if 'delete_selected' in actions:
del actions['delete_selected']
return actions
admin.site.register(DatabaseUser, DatabaseUserAdmin)
admin.site.register(UserDatabase, UserDatabaseAdmin)

View file

@ -0,0 +1,18 @@
"""
This module contains the :py:class:`django.apps.AppConfig` instance for the
:py:mod:`userdbs` app.
"""
from __future__ import unicode_literals
from django.apps import AppConfig
from django.utils.translation import ugettext_lazy as _
class UserdbsAppConfig(AppConfig):
"""
AppConfig for the :py:mod:`userdbs` app.
"""
name = 'userdbs'
verbose_name = _('Database Users and their Databases')

View file

@ -0,0 +1,55 @@
# -*- 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='DatabaseUser',
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)),
('name', models.CharField(max_length=63, verbose_name='username')),
('db_type', models.PositiveSmallIntegerField(verbose_name='database type', choices=[(0, 'PostgreSQL'), (1, 'MySQL')])),
('osuser', models.ForeignKey(to='osusers.User')),
],
options={
'verbose_name': 'database user',
'verbose_name_plural': 'database users',
},
bases=(models.Model,),
),
migrations.CreateModel(
name='UserDatabase',
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)),
('db_name', models.CharField(max_length=63, verbose_name='database name')),
('db_user', models.ForeignKey(verbose_name='database user', to='userdbs.DatabaseUser')),
],
options={
'verbose_name': 'user database',
'verbose_name_plural': 'user specific database',
},
bases=(models.Model,),
),
migrations.AlterUniqueTogether(
name='userdatabase',
unique_together=set([('db_name', 'db_user')]),
),
migrations.AlterUniqueTogether(
name='databaseuser',
unique_together=set([('name', 'db_type')]),
),
]

View file

@ -0,0 +1,276 @@
from __future__ import unicode_literals
from django.db import models
from django.db import transaction
from django.utils.encoding import python_2_unicode_compatible
from django.utils.translation import ugettext as _
from model_utils import Choices
from model_utils.models import TimeStampedModel
from passlib.utils import generate_password
from osusers.models import User as OsUser
from mysqltasks.tasks import (
create_mysql_database,
create_mysql_user,
delete_mysql_database,
delete_mysql_user,
set_mysql_userpassword,
)
from pgsqltasks.tasks import (
create_pgsql_database,
create_pgsql_user,
delete_pgsql_database,
delete_pgsql_user,
set_pgsql_userpassword,
)
DB_TYPES = Choices(
(0, 'pgsql', _('PostgreSQL')),
(1, 'mysql', _('MySQL')),
)
"""
Database type choice enumeration.
"""
class DatabaseUserManager(models.Manager):
"""
Default Manager for :py:class:`userdbs.models.DatabaseUser`.
"""
def _get_next_dbuser_name(self, osuser, db_type):
"""
Get the next available database user name.
:param osuser: :py:class:`osusers.models.User` instance
:param db_type: value from :py:data:`DB_TYPES`
:return: database user name
:rtype: str
"""
count = 1
dbuser_name_format = "{0}db{{0:02d}}".format(osuser.username)
nextname = dbuser_name_format.format(count)
for user in self.values('name').filter(
osuser=osuser, db_type=db_type
).order_by('name'):
if user['name'] == nextname:
count += 1
nextname = dbuser_name_format.format(count)
else:
break
return nextname
@transaction.atomic
def create_database_user(
self, osuser, db_type, username=None, password=None, commit=True
):
"""
Create a database user of the given type for the given OS user.
If username or password are not specified they are generated.
:param osuser: the :py:class:`osusers.models.User` instance
:param db_type: value from :py:data:`DB_TYPES`
:param str username: database user name
:param str password: initial password or None
:param boolean commit: whether the user should be persisted
:return: :py:class:`userdbs.models.DatabaseUser` instance
.. note::
The password is not persisted it is only used to set the password
on the database side.
"""
if username is None:
username = self._get_next_dbuser_name(osuser, db_type)
db_user = DatabaseUser(
osuser=osuser, db_type=db_type, name=username)
if commit:
db_user.create_in_database(password=password)
db_user.save()
return db_user
@python_2_unicode_compatible
class DatabaseUser(TimeStampedModel, models.Model):
osuser = models.ForeignKey(OsUser)
name = models.CharField(
_('username'), max_length=63)
db_type = models.PositiveSmallIntegerField(
_('database type'), choices=DB_TYPES)
objects = DatabaseUserManager()
class Meta:
unique_together = ['name', 'db_type']
verbose_name = _('database user')
verbose_name_plural = _('database users')
def __str__(self):
return "%(name)s (%(db_type)s for %(osuser)s)" % {
'name': self.name,
'db_type': self.get_db_type_display(),
'osuser': self.osuser.username,
}
def create_in_database(self, password=None):
"""
Create this user in the target database.
:param str password: initial password for the database user
"""
if password is None:
password = generate_password()
# TODO: send GPG encrypted mail with this information
if self.db_type == DB_TYPES.pgsql:
create_pgsql_user.delay(self.name, password).get()
elif self.db_type == DB_TYPES.mysql:
create_mysql_user.delay(self.name, password).get()
else:
raise ValueError('Unknown database type %d' % self.db_type)
def set_password(self, password):
"""
Set an existing user's password.
:param str password: new password for the database user
"""
if self.db_type == DB_TYPES.pgsql:
set_pgsql_userpassword.delay(self.name, password).get(timeout=5)
elif self.db_type == DB_TYPES.mysql:
set_mysql_userpassword.delay(self.name, password).get(timeout=5)
else:
raise ValueError('Unknown database type %d' % self.db_type)
@transaction.atomic
def delete(self, *args, **kwargs):
"""
Delete the database user from the target database and the Django
database.
:param args: positional arguments for
:py:meth:`django.db.models.Model.delete`
:param kwargs: keyword arguments for
:py:meth:`django.db.models.Model.delete`
"""
for database in self.userdatabase_set.all():
database.delete()
if self.db_type == DB_TYPES.pgsql:
delete_pgsql_user.delay(self.name).get(propagate=False, timeout=5)
elif self.db_type == DB_TYPES.mysql:
delete_mysql_user.delay(self.name).get(propagate=False, timeout=5)
else:
raise ValueError('Unknown database type %d' % self.db_type)
super(DatabaseUser, self).delete(*args, **kwargs)
class UserDatabaseManager(models.Manager):
"""
Default manager for :py:class:`userdbs.models.UserDatabase` instances.
"""
def _get_next_dbname(self, db_user):
"""
Get the next available database name for the given database user.
:param db_user: :py:class:`userdbs.models.DatabaseUser` instance
:return: database name
:rtype: str
"""
count = 1
db_name_format = "{0}_{{0:02d}}".format(db_user.name)
# first db is named the same as the user
nextname = db_user.name
for name in self.values('db_name').filter(db_user=db_user).order_by(
'db_name'
):
if name['db_name'] == nextname:
count += 1
nextname = db_name_format.format(count)
else:
break
return nextname
@transaction.atomic
def create_userdatabase(self, db_user, db_name=None, commit=True):
"""
Creates a new user database.
:param db_user: :py:class:`userdbs.models.DatabaseUser` instance
:param str db_name: database name
:param boolean commit: whether the database should be persisted
:return: :py:class:`userdbs.models.UserDatabase` instance
"""
if db_name is None:
db_name = self._get_next_dbname(db_user)
database = UserDatabase(db_user=db_user, db_name=db_name)
if commit:
database.create_in_database()
database.save()
return database
@python_2_unicode_compatible
class UserDatabase(TimeStampedModel, models.Model):
# MySQL limits to 64, PostgreSQL to 63 characters
db_name = models.CharField(
_('database name'), max_length=63)
db_user = models.ForeignKey(DatabaseUser, verbose_name=_('database user'))
objects = UserDatabaseManager()
class Meta:
unique_together = ['db_name', 'db_user']
verbose_name = _('user database')
verbose_name_plural = _('user specific database')
def __str__(self):
return "%(db_name)s (%(db_user)s)" % {
'db_name': self.db_name,
'db_user': self.db_user,
}
def create_in_database(self):
"""
Create this database (schema) in the target database.
"""
# TODO: send GPG encrypted mail with this information
if self.db_user.db_type == DB_TYPES.pgsql:
create_pgsql_database.delay(self.db_name, self.db_user.name).get()
elif self.db_user.db_type == DB_TYPES.mysql:
create_mysql_database.delay(self.db_name, self.db_user.name).get()
else:
raise ValueError('Unknown database type %d' % self.db_type)
@transaction.atomic
def delete(self, *args, **kwargs):
"""
Delete the database (schema) from the target database and the Django
database.
:param args: positional arguments for
:py:meth:`django.db.models.Model.delete`
:param kwargs: keyword arguments for
:py:meth:`django.db.models.Model.delete`
"""
if self.db_user.db_type == DB_TYPES.pgsql:
delete_pgsql_database.delay(self.db_name).get()
elif self.db_user.db_type == DB_TYPES.mysql:
delete_mysql_database.delay(self.db_name, self.db_user.name).get()
else:
raise ValueError('Unknown database type %d' % self.db_type)
super(UserDatabase, self).delete(*args, **kwargs)

View file

@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

View file

@ -0,0 +1,3 @@
from django.shortcuts import render
# Create your views here.