From f83571afc940560b3a508df4c055234b9b94fc54 Mon Sep 17 00:00:00 2001 From: Marc Date: Tue, 30 Sep 2014 10:20:23 +0000 Subject: [PATCH] Added legacy users app --- orchestra/apps/users/backends.py | 123 ++++++++++++++ orchestra/apps/users/roles/__init__.py | 27 +++ orchestra/apps/users/roles/admin.py | 145 ++++++++++++++++ orchestra/apps/users/roles/filters.py | 23 +++ orchestra/apps/users/roles/forms.py | 17 ++ orchestra/apps/users/roles/jabber/__init__.py | 0 orchestra/apps/users/roles/jabber/admin.py | 15 ++ orchestra/apps/users/roles/jabber/models.py | 15 ++ orchestra/apps/users/roles/mail/__init__.py | 0 orchestra/apps/users/roles/mail/admin.py | 124 ++++++++++++++ orchestra/apps/users/roles/mail/api.py | 27 +++ orchestra/apps/users/roles/mail/backends.py | 160 ++++++++++++++++++ orchestra/apps/users/roles/mail/forms.py | 53 ++++++ orchestra/apps/users/roles/mail/models.py | 110 ++++++++++++ .../apps/users/roles/mail/serializers.py | 43 +++++ orchestra/apps/users/roles/mail/settings.py | 29 ++++ orchestra/apps/users/roles/mail/validators.py | 62 +++++++ .../apps/users/roles/owncloud/__init__.py | 0 orchestra/apps/users/roles/posix/__init__.py | 0 orchestra/apps/users/roles/posix/admin.py | 15 ++ orchestra/apps/users/roles/posix/models.py | 22 +++ .../apps/users/roles/posix/serializers.py | 14 ++ orchestra/apps/users/roles/posix/settings.py | 11 ++ .../admin/users/user/change_form.html | 15 ++ .../admin/users/user/delete_role.html | 15 ++ .../templates/admin/users/user/role.html | 70 ++++++++ 26 files changed, 1135 insertions(+) create mode 100644 orchestra/apps/users/backends.py create mode 100644 orchestra/apps/users/roles/__init__.py create mode 100644 orchestra/apps/users/roles/admin.py create mode 100644 orchestra/apps/users/roles/filters.py create mode 100644 orchestra/apps/users/roles/forms.py create mode 100644 orchestra/apps/users/roles/jabber/__init__.py create mode 100644 orchestra/apps/users/roles/jabber/admin.py create mode 100644 orchestra/apps/users/roles/jabber/models.py create mode 100644 orchestra/apps/users/roles/mail/__init__.py create mode 100644 orchestra/apps/users/roles/mail/admin.py create mode 100644 orchestra/apps/users/roles/mail/api.py create mode 100644 orchestra/apps/users/roles/mail/backends.py create mode 100644 orchestra/apps/users/roles/mail/forms.py create mode 100644 orchestra/apps/users/roles/mail/models.py create mode 100644 orchestra/apps/users/roles/mail/serializers.py create mode 100644 orchestra/apps/users/roles/mail/settings.py create mode 100644 orchestra/apps/users/roles/mail/validators.py create mode 100644 orchestra/apps/users/roles/owncloud/__init__.py create mode 100644 orchestra/apps/users/roles/posix/__init__.py create mode 100644 orchestra/apps/users/roles/posix/admin.py create mode 100644 orchestra/apps/users/roles/posix/models.py create mode 100644 orchestra/apps/users/roles/posix/serializers.py create mode 100644 orchestra/apps/users/roles/posix/settings.py create mode 100644 orchestra/apps/users/templates/admin/users/user/change_form.html create mode 100644 orchestra/apps/users/templates/admin/users/user/delete_role.html create mode 100644 orchestra/apps/users/templates/admin/users/user/role.html diff --git a/orchestra/apps/users/backends.py b/orchestra/apps/users/backends.py new file mode 100644 index 00000000..1af71f40 --- /dev/null +++ b/orchestra/apps/users/backends.py @@ -0,0 +1,123 @@ +import textwrap + +from django.utils import timezone +from django.utils.translation import ugettext_lazy as _ + +from orchestra.apps.orchestration import ServiceController +from orchestra.apps.resources import ServiceMonitor + +from . import settings + + +class SystemUserBackend(ServiceController): + verbose_name = _("System User") + model = 'users.User' + ignore_fields = ['last_login'] + + def save(self, user): + context = self.get_context(user) + if user.is_main: + self.append(textwrap.dedent(""" + if [[ $( id %(username)s ) ]]; then + usermod --password '%(password)s' %(username)s + else + useradd %(username)s --password '%(password)s' \\ + --shell /bin/false + fi + mkdir -p %(home)s + chown %(username)s.%(username)s %(home)s""" % context + )) + else: + self.delete(user) + + def delete(self, user): + context = self.get_context(user) + self.append("{ sleep 2 && killall -u %(username)s -s KILL; } &" % context) + self.append("killall -u %(username)s" % context) + self.append("userdel %(username)s" % context) + + def get_context(self, user): + context = { + 'username': user.username, + 'password': user.password if user.is_active else '*%s' % user.password, + } + context['home'] = settings.USERS_SYSTEMUSER_HOME % context + return context + + +class SystemUserDisk(ServiceMonitor): + model = 'users.User' + resource = ServiceMonitor.DISK + verbose_name = _('System user disk') + + def monitor(self, user): + context = self.get_context(user) + self.append("du -s %(home)s | xargs echo %(object_id)s" % context) + + def get_context(self, user): + context = SystemUserBackend().get_context(user) + context['object_id'] = user.pk + return context + + +class FTPTraffic(ServiceMonitor): + model = 'users.User' + resource = ServiceMonitor.TRAFFIC + verbose_name = _('FTP traffic') + + def prepare(self): + current_date = timezone.localtime(self.current_date) + current_date = current_date.strftime("%Y%m%d%H%M%S") + self.append(textwrap.dedent(""" + function monitor () { + OBJECT_ID=$1 + INI_DATE=$2 + USERNAME="$3" + LOG_FILE="$4" + grep "UPLOAD\|DOWNLOAD" "${LOG_FILE}" \\ + | grep " \\[${USERNAME}\\] " \\ + | awk -v ini="${INI_DATE}" ' + BEGIN { + end = "%s" + sum = 0 + months["Jan"] = "01" + months["Feb"] = "02" + months["Mar"] = "03" + months["Apr"] = "04" + months["May"] = "05" + months["Jun"] = "06" + months["Jul"] = "07" + months["Aug"] = "08" + months["Sep"] = "09" + months["Oct"] = "10" + months["Nov"] = "11" + months["Dec"] = "12" + } { + # log: Fri Jul 11 13:23:17 2014 + split($4, t, ":") + # line_date = year month day hour minute second + line_date = $5 months[$2] $3 t[1] t[2] t[3] + if ( line_date > ini && line_date < end) + split($0, l, "\\", ") + split(l[3], b, " ") + sum += b[1] + } END { + print sum + } + ' | xargs echo ${OBJECT_ID} + }""" % current_date)) + + def monitor(self, user): + context = self.get_context(user) + self.append( + 'monitor %(object_id)i %(last_date)s "%(username)s" "%(log_file)s"' % context) + + def get_context(self, user): + last_date = timezone.localtime(self.get_last_date(user.pk)) + return { + 'log_file': settings.USERS_FTP_LOG_PATH, + 'last_date': last_date.strftime("%Y%m%d%H%M%S"), + 'object_id': user.pk, + 'username': user.username, + } + diff --git a/orchestra/apps/users/roles/__init__.py b/orchestra/apps/users/roles/__init__.py new file mode 100644 index 00000000..62b9ee6f --- /dev/null +++ b/orchestra/apps/users/roles/__init__.py @@ -0,0 +1,27 @@ +from ..models import User + + +class Register(object): + def __init__(self): + self._registry = {} + + def __contains__(self, key): + return key in self._registry + + def register(self, name, model): + if name in self._registry: + raise KeyError("%s already registered" % name) + def has_role(user, model=model): + try: + getattr(user, name) + except model.DoesNotExist: + return False + return True + setattr(User, 'has_%s' % name, has_role) + self._registry[name] = model + + def get(self): + return self._registry + + +roles = Register() diff --git a/orchestra/apps/users/roles/admin.py b/orchestra/apps/users/roles/admin.py new file mode 100644 index 00000000..0432a682 --- /dev/null +++ b/orchestra/apps/users/roles/admin.py @@ -0,0 +1,145 @@ +from django.contrib import messages +from django.contrib.admin.util import unquote, get_deleted_objects +from django.contrib.admin.templatetags.admin_urls import add_preserved_filters +from django.db import router +from django.http import Http404, HttpResponseRedirect +from django.template.response import TemplateResponse +from django.shortcuts import redirect +from django.utils.encoding import force_text +from django.utils.html import escape +from django.utils.translation import ugettext, ugettext_lazy as _ + +from orchestra.admin.utils import get_modeladmin, change_url + +from .forms import role_form_factory +from ..models import User + + +class RoleAdmin(object): + model = None + name = '' + url_name = '' + form = None + + def __init__(self, user=None): + self.user = user + + @property + def exists(self): + try: + return getattr(self.user, self.name) + except self.model.DoesNotExist: + return False + + def get_user(self, request, object_id): + try: + user = User.objects.get(pk=unquote(object_id)) + except User.DoesNotExist: + opts = self.model._meta + raise Http404( + _('%(name)s object with primary key %(key)r does not exist.') % + {'name': force_text(opts.verbose_name), 'key': escape(object_id)} + ) + return user + + def change_view(self, request, object_id): + modeladmin = get_modeladmin(User) + user = self.get_user(request, object_id) + self.user = user + obj = None + exists = self.exists + if exists: + obj = getattr(user, self.name) + form_class = self.form if self.form else role_form_factory(self) + form = form_class(instance=obj) + opts = User._meta + app_label = opts.app_label + title = _("Add %s for user %s" % (self.name, user)) + action = _("Create") + # User has submitted the form + if request.method == 'POST': + form = form_class(request.POST, instance=obj) + form.user = user + if form.is_valid(): + obj = form.save() + context = { + 'name': obj._meta.verbose_name, + 'obj': obj, + 'action': _("saved" if exists else "created") + } + modeladmin.log_change(request, request.user, "%s saved" % self.name.capitalize()) + msg = _('The role %(name)s for user "%(obj)s" was %(action)s successfully.') % context + modeladmin.message_user(request, msg, messages.SUCCESS) + if not "_continue" in request.POST: + return redirect(change_url(user)) + exists = True + + if exists: + title = _("Change %s %s settings" % (user, self.name)) + action = _("Save") + form = form_class(instance=obj) + + context = { + 'title': title, + 'opts': opts, + 'app_label': app_label, + 'form': form, + 'action': action, + 'role': self, + 'roles': [ role(user=user) for role in modeladmin.roles ], + 'media': modeladmin.media + } + + template = 'admin/users/user/role.html' + app = modeladmin.admin_site.name + return TemplateResponse(request, template, context, current_app=app) + + def delete_view(self, request, object_id): + "The 'delete' admin view for this model." + opts = self.model._meta + app_label = opts.app_label + modeladmin = get_modeladmin(User) + user = self.get_user(request, object_id) + obj = getattr(user, self.name) + + using = router.db_for_write(self.model) + + # Populate deleted_objects, a data structure of all related objects that + # will also be deleted. + (deleted_objects, perms_needed, protected) = get_deleted_objects( + [obj], opts, request.user, modeladmin.admin_site, using) + + if request.POST: # The user has already confirmed the deletion. + if perms_needed: + raise PermissionDenied + obj_display = force_text(obj) + modeladmin.log_deletion(request, obj, obj_display) + modeladmin.delete_model(request, obj) + post_url = change_url(user) + preserved_filters = modeladmin.get_preserved_filters(request) + post_url = add_preserved_filters( + {'preserved_filters': preserved_filters, 'opts': opts}, post_url + ) + return HttpResponseRedirect(post_url) + + object_name = force_text(opts.verbose_name) + + if perms_needed or protected: + title = _("Cannot delete %(name)s") % {"name": object_name} + else: + title = _("Are you sure?") + + context = { + "title": title, + "object_name": object_name, + "object": obj, + "deleted_objects": deleted_objects, + "perms_lacking": perms_needed, + "protected": protected, + "opts": opts, + "app_label": app_label, + 'preserved_filters': modeladmin.get_preserved_filters(request), + 'role': self, + } + return TemplateResponse(request, 'admin/users/user/delete_role.html', + context, current_app=modeladmin.admin_site.name) diff --git a/orchestra/apps/users/roles/filters.py b/orchestra/apps/users/roles/filters.py new file mode 100644 index 00000000..7bac9a3b --- /dev/null +++ b/orchestra/apps/users/roles/filters.py @@ -0,0 +1,23 @@ +from django.contrib.admin import SimpleListFilter +from django.utils.translation import ugettext_lazy as _ + + +def role_list_filter_factory(role): + class RoleListFilter(SimpleListFilter): + """ Filter Nodes by group according to request.user """ + title = _("has %s" % role.name) + parameter_name = role.url_name + + def lookups(self, request, model_admin): + return ( + ('True', _("Yes")), + ('False', _("No")), + ) + + def queryset(self, request, queryset): + if self.value() == 'True': + return queryset.filter(**{ '%s__isnull' % role.name: False }) + if self.value() == 'False': + return queryset.filter(**{ '%s__isnull' % role.name: True }) + + return RoleListFilter diff --git a/orchestra/apps/users/roles/forms.py b/orchestra/apps/users/roles/forms.py new file mode 100644 index 00000000..decc6610 --- /dev/null +++ b/orchestra/apps/users/roles/forms.py @@ -0,0 +1,17 @@ +from django import forms + + +class RoleAdminBaseForm(forms.ModelForm): + class Meta: + exclude = ('user', ) + + def save(self, *args, **kwargs): + self.instance.user = self.user + return super(RoleAdminBaseForm, self).save(*args, **kwargs) + + +def role_form_factory(role): + class RoleAdminForm(RoleAdminBaseForm): + class Meta(RoleAdminBaseForm.Meta): + model = role.model + return RoleAdminForm diff --git a/orchestra/apps/users/roles/jabber/__init__.py b/orchestra/apps/users/roles/jabber/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/orchestra/apps/users/roles/jabber/admin.py b/orchestra/apps/users/roles/jabber/admin.py new file mode 100644 index 00000000..ba126c0f --- /dev/null +++ b/orchestra/apps/users/roles/jabber/admin.py @@ -0,0 +1,15 @@ +from django.contrib.auth import get_user_model + +from orchestra.admin.utils import insertattr +from orchestra.apps.users.roles.admin import RoleAdmin + +from .models import Jabber + + +class JabberRoleAdmin(RoleAdmin): + model = Jabber + name = 'jabber' + url_name = 'jabber' + + +insertattr(get_user_model(), 'roles', JabberRoleAdmin) diff --git a/orchestra/apps/users/roles/jabber/models.py b/orchestra/apps/users/roles/jabber/models.py new file mode 100644 index 00000000..32850579 --- /dev/null +++ b/orchestra/apps/users/roles/jabber/models.py @@ -0,0 +1,15 @@ +from django.db import models +from django.utils.translation import ugettext_lazy as _ + +from .. import roles + + +class Jabber(models.Model): + user = models.OneToOneField('users.User', verbose_name=_("user"), + related_name='jabber') + + def __unicode__(self): + return str(self.user) + + +roles.register('jabber', Jabber) diff --git a/orchestra/apps/users/roles/mail/__init__.py b/orchestra/apps/users/roles/mail/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/orchestra/apps/users/roles/mail/admin.py b/orchestra/apps/users/roles/mail/admin.py new file mode 100644 index 00000000..58d3fa56 --- /dev/null +++ b/orchestra/apps/users/roles/mail/admin.py @@ -0,0 +1,124 @@ +from django import forms +from django.contrib import admin +from django.contrib.auth import get_user_model +from django.core.urlresolvers import reverse +from django.utils.translation import ugettext_lazy as _ + +from orchestra.admin import ExtendedModelAdmin +from orchestra.admin.utils import insertattr, admin_link +from orchestra.apps.accounts.admin import SelectAccountAdminMixin +from orchestra.apps.users.roles.admin import RoleAdmin + +from .forms import MailRoleAdminForm +from .models import Mailbox, Address, Autoresponse + + +class AutoresponseInline(admin.StackedInline): + model = Autoresponse + verbose_name_plural = _("autoresponse") + + def formfield_for_dbfield(self, db_field, **kwargs): + if db_field.name == 'subject': + kwargs['widget'] = forms.TextInput(attrs={'size':'118'}) + return super(AutoresponseInline, self).formfield_for_dbfield(db_field, **kwargs) + + +#class AddressAdmin(SelectAccountAdminMixin, ExtendedModelAdmin): +# list_display = ('email', 'domain_link', 'mailboxes', 'forwards', 'account_link') +# fields = ('account_link', ('name', 'domain'), 'destination') +# inlines = [AutoresponseInline] +# search_fields = ('name', 'domain__name',) +# readonly_fields = ('account_link', 'domain_link', 'email_link') +# filter_by_account_fields = ['domain'] +# +# domain_link = link('domain', order='domain__name') +# +# def email_link(self, address): +# link = self.domain_link(address) +# return "%s@%s" % (address.name, link) +# email_link.short_description = _("Email") +# email_link.allow_tags = True +# +# def mailboxes(self, address): +# boxes = [] +# for mailbox in address.get_mailboxes(): +# user = mailbox.user +# url = reverse('admin:users_user_mailbox_change', args=(user.pk,)) +# boxes.append('%s' % (url, user.username)) +# return '
'.join(boxes) +# mailboxes.allow_tags = True +# +# def forwards(self, address): +# values = [ dest for dest in address.destination.split() if '@' in dest ] +# return '
'.join(values) +# forwards.allow_tags = True +# +# def formfield_for_dbfield(self, db_field, **kwargs): +# if db_field.name == 'destination': +# kwargs['widget'] = forms.TextInput(attrs={'size':'118'}) +# return super(AddressAdmin, self).formfield_for_dbfield(db_field, **kwargs) +# +# def queryset(self, request): +# """ Select related for performance """ +# qs = super(AddressAdmin, self).queryset(request) +# return qs.select_related('domain') + + +class AddressAdmin(SelectAccountAdminMixin, ExtendedModelAdmin): + list_display = ( + 'email', 'domain_link', 'display_mailboxes', 'display_forward', 'account_link' + ) + fields = ('account_link', ('name', 'domain'), 'mailboxes', 'forward') + inlines = [AutoresponseInline] + search_fields = ('name', 'domain__name',) + readonly_fields = ('account_link', 'domain_link', 'email_link') + filter_by_account_fields = ['domain'] + filter_horizontal = ['mailboxes'] + + domain_link = admin_link('domain', order='domain__name') + + def email_link(self, address): + link = self.domain_link(address) + return "%s@%s" % (address.name, link) + email_link.short_description = _("Email") + email_link.allow_tags = True + + def display_mailboxes(self, address): + boxes = [] + for mailbox in address.mailboxes.all(): + user = mailbox.user + url = reverse('admin:users_user_mailbox_change', args=(user.pk,)) + boxes.append('%s' % (url, user.username)) + return '
'.join(boxes) + display_mailboxes.short_description = _("Mailboxes") + display_mailboxes.allow_tags = True + + def display_forward(self, address): + values = [ dest for dest in address.forward.split() ] + return '
'.join(values) + display_forward.short_description = _("Forward") + display_forward.allow_tags = True + + def formfield_for_dbfield(self, db_field, **kwargs): + if db_field.name == 'forward': + kwargs['widget'] = forms.TextInput(attrs={'size':'118'}) + if db_field.name == 'mailboxes': + mailboxes = db_field.rel.to.objects.select_related('user') + kwargs['queryset'] = mailboxes.filter(user__account=self.account) + return super(AddressAdmin, self).formfield_for_dbfield(db_field, **kwargs) + + def get_queryset(self, request): + """ Select related for performance """ + qs = super(AddressAdmin, self).get_queryset(request) + return qs.select_related('domain') + + +class MailRoleAdmin(RoleAdmin): + model = Mailbox + name = 'mailbox' + url_name = 'mailbox' + form = MailRoleAdminForm + + +admin.site.register(Address, AddressAdmin) +insertattr(get_user_model(), 'roles', MailRoleAdmin) diff --git a/orchestra/apps/users/roles/mail/api.py b/orchestra/apps/users/roles/mail/api.py new file mode 100644 index 00000000..410d8a1c --- /dev/null +++ b/orchestra/apps/users/roles/mail/api.py @@ -0,0 +1,27 @@ +from rest_framework import viewsets + +from orchestra.api import router +from orchestra.apps.accounts.api import AccountApiMixin + +from .models import Address, Mailbox +from .serializers import AddressSerializer, MailboxSerializer + + +class AddressViewSet(AccountApiMixin, viewsets.ModelViewSet): + model = Address + serializer_class = AddressSerializer + + + +class MailboxViewSet(viewsets.ModelViewSet): + model = Mailbox + serializer_class = MailboxSerializer + + def get_queryset(self): + qs = super(MailboxViewSet, self).get_queryset() + qs = qs.select_related('user') + return qs.filter(user__account=self.request.user.account_id) + + +router.register(r'mailboxes', MailboxViewSet) +router.register(r'addresses', AddressViewSet) diff --git a/orchestra/apps/users/roles/mail/backends.py b/orchestra/apps/users/roles/mail/backends.py new file mode 100644 index 00000000..b29d03ae --- /dev/null +++ b/orchestra/apps/users/roles/mail/backends.py @@ -0,0 +1,160 @@ +import os + +from django.utils import timezone +from django.utils.translation import ugettext_lazy as _ + +from orchestra.apps.orchestration import ServiceController +from orchestra.apps.resources import ServiceMonitor + +from . import settings +from .models import Address + + +class MailSystemUserBackend(ServiceController): + verbose_name = _("Mail system user") + model = 'mail.Mailbox' + # TODO related_models = ('resources__content_type') ?? + + DEFAULT_GROUP = 'postfix' + + def create_user(self, context): + self.append( + "if [[ $( id %(username)s ) ]]; then \n" + " usermod -p '%(password)s' %(username)s \n" + "else \n" + " useradd %(username)s --password '%(password)s' \\\n" + " --shell /dev/null \n" + "fi" % context + ) + self.append("mkdir -p %(home)s" % context) + self.append("chown %(username)s.%(group)s %(home)s" % context) + + def generate_filter(self, mailbox, context): + now = timezone.now().strftime("%B %d, %Y, %H:%M") + context['filtering'] = ( + "# Sieve Filter\n" + "# Generated by Orchestra %s\n\n" % now + ) + if mailbox.use_custom_filtering: + context['filtering'] += mailbox.custom_filtering + else: + context['filtering'] += settings.EMAILS_DEFAUL_FILTERING + context['filter_path'] = os.path.join(context['home'], '.orchestra.sieve') + self.append("echo '%(filtering)s' > %(filter_path)s" % context) + + def save(self, mailbox): + context = self.get_context(mailbox) + self.create_user(context) + self.generate_filter(mailbox, context) + + def delete(self, mailbox): + context = self.get_context(mailbox) + self.append("{ sleep 2 && killall -u %(username)s -s KILL; } &" % context) + self.append("killall -u %(username)s" % context) + self.append("userdel %(username)s" % context) + self.append("rm -fr %(home)s" % context) + + def get_context(self, mailbox): + user = mailbox.user + context = { + 'username': user.username, + 'password': user.password if user.is_active else '*%s' % user.password, + 'group': self.DEFAULT_GROUP + } + context['home'] = settings.EMAILS_HOME % context + return context + + +class PostfixAddressBackend(ServiceController): + verbose_name = _("Postfix address") + model = 'mail.Address' + + def include_virtdomain(self, context): + self.append( + '[[ $(grep "^\s*%(domain)s\s*$" %(virtdomains)s) ]]' + ' || { echo "%(domain)s" >> %(virtdomains)s; UPDATED=1; }' % context + ) + + def exclude_virtdomain(self, context): + domain = context['domain'] + if not Address.objects.filter(domain=domain).exists(): + self.append('sed -i "s/^%(domain)s//" %(virtdomains)s' % context) + + def update_virtusertable(self, context): + self.append( + 'LINE="%(email)s\t%(destination)s"\n' + 'if [[ ! $(grep "^%(email)s\s" %(virtusertable)s) ]]; then\n' + ' echo "$LINE" >> %(virtusertable)s\n' + ' UPDATED=1\n' + 'else\n' + ' if [[ ! $(grep "^${LINE}$" %(virtusertable)s) ]]; then\n' + ' sed -i "s/^%(email)s\s.*$/${LINE}/" %(virtusertable)s\n' + ' UPDATED=1\n' + ' fi\n' + 'fi' % context + ) + + def exclude_virtusertable(self, context): + self.append( + 'if [[ $(grep "^%(email)s\s") ]]; then\n' + ' sed -i "s/^%(email)s\s.*$//" %(virtusertable)s\n' + ' UPDATED=1\n' + 'fi' + ) + + def save(self, address): + context = self.get_context(address) + self.include_virtdomain(context) + self.update_virtusertable(context) + + def delete(self, address): + context = self.get_context(address) + self.exclude_virtdomain(context) + self.exclude_virtusertable(context) + + def commit(self): + context = self.get_context_files() + self.append('[[ $UPDATED == 1 ]] && { ' + 'postmap %(virtdomains)s;' + 'postmap %(virtusertable)s;' + '}' % context) + + def get_context_files(self): + return { + 'virtdomains': settings.EMAILS_VIRTDOMAINS_PATH, + 'virtusertable': settings.EMAILS_VIRTUSERTABLE_PATH, + } + + def get_context(self, address): + context = self.get_context_files() + context.update({ + 'domain': address.domain, + 'email': address.email, + 'destination': address.destination, + }) + return context + + +class AutoresponseBackend(ServiceController): + verbose_name = _("Mail autoresponse") + model = 'mail.Autoresponse' + + +class MaildirDisk(ServiceMonitor): + model = 'email.Mailbox' + resource = ServiceMonitor.DISK + verbose_name = _("Maildir disk usage") + + def monitor(self, mailbox): + context = self.get_context(mailbox) + self.append( + "SIZE=$(sed -n '2p' %(maildir_path)s | cut -d' ' -f1)\n" + "echo %(object_id)s ${SIZE:-0}" % context + ) + + def get_context(self, mailbox): + context = MailSystemUserBackend().get_context(mailbox) + context['home'] = settings.EMAILS_HOME % context + context['maildir_path'] = os.path.join(context['home'], 'Maildir/maildirsize') + context['object_id'] = mailbox.pk + return context diff --git a/orchestra/apps/users/roles/mail/forms.py b/orchestra/apps/users/roles/mail/forms.py new file mode 100644 index 00000000..7066e5d0 --- /dev/null +++ b/orchestra/apps/users/roles/mail/forms.py @@ -0,0 +1,53 @@ +from django import forms +from django.core.urlresolvers import reverse +from django.utils.safestring import mark_safe +from django.utils.translation import ugettext_lazy as _ + +from orchestra.forms.widgets import ReadOnlyWidget + +from .models import Mailbox +from ..forms import RoleAdminBaseForm + + +class MailRoleAdminForm(RoleAdminBaseForm): + class Meta(RoleAdminBaseForm.Meta): + model = Mailbox + + def __init__(self, *args, **kwargs): + super(MailRoleAdminForm, self).__init__(*args, **kwargs) + instance = kwargs.get('instance') + if instance: + widget = ReadOnlyWidget(self.addresses(instance)) + self.fields['addresses'] = forms.CharField(widget=widget, + label=_("Addresses")) + +# def addresses(self, mailbox): +# account = mailbox.user.account +# addresses = account.addresses.filter(destination__contains=mailbox.user.username) +# add_url = reverse('admin:mail_address_add') +# add_url += '?account=%d&destination=%s' % (account.pk, mailbox.user.username) +# img = 'Add Another' +# onclick = 'onclick="return showAddAnotherPopup(this);"' +# add_link = '%s Add address' % (add_url, onclick, img) +# value = '%s

' % add_link +# for pk, name, domain in addresses.values_list('pk', 'name', 'domain__name'): +# url = reverse('admin:mail_address_change', args=(pk,)) +# name = '%s@%s' % (name, domain) +# value += '
  • %s
  • ' % (url, name) +# value = '' % value +# return mark_safe('
    %s
    ' % value) + + def addresses(self, mailbox): + account = mailbox.user.account + add_url = reverse('admin:mail_address_add') + add_url += '?account=%d&mailboxes=%s' % (account.pk, mailbox.pk) + img = 'Add Another' + onclick = 'onclick="return showAddAnotherPopup(this);"' + add_link = '%s Add address' % (add_url, onclick, img) + value = '%s

    ' % add_link + for pk, name, domain in mailbox.addresses.values_list('pk', 'name', 'domain__name'): + url = reverse('admin:mail_address_change', args=(pk,)) + name = '%s@%s' % (name, domain) + value += '
  • %s
  • ' % (url, name) + value = '' % value + return mark_safe('
    %s
    ' % value) diff --git a/orchestra/apps/users/roles/mail/models.py b/orchestra/apps/users/roles/mail/models.py new file mode 100644 index 00000000..35d28355 --- /dev/null +++ b/orchestra/apps/users/roles/mail/models.py @@ -0,0 +1,110 @@ +from django.db import models +from django.utils.translation import ugettext_lazy as _ + +from orchestra.core import services + +from .. import roles + +from . import validators, settings + + +class Mailbox(models.Model): + user = models.OneToOneField('users.User', verbose_name=_("User"), + related_name='mailbox') + use_custom_filtering = models.BooleanField(_("Use custom filtering"), + default=False) + custom_filtering = models.TextField(_("filtering"), blank=True, + validators=[validators.validate_sieve], + help_text=_("Arbitrary email filtering in sieve language.")) + + class Meta: + verbose_name_plural = _("mailboxes") + + def __unicode__(self): + return self.user.username + +# def get_addresses(self): +# regex = r'(^|\s)+%s(\s|$)+' % self.user.username +# return Address.objects.filter(destination__regex=regex) +# +# def delete(self, *args, **kwargs): +# """ Update related addresses """ +# regex = re.compile(r'(^|\s)+(\s*%s)(\s|$)+' % self.user.username) +# super(Mailbox, self).delete(*args, **kwargs) +# for address in self.get_addresses(): +# address.destination = regex.sub(r'\3', address.destination).strip() +# if not address.destination: +# address.delete() +# else: +# address.save() + + +#class Address(models.Model): +# name = models.CharField(_("name"), max_length=64, +# validators=[validators.validate_emailname]) +# domain = models.ForeignKey(settings.EMAILS_DOMAIN_MODEL, +# verbose_name=_("domain"), +# related_name='addresses') +# destination = models.CharField(_("destination"), max_length=256, +# validators=[validators.validate_destination], +# help_text=_("Space separated mailbox names or email addresses")) +# account = models.ForeignKey('accounts.Account', verbose_name=_("Account"), +# related_name='addresses') +# +# class Meta: +# verbose_name_plural = _("addresses") +# unique_together = ('name', 'domain') +# +# def __unicode__(self): +# return self.email +# +# @property +# def email(self): +# return "%s@%s" % (self.name, self.domain) +# +# def get_mailboxes(self): +# for dest in self.destination.split(): +# if '@' not in dest: +# yield Mailbox.objects.select_related('user').get(user__username=dest) + + +class Address(models.Model): + name = models.CharField(_("name"), max_length=64, + validators=[validators.validate_emailname]) + domain = models.ForeignKey(settings.EMAILS_DOMAIN_MODEL, + verbose_name=_("domain"), + related_name='addresses') + mailboxes = models.ManyToManyField('mail.Mailbox', + verbose_name=_("mailboxes"), + related_name='addresses', blank=True) + forward = models.CharField(_("forward"), max_length=256, blank=True, + validators=[validators.validate_forward]) + account = models.ForeignKey('accounts.Account', verbose_name=_("Account"), + related_name='addresses') + + class Meta: + verbose_name_plural = _("addresses") + unique_together = ('name', 'domain') + + def __unicode__(self): + return self.email + + @property + def email(self): + return "%s@%s" % (self.name, self.domain) + + +class Autoresponse(models.Model): + address = models.OneToOneField(Address, verbose_name=_("address"), + related_name='autoresponse') + # TODO initial_date + subject = models.CharField(_("subject"), max_length=256) + message = models.TextField(_("message")) + enabled = models.BooleanField(_("enabled"), default=False) + + def __unicode__(self): + return self.address + + +services.register(Address) +roles.register('mailbox', Mailbox) diff --git a/orchestra/apps/users/roles/mail/serializers.py b/orchestra/apps/users/roles/mail/serializers.py new file mode 100644 index 00000000..24bf1a70 --- /dev/null +++ b/orchestra/apps/users/roles/mail/serializers.py @@ -0,0 +1,43 @@ +from rest_framework import serializers + +from orchestra.api import router +from orchestra.apps.accounts.serializers import AccountSerializerMixin + +from .models import Address, Mailbox + + +#class AddressSerializer(serializers.HyperlinkedModelSerializer): +# class Meta: +# model = Address +# fields = ('url', 'name', 'domain', 'destination') + + +class NestedMailboxSerializer(serializers.HyperlinkedModelSerializer): + class Meta: + model = Mailbox + fields = ('url', 'use_custom_filtering', 'custom_filtering') + + +class MailboxSerializer(serializers.HyperlinkedModelSerializer): + class Meta: + model = Mailbox + fields = ('url', 'user', 'use_custom_filtering', 'custom_filtering') + + +class AddressSerializer(AccountSerializerMixin, serializers.HyperlinkedModelSerializer): + class Meta: + model = Address + fields = ('url', 'name', 'domain', 'mailboxes', 'forward') + + def get_fields(self, *args, **kwargs): + fields = super(AddressSerializer, self).get_fields(*args, **kwargs) + account = self.context['view'].request.user.account_id + mailboxes = fields['mailboxes'].queryset.select_related('user') + fields['mailboxes'].queryset = mailboxes.filter(user__account=account) + # TODO do it on permissions or in self.filter_by_account_field ? + domain = fields['domain'].queryset + fields['domain'].queryset = domain .filter(account=account) + return fields + + +router.insert('users', 'mailbox', NestedMailboxSerializer, required=False) diff --git a/orchestra/apps/users/roles/mail/settings.py b/orchestra/apps/users/roles/mail/settings.py new file mode 100644 index 00000000..fcca1e32 --- /dev/null +++ b/orchestra/apps/users/roles/mail/settings.py @@ -0,0 +1,29 @@ +from django.conf import settings + + +EMAILS_DOMAIN_MODEL = getattr(settings, 'EMAILS_DOMAIN_MODEL', 'domains.Domain') + +EMAILS_HOME = getattr(settings, 'EMAILS_HOME', '/home/%(username)s/') + +EMAILS_SIEVETEST_PATH = getattr(settings, 'EMAILS_SIEVETEST_PATH', '/dev/shm') + +EMAILS_SIEVETEST_BIN_PATH = getattr(settings, 'EMAILS_SIEVETEST_BIN_PATH', + '%(orchestra_root)s/bin/sieve-test') + + +EMAILS_VIRTUSERTABLE_PATH = getattr(settings, 'EMAILS_VIRTUSERTABLE_PATH', + '/etc/postfix/virtusertable') + + +EMAILS_VIRTDOMAINS_PATH = getattr(settings, 'EMAILS_VIRTDOMAINS_PATH', + '/etc/postfix/virtdomains') + + +EMAILS_DEFAUL_FILTERING = getattr(settings, 'EMAILS_DEFAULT_FILTERING', + 'require ["fileinto","regex","envelope","vacation","reject","relational","comparator-i;ascii-numeric"];\n' + '\n' + 'if header :value "ge" :comparator "i;ascii-numeric" "X-Spam-Score" "5" {\n' + ' fileinto "Junk";\n' + ' discard;\n' + '}' +) diff --git a/orchestra/apps/users/roles/mail/validators.py b/orchestra/apps/users/roles/mail/validators.py new file mode 100644 index 00000000..eab400fa --- /dev/null +++ b/orchestra/apps/users/roles/mail/validators.py @@ -0,0 +1,62 @@ +import hashlib +import os +import re + +from django.core.validators import ValidationError, EmailValidator +from django.utils.translation import ugettext_lazy as _ + +from orchestra.utils import paths +from orchestra.utils.system import run + +from . import settings + + +def validate_emailname(value): + msg = _("'%s' is not a correct email name" % value) + if '@' in value: + raise ValidationError(msg) + value += '@localhost' + try: + EmailValidator(value) + except ValidationError: + raise ValidationError(msg) + + +#def validate_destination(value): +# """ space separated mailboxes or emails """ +# for destination in value.split(): +# msg = _("'%s' is not an existent mailbox" % destination) +# if '@' in destination: +# if not destination[-1].isalpha(): +# raise ValidationError(msg) +# EmailValidator(destination) +# else: +# from .models import Mailbox +# if not Mailbox.objects.filter(user__username=destination).exists(): +# raise ValidationError(msg) +# validate_emailname(destination) + + +def validate_forward(value): + """ space separated mailboxes or emails """ + for destination in value.split(): + EmailValidator(destination) + + +def validate_sieve(value): + sieve_name = '%s.sieve' % hashlib.md5(value).hexdigest() + path = os.path.join(settings.EMAILS_SIEVETEST_PATH, sieve_name) + with open(path, 'wb') as f: + f.write(value) + context = { + 'orchestra_root': paths.get_orchestra_root() + } + sievetest = settings.EMAILS_SIEVETEST_BIN_PATH % context + test = run(' '.join([sievetest, path, '/dev/null']), display=False) + if test.return_code: + errors = [] + for line in test.stderr.splitlines(): + error = re.match(r'^.*(line\s+[0-9]+:.*)', line) + if error: + errors += error.groups() + raise ValidationError(' '.join(errors)) diff --git a/orchestra/apps/users/roles/owncloud/__init__.py b/orchestra/apps/users/roles/owncloud/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/orchestra/apps/users/roles/posix/__init__.py b/orchestra/apps/users/roles/posix/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/orchestra/apps/users/roles/posix/admin.py b/orchestra/apps/users/roles/posix/admin.py new file mode 100644 index 00000000..0ef5af77 --- /dev/null +++ b/orchestra/apps/users/roles/posix/admin.py @@ -0,0 +1,15 @@ +from django.contrib.auth import get_user_model + +from orchestra.admin.utils import insertattr +from orchestra.apps.users.roles.admin import RoleAdmin + +from .models import POSIX + + +class POSIXRoleAdmin(RoleAdmin): + model = POSIX + name = 'posix' + url_name = 'posix' + + +insertattr(get_user_model(), 'roles', POSIXRoleAdmin) diff --git a/orchestra/apps/users/roles/posix/models.py b/orchestra/apps/users/roles/posix/models.py new file mode 100644 index 00000000..3639afc9 --- /dev/null +++ b/orchestra/apps/users/roles/posix/models.py @@ -0,0 +1,22 @@ +from django.db import models +from django.utils.translation import ugettext_lazy as _ + +from .. import roles + +from . import settings + + +class POSIX(models.Model): + user = models.OneToOneField('users.User', verbose_name=_("user"), + related_name='posix') + home = models.CharField(_("home"), max_length=256, blank=True, + help_text=_("Home directory relative to account's ~primary_user")) + shell = models.CharField(_("shell"), max_length=32, + choices=settings.POSIX_SHELLS, default=settings.POSIX_DEFAULT_SHELL) + + def __unicode__(self): + return str(self.user) + +# TODO groups + +roles.register('posix', POSIX) diff --git a/orchestra/apps/users/roles/posix/serializers.py b/orchestra/apps/users/roles/posix/serializers.py new file mode 100644 index 00000000..3dc341c5 --- /dev/null +++ b/orchestra/apps/users/roles/posix/serializers.py @@ -0,0 +1,14 @@ +from rest_framework import serializers + +from orchestra.api import router + +from .models import POSIX + + +class POSIXSerializer(serializers.ModelSerializer): + class Meta: + model = POSIX + fields = ('home', 'shell') + + +router.insert('users', 'posix', POSIXSerializer, required=False) diff --git a/orchestra/apps/users/roles/posix/settings.py b/orchestra/apps/users/roles/posix/settings.py new file mode 100644 index 00000000..36860863 --- /dev/null +++ b/orchestra/apps/users/roles/posix/settings.py @@ -0,0 +1,11 @@ +from django.conf import settings +from django.utils.translation import ugettext, ugettext_lazy as _ + + +POSIX_SHELLS = getattr(settings, 'POSIX_SHELLS', ( + ('/bin/false', _("FTP/sFTP only")), + ('/bin/rsync', _("rsync shell")), + ('/bin/bash', "Bash"), +)) + +POSIX_DEFAULT_SHELL = getattr(settings, 'POSIX_DEFAULT_SHELL', '/bin/false') diff --git a/orchestra/apps/users/templates/admin/users/user/change_form.html b/orchestra/apps/users/templates/admin/users/user/change_form.html new file mode 100644 index 00000000..82814f98 --- /dev/null +++ b/orchestra/apps/users/templates/admin/users/user/change_form.html @@ -0,0 +1,15 @@ +{% extends "admin/change_form.html" %} +{% load i18n admin_urls %} + +{% block object-tools-items %} +
  • {% trans "User" %}
  • +{% for item in roles %} +
  • {% if item.exists %}{{ item.name.capitalize }}{% else %}Add {{ item.name }}{% endif %}
  • +{% endfor %} +
  • + {% url opts|admin_urlname:'history' original.pk|admin_urlquote as history_url %} + {% trans "History" %} +
  • +{% if has_absolute_url %}
  • {% trans "View on site" %}
  • {% endif%} +{% endblock %} + diff --git a/orchestra/apps/users/templates/admin/users/user/delete_role.html b/orchestra/apps/users/templates/admin/users/user/delete_role.html new file mode 100644 index 00000000..ae94df22 --- /dev/null +++ b/orchestra/apps/users/templates/admin/users/user/delete_role.html @@ -0,0 +1,15 @@ +{% extends "admin/delete_confirmation.html" %} +{% load i18n admin_urls %} + + +{% block breadcrumbs %} + +{% endblock %} + diff --git a/orchestra/apps/users/templates/admin/users/user/role.html b/orchestra/apps/users/templates/admin/users/user/role.html new file mode 100644 index 00000000..75927310 --- /dev/null +++ b/orchestra/apps/users/templates/admin/users/user/role.html @@ -0,0 +1,70 @@ +{% extends "admin/base_site.html" %} +{% load i18n admin_urls admin_static admin_modify utils %} + + +{% block extrastyle %} +{{ block.super }} + +{{ media }} +{% endblock %} + +{% block coltype %}colM{% endblock %} +{% block bodyclass %}{{ block.super }} app-{{ opts.app_label }} model-{{ opts.model_name }} change-form{% endblock %} + + +{% block breadcrumbs %} + +{% endblock %} + + + +{% block content %}
    +{% block object-tools %} + +{% endblock %} + +
    {% csrf_token %} +
    + {% for field in form %} +
    + + {% if not line.fields|length_is:'1' and not field.is_readonly %}{{ field.errors }}{% endif %} + {% if field|is_checkbox %} + {{ field }} + {% else %} + {{ field.label_tag }} {{ field }} + {% endif %} + {% if field.help_text %} +

    {{ field.help_text|safe }}

    + {% endif %} +
    +
    + {% endfor %} + + +
    + + {% if role.exists %}{% endif %} + +
    + + +{% endblock %}