From 49e5362f411f5ca31c09d27df08632fb16250280 Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Sat, 16 Apr 2016 15:27:33 +0200 Subject: [PATCH 01/15] WIP. see #799 --- modoboa/admin/forms/account.py | 27 ++++--- modoboa/admin/forms/alias.py | 9 +++ modoboa/admin/forms/domain.py | 36 ++++----- modoboa/admin/models/domain.py | 1 + modoboa/admin/tests/test_account.py | 4 +- modoboa/admin/views/alias.py | 2 +- modoboa/admin/views/domain.py | 2 +- modoboa/core/signals.py | 2 +- .../lib/migrations/0005_auto_20160416_1449.py | 23 ++++++ modoboa/limits/app_settings.py | 57 ++++++++++++-- modoboa/limits/constants.py | 17 ++++- modoboa/limits/controls.py | 8 +- modoboa/limits/forms.py | 57 +++++++++++--- modoboa/limits/general_callbacks.py | 34 +++++++-- modoboa/limits/handlers.py | 34 +++++++-- .../migrations/0003_auto_20160413_1046.py | 10 +-- .../migrations/0005_auto_20160415_1654.py | 50 +++++++++++++ modoboa/limits/models.py | 74 ++++++++++++++++--- modoboa/limits/tests.py | 14 ++-- modoboa/limits/utils.py | 9 ++- modoboa/relaydomains/tests.py | 6 +- 21 files changed, 375 insertions(+), 101 deletions(-) create mode 100644 modoboa/lib/migrations/0005_auto_20160416_1449.py create mode 100644 modoboa/limits/migrations/0005_auto_20160415_1654.py diff --git a/modoboa/admin/forms/account.py b/modoboa/admin/forms/account.py index 098c6cc56..455290f6f 100644 --- a/modoboa/admin/forms/account.py +++ b/modoboa/admin/forms/account.py @@ -13,7 +13,7 @@ from modoboa.core.models import User from modoboa.core import signals as core_signals from modoboa.lib import events, parameters from modoboa.lib.email_utils import split_mailbox -from modoboa.lib.exceptions import PermDeniedException +from modoboa.lib import exceptions as lib_exceptions from modoboa.lib import fields as lib_fields from modoboa.lib.form_utils import ( DynamicForm, TabForms, WizardForm, WizardStep @@ -161,7 +161,8 @@ class AccountFormGeneral(forms.ModelForm): def save(self, commit=True): account = super(AccountFormGeneral, self).save(commit=False) if self.user == account and not self.cleaned_data["is_active"]: - raise PermDeniedException(_("You can't disable your own account")) + raise lib_exceptions.PermDeniedException( + _("You can't disable your own account")) if commit: if "password1" in self.cleaned_data \ and self.cleaned_data["password1"] != "": @@ -198,11 +199,7 @@ class AccountFormMail(forms.Form, DynamicForm): ) def __init__(self, *args, **kwargs): - if "instance" in kwargs: - self.mb = kwargs["instance"] - del kwargs["instance"] - else: - self.mb = None + self.mb = kwargs.pop("instance", None) super(AccountFormMail, self).__init__(*args, **kwargs) self.field_widths = { "quota": 3 @@ -245,6 +242,13 @@ class AccountFormMail(forms.Form, DynamicForm): self.domain = Domain.objects.get(name=domname) except Domain.DoesNotExist: raise forms.ValidationError(_("Domain does not exist")) + if not self.mb: + try: + core_signals.can_create_object.send( + sender=self.__class__, context=self.domain, + object_type="mailboxes") + except lib_exceptions.ModoboaException as inst: + raise forms.ValidationError(inst) return email def clean(self): @@ -262,9 +266,9 @@ class AccountFormMail(forms.Form, DynamicForm): def create_mailbox(self, user, account): """Create a mailbox associated to :kw:`account`.""" if not user.can_access(self.domain): - raise PermDeniedException + raise lib_exceptions.PermDeniedException core_signals.can_create_object.send( - self.__class__, user=user, object_type="mailboxes") + self.__class__, context=user, object_type="mailboxes") self.mb = Mailbox( address=self.locpart, domain=self.domain, user=account, use_domain_quota=self.cleaned_data["quota_act"]) @@ -296,8 +300,11 @@ class AccountFormMail(forms.Form, DynamicForm): if not aliases: return core_signals.can_create_object.send( - self.__class__, user=user, object_type="mailbox_aliases", + self.__class__, context=user, object_type="mailbox_aliases", count=len(aliases)) + core_signals.can_create_object.send( + self.__class__, context=self.mb.domain, + object_type="mailbox_aliases", count=len(aliases)) for alias in aliases: if self.mb.aliasrecipient_set.select_related("alias").filter( alias__address=alias).exists(): diff --git a/modoboa/admin/forms/alias.py b/modoboa/admin/forms/alias.py index 76fa537ed..f18063c58 100644 --- a/modoboa/admin/forms/alias.py +++ b/modoboa/admin/forms/alias.py @@ -6,6 +6,8 @@ from django import forms from django.http import QueryDict from django.utils.translation import ugettext as _, ugettext_lazy +from modoboa.core import signals as core_signals +from modoboa.lib import exceptions as lib_exceptions from modoboa.lib.email_utils import split_mailbox from modoboa.lib import fields as lib_fields from modoboa.lib.form_utils import ( @@ -74,6 +76,13 @@ class AliasForm(forms.ModelForm, DynamicForm): raise forms.ValidationError( _("You don't have access to this domain") ) + if self.instance.pk: + try: + core_signals.can_create_object.send( + sender=self.__class__, context=domain, + object_type="mailbox_aliases") + except lib_exceptions.ModoboaException as inst: + raise forms.ValidationError(inst) return self.cleaned_data["address"].lower() def clean(self): diff --git a/modoboa/admin/forms/domain.py b/modoboa/admin/forms/domain.py index 636923d77..eb9bbd9b0 100644 --- a/modoboa/admin/forms/domain.py +++ b/modoboa/admin/forms/domain.py @@ -101,6 +101,7 @@ class DomainFormGeneral(forms.ModelForm, DynamicForm): "name", _("A %s with this name already exists") % unicode(label) ) + self.aliases = [] for k in cleaned_data.keys(): if not k.startswith("aliases"): continue @@ -120,6 +121,7 @@ class DomainFormGeneral(forms.ModelForm, DynamicForm): k, _("A %s with this name already exists") % unicode(label) ) + self.aliases.append(cleaned_data[k]) return cleaned_data def save(self, user, commit=True, domalias_post_create=False): @@ -134,28 +136,20 @@ class DomainFormGeneral(forms.ModelForm, DynamicForm): if not commit: return d d.save() - aliases = [] - for k, v in self.cleaned_data.iteritems(): - if not k.startswith("aliases"): - continue - if v in ["", None]: - continue - aliases.append(v) for dalias in d.domainalias_set.all(): - if dalias.name not in aliases: + if dalias.name not in self.aliases: dalias.delete() else: - aliases.remove(dalias.name) - if aliases: + self.aliases.remove(dalias.name) + if self.aliases: core_signals.can_create_object.send( - self.__class__, user=user, object_type="domain_aliases", - count=len(aliases)) - for alias in aliases: - try: - d.domainalias_set.get(name=alias) - except DomainAlias.DoesNotExist: - pass - else: + self.__class__, context=user, object_type="domain_aliases", + count=len(self.aliases)) + core_signals.can_create_object.send( + self.__class__, context=d, object_type="domain_aliases", + count=len(self.aliases)) + for alias in self.aliases: + if d.domainalias_set.filter(name=alias).exists(): continue options = {"creator": user} if domalias_post_create else {} DomainAlias(name=alias, target=d, enabled=d.enabled).save( @@ -225,9 +219,9 @@ class DomainFormOptions(forms.Form): except User.DoesNotExist: pass else: - raise Conflict(_("User '%s' already exists" % username)) + raise Conflict(_("User '%s' already exists") % username) core_signals.can_create_object.send( - self.__class__, user=user, object_type="mailboxes") + self.__class__, context=user, object_type="mailboxes") da = User(username=username, email=username, is_active=True) da.set_password("password") da.save() @@ -245,7 +239,7 @@ class DomainFormOptions(forms.Form): if domain.type == "domain" and \ self.cleaned_data["create_aliases"] == "yes": core_signals.can_create_object.send( - self.__class__, user=user, object_type="mailbox_aliases") + self.__class__, context=user, object_type="mailbox_aliases") alias = Alias( address=u"postmaster@{}".format(domain.name), domain=domain, enabled=True diff --git a/modoboa/admin/models/domain.py b/modoboa/admin/models/domain.py index 65267a6a9..cd4d425d8 100644 --- a/modoboa/admin/models/domain.py +++ b/modoboa/admin/models/domain.py @@ -101,6 +101,7 @@ class Domain(AdminObject): :param User account: the administrotor to add """ from modoboa.lib.permissions import grant_access_to_object + grant_access_to_object(account, self) for mb in self.mailbox_set.all(): if mb.user.has_perm("admin.add_domain"): diff --git a/modoboa/admin/tests/test_account.py b/modoboa/admin/tests/test_account.py index 522899025..4ecd3ae5e 100644 --- a/modoboa/admin/tests/test_account.py +++ b/modoboa/admin/tests/test_account.py @@ -161,9 +161,9 @@ class PermissionsTestCase(ModoTestCase): from modoboa.limits import utils as limits_utils super(PermissionsTestCase, cls).setUpTestData() - for name, tpl in limits_utils.get_limit_templates(): + for name, tpl in limits_utils.get_user_limit_templates(): parameters.save_admin( - "DEFLT_{0}_LIMIT".format(name.upper()), 2, + "DEFLT_USER_{0}_LIMIT".format(name.upper()), 2, app="limits" ) factories.populate_database() diff --git a/modoboa/admin/views/alias.py b/modoboa/admin/views/alias.py index a40bd29cf..ef771b8f1 100644 --- a/modoboa/admin/views/alias.py +++ b/modoboa/admin/views/alias.py @@ -37,7 +37,7 @@ def _validate_alias(request, form, successmsg, callback=None): def _new_alias(request, title, action, successmsg, tplname="admin/aliasform.html"): core_signals.can_create_object.send( - "new_alias", user=request.user, object_type="mailbox_aliases") + "new_alias", context=request.user, object_type="mailbox_aliases") if request.method == "POST": def callback(user, alias): alias.post_create(user) diff --git a/modoboa/admin/views/domain.py b/modoboa/admin/views/domain.py index 0d222542f..2e3f57289 100644 --- a/modoboa/admin/views/domain.py +++ b/modoboa/admin/views/domain.py @@ -100,7 +100,7 @@ def domains_list(request): @reversion.create_revision() def newdomain(request): core_signals.can_create_object.send( - "newdomain", user=request.user, object_type="domains") + "newdomain", context=request.user, object_type="domains") return DomainWizard(request).process() diff --git a/modoboa/core/signals.py b/modoboa/core/signals.py index 1171e3747..1275ffe1b 100644 --- a/modoboa/core/signals.py +++ b/modoboa/core/signals.py @@ -3,4 +3,4 @@ import django.dispatch can_create_object = django.dispatch.Signal( - providing_args=["user", "object_type", "count"]) + providing_args=["context", "object_type", "count"]) diff --git a/modoboa/lib/migrations/0005_auto_20160416_1449.py b/modoboa/lib/migrations/0005_auto_20160416_1449.py new file mode 100644 index 000000000..6c594dec3 --- /dev/null +++ b/modoboa/lib/migrations/0005_auto_20160416_1449.py @@ -0,0 +1,23 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations + + +def rename_limits_parameters(apps, schema_editor): + """Rename parameters.""" + Parameter = apps.get_model("lib", "Parameter") + for param in Parameter.objects.filter(name__startswith="DEFLT_"): + param.name = param.name.replace("DEFLT_", "DEFLT_USER_") + param.save() + + +class Migration(migrations.Migration): + + dependencies = [ + ('lib', '0004_auto_20151114_1409'), + ] + + operations = [ + migrations.RunPython(rename_limits_parameters) + ] diff --git a/modoboa/limits/app_settings.py b/modoboa/limits/app_settings.py index 8c2129ccb..0c4f1f98a 100644 --- a/modoboa/limits/app_settings.py +++ b/modoboa/limits/app_settings.py @@ -10,9 +10,9 @@ from modoboa.lib.parameters import AdminParametersForm class ParametersForm(AdminParametersForm): app = "limits" - defv_sep = SeparatorField(label=_("Default limits")) + defv_sep = SeparatorField(label=_("Default per-admin limits")) - deflt_domain_admins_limit = forms.IntegerField( + deflt_user_domain_admins_limit = forms.IntegerField( label=_("Domain admins"), initial=0, help_text=_( @@ -23,7 +23,7 @@ class ParametersForm(AdminParametersForm): widget=forms.widgets.TextInput( attrs={"class": "col-md-1 form-control"}) ) - deflt_domains_limit = forms.IntegerField( + deflt_user_domains_limit = forms.IntegerField( label=_("Domains"), initial=0, help_text=_( @@ -33,7 +33,7 @@ class ParametersForm(AdminParametersForm): widget=forms.widgets.TextInput( attrs={"class": "col-md-1 form-control"}) ) - deflt_domain_aliases_limit = forms.IntegerField( + deflt_user_domain_aliases_limit = forms.IntegerField( label=_("Domain aliases"), initial=0, help_text=_( @@ -44,7 +44,7 @@ class ParametersForm(AdminParametersForm): widget=forms.widgets.TextInput( attrs={"class": "col-md-1 form-control"}) ) - deflt_mailboxes_limit = forms.IntegerField( + deflt_user_mailboxes_limit = forms.IntegerField( label=_("Mailboxes"), initial=0, help_text=_( @@ -54,7 +54,7 @@ class ParametersForm(AdminParametersForm): widget=forms.widgets.TextInput( attrs={"class": "col-md-1 form-control"}) ) - deflt_mailbox_aliases_limit = forms.IntegerField( + deflt_user_mailbox_aliases_limit = forms.IntegerField( label=_("Mailbox aliases"), initial=0, help_text=_( @@ -65,6 +65,51 @@ class ParametersForm(AdminParametersForm): attrs={"class": "col-md-1 form-control"}) ) + domain_limits_sep = SeparatorField(label=_("Default per-domain limits")) + + # deflt_domain_domain_admins_limit = forms.IntegerField( + # label=_("Domain admins"), + # initial=0, + # help_text=_( + # "Maximum number of allowed domain administrators for a new " + # "domain. (0 to deny any creation, -1 to allow unlimited " + # "creations)" + # ), + # widget=forms.widgets.TextInput( + # attrs={"class": "col-md-1 form-control"}) + # ) + deflt_domain_domain_aliases_limit = forms.IntegerField( + label=_("Domain aliases"), + initial=0, + help_text=_( + "Maximum number of allowed domain aliases for a new " + "domain. (0 to deny any creation, -1 to allow " + "unlimited creations)" + ), + widget=forms.widgets.TextInput( + attrs={"class": "col-md-1 form-control"}) + ) + deflt_domain_mailboxes_limit = forms.IntegerField( + label=_("Mailboxes"), + initial=0, + help_text=_( + "Maximum number of allowed mailboxes for a new domain. " + "(0 to deny any creation, -1 to allow unlimited creations)" + ), + widget=forms.widgets.TextInput( + attrs={"class": "col-md-1 form-control"}) + ) + deflt_domain_mailbox_aliases_limit = forms.IntegerField( + label=_("Mailbox aliases"), + initial=0, + help_text=_( + "Maximum number of allowed aliases for a new domain. " + "(0 to deny any creation, -1 to allow unlimited creations)" + ), + widget=forms.widgets.TextInput( + attrs={"class": "col-md-1 form-control"}) + ) + def __init__(self, *args, **kwargs): super(AdminParametersForm, self).__init__(*args, **kwargs) self._load_extra_parameters('A') diff --git a/modoboa/limits/constants.py b/modoboa/limits/constants.py index 947adbe62..16a074c1b 100644 --- a/modoboa/limits/constants.py +++ b/modoboa/limits/constants.py @@ -5,7 +5,7 @@ import collections from django.utils.translation import ugettext_lazy as _ -DEFAULT_LIMITS = collections.OrderedDict(( +DEFAULT_USER_LIMITS = collections.OrderedDict(( ("domains", { "content_type": "admin.domain", "label": _("Domains"), "help": _("Maximum number of domains this user can create"), @@ -28,3 +28,18 @@ DEFAULT_LIMITS = collections.OrderedDict(( "required_role": "Resellers", "extra_filters": {"groups__name": "DomainAdmins"}}) )) + + +DEFAULT_DOMAIN_LIMITS = collections.OrderedDict(( + ("domain_aliases", { + "relation": "domainalias_set", "label": _("Domain aliases"), + "help": _("Maximum number of domain aliases allowed for this domain.") + }), + ("mailboxes", { + "relation": "mailbox_set", "label": _("Mailboxes"), + "help": _("Maximum number of mailboxes allowed for this domain.")}), + ("mailbox_aliases", { + "relation": "alias_set", "label": _("Mailbox aliases"), + "help": _("Maximum number of mailbox aliases allowed for this domain.") + }) +)) diff --git a/modoboa/limits/controls.py b/modoboa/limits/controls.py index 4d0c20c35..bec50673e 100644 --- a/modoboa/limits/controls.py +++ b/modoboa/limits/controls.py @@ -18,11 +18,11 @@ def move_pool_resource(owner, user): associated resource to the pool of its owner. """ if not owner.is_superuser: - for name, ltpl in utils.get_limit_templates(): - l = user.objectlimit_set.get(name=name) + for name, ltpl in utils.get_user_limit_templates(): + l = user.userobjectlimit_set.get(name=name) if l.max_value < 0: continue - ol = owner.objectlimit_set.get(name=name) + ol = owner.userobjectlimit_set.get(name=name) ol.max_value += l.max_value ol.save() @@ -44,7 +44,7 @@ def user_can_set_role(user, role, account=None): lname = "domain_admins" condition = ( user.is_superuser or - not user.objectlimit_set.get(name=lname).is_exceeded() + not user.userobjectlimit_set.get(name=lname).is_exceeded() ) if condition: return [True] diff --git a/modoboa/limits/forms.py b/modoboa/limits/forms.py index 8d3285cf2..0af239697 100644 --- a/modoboa/limits/forms.py +++ b/modoboa/limits/forms.py @@ -5,7 +5,6 @@ from django import forms from django.utils.translation import ugettext as _ -from modoboa.lib import parameters from modoboa.lib.permissions import get_object_owner from .lib import BadLimitValue, UnsufficientResource @@ -16,12 +15,9 @@ class ResourcePoolForm(forms.Form): """Dynamic form to display user limits.""" def __init__(self, *args, **kwargs): - self.account = None - if "instance" in kwargs: - self.account = kwargs["instance"] - del kwargs["instance"] + self.account = kwargs.pop("instance", None) super(ResourcePoolForm, self).__init__(*args, **kwargs) - for name, tpl in utils.get_limit_templates(): + for name, tpl in utils.get_user_limit_templates(): if "required_role" in tpl: condition = ( self.account is not None and @@ -32,7 +28,7 @@ class ResourcePoolForm(forms.Form): self.fields["{}_limit".format(name)] = forms.IntegerField( label=tpl["label"], help_text=tpl["help"] ) - if hasattr(self, "account"): + if self.account: self.load_from_user(self.account) def clean(self): @@ -50,7 +46,7 @@ class ResourcePoolForm(forms.Form): for fieldname in self.fields.keys(): lname = fieldname.replace("_limit", "") self.fields[fieldname].initial = ( - user.objectlimit_set.get(name=lname).max_value) + user.userobjectlimit_set.get(name=lname).max_value) def allocate_from_user(self, limit, user): """Allocate resource using an existing user. @@ -60,7 +56,7 @@ class ResourcePoolForm(forms.Form): also be limited, the resource he gives is taken from its own pool. """ - ol = user.objectlimit_set.get(name=limit.name) + ol = user.userobjectlimit_set.get(name=limit.name) fieldname = "{}_limit".format(limit.name) newvalue = self.cleaned_data[fieldname] if newvalue == -1 and ol.max_value != -1: @@ -80,12 +76,51 @@ class ResourcePoolForm(forms.Form): def save(self): owner = get_object_owner(self.account) - for name, ltpl in utils.get_limit_templates(): + for name, ltpl in utils.get_user_limit_templates(): fieldname = "{}_limit".format(name) if fieldname not in self.cleaned_data: continue - l = self.account.objectlimit_set.get(name=name) + l = self.account.userobjectlimit_set.get(name=name) if not owner.is_superuser: self.allocate_from_user(l, owner) l.max_value = self.cleaned_data[fieldname] l.save() + + +class DomainLimitsForm(forms.Form): + """Per-domain limits form.""" + + def __init__(self, *args, **kwargs): + """Define limits as fields.""" + self.domain = kwargs.pop("instance", None) + super(DomainLimitsForm, self).__init__(*args, **kwargs) + for name, tpl in utils.get_domain_limit_templates(): + self.fields["{}_limit".format(name)] = forms.IntegerField( + label=tpl["label"], help_text=tpl["help"] + ) + if not self.domain: + return + for fieldname in self.fields.keys(): + lname = fieldname.replace("_limit", "") + self.fields[fieldname].initial = ( + self.domain.domainobjectlimit_set.get(name=lname).max_value) + + def clean(self): + """Ensure limit values are correct.""" + cleaned_data = super(DomainLimitsForm, self).clean() + if self.errors: + return cleaned_data + for lname in self.fields.keys(): + if cleaned_data[lname] < -1: + self.add_error(lname, _("Invalid limit")) + return cleaned_data + + def save(self, user): + """Set limits.""" + for name, ltpl in utils.get_domain_limit_templates(): + fieldname = "{}_limit".format(name) + if fieldname not in self.cleaned_data: + continue + l = self.domain.domainobjectlimit_set.get(name=name) + l.max_value = self.cleaned_data[fieldname] + l.save() diff --git a/modoboa/limits/general_callbacks.py b/modoboa/limits/general_callbacks.py index 655497c82..92d6aded6 100644 --- a/modoboa/limits/general_callbacks.py +++ b/modoboa/limits/general_callbacks.py @@ -4,7 +4,7 @@ from django.utils.translation import ugettext as _ from modoboa.lib import events -from .forms import ResourcePoolForm +from . import forms from . import utils @@ -21,13 +21,13 @@ def display_pool_usage(user, target, currentpage): else: exceptions = ["domain_admins", "mailboxes", "mailbox_aliases"] names = [ - name for name, tpl in utils.get_limit_templates() + name for name, tpl in utils.get_user_limit_templates() if name not in exceptions and ("required_role" not in tpl or tpl["required_role"] == user.group) ] - limits = user.objectlimit_set.filter(name__in=names, max_value__gt=0) + limits = user.userobjectlimit_set.filter(name__in=names, max_value__gt=0) if len(limits) == 0: return [] return [ @@ -44,11 +44,29 @@ def extra_account_form(user, account=None): account.group not in ["Resellers", "DomainAdmins"]: return [] - return [ - dict( - id="resources", title=_("Resources"), cls=ResourcePoolForm - ) - ] + return [{ + "id": "resources", "title": _("Resources"), + "cls": forms.ResourcePoolForm + }] + + +@events.observe("ExtraDomainForm") +def extra_domain_form(user, domain): + """Include domain limits form.""" + if not user.has_perm("admin.change_domain"): + return [] + return [{ + "id": "resources", "title": _("Resources"), + "cls": forms.DomainLimitsForm + }] + + +@events.observe("FillDomainInstances") +def fill_domain_instances(user, domain, instances): + """Set domain instance for resources form.""" + if not user.has_perm("admin.change_domain"): + return + instances["resources"] = domain @events.observe("CheckExtraAccountForm") diff --git a/modoboa/limits/handlers.py b/modoboa/limits/handlers.py index 834c36bab..27dd1a19c 100644 --- a/modoboa/limits/handlers.py +++ b/modoboa/limits/handlers.py @@ -5,6 +5,7 @@ from django.dispatch import receiver from django.contrib.contenttypes.models import ContentType +from modoboa.admin import models as admin_models from modoboa.core import models as core_models from modoboa.core import signals as core_signals from modoboa.lib import signals as lib_signals @@ -16,11 +17,16 @@ from . import utils @receiver(core_signals.can_create_object) -def check_object_limit(sender, user, object_type, **kwargs): +def check_object_limit(sender, context, object_type, **kwargs): """Check if user can create a new object.""" - if user.is_superuser: - return True - limit = user.objectlimit_set.get(name=object_type) + if context.__class__.__name__ == "User": + if context.is_superuser: + return True + limit = context.userobjectlimit_set.get(name=object_type) + elif context.__class__.__name__ == "Domain": + limit = context.domainobjectlimit_set.get(name=object_type) + else: + raise NotImplementedError count = kwargs.get("count", 1) if limit.is_exceeded(count): raise lib.LimitReached(limit) @@ -33,13 +39,27 @@ def create_user_limits(sender, instance, **kwargs): return request = lib_signals.get_request() creator = request.user if request else None - for name, definition in utils.get_limit_templates(): + for name, definition in utils.get_user_limit_templates(): ct = ContentType.objects.get_by_natural_key( *definition["content_type"].split(".")) max_value = 0 # creator can be None if user was created by a factory if not creator or creator.is_superuser: max_value = int( - parameters.get_admin("DEFLT_{}_LIMIT".format(name.upper()))) - models.ObjectLimit.objects.create( + parameters.get_admin( + "DEFLT_USER_{}_LIMIT".format(name.upper()))) + models.UserObjectLimit.objects.create( user=instance, name=name, content_type=ct, max_value=max_value) + + +@receiver(signals.post_save, sender=admin_models.Domain) +def create_domain_limits(sender, instance, **kwargs): + """Create limits for new domain.""" + if not kwargs.get("created"): + return + for name, definition in utils.get_domain_limit_templates(): + max_value = 0 + max_value = int( + parameters.get_admin("DEFLT_DOMAIN_{}_LIMIT".format(name.upper()))) + models.DomainObjectLimit.objects.create( + domain=instance, name=name, max_value=max_value) diff --git a/modoboa/limits/migrations/0003_auto_20160413_1046.py b/modoboa/limits/migrations/0003_auto_20160413_1046.py index 5a5fa13d7..438479931 100644 --- a/modoboa/limits/migrations/0003_auto_20160413_1046.py +++ b/modoboa/limits/migrations/0003_auto_20160413_1046.py @@ -8,7 +8,7 @@ from django.conf import settings def move_limits_to_user(apps, schema_editor): """Move limits from Pool to User.""" LimitsPool = apps.get_model("limits", "LimitsPool") - ObjectLimit = apps.get_model("limits", "ObjectLimit") + UserObjectLimit = apps.get_model("limits", "UserObjectLimit") ContentType = apps.get_model("contenttypes", "ContentType") if not ContentType.objects.exists(): @@ -34,12 +34,12 @@ def move_limits_to_user(apps, schema_editor): .prefetch_related("limit_set")) for pool in qset: for limit in pool.limit_set.all(): - objlimit = ObjectLimit( + objlimit = UserObjectLimit( user=pool.user, max_value=limit.maxvalue, name=limit.name.replace("_limit", ""), content_type=name_to_content_type[limit.name]) to_create.append(objlimit) - ObjectLimit.objects.bulk_create(to_create) + UserObjectLimit.objects.bulk_create(to_create) class Migration(migrations.Migration): @@ -53,7 +53,7 @@ class Migration(migrations.Migration): operations = [ migrations.CreateModel( - name='ObjectLimit', + name='UserObjectLimit', fields=[ ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), ('name', models.CharField(max_length=254)), @@ -63,7 +63,7 @@ class Migration(migrations.Migration): ], ), migrations.AlterUniqueTogether( - name='objectlimit', + name='userobjectlimit', unique_together=set([('user', 'name')]), ), migrations.RunPython(move_limits_to_user) diff --git a/modoboa/limits/migrations/0005_auto_20160415_1654.py b/modoboa/limits/migrations/0005_auto_20160415_1654.py new file mode 100644 index 000000000..ab97e7807 --- /dev/null +++ b/modoboa/limits/migrations/0005_auto_20160415_1654.py @@ -0,0 +1,50 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models +import modoboa.limits.models + +from .. import utils + + +def create_limits(apps, schema_editor): + """Create limits for existing domains.""" + Domain = apps.get_model("admin", "Domain") + DomainObjectLimit = apps.get_model("limits", "DomainObjectLimit") + to_create = [] + for domain in Domain.objects.all(): + for name, tpl in utils.get_domain_limit_templates(): + to_create.append( + DomainObjectLimit(domain=domain, name=name, max_value=-1)) + DomainObjectLimit.objects.bulk_create(to_create) + + +class Migration(migrations.Migration): + + dependencies = [ + ('admin', '0003_auto_20151118_1215'), + ('limits', '0004_auto_20160413_1312'), + ] + + operations = [ + migrations.CreateModel( + name='DomainObjectLimit', + fields=[ + ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ('name', models.CharField(max_length=254)), + ('max_value', models.IntegerField(default=0)), + ('domain', models.ForeignKey(to='admin.Domain')), + ], + bases=(modoboa.limits.models.ObjectLimitMixin, models.Model), + ), + migrations.AlterField( + model_name='userobjectlimit', + name='max_value', + field=models.IntegerField(default=0), + ), + migrations.AlterUniqueTogether( + name='domainobjectlimit', + unique_together=set([('domain', 'name')]), + ), + migrations.RunPython(create_limits) + ] diff --git a/modoboa/limits/models.py b/modoboa/limits/models.py index c97deadff..3a3d3dba0 100644 --- a/modoboa/limits/models.py +++ b/modoboa/limits/models.py @@ -12,14 +12,42 @@ from modoboa.core import models as core_models from . import utils +class ObjectLimitMixin(object): + """Common methods.""" + + @property + def label(self): + """Return display name.""" + return self.definition["label"] + + @property + def usage(self): + """Return current limit usage in %.""" + if self.max_value < 0: + return -1 + if self.max_value == 0: + return 100 + return int(float(self.current_value) / self.max_value * 100) + + def is_exceeded(self, count=1): + """Check if limit will be reached if we add count object(s).""" + return self.current_value + 1 > self.max_value + + def __str__(self): + """Display current usage.""" + if self.max_value == -1: + return _("unlimited") + return "{}%".format(self.usage) + + @python_2_unicode_compatible -class ObjectLimit(models.Model): - """Per-user limits on object creation.""" +class UserObjectLimit(ObjectLimitMixin, models.Model): + """Object level limit.""" user = models.ForeignKey(settings.AUTH_USER_MODEL) name = models.CharField(max_length=254) content_type = models.ForeignKey("contenttypes.ContentType") - max_value = models.IntegerField(default=-2) + max_value = models.IntegerField(default=0) class Meta: unique_together = (("user", "name"), ) @@ -27,7 +55,7 @@ class ObjectLimit(models.Model): @property def definition(self): """Return the definition of this limit.""" - for name, tpl in utils.get_limit_templates(): + for name, tpl in utils.get_user_limit_templates(): if name == self.name: return tpl return None @@ -46,6 +74,11 @@ class ObjectLimit(models.Model): return model_class.objects.filter( pk__in=id_list, **self.definition["extra_filters"]).count() + @property + def label(self): + """Return display name.""" + return self.definition["label"] + @property def usage(self): """Return current limit usage in %.""" @@ -55,19 +88,38 @@ class ObjectLimit(models.Model): return 100 return int(float(self.current_value) / self.max_value * 100) - @property - def label(self): - """Return display name.""" - return self.definition["label"] - def is_exceeded(self, count=1): """Check if limit will be reached if we add count object(s).""" return self.current_value + 1 > self.max_value def __str__(self): """Display current usage.""" - if self.max_value == -2: - return _("undefined") if self.max_value == -1: return _("unlimited") return "{}%".format(self.usage) + + +class DomainObjectLimit(ObjectLimitMixin, models.Model): + """Per-domain limits on object creation.""" + + domain = models.ForeignKey("admin.Domain") + name = models.CharField(max_length=254) + max_value = models.IntegerField(default=0) + + class Meta: + unique_together = (("domain", "name"), ) + + @property + def definition(self): + """Return the definition of this limit.""" + for name, tpl in utils.get_domain_limit_templates(): + if name == self.name: + return tpl + return None + + @property + def current_value(self): + """Return the current number of objects.""" + if not self.definition: + raise RuntimeError("Bad limit {}".format(self.name)) + return getattr(self.domain, self.definition["relation"]).count() diff --git a/modoboa/limits/tests.py b/modoboa/limits/tests.py index 40af6cc93..0b6616a21 100644 --- a/modoboa/limits/tests.py +++ b/modoboa/limits/tests.py @@ -47,9 +47,9 @@ class ResourceTestCase(ModoTestCase): def setUpTestData(cls): """Custom setUpTestData method.""" super(ResourceTestCase, cls).setUpTestData() - for name, tpl in utils.get_limit_templates(): + for name, tpl in utils.get_user_limit_templates(): parameters.save_admin( - "DEFLT_{0}_LIMIT".format(name.upper()), 2, + "DEFLT_USER_{0}_LIMIT".format(name.upper()), 2, app="limits" ) populate_database() @@ -107,7 +107,7 @@ class ResourceTestCase(ModoTestCase): ) def _check_limit(self, name, curvalue, maxvalue): - l = self.user.objectlimit_set.get(name=name) + l = self.user.userobjectlimit_set.get(name=name) self.assertEqual(l.current_value, curvalue) self.assertEqual(l.max_value, maxvalue) @@ -119,7 +119,7 @@ class DomainAdminTestCase(ResourceTestCase): """Create test data.""" super(DomainAdminTestCase, cls).setUpTestData() cls.user = User.objects.get(username='admin@test.com') - cls.user.objectlimit_set.filter( + cls.user.userobjectlimit_set.filter( name__in=["mailboxes", "mailbox_aliases"]).update(max_value=2) def setUp(self): @@ -240,7 +240,7 @@ class ResellerTestCase(ResourceTestCase): ) self._check_limit('domain_admins', 2, 2) - self.user.objectlimit_set.filter(name="mailboxes").update(max_value=3) + self.user.userobjectlimit_set.filter(name="mailboxes").update(max_value=3) self._create_account('user1@domain.tld') user = User.objects.get(username='user1@domain.tld') values = { @@ -265,11 +265,11 @@ class ResellerTestCase(ResourceTestCase): domadmin = User.objects.get(username='admin1@domain.tld') for l in ['mailboxes', 'mailbox_aliases']: self.assertEqual( - domadmin.objectlimit_set.get(name=l).max_value, 0 + domadmin.userobjectlimit_set.get(name=l).max_value, 0 ) def test_domain_admins_limit_from_domain_tpl(self): - self.user.objectlimit_set.filter(name="domains").update(max_value=3) + self.user.userobjectlimit_set.filter(name="domains").update(max_value=3) self._create_domain('domain1.tld', withtpl=True) self._create_domain('domain2.tld', withtpl=True) self._check_limit('domain_admins', 2, 2) diff --git a/modoboa/limits/utils.py b/modoboa/limits/utils.py index 1a8e4343f..6cbf3d1b0 100644 --- a/modoboa/limits/utils.py +++ b/modoboa/limits/utils.py @@ -3,7 +3,12 @@ from . import constants -def get_limit_templates(): +def get_user_limit_templates(): """Return defined templates.""" # FIXME: change GetExtraLimitTemplates - return constants.DEFAULT_LIMITS.items() + return constants.DEFAULT_USER_LIMITS.items() + + +def get_domain_limit_templates(): + """Return defined templates.""" + return constants.DEFAULT_DOMAIN_LIMITS.items() diff --git a/modoboa/relaydomains/tests.py b/modoboa/relaydomains/tests.py index 9e9dbbcb2..1c78f4ce3 100644 --- a/modoboa/relaydomains/tests.py +++ b/modoboa/relaydomains/tests.py @@ -50,7 +50,7 @@ class Operations(object): ) def _check_limit(self, name, curvalue, maxvalue): - l = self.user.objectlimit_set.get(name=name) + l = self.user.userobjectlimit_set.get(name=name) self.assertEqual(l.current_value, curvalue) self.assertEqual(l.max_value, maxvalue) @@ -233,9 +233,9 @@ class LimitsTestCase(ModoTestCase, Operations): """Create test data.""" super(LimitsTestCase, cls).setUpTestData() - for name, tpl in limits_utils.get_limit_templates(): + for name, tpl in limits_utils.get_user_limit_templates(): parameters.save_admin( - "DEFLT_{0}_LIMIT".format(name.upper()), 2, app="limits" + "DEFLT_USER_{0}_LIMIT".format(name.upper()), 2, app="limits" ) cls.user = UserFactory.create( username='reseller', groups=('Resellers',) From 5b0525f25762f8a0e772c70b0e828dc47bd7c0bb Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Sat, 16 Apr 2016 15:40:41 +0200 Subject: [PATCH 02/15] Add flag to enable/disable per-domain limits. see #799 --- modoboa/limits/app_settings.py | 14 +++++++++++++- modoboa/limits/general_callbacks.py | 6 +++++- modoboa/limits/handlers.py | 2 ++ 3 files changed, 20 insertions(+), 2 deletions(-) diff --git a/modoboa/limits/app_settings.py b/modoboa/limits/app_settings.py index 0c4f1f98a..42aaf5068 100644 --- a/modoboa/limits/app_settings.py +++ b/modoboa/limits/app_settings.py @@ -3,7 +3,7 @@ from django import forms from django.utils.translation import ugettext_lazy as _ -from modoboa.lib.form_utils import SeparatorField +from modoboa.lib.form_utils import SeparatorField, YesNoField from modoboa.lib.parameters import AdminParametersForm @@ -67,6 +67,12 @@ class ParametersForm(AdminParametersForm): domain_limits_sep = SeparatorField(label=_("Default per-domain limits")) + enable_domain_limits = YesNoField( + label=_("Enable per-domain limits"), + initial="no", + help_text=_("Enable or disable per-domain limits") + ) + # deflt_domain_domain_admins_limit = forms.IntegerField( # label=_("Domain admins"), # initial=0, @@ -110,6 +116,12 @@ class ParametersForm(AdminParametersForm): attrs={"class": "col-md-1 form-control"}) ) + visibility_rules = { + "deflt_domain_mailboxes_limit": "enable_domain_limits=yes", + "deflt_domain_mailbox_aliases_limit": "enable_domain_limits=yes", + "deflt_domain_domain_aliases_limit": "enable_domain_limits=yes", + } + def __init__(self, *args, **kwargs): super(AdminParametersForm, self).__init__(*args, **kwargs) self._load_extra_parameters('A') diff --git a/modoboa/limits/general_callbacks.py b/modoboa/limits/general_callbacks.py index 92d6aded6..068b9abd9 100644 --- a/modoboa/limits/general_callbacks.py +++ b/modoboa/limits/general_callbacks.py @@ -2,7 +2,7 @@ from django.utils.translation import ugettext as _ -from modoboa.lib import events +from modoboa.lib import events, parameters from . import forms from . import utils @@ -53,6 +53,8 @@ def extra_account_form(user, account=None): @events.observe("ExtraDomainForm") def extra_domain_form(user, domain): """Include domain limits form.""" + if parameters.get_admin("ENABLE_DOMAIN_LIMITS") == "no": + return [] if not user.has_perm("admin.change_domain"): return [] return [{ @@ -64,6 +66,8 @@ def extra_domain_form(user, domain): @events.observe("FillDomainInstances") def fill_domain_instances(user, domain, instances): """Set domain instance for resources form.""" + if parameters.get_admin("ENABLE_DOMAIN_LIMITS") == "no": + return if not user.has_perm("admin.change_domain"): return instances["resources"] = domain diff --git a/modoboa/limits/handlers.py b/modoboa/limits/handlers.py index 27dd1a19c..427d48f1e 100644 --- a/modoboa/limits/handlers.py +++ b/modoboa/limits/handlers.py @@ -24,6 +24,8 @@ def check_object_limit(sender, context, object_type, **kwargs): return True limit = context.userobjectlimit_set.get(name=object_type) elif context.__class__.__name__ == "Domain": + if parameters.get_admin("ENABLE_DOMAIN_LIMITS") == "no": + return limit = context.domainobjectlimit_set.get(name=object_type) else: raise NotImplementedError From 6ae2987a0e2e75af64838a6b8d81630ec574f18a Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Sun, 17 Apr 2016 13:41:03 +0200 Subject: [PATCH 03/15] Fix unit test. see #799 --- modoboa/admin/forms/domain.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/modoboa/admin/forms/domain.py b/modoboa/admin/forms/domain.py index eb9bbd9b0..52e4b286a 100644 --- a/modoboa/admin/forms/domain.py +++ b/modoboa/admin/forms/domain.py @@ -121,7 +121,8 @@ class DomainFormGeneral(forms.ModelForm, DynamicForm): k, _("A %s with this name already exists") % unicode(label) ) - self.aliases.append(cleaned_data[k]) + else: + self.aliases.append(cleaned_data[k]) return cleaned_data def save(self, user, commit=True, domalias_post_create=False): From 4ca2981a30a5b363ec21ab4f66709b367af06925 Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Sun, 17 Apr 2016 14:10:23 +0200 Subject: [PATCH 04/15] Increase coverage. see #799 --- modoboa/limits/tests.py | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/modoboa/limits/tests.py b/modoboa/limits/tests.py index 0b6616a21..5abaa18ac 100644 --- a/modoboa/limits/tests.py +++ b/modoboa/limits/tests.py @@ -417,3 +417,37 @@ class ResellerTestCase(ResourceTestCase): self.assertEqual(resp, 'Not enough resources') self._check_limit('mailboxes', 1, 2) self._check_limit('mailbox_aliases', 0, 2) + + +class DomainLimitsTestCase(ModoTestCase): + """Per-domain limits tests.""" + + @classmethod + def setUpTestData(cls): + """Create test data.""" + super(DomainLimitsTestCase, cls).setUpTestData() + populate_database() + parameters.save_admin("ENABLE_DOMAIN_LIMITS", "yes") + + def test_set_limits(self): + """Try to set limits for a given domain.""" + domain = Domain.objects.get(name="test.com") + values = { + "name": domain.name, "quota": domain.quota, + "enabled": domain.enabled, "type": "domain", + "mailboxes_limit": 2, "mailbox_aliases_limit": 2, + "domain_aliases_limit": 2 + } + self.ajax_post( + reverse("admin:domain_change", args=[domain.id]), + values + ) + domain.refresh_from_db() + self.assertEqual( + domain.domainobjectlimit_set.get(name="mailboxes").max_value, 2) + self.assertEqual( + domain.domainobjectlimit_set.get( + name="mailbox_aliases").max_value, 2) + self.assertEqual( + domain.domainobjectlimit_set.get( + name="domain_aliases").max_value, 2) From 58c859febb748fe3a75574324dd850269d00c1df Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Sun, 17 Apr 2016 15:21:14 +0200 Subject: [PATCH 05/15] Increase coverage. see #799 --- modoboa/limits/handlers.py | 1 - modoboa/limits/tests.py | 36 ++++++++++++++++++++++++++++++------ 2 files changed, 30 insertions(+), 7 deletions(-) diff --git a/modoboa/limits/handlers.py b/modoboa/limits/handlers.py index 427d48f1e..31674f224 100644 --- a/modoboa/limits/handlers.py +++ b/modoboa/limits/handlers.py @@ -60,7 +60,6 @@ def create_domain_limits(sender, instance, **kwargs): if not kwargs.get("created"): return for name, definition in utils.get_domain_limit_templates(): - max_value = 0 max_value = int( parameters.get_admin("DEFLT_DOMAIN_{}_LIMIT".format(name.upper()))) models.DomainObjectLimit.objects.create( diff --git a/modoboa/limits/tests.py b/modoboa/limits/tests.py index 5abaa18ac..f3f930137 100644 --- a/modoboa/limits/tests.py +++ b/modoboa/limits/tests.py @@ -426,8 +426,11 @@ class DomainLimitsTestCase(ModoTestCase): def setUpTestData(cls): """Create test data.""" super(DomainLimitsTestCase, cls).setUpTestData() - populate_database() parameters.save_admin("ENABLE_DOMAIN_LIMITS", "yes") + for name, tpl in utils.get_domain_limit_templates(): + parameters.save_admin( + "DEFLT_DOMAIN_{}_LIMIT".format(name.upper()), 2) + populate_database() def test_set_limits(self): """Try to set limits for a given domain.""" @@ -435,8 +438,8 @@ class DomainLimitsTestCase(ModoTestCase): values = { "name": domain.name, "quota": domain.quota, "enabled": domain.enabled, "type": "domain", - "mailboxes_limit": 2, "mailbox_aliases_limit": 2, - "domain_aliases_limit": 2 + "mailboxes_limit": 3, "mailbox_aliases_limit": 3, + "domain_aliases_limit": 3 } self.ajax_post( reverse("admin:domain_change", args=[domain.id]), @@ -444,10 +447,31 @@ class DomainLimitsTestCase(ModoTestCase): ) domain.refresh_from_db() self.assertEqual( - domain.domainobjectlimit_set.get(name="mailboxes").max_value, 2) + domain.domainobjectlimit_set.get(name="mailboxes").max_value, 3) self.assertEqual( domain.domainobjectlimit_set.get( - name="mailbox_aliases").max_value, 2) + name="mailbox_aliases").max_value, 3) self.assertEqual( domain.domainobjectlimit_set.get( - name="domain_aliases").max_value, 2) + name="domain_aliases").max_value, 3) + + def test_mailboxes_limit(self): + """Try to exceed defined limits.""" + domain = Domain.objects.get(name="test.com") + domain.domainobjectlimit_set.filter(name="mailboxes").update( + max_value=3) + limit = domain.domainobjectlimit_set.get(name="mailboxes") + self.assertFalse(limit.is_exceeded()) + username = "toto@test.com" + values = { + "username": username, + "first_name": "Tester", "last_name": "Toto", + "password1": "Toto1234", "password2": "Toto1234", + "role": "SimpleUsers", "quota_act": True, + "is_active": True, "email": username, "stepid": "step2", + } + self.ajax_post(reverse("admin:account_add"), values, 200) + self.assertTrue(limit.is_exceeded()) + + username = "titi@test.com" + self.ajax_post(reverse("admin:account_add"), values, 400) From 12139ddfe660c289c7e5c754bdaa781d1e94d987 Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Wed, 20 Apr 2016 14:31:56 +0200 Subject: [PATCH 06/15] Unit tests added. see #799 --- modoboa/admin/forms/alias.py | 2 +- modoboa/limits/constants.py | 4 +- modoboa/limits/models.py | 8 +++- modoboa/limits/tests.py | 74 +++++++++++++++++++++++++++++++++++- 4 files changed, 82 insertions(+), 6 deletions(-) diff --git a/modoboa/admin/forms/alias.py b/modoboa/admin/forms/alias.py index f18063c58..1f12b6bc4 100644 --- a/modoboa/admin/forms/alias.py +++ b/modoboa/admin/forms/alias.py @@ -76,7 +76,7 @@ class AliasForm(forms.ModelForm, DynamicForm): raise forms.ValidationError( _("You don't have access to this domain") ) - if self.instance.pk: + if not self.instance.pk: try: core_signals.can_create_object.send( sender=self.__class__, context=domain, diff --git a/modoboa/limits/constants.py b/modoboa/limits/constants.py index 16a074c1b..4150911df 100644 --- a/modoboa/limits/constants.py +++ b/modoboa/limits/constants.py @@ -40,6 +40,8 @@ DEFAULT_DOMAIN_LIMITS = collections.OrderedDict(( "help": _("Maximum number of mailboxes allowed for this domain.")}), ("mailbox_aliases", { "relation": "alias_set", "label": _("Mailbox aliases"), - "help": _("Maximum number of mailbox aliases allowed for this domain.") + "help": _( + "Maximum number of mailbox aliases allowed for this domain."), + "extra_filters": {"internal": False} }) )) diff --git a/modoboa/limits/models.py b/modoboa/limits/models.py index 3a3d3dba0..aca757b42 100644 --- a/modoboa/limits/models.py +++ b/modoboa/limits/models.py @@ -120,6 +120,10 @@ class DomainObjectLimit(ObjectLimitMixin, models.Model): @property def current_value(self): """Return the current number of objects.""" - if not self.definition: + definition = self.definition + if not definition: raise RuntimeError("Bad limit {}".format(self.name)) - return getattr(self.domain, self.definition["relation"]).count() + relation = getattr(self.domain, definition["relation"]) + if "extra_filters" in definition: + relation = relation.filter(**definition["extra_filters"]) + return relation.count() diff --git a/modoboa/limits/tests.py b/modoboa/limits/tests.py index f3f930137..7c36841e2 100644 --- a/modoboa/limits/tests.py +++ b/modoboa/limits/tests.py @@ -240,7 +240,8 @@ class ResellerTestCase(ResourceTestCase): ) self._check_limit('domain_admins', 2, 2) - self.user.userobjectlimit_set.filter(name="mailboxes").update(max_value=3) + self.user.userobjectlimit_set.filter( + name="mailboxes").update(max_value=3) self._create_account('user1@domain.tld') user = User.objects.get(username='user1@domain.tld') values = { @@ -269,7 +270,8 @@ class ResellerTestCase(ResourceTestCase): ) def test_domain_admins_limit_from_domain_tpl(self): - self.user.userobjectlimit_set.filter(name="domains").update(max_value=3) + self.user.userobjectlimit_set.filter( + name="domains").update(max_value=3) self._create_domain('domain1.tld', withtpl=True) self._create_domain('domain2.tld', withtpl=True) self._check_limit('domain_admins', 2, 2) @@ -455,8 +457,36 @@ class DomainLimitsTestCase(ModoTestCase): domain.domainobjectlimit_set.get( name="domain_aliases").max_value, 3) + def test_domain_aliases_limit(self): + """Try to exceed defined limit.""" + # Import + # API + domain = Domain.objects.get(name="test.com") + limit = domain.domainobjectlimit_set.get(name="domain_aliases") + self.assertFalse(limit.is_exceeded()) + values = { + "name": domain.name, "quota": domain.quota, + "enabled": domain.enabled, "type": "domain", + "mailboxes_limit": 2, "mailbox_aliases_limit": 2, + "domain_aliases_limit": 2, + "aliases": "alias1.com", "aliases_1": "alias2.com" + } + self.ajax_post( + reverse("admin:domain_change", args=[domain.id]), + values + ) + self.assertTrue(limit.is_exceeded()) + values["aliases_2"] = "alias3.com" + self.ajax_post( + reverse("admin:domain_change", args=[domain.id]), + values, 403 + ) + def test_mailboxes_limit(self): """Try to exceed defined limits.""" + # Import + # API + # LDAP ? domain = Domain.objects.get(name="test.com") domain.domainobjectlimit_set.filter(name="mailboxes").update( max_value=3) @@ -475,3 +505,43 @@ class DomainLimitsTestCase(ModoTestCase): username = "titi@test.com" self.ajax_post(reverse("admin:account_add"), values, 400) + + def test_mailbox_aliases_limit(self): + """Try to exceed defined limits.""" + # Import + # API + domain = Domain.objects.get(name="test.com") + user = User.objects.get(username="user@test.com") + limit = domain.domainobjectlimit_set.get(name="mailbox_aliases") + limit.max_value = 4 + limit.save() + self.assertFalse(limit.is_exceeded()) + values = { + "username": user.username, "role": user.group, + "is_active": user.is_active, "email": user.email, + "quota_act": True, + "aliases": "alias@test.com", "aliases_1": "alias1@test.com" + } + self.ajax_post( + reverse("admin:account_change", args=[user.id]), + values + ) + self.assertTrue(limit.is_exceeded()) + + values["aliases_2"] = "alias2@test.com" + self.ajax_post( + reverse("admin:account_change", args=[user.id]), + values, 403 + ) + + limit.max_value = 5 + limit.save() + values = { + "address": "forward2@test.com", "recipients": "user@test.com", + "enabled": True + } + self.ajax_post(reverse("admin:alias_add"), values) + self.assertTrue(limit.is_exceeded()) + + values["address"] = "forward3@test.com" + self.ajax_post(reverse("admin:alias_add"), values, 400) From ec698a71d590326056ed789d98377fad55af0db7 Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Wed, 20 Apr 2016 15:52:29 +0200 Subject: [PATCH 07/15] Unit tests added. see #799 --- modoboa/admin/forms/alias.py | 1 - modoboa/admin/serializers.py | 38 ++++++++++++++++++ modoboa/limits/tests.py | 75 ++++++++++++++++++++++++++++++++---- 3 files changed, 106 insertions(+), 8 deletions(-) diff --git a/modoboa/admin/forms/alias.py b/modoboa/admin/forms/alias.py index 1f12b6bc4..8195d5613 100644 --- a/modoboa/admin/forms/alias.py +++ b/modoboa/admin/forms/alias.py @@ -18,7 +18,6 @@ from ..models import Domain, Alias class AliasForm(forms.ModelForm, DynamicForm): - """A form to create/modify an alias.""" address = lib_fields.UTF8AndEmptyUserEmailField( diff --git a/modoboa/admin/serializers.py b/modoboa/admin/serializers.py index b61dc9acd..d7f12c966 100644 --- a/modoboa/admin/serializers.py +++ b/modoboa/admin/serializers.py @@ -9,6 +9,8 @@ from passwords import validators from modoboa.admin import models as admin_models from modoboa.core import models as core_models +from modoboa.core import signals as core_signals +from modoboa.lib import exceptions as lib_exceptions from modoboa.lib import permissions, email_utils, fields as lib_fields from . import models @@ -42,6 +44,23 @@ class DomainAliasSerializer(serializers.ModelSerializer): raise serializers.ValidationError(_("Permission denied.")) return value + def create(self, validated_data): + """Custom creation.""" + domain_alias = models.DomainAlias(**validated_data) + creator = self.context["request"].user + try: + core_signals.can_create_object.send( + sender=self.__class__, context=creator, + object_type="domain_aliases") + core_signals.can_create_object.send( + sender=self.__class__, context=domain_alias.target, + object_type="domain_aliases") + except lib_exceptions.ModoboaException as inst: + raise serializers.ValidationError({ + "domain": unicode(inst)}) + domain_alias.save(creator=creator) + return domain_alias + class MailboxSerializer(serializers.ModelSerializer): """Base mailbox serializer.""" @@ -175,6 +194,16 @@ class WritableAccountSerializer(AccountSerializer): if not creator.can_access(domain): raise serializers.ValidationError({ "domain": _("Permission denied.")}) + try: + core_signals.can_create_object.send( + sender=self.__class__, context=creator, + object_type="mailboxes") + core_signals.can_create_object.send( + sender=self.__class__, context=domain, + object_type="mailboxes") + except lib_exceptions.ModoboaException as inst: + raise serializers.ValidationError({ + "domain": unicode(inst)}) mb = admin_models.Mailbox( user=account, address=address, domain=domain, **data) mb.set_quota( @@ -241,6 +270,15 @@ class AliasSerializer(serializers.ModelSerializer): def create(self, validated_data): """Create appropriate objects.""" creator = self.context["request"].user + try: + core_signals.can_create_object.send( + sender=self.__class__, context=creator, + object_type="mailbox_aliases") + core_signals.can_create_object.send( + sender=self.__class__, context=self.domain, + object_type="mailbox_aliases") + except lib_exceptions.ModoboaException as inst: + raise serializers.ValidationError(unicode(inst)) recipients = validated_data.pop("recipients", None) alias = admin_models.Alias(domain=self.domain, **validated_data) alias.save(creator=creator) diff --git a/modoboa/limits/tests.py b/modoboa/limits/tests.py index 7c36841e2..5f49ebac8 100644 --- a/modoboa/limits/tests.py +++ b/modoboa/limits/tests.py @@ -1,3 +1,4 @@ +# coding: utf-8 """Test cases for the limits extension.""" from django.core.urlresolvers import reverse @@ -7,12 +8,12 @@ from modoboa.admin.models import Alias, Domain from modoboa.core.factories import UserFactory from modoboa.core.models import User from modoboa.lib import parameters -from modoboa.lib.tests import ModoTestCase +from modoboa.lib import tests as lib_tests from . import utils -class PermissionsTestCase(ModoTestCase): +class PermissionsTestCase(lib_tests.ModoTestCase): @classmethod def setUpTestData(cls): @@ -41,7 +42,7 @@ class PermissionsTestCase(ModoTestCase): self.assertEqual(resp, "Permission denied") -class ResourceTestCase(ModoTestCase): +class ResourceTestCase(lib_tests.ModoTestCase): @classmethod def setUpTestData(cls): @@ -421,7 +422,7 @@ class ResellerTestCase(ResourceTestCase): self._check_limit('mailbox_aliases', 0, 2) -class DomainLimitsTestCase(ModoTestCase): +class DomainLimitsTestCase(lib_tests.ModoTestCase): """Per-domain limits tests.""" @classmethod @@ -460,7 +461,6 @@ class DomainLimitsTestCase(ModoTestCase): def test_domain_aliases_limit(self): """Try to exceed defined limit.""" # Import - # API domain = Domain.objects.get(name="test.com") limit = domain.domainobjectlimit_set.get(name="domain_aliases") self.assertFalse(limit.is_exceeded()) @@ -485,7 +485,6 @@ class DomainLimitsTestCase(ModoTestCase): def test_mailboxes_limit(self): """Try to exceed defined limits.""" # Import - # API # LDAP ? domain = Domain.objects.get(name="test.com") domain.domainobjectlimit_set.filter(name="mailboxes").update( @@ -509,7 +508,6 @@ class DomainLimitsTestCase(ModoTestCase): def test_mailbox_aliases_limit(self): """Try to exceed defined limits.""" # Import - # API domain = Domain.objects.get(name="test.com") user = User.objects.get(username="user@test.com") limit = domain.domainobjectlimit_set.get(name="mailbox_aliases") @@ -545,3 +543,66 @@ class DomainLimitsTestCase(ModoTestCase): values["address"] = "forward3@test.com" self.ajax_post(reverse("admin:alias_add"), values, 400) + + +class APIDomainLimitsTestCase(lib_tests.ModoAPITestCase): + """Check that limits are used also by the API.""" + + @classmethod + def setUpTestData(cls): + """Create test data.""" + super(APIDomainLimitsTestCase, cls).setUpTestData() + parameters.save_admin("ENABLE_DOMAIN_LIMITS", "yes") + for name, tpl in utils.get_domain_limit_templates(): + parameters.save_admin( + "DEFLT_DOMAIN_{}_LIMIT".format(name.upper()), 2) + populate_database() + + def test_mailboxes_limit(self): + """Check mailboxes limit.""" + domain = Domain.objects.get(name="test.com") + limit = domain.domainobjectlimit_set.get(name="mailboxes") + self.assertTrue(limit.is_exceeded()) + url = reverse("external_api:account-list") + data = { + "username": "fromapi@test.com", + "role": "SimpleUsers", + "password": "Toto1234", + "mailbox": { + "full_address": "fromapi@test.com", + "quota": 10 + } + } + response = self.client.post(url, data, format="json") + self.assertEqual(response.status_code, 400) + + def test_domain_aliases_limit(self): + """Check domain_aliases limit.""" + domain = Domain.objects.get(name="test.com") + limit = domain.domainobjectlimit_set.get(name="domain_aliases") + url = reverse("external_api:domain_alias-list") + data = {"name": "dalias1.com", "target": domain.pk} + response = self.client.post(url, data, format="json") + self.assertEqual(response.status_code, 201) + data["name"] = "dalias2.com" + response = self.client.post(url, data, format="json") + self.assertEqual(response.status_code, 201) + self.assertTrue(limit.is_exceeded()) + data["name"] = "dalias3.com" + response = self.client.post(url, data, format="json") + self.assertEqual(response.status_code, 400) + + def test_mailbox_aliases_limit(self): + """Check mailbox_aliases limit.""" + domain = Domain.objects.get(name="test.com") + limit = domain.domainobjectlimit_set.get(name="mailbox_aliases") + self.assertTrue(limit.is_exceeded()) + url = reverse("external_api:alias-list") + data = { + "address": "alias_fromapi@test.com", + "recipients": [ + "user@test.com", "postmaster@test.com", "user_éé@nonlocal.com" + ] + } + response = self.client.post(url, data, format="json") + self.assertEqual(response.status_code, 400) From 0a902c05a3259630cac9b0a2bbf5bdd8ee9097b5 Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Wed, 20 Apr 2016 22:17:37 +0200 Subject: [PATCH 08/15] New setting to enable/disable per-admin limits. see #799 --- modoboa/admin/tests/test_api.py | 2 ++ modoboa/limits/app_settings.py | 11 +++++++++++ modoboa/limits/handlers.py | 2 ++ 3 files changed, 15 insertions(+) diff --git a/modoboa/admin/tests/test_api.py b/modoboa/admin/tests/test_api.py index 144073442..58204920e 100644 --- a/modoboa/admin/tests/test_api.py +++ b/modoboa/admin/tests/test_api.py @@ -9,6 +9,7 @@ from django.core.urlresolvers import reverse from rest_framework.authtoken.models import Token from modoboa.core import models as core_models +from modoboa.lib import parameters from modoboa.lib.tests import ModoAPITestCase from .. import factories @@ -401,6 +402,7 @@ class AliasAPITestCase(ModoAPITestCase): def setUpTestData(cls): """Create test data.""" super(AliasAPITestCase, cls).setUpTestData() + parameters.save_admin("ENABLE_ADMIN_LIMITS", "no", app="limits") factories.populate_database() cls.da_token = Token.objects.create( user=core_models.User.objects.get(username="admin@test.com")) diff --git a/modoboa/limits/app_settings.py b/modoboa/limits/app_settings.py index 42aaf5068..3e5ba0dfa 100644 --- a/modoboa/limits/app_settings.py +++ b/modoboa/limits/app_settings.py @@ -12,6 +12,12 @@ class ParametersForm(AdminParametersForm): defv_sep = SeparatorField(label=_("Default per-admin limits")) + enable_admin_limits = YesNoField( + label=_("Enable per-admin limits"), + initial="yes", + help_text=_("Enable or disable per-admin limits") + ) + deflt_user_domain_admins_limit = forms.IntegerField( label=_("Domain admins"), initial=0, @@ -117,6 +123,11 @@ class ParametersForm(AdminParametersForm): ) visibility_rules = { + "deflt_user_domains_limit": "enable_admin_limits=yes", + "deflt_user_domain_aliases_limit": "enable_admin_limits=yes", + "deflt_user_mailboxes_limit": "enable_admin_limits=yes", + "deflt_user_mailbox_aliases_limit": "enable_admin_limits=yes", + "deflt_user_domain_admins_limit": "enable_admin_limits=yes", "deflt_domain_mailboxes_limit": "enable_domain_limits=yes", "deflt_domain_mailbox_aliases_limit": "enable_domain_limits=yes", "deflt_domain_domain_aliases_limit": "enable_domain_limits=yes", diff --git a/modoboa/limits/handlers.py b/modoboa/limits/handlers.py index 31674f224..fc847dc28 100644 --- a/modoboa/limits/handlers.py +++ b/modoboa/limits/handlers.py @@ -20,6 +20,8 @@ from . import utils def check_object_limit(sender, context, object_type, **kwargs): """Check if user can create a new object.""" if context.__class__.__name__ == "User": + if parameters.get_admin("ENABLE_ADMIN_LIMITS") == "no": + return if context.is_superuser: return True limit = context.userobjectlimit_set.get(name=object_type) From f55bda69293ffe69e6b8b859a895cd4f20a16581 Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Sun, 24 Apr 2016 14:38:19 +0200 Subject: [PATCH 09/15] Add unit tests. see #799 --- modoboa/core/__init__.py | 5 +- modoboa/limits/tests.py | 608 --------------------------------------- 2 files changed, 4 insertions(+), 609 deletions(-) delete mode 100644 modoboa/limits/tests.py diff --git a/modoboa/core/__init__.py b/modoboa/core/__init__.py index adbda48b9..fd3f335b2 100644 --- a/modoboa/core/__init__.py +++ b/modoboa/core/__init__.py @@ -39,7 +39,10 @@ PERMISSIONS = { ["admin", "domain", "view_domains"], ["admin", "domain", "add_domain"], ["admin", "domain", "change_domain"], - ["admin", "domain", "delete_domain"] + ["admin", "domain", "delete_domain"], + ["admin", "domainalias", "add_domainalias"], + ["admin", "domainalias", "change_domainalias"], + ["admin", "domainalias", "delete_domainalias"], ] } diff --git a/modoboa/limits/tests.py b/modoboa/limits/tests.py deleted file mode 100644 index 5f49ebac8..000000000 --- a/modoboa/limits/tests.py +++ /dev/null @@ -1,608 +0,0 @@ -# coding: utf-8 -"""Test cases for the limits extension.""" - -from django.core.urlresolvers import reverse - -from modoboa.admin.factories import populate_database -from modoboa.admin.models import Alias, Domain -from modoboa.core.factories import UserFactory -from modoboa.core.models import User -from modoboa.lib import parameters -from modoboa.lib import tests as lib_tests - -from . import utils - - -class PermissionsTestCase(lib_tests.ModoTestCase): - - @classmethod - def setUpTestData(cls): - """Create test data.""" - super(PermissionsTestCase, cls).setUpTestData() - populate_database() - - def test_domainadmin_deletes_reseller(self): - """Check if a domain admin can delete a reseller. - - Expected result: no. - """ - values = dict( - username="reseller@test.com", first_name="Reseller", last_name="", - password1="Toto1234", password2="Toto1234", role="Resellers", - is_active=True, email="reseller@test.com", stepid='step2' - ) - self.ajax_post(reverse("admin:account_add"), values) - account = User.objects.get(username="reseller@test.com") - self.client.logout() - self.client.login(username="admin@test.com", password="toto") - resp = self.ajax_post( - reverse("admin:account_delete", args=[account.id]), - {}, status=403 - ) - self.assertEqual(resp, "Permission denied") - - -class ResourceTestCase(lib_tests.ModoTestCase): - - @classmethod - def setUpTestData(cls): - """Custom setUpTestData method.""" - super(ResourceTestCase, cls).setUpTestData() - for name, tpl in utils.get_user_limit_templates(): - parameters.save_admin( - "DEFLT_USER_{0}_LIMIT".format(name.upper()), 2, - app="limits" - ) - populate_database() - - def _create_account(self, username, role='SimpleUsers', status=200): - values = dict( - username=username, first_name="Tester", last_name="Toto", - password1="Toto1234", password2="Toto1234", role=role, - quota_act=True, - is_active=True, email=username, stepid='step2', - ) - return self.ajax_post( - reverse("admin:account_add"), values, status - ) - - def _create_alias(self, email, rcpt='user@test.com', status=200): - values = dict( - address=email, recipients=rcpt, enabled=True - ) - return self.ajax_post( - reverse("admin:alias_add"), values, status - ) - - def _create_domain(self, name, status=200, withtpl=False): - values = { - "name": name, "quota": 100, "create_dom_admin": "no", - "create_aliases": "no", "stepid": 'step3', "type": "domain" - } - if withtpl: - values['create_dom_admin'] = 'yes' - values['dom_admin_username'] = 'admin' - values['create_aliases'] = 'yes' - response = self.ajax_post( - reverse("admin:domain_add"), values, status - ) - return response - - def _domain_alias_operation(self, optype, domain, name, status=200): - dom = Domain.objects.get(name=domain) - values = { - 'name': dom.name, 'quota': dom.quota, 'enabled': dom.enabled, - "type": "domain" - } - aliases = [alias.name for alias in dom.domainalias_set.all()] - if optype == 'add': - aliases.append(name) - else: - aliases.remove(name) - for cpt, alias in enumerate(aliases): - fname = 'aliases' if not cpt else 'aliases_%d' % cpt - values[fname] = alias - self.ajax_post( - reverse("admin:domain_change", args=[dom.id]), - values, status - ) - - def _check_limit(self, name, curvalue, maxvalue): - l = self.user.userobjectlimit_set.get(name=name) - self.assertEqual(l.current_value, curvalue) - self.assertEqual(l.max_value, maxvalue) - - -class DomainAdminTestCase(ResourceTestCase): - - @classmethod - def setUpTestData(cls): - """Create test data.""" - super(DomainAdminTestCase, cls).setUpTestData() - cls.user = User.objects.get(username='admin@test.com') - cls.user.userobjectlimit_set.filter( - name__in=["mailboxes", "mailbox_aliases"]).update(max_value=2) - - def setUp(self): - """Test initialization.""" - super(DomainAdminTestCase, self).setUp() - self.client.logout() - self.client.login(username='admin@test.com', password='toto') - - def test_mailboxes_limit(self): - self._create_account('tester1@test.com') - self._check_limit('mailboxes', 1, 2) - self._create_account('tester2@test.com') - self._check_limit('mailboxes', 2, 2) - self._create_account('tester3@test.com', status=403) - self._check_limit('mailboxes', 2, 2) - self.ajax_post( - reverse('admin:account_delete', - args=[User.objects.get(username='tester2@test.com').id]), - {} - ) - self._check_limit('mailboxes', 1, 2) - - def test_aliases_limit(self): - self._create_alias('alias1@test.com') - self._check_limit('mailbox_aliases', 1, 2) - self._create_alias('alias2@test.com') - self._check_limit('mailbox_aliases', 2, 2) - self._create_alias('alias3@test.com', status=403) - self._check_limit('mailbox_aliases', 2, 2) - self.ajax_post( - reverse('admin:alias_delete') + '?selection=%d' - % Alias.objects.get(address='alias2@test.com').id, - {} - ) - self._check_limit('mailbox_aliases', 1, 2) - - def test_aliases_limit_through_account_form(self): - user = User.objects.get(username='user@test.com') - values = dict( - username=user.username, role=user.group, - is_active=user.is_active, email=user.email, quota_act=True, - aliases="alias1@test.com", aliases_1="alias2@test.com" - ) - self.ajax_post( - reverse("admin:account_change", args=[user.id]), - values - ) - Alias.objects.get(address='alias1@test.com') - self._check_limit('mailbox_aliases', 2, 2) - - -class ResellerTestCase(ResourceTestCase): - - @classmethod - def setUpTestData(cls): - """Create test data.""" - super(ResellerTestCase, cls).setUpTestData() - cls.user = UserFactory( - username='reseller', groups=('Resellers',) - ) - - def setUp(self): - """Test initialization.""" - super(ResellerTestCase, self).setUp() - self.client.logout() - self.client.login(username='reseller', password='toto') - - def test_domains_limit(self): - response = self.client.get(reverse("admin:domain_list")) - self.assertContains(response, "Domains (0%)") - self.assertContains(response, "Domain aliases (0%)") - self._create_domain('domain1.tld') - self._check_limit('domains', 1, 2) - self._create_domain('domain2.tld') - self._check_limit('domains', 2, 2) - self._create_domain('domain3.tld', 403) - self._check_limit('domains', 2, 2) - self.ajax_post( - reverse('admin:domain_delete', - args=[Domain.objects.get(name='domain2.tld').id]), - {} - ) - self._check_limit('domains', 1, 2) - - def test_domain_aliases_limit(self): - self._create_domain('pouet.com') - self._domain_alias_operation('add', 'pouet.com', 'domain-alias1.tld') - self._check_limit('domain_aliases', 1, 2) - self._domain_alias_operation('add', 'pouet.com', 'domain-alias2.tld') - self._check_limit('domain_aliases', 2, 2) - self._domain_alias_operation( - 'add', 'pouet.com', 'domain-alias3.tld', 403 - ) - self._check_limit('domain_aliases', 2, 2) - self._domain_alias_operation( - 'delete', 'pouet.com', 'domain-alias2.tld') - self._check_limit('domain_aliases', 1, 2) - - def test_domain_admins_limit(self): - response = self.client.get(reverse("admin:identity_list")) - self.assertContains(response, "Domain admins (0%)") - self.assertContains(response, "Mailboxes (0%)") - self.assertContains(response, "Mailbox aliases (0%)") - - self._create_domain('domain.tld') - self._create_account('admin1@domain.tld', role='DomainAdmins') - self._check_limit('domain_admins', 1, 2) - self._create_account('admin2@domain.tld', role='DomainAdmins') - self._check_limit('domain_admins', 2, 2) - resp = self._create_account( - 'admin3@domain.tld', - role='DomainAdmins', - status=400) - self.assertEqual( - resp['form_errors']['role'][0], - 'Select a valid choice. DomainAdmins is not one of the available ' - 'choices.' - ) - self._check_limit('domain_admins', 2, 2) - - self.user.userobjectlimit_set.filter( - name="mailboxes").update(max_value=3) - self._create_account('user1@domain.tld') - user = User.objects.get(username='user1@domain.tld') - values = { - 'username': user.username, 'role': 'DomainAdmins', - 'quota_act': True, 'is_active': user.is_active, - 'email': user.email - } - resp = self.ajax_post( - reverse("admin:account_change", args=[user.id]), - values, status=400 - ) - self.assertEqual( - resp['form_errors']['role'][0], - 'Select a valid choice. DomainAdmins is not one of the available ' - 'choices.' - ) - self._check_limit('domain_admins', 2, 2) - - def test_domain_admin_resource_are_empty(self): - self._create_domain('domain.tld') - self._create_account('admin1@domain.tld', role='DomainAdmins') - domadmin = User.objects.get(username='admin1@domain.tld') - for l in ['mailboxes', 'mailbox_aliases']: - self.assertEqual( - domadmin.userobjectlimit_set.get(name=l).max_value, 0 - ) - - def test_domain_admins_limit_from_domain_tpl(self): - self.user.userobjectlimit_set.filter( - name="domains").update(max_value=3) - self._create_domain('domain1.tld', withtpl=True) - self._create_domain('domain2.tld', withtpl=True) - self._check_limit('domain_admins', 2, 2) - self._check_limit('domains', 2, 3) - self._create_domain('domain3.tld', status=200, withtpl=True) - self._check_limit('domain_admins', 2, 2) - self._check_limit('domains', 3, 3) - - def test_reseller_deletes_domain(self): - """Check if all resources are restored after the deletion. - """ - self._create_domain('domain.tld', withtpl=True) - dom = Domain.objects.get(name="domain.tld") - self.ajax_post( - reverse("admin:domain_delete", args=[dom.id]), - {} - ) - self._check_limit('domains', 0, 2) - self._check_limit('domain_admins', 1, 2) - self._check_limit('mailboxes', 0, 2) - self._check_limit('mailbox_aliases', 0, 2) - - def test_sadmin_removes_ownership(self): - self._create_domain('domain.tld', withtpl=True) - dom = Domain.objects.get(name="domain.tld") - self.client.logout() - self.client.login(username='admin', password='password') - self.ajax_get( - "{0}?domid={1}&daid={2}".format( - reverse('admin:permission_remove'), - dom.id, self.user.id - ), {} - ) - self._check_limit('domains', 0, 2) - self._check_limit('domain_admins', 0, 2) - self._check_limit('mailboxes', 0, 2) - self._check_limit('mailbox_aliases', 0, 2) - - def test_allocate_from_pool(self): - self._create_domain('domain.tld') - self._create_account('admin1@domain.tld', role='DomainAdmins') - user = User.objects.get(username='admin1@domain.tld') - - # Give 1 mailbox and 2 aliases to the admin -> should work - values = { - 'username': user.username, 'role': user.group, 'quota_act': True, - 'is_active': user.is_active, 'email': user.email, - 'mailboxes_limit': 1, 'mailbox_aliases_limit': 2 - } - self.ajax_post( - reverse("admin:account_change", args=[user.id]), - values - ) - self._check_limit('mailboxes', 1, 1) - self._check_limit('mailbox_aliases', 0, 0) - - # Delete the admin -> resources should go back to the - # reseller's pool - self.ajax_post( - reverse("admin:account_delete", args=[user.id]), - {} - ) - self._check_limit('mailboxes', 0, 2) - self._check_limit('mailbox_aliases', 0, 2) - - def test_restore_resources(self): - """Give resource to a domain admin and restore them.""" - self._create_domain('domain.tld') - dom = Domain.objects.get(name='domain.tld') - self._create_account('admin1@domain.tld', role='DomainAdmins') - user = User.objects.get(username='admin1@domain.tld') - values = { - 'username': user.username, 'role': user.group, 'quota_act': True, - 'is_active': user.is_active, 'email': user.email, - 'mailboxes_limit': 1, 'mailbox_aliases_limit': 2 - } - self.ajax_post( - reverse("admin:account_change", args=[user.id]), - values - ) - dom.add_admin(user) - self.client.logout() - self.client.login(username='admin1@domain.tld', password='Toto1234') - self._create_account('user1@domain.tld') - self._create_alias('alias1@domain.tld', 'user1@domain.tld') - self._create_alias('alias2@domain.tld', 'user1@domain.tld') - self.client.logout() - self.client.login(username='reseller', password='toto') - # Delete the admin -> resources should go back to the - # reseller's pool - self.ajax_post( - reverse("admin:account_delete", args=[user.id]), - {} - ) - self._check_limit('mailboxes', 1, 2) - self._check_limit('mailbox_aliases', 2, 2) - - def test_change_role(self): - self._create_domain('domain.tld') - self._create_account('admin1@domain.tld', role='DomainAdmins') - user = User.objects.get(username='admin1@domain.tld') - - # Give 1 mailbox and 2 aliases to the admin -> should work - values = { - 'username': user.username, 'role': user.group, 'quota_act': True, - 'is_active': user.is_active, 'email': user.email, - 'mailboxes_limit': 1, 'mailbox_aliases_limit': 2 - } - self.ajax_post( - reverse("admin:account_change", args=[user.id]), - values - ) - self._check_limit('mailboxes', 1, 1) - self._check_limit('mailbox_aliases', 0, 0) - - # Change admin role to SimpleUser -> resources should go back - # to the reseller. - values = { - 'username': user.username, 'role': 'SimpleUsers', - 'quota_act': True, - 'is_active': user.is_active, 'email': user.email, - } - self.ajax_post( - reverse("admin:account_change", args=[user.id]), - values - ) - self._check_limit('mailboxes', 1, 2) - self._check_limit('mailbox_aliases', 0, 2) - - def test_allocate_too_much(self): - self._create_domain('domain.tld') - self._create_account('admin1@domain.tld', role='DomainAdmins') - user = User.objects.get(username='admin1@domain.tld') - - # Give 2 mailboxes and 3 aliases to the admin -> should fail. - values = { - 'username': user.username, 'role': user.group, 'quota_act': True, - 'is_active': user.is_active, 'email': user.email, - 'mailboxes_limit': 2, 'mailbox_aliases_limit': 3 - } - resp = self.ajax_post( - reverse("admin:account_change", args=[user.id]), - values, 424 - ) - self.assertEqual(resp, 'Not enough resources') - self._check_limit('mailboxes', 1, 2) - self._check_limit('mailbox_aliases', 0, 2) - - -class DomainLimitsTestCase(lib_tests.ModoTestCase): - """Per-domain limits tests.""" - - @classmethod - def setUpTestData(cls): - """Create test data.""" - super(DomainLimitsTestCase, cls).setUpTestData() - parameters.save_admin("ENABLE_DOMAIN_LIMITS", "yes") - for name, tpl in utils.get_domain_limit_templates(): - parameters.save_admin( - "DEFLT_DOMAIN_{}_LIMIT".format(name.upper()), 2) - populate_database() - - def test_set_limits(self): - """Try to set limits for a given domain.""" - domain = Domain.objects.get(name="test.com") - values = { - "name": domain.name, "quota": domain.quota, - "enabled": domain.enabled, "type": "domain", - "mailboxes_limit": 3, "mailbox_aliases_limit": 3, - "domain_aliases_limit": 3 - } - self.ajax_post( - reverse("admin:domain_change", args=[domain.id]), - values - ) - domain.refresh_from_db() - self.assertEqual( - domain.domainobjectlimit_set.get(name="mailboxes").max_value, 3) - self.assertEqual( - domain.domainobjectlimit_set.get( - name="mailbox_aliases").max_value, 3) - self.assertEqual( - domain.domainobjectlimit_set.get( - name="domain_aliases").max_value, 3) - - def test_domain_aliases_limit(self): - """Try to exceed defined limit.""" - # Import - domain = Domain.objects.get(name="test.com") - limit = domain.domainobjectlimit_set.get(name="domain_aliases") - self.assertFalse(limit.is_exceeded()) - values = { - "name": domain.name, "quota": domain.quota, - "enabled": domain.enabled, "type": "domain", - "mailboxes_limit": 2, "mailbox_aliases_limit": 2, - "domain_aliases_limit": 2, - "aliases": "alias1.com", "aliases_1": "alias2.com" - } - self.ajax_post( - reverse("admin:domain_change", args=[domain.id]), - values - ) - self.assertTrue(limit.is_exceeded()) - values["aliases_2"] = "alias3.com" - self.ajax_post( - reverse("admin:domain_change", args=[domain.id]), - values, 403 - ) - - def test_mailboxes_limit(self): - """Try to exceed defined limits.""" - # Import - # LDAP ? - domain = Domain.objects.get(name="test.com") - domain.domainobjectlimit_set.filter(name="mailboxes").update( - max_value=3) - limit = domain.domainobjectlimit_set.get(name="mailboxes") - self.assertFalse(limit.is_exceeded()) - username = "toto@test.com" - values = { - "username": username, - "first_name": "Tester", "last_name": "Toto", - "password1": "Toto1234", "password2": "Toto1234", - "role": "SimpleUsers", "quota_act": True, - "is_active": True, "email": username, "stepid": "step2", - } - self.ajax_post(reverse("admin:account_add"), values, 200) - self.assertTrue(limit.is_exceeded()) - - username = "titi@test.com" - self.ajax_post(reverse("admin:account_add"), values, 400) - - def test_mailbox_aliases_limit(self): - """Try to exceed defined limits.""" - # Import - domain = Domain.objects.get(name="test.com") - user = User.objects.get(username="user@test.com") - limit = domain.domainobjectlimit_set.get(name="mailbox_aliases") - limit.max_value = 4 - limit.save() - self.assertFalse(limit.is_exceeded()) - values = { - "username": user.username, "role": user.group, - "is_active": user.is_active, "email": user.email, - "quota_act": True, - "aliases": "alias@test.com", "aliases_1": "alias1@test.com" - } - self.ajax_post( - reverse("admin:account_change", args=[user.id]), - values - ) - self.assertTrue(limit.is_exceeded()) - - values["aliases_2"] = "alias2@test.com" - self.ajax_post( - reverse("admin:account_change", args=[user.id]), - values, 403 - ) - - limit.max_value = 5 - limit.save() - values = { - "address": "forward2@test.com", "recipients": "user@test.com", - "enabled": True - } - self.ajax_post(reverse("admin:alias_add"), values) - self.assertTrue(limit.is_exceeded()) - - values["address"] = "forward3@test.com" - self.ajax_post(reverse("admin:alias_add"), values, 400) - - -class APIDomainLimitsTestCase(lib_tests.ModoAPITestCase): - """Check that limits are used also by the API.""" - - @classmethod - def setUpTestData(cls): - """Create test data.""" - super(APIDomainLimitsTestCase, cls).setUpTestData() - parameters.save_admin("ENABLE_DOMAIN_LIMITS", "yes") - for name, tpl in utils.get_domain_limit_templates(): - parameters.save_admin( - "DEFLT_DOMAIN_{}_LIMIT".format(name.upper()), 2) - populate_database() - - def test_mailboxes_limit(self): - """Check mailboxes limit.""" - domain = Domain.objects.get(name="test.com") - limit = domain.domainobjectlimit_set.get(name="mailboxes") - self.assertTrue(limit.is_exceeded()) - url = reverse("external_api:account-list") - data = { - "username": "fromapi@test.com", - "role": "SimpleUsers", - "password": "Toto1234", - "mailbox": { - "full_address": "fromapi@test.com", - "quota": 10 - } - } - response = self.client.post(url, data, format="json") - self.assertEqual(response.status_code, 400) - - def test_domain_aliases_limit(self): - """Check domain_aliases limit.""" - domain = Domain.objects.get(name="test.com") - limit = domain.domainobjectlimit_set.get(name="domain_aliases") - url = reverse("external_api:domain_alias-list") - data = {"name": "dalias1.com", "target": domain.pk} - response = self.client.post(url, data, format="json") - self.assertEqual(response.status_code, 201) - data["name"] = "dalias2.com" - response = self.client.post(url, data, format="json") - self.assertEqual(response.status_code, 201) - self.assertTrue(limit.is_exceeded()) - data["name"] = "dalias3.com" - response = self.client.post(url, data, format="json") - self.assertEqual(response.status_code, 400) - - def test_mailbox_aliases_limit(self): - """Check mailbox_aliases limit.""" - domain = Domain.objects.get(name="test.com") - limit = domain.domainobjectlimit_set.get(name="mailbox_aliases") - self.assertTrue(limit.is_exceeded()) - url = reverse("external_api:alias-list") - data = { - "address": "alias_fromapi@test.com", - "recipients": [ - "user@test.com", "postmaster@test.com", "user_éé@nonlocal.com" - ] - } - response = self.client.post(url, data, format="json") - self.assertEqual(response.status_code, 400) From 2bf3a4d5cb0a6dc5f26dcabcc29391271ffea5e0 Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Sun, 24 Apr 2016 15:18:14 +0200 Subject: [PATCH 10/15] Unit tests and fixes. see #799 --- modoboa/admin/callbacks.py | 7 +- modoboa/admin/lib.py | 5 + modoboa/admin/models/alias.py | 5 + modoboa/limits/tests/__init__.py | 0 modoboa/limits/tests/test_api.py | 237 ++++++++++++ modoboa/limits/tests/test_domain_limits.py | 135 +++++++ modoboa/limits/tests/test_import.py | 104 +++++ modoboa/limits/tests/test_user_limits.py | 422 +++++++++++++++++++++ 8 files changed, 914 insertions(+), 1 deletion(-) create mode 100644 modoboa/limits/tests/__init__.py create mode 100644 modoboa/limits/tests/test_api.py create mode 100644 modoboa/limits/tests/test_domain_limits.py create mode 100644 modoboa/limits/tests/test_import.py create mode 100644 modoboa/limits/tests/test_user_limits.py diff --git a/modoboa/admin/callbacks.py b/modoboa/admin/callbacks.py index b894673c0..163bf49aa 100644 --- a/modoboa/admin/callbacks.py +++ b/modoboa/admin/callbacks.py @@ -3,6 +3,7 @@ from django.core.urlresolvers import reverse from django.utils.translation import ugettext as _, ugettext_lazy +from modoboa.core import signals as core_signals from modoboa.lib import parameters, events from modoboa.lib.email_utils import split_mailbox from modoboa.lib.exceptions import PermDeniedException, BadRequest, Conflict @@ -103,7 +104,6 @@ def import_account_mailbox(user, account, row): """ account.email = row[0].strip() if account.email: - account.save() mailbox, domname = split_mailbox(account.email) try: domain = Domain.objects.get(name=domname) @@ -114,6 +114,11 @@ def import_account_mailbox(user, account, row): ) if not user.can_access(domain): raise PermDeniedException + core_signals.can_create_object.send( + sender="import", context=user, object_type="mailboxes") + core_signals.can_create_object.send( + sender="import", context=domain, object_type="mailboxes") + account.save() if Mailbox.objects.filter(address=mailbox, domain=domain).exists(): raise Conflict( _("Mailbox {} already exists").format(account.email)) diff --git a/modoboa/admin/lib.py b/modoboa/admin/lib.py index 547d487ca..075663131 100644 --- a/modoboa/admin/lib.py +++ b/modoboa/admin/lib.py @@ -10,6 +10,7 @@ from django.db.models import Q from django.utils.translation import ugettext as _ from modoboa.core.models import User +from modoboa.core import signals as core_signals from modoboa.lib import events from modoboa.lib.exceptions import PermDeniedException @@ -118,6 +119,8 @@ def import_domain(user, row, formopts): """Specific code for domains import""" if not user.has_perm("admin.add_domain"): raise PermDeniedException(_("You are not allowed to import domains")) + core_signals.can_create_object.send( + sender="import", context=user, object_type="domains") dom = Domain() dom.from_csv(user, row) @@ -127,6 +130,8 @@ def import_domainalias(user, row, formopts): if not user.has_perm("admin.add_domainalias"): raise PermDeniedException( _("You are not allowed to import domain aliases.")) + core_signals.can_create_object.send( + sender="import", context=user, object_type="domain_aliases") domalias = DomainAlias() domalias.from_csv(user, row) diff --git a/modoboa/admin/models/alias.py b/modoboa/admin/models/alias.py index 3948966b7..c8626267c 100644 --- a/modoboa/admin/models/alias.py +++ b/modoboa/admin/models/alias.py @@ -6,6 +6,7 @@ from django.utils.translation import ugettext as _, ugettext_lazy import reversion +from modoboa.core import signals as core_signals from modoboa.lib.email_utils import split_mailbox from modoboa.lib.exceptions import ( PermDeniedException, BadRequest, Conflict, NotFound @@ -171,6 +172,10 @@ class Alias(AdminObject): raise BadRequest(_("Domain '%s' does not exist" % domname)) if not user.can_access(domain): raise PermDeniedException + core_signals.can_create_object.send( + sender="import", context=user, object_type="mailbox_aliases") + core_signals.can_create_object.send( + sender="import", context=domain, object_type="mailbox_aliases") if Alias.objects.filter(address=address).exists(): raise Conflict self.address = address diff --git a/modoboa/limits/tests/__init__.py b/modoboa/limits/tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/modoboa/limits/tests/test_api.py b/modoboa/limits/tests/test_api.py new file mode 100644 index 000000000..5d7c99a7d --- /dev/null +++ b/modoboa/limits/tests/test_api.py @@ -0,0 +1,237 @@ +# coding: utf-8 +"""Test cases for the limits extension.""" + +from django.core.urlresolvers import reverse + +from rest_framework.authtoken.models import Token + +from modoboa.admin.factories import populate_database +from modoboa.admin.models import Domain +from modoboa.core import factories as core_factories +from modoboa.core.models import User +from modoboa.lib import parameters +from modoboa.lib import tests as lib_tests + +from .. import utils + + +class APIAdminLimitsTestCase(lib_tests.ModoAPITestCase): + """Check that limits are used also by the API.""" + + @classmethod + def setUpTestData(cls): + """Create test data.""" + super(APIAdminLimitsTestCase, cls).setUpTestData() + for name, tpl in utils.get_user_limit_templates(): + parameters.save_admin( + "DEFLT_USER_{}_LIMIT".format(name.upper()), 2) + populate_database() + cls.user = User.objects.get(username="admin@test.com") + cls.da_token = Token.objects.create(user=cls.user) + cls.reseller = core_factories.UserFactory( + username="reseller", groups=("Resellers", ), + ) + cls.r_token = Token.objects.create(user=cls.reseller) + + def test_domadmins_limit(self): + """Check domain admins limit.""" + self.client.credentials( + HTTP_AUTHORIZATION='Token ' + self.r_token.key) + + limit = self.reseller.userobjectlimit_set.get(name="domain_admins") + url = reverse("external_api:account-list") + data = { + "username": "fromapi@test.com", + "role": "DomainAdmins", + "password": "Toto1234", + } + response = self.client.post(url, data, format="json") + self.assertEqual(response.status_code, 201) + self.assertFalse(limit.is_exceeded()) + + data["username"] = "fromapi2@test.com" + response = self.client.post(url, data, format="json") + self.assertEqual(response.status_code, 201) + self.assertTrue(limit.is_exceeded()) + + data["username"] = "fromapi3@test.com" + response = self.client.post(url, data, format="json") + self.assertEqual(response.status_code, 400) + + user = User.objects.get(username="user@test.com") + domain = Domain.objects.get(name="test.com") + domain.add_admin(self.reseller) + url = reverse("external_api:account-detail", args=[user.pk]) + data = { + "username": user.username, + "role": "DomainAdmins", + "password": "Toto1234", + "mailbox": { + "full_address": user.username, + "quota": user.mailbox.quota + } + } + response = self.client.put(url, data, format="json") + self.assertEqual(response.status_code, 400) + + def test_domains_limit(self): + """Check domains limit.""" + self.client.credentials( + HTTP_AUTHORIZATION='Token ' + self.r_token.key) + limit = self.reseller.userobjectlimit_set.get(name="domains") + url = reverse("external_api:domain-list") + data = {"name": "test3.com", "quota": 10} + response = self.client.post(url, data, format="json") + self.assertEqual(response.status_code, 201) + self.assertFalse(limit.is_exceeded()) + + data["name"] = "test4.com" + response = self.client.post(url, data, format="json") + self.assertEqual(response.status_code, 201) + self.assertTrue(limit.is_exceeded()) + + data["username"] = "test5.com" + response = self.client.post(url, data, format="json") + self.assertEqual(response.status_code, 400) + + def test_domain_aliases_limit(self): + """Check domain aliases limit.""" + self.client.credentials( + HTTP_AUTHORIZATION='Token ' + self.r_token.key) + domain = Domain.objects.get(name="test.com") + domain.add_admin(self.reseller) + limit = self.reseller.userobjectlimit_set.get(name="domain_aliases") + url = reverse("external_api:domain_alias-list") + data = {"name": "dalias1.com", "target": domain.pk} + response = self.client.post(url, data, format="json") + self.assertEqual(response.status_code, 201) + self.assertFalse(limit.is_exceeded()) + + data["name"] = "dalias2.com" + response = self.client.post(url, data, format="json") + self.assertEqual(response.status_code, 201) + self.assertTrue(limit.is_exceeded()) + + data["username"] = "dalias3.com" + response = self.client.post(url, data, format="json") + self.assertEqual(response.status_code, 400) + + def test_mailboxes_limit(self): + """Check mailboxes limit.""" + self.client.credentials( + HTTP_AUTHORIZATION='Token ' + self.da_token.key) + + limit = self.user.userobjectlimit_set.get(name="mailboxes") + url = reverse("external_api:account-list") + data = { + "username": "fromapi@test.com", + "role": "SimpleUsers", + "password": "Toto1234", + "mailbox": { + "full_address": "fromapi@test.com", + "quota": 10 + } + } + response = self.client.post(url, data, format="json") + self.assertEqual(response.status_code, 201) + self.assertFalse(limit.is_exceeded()) + + data["username"] = "fromapi2@test.com" + data["mailbox"]["full_address"] = "fromapi2@test.com" + response = self.client.post(url, data, format="json") + self.assertEqual(response.status_code, 201) + self.assertTrue(limit.is_exceeded()) + + data["username"] = "fromapi3@test.com" + data["mailbox"]["full_address"] = "fromapi3@test.com" + response = self.client.post(url, data, format="json") + self.assertEqual(response.status_code, 400) + + def test_aliases_limit(self): + """Check mailbox aliases limit.""" + self.client.credentials( + HTTP_AUTHORIZATION='Token ' + self.da_token.key) + + limit = self.user.userobjectlimit_set.get(name="mailbox_aliases") + url = reverse("external_api:alias-list") + data = { + "address": "alias_fromapi@test.com", + "recipients": [ + "user@test.com", "postmaster@test.com", "user_éé@nonlocal.com" + ] + } + response = self.client.post(url, data, format="json") + self.assertEqual(response.status_code, 201) + self.assertFalse(limit.is_exceeded()) + + data["address"] = "alias_fromapi2@test.com" + response = self.client.post(url, data, format="json") + self.assertEqual(response.status_code, 201) + self.assertTrue(limit.is_exceeded()) + + data["address"] = "alias_fromapi3@test.com" + response = self.client.post(url, data, format="json") + self.assertEqual(response.status_code, 400) + + +class APIDomainLimitsTestCase(lib_tests.ModoAPITestCase): + """Check that limits are used also by the API.""" + + @classmethod + def setUpTestData(cls): + """Create test data.""" + super(APIDomainLimitsTestCase, cls).setUpTestData() + parameters.save_admin("ENABLE_DOMAIN_LIMITS", "yes") + for name, tpl in utils.get_domain_limit_templates(): + parameters.save_admin( + "DEFLT_DOMAIN_{}_LIMIT".format(name.upper()), 2) + populate_database() + + def test_mailboxes_limit(self): + """Check mailboxes limit.""" + domain = Domain.objects.get(name="test.com") + limit = domain.domainobjectlimit_set.get(name="mailboxes") + self.assertTrue(limit.is_exceeded()) + url = reverse("external_api:account-list") + data = { + "username": "fromapi@test.com", + "role": "SimpleUsers", + "password": "Toto1234", + "mailbox": { + "full_address": "fromapi@test.com", + "quota": 10 + } + } + response = self.client.post(url, data, format="json") + self.assertEqual(response.status_code, 400) + + def test_domain_aliases_limit(self): + """Check domain_aliases limit.""" + domain = Domain.objects.get(name="test.com") + limit = domain.domainobjectlimit_set.get(name="domain_aliases") + url = reverse("external_api:domain_alias-list") + data = {"name": "dalias1.com", "target": domain.pk} + response = self.client.post(url, data, format="json") + self.assertEqual(response.status_code, 201) + data["name"] = "dalias2.com" + response = self.client.post(url, data, format="json") + self.assertEqual(response.status_code, 201) + self.assertTrue(limit.is_exceeded()) + data["name"] = "dalias3.com" + response = self.client.post(url, data, format="json") + self.assertEqual(response.status_code, 400) + + def test_mailbox_aliases_limit(self): + """Check mailbox_aliases limit.""" + domain = Domain.objects.get(name="test.com") + limit = domain.domainobjectlimit_set.get(name="mailbox_aliases") + self.assertTrue(limit.is_exceeded()) + url = reverse("external_api:alias-list") + data = { + "address": "alias_fromapi@test.com", + "recipients": [ + "user@test.com", "postmaster@test.com", "user_éé@nonlocal.com" + ] + } + response = self.client.post(url, data, format="json") + self.assertEqual(response.status_code, 400) diff --git a/modoboa/limits/tests/test_domain_limits.py b/modoboa/limits/tests/test_domain_limits.py new file mode 100644 index 000000000..be539ef25 --- /dev/null +++ b/modoboa/limits/tests/test_domain_limits.py @@ -0,0 +1,135 @@ +# coding: utf-8 +"""Test cases for the limits extension.""" + +from django.core.urlresolvers import reverse + +from modoboa.admin.factories import populate_database +from modoboa.admin.models import Domain +from modoboa.core.models import User +from modoboa.lib import parameters +from modoboa.lib import tests as lib_tests + +from .. import utils + + +class DomainLimitsTestCase(lib_tests.ModoTestCase): + """Per-domain limits tests.""" + + @classmethod + def setUpTestData(cls): + """Create test data.""" + super(DomainLimitsTestCase, cls).setUpTestData() + parameters.save_admin("ENABLE_DOMAIN_LIMITS", "yes") + for name, tpl in utils.get_domain_limit_templates(): + parameters.save_admin( + "DEFLT_DOMAIN_{}_LIMIT".format(name.upper()), 2) + populate_database() + + def test_set_limits(self): + """Try to set limits for a given domain.""" + domain = Domain.objects.get(name="test.com") + values = { + "name": domain.name, "quota": domain.quota, + "enabled": domain.enabled, "type": "domain", + "mailboxes_limit": 3, "mailbox_aliases_limit": 3, + "domain_aliases_limit": 3 + } + self.ajax_post( + reverse("admin:domain_change", args=[domain.id]), + values + ) + domain.refresh_from_db() + self.assertEqual( + domain.domainobjectlimit_set.get(name="mailboxes").max_value, 3) + self.assertEqual( + domain.domainobjectlimit_set.get( + name="mailbox_aliases").max_value, 3) + self.assertEqual( + domain.domainobjectlimit_set.get( + name="domain_aliases").max_value, 3) + + def test_domain_aliases_limit(self): + """Try to exceed defined limit.""" + # Import + domain = Domain.objects.get(name="test.com") + limit = domain.domainobjectlimit_set.get(name="domain_aliases") + self.assertFalse(limit.is_exceeded()) + values = { + "name": domain.name, "quota": domain.quota, + "enabled": domain.enabled, "type": "domain", + "mailboxes_limit": 2, "mailbox_aliases_limit": 2, + "domain_aliases_limit": 2, + "aliases": "alias1.com", "aliases_1": "alias2.com" + } + self.ajax_post( + reverse("admin:domain_change", args=[domain.id]), + values + ) + self.assertTrue(limit.is_exceeded()) + values["aliases_2"] = "alias3.com" + self.ajax_post( + reverse("admin:domain_change", args=[domain.id]), + values, 403 + ) + + def test_mailboxes_limit(self): + """Try to exceed defined limits.""" + # Import + # LDAP ? + domain = Domain.objects.get(name="test.com") + domain.domainobjectlimit_set.filter(name="mailboxes").update( + max_value=3) + limit = domain.domainobjectlimit_set.get(name="mailboxes") + self.assertFalse(limit.is_exceeded()) + username = "toto@test.com" + values = { + "username": username, + "first_name": "Tester", "last_name": "Toto", + "password1": "Toto1234", "password2": "Toto1234", + "role": "SimpleUsers", "quota_act": True, + "is_active": True, "email": username, "stepid": "step2", + } + self.ajax_post(reverse("admin:account_add"), values, 200) + self.assertTrue(limit.is_exceeded()) + + username = "titi@test.com" + self.ajax_post(reverse("admin:account_add"), values, 400) + + def test_mailbox_aliases_limit(self): + """Try to exceed defined limits.""" + # Import + domain = Domain.objects.get(name="test.com") + user = User.objects.get(username="user@test.com") + limit = domain.domainobjectlimit_set.get(name="mailbox_aliases") + limit.max_value = 4 + limit.save() + self.assertFalse(limit.is_exceeded()) + values = { + "username": user.username, "role": user.group, + "is_active": user.is_active, "email": user.email, + "quota_act": True, + "aliases": "alias@test.com", "aliases_1": "alias1@test.com" + } + self.ajax_post( + reverse("admin:account_change", args=[user.id]), + values + ) + self.assertTrue(limit.is_exceeded()) + + values["aliases_2"] = "alias2@test.com" + self.ajax_post( + reverse("admin:account_change", args=[user.id]), + values, 403 + ) + + limit.max_value = 5 + limit.save() + values = { + "address": "forward2@test.com", "recipients": "user@test.com", + "enabled": True + } + self.ajax_post(reverse("admin:alias_add"), values) + self.assertTrue(limit.is_exceeded()) + + values["address"] = "forward3@test.com" + self.ajax_post(reverse("admin:alias_add"), values, 400) diff --git a/modoboa/limits/tests/test_import.py b/modoboa/limits/tests/test_import.py new file mode 100644 index 000000000..a36114d58 --- /dev/null +++ b/modoboa/limits/tests/test_import.py @@ -0,0 +1,104 @@ +# coding: utf-8 +from django.core.files.base import ContentFile +from django.core.urlresolvers import reverse + +from modoboa.admin import factories as admin_factories +from modoboa.core import factories as core_factories +from modoboa.core import models as core_models +from modoboa.lib import parameters +from modoboa.lib.tests import ModoTestCase + +from .. import utils + + +class UserLimitImportTestCase(ModoTestCase): + + @classmethod + def setUpTestData(cls): + """Create test data.""" + super(UserLimitImportTestCase, cls).setUpTestData() + for name, tpl in utils.get_user_limit_templates(): + parameters.save_admin( + "DEFLT_USER_{}_LIMIT".format(name.upper()), 2) + admin_factories.populate_database() + cls.reseller = core_factories.UserFactory( + username="reseller", groups=("Resellers", )) + cls.dadmin = core_models.User.objects.get(username="admin@test.com") + + def test_domains_import(self): + """Check domains limit.""" + self.client.login( + username=self.reseller.username, password="toto") + limit = self.reseller.userobjectlimit_set.get(name="domains") + self.assertFalse(limit.is_exceeded()) + f = ContentFile(b"""domain; domain1.com; 100; True +domain; domain2.com; 200; False +""", name="domains.csv") + self.client.post( + reverse("admin:domain_import"), { + "sourcefile": f + } + ) + self.assertTrue(limit.is_exceeded()) + + f = ContentFile(b"""domain; domain3.com; 100; True +""", name="domains.csv") + response = self.client.post( + reverse("admin:domain_import"), { + "sourcefile": f + } + ) + self.assertContains(response, "Domains: limit reached") + + def test_domain_alias_import(self): + """Check domain aliases limit.""" + self.client.login( + username=self.reseller.username, password="toto") + limit = self.reseller.userobjectlimit_set.get(name="domain_aliases") + self.assertFalse(limit.is_exceeded()) + f = ContentFile(b"""domainalias; domalias1.com; test.com; True +domainalias; domalias2.com; test.com; True +""", name="domains.csv") + self.client.post( + reverse("admin:domain_import"), { + "sourcefile": f + } + ) + self.assertTrue(limit.is_exceeded()) + + f = ContentFile( + "domainalias; domalias3.com; test.com; True", name="domains.csv") + response = self.client.post( + reverse("admin:domain_import"), { + "sourcefile": f + } + ) + self.assertContains(response, "Domain aliases: limit reached") + + def test_mailboxes_import(self): + """Check mailboxes limit.""" + self.client.login( + username=self.dadmin.username, password="toto") + limit = self.dadmin.userobjectlimit_set.get(name="mailboxes") + self.assertFalse(limit.is_exceeded()) + f = ContentFile(b"""account; user1@test.com; toto; User; One; True; SimpleUsers; user1@test.com; 5 +account; truc@test.com; toto; René; Truc; True; SimpleUsers; truc@test.com; 5 +""", name="domains.csv") + self.client.post( + reverse("admin:identity_import"), { + "sourcefile": f + } + ) + self.assertTrue(limit.is_exceeded()) + + f = ContentFile(""" +account; user3@test.com; toto; User; One; True; SimpleUsers; user3@test.com; 5 +""", name="domains.csv") + response = self.client.post( + reverse("admin:identity_import"), { + "sourcefile": f + } + ) + self.assertContains(response, "Mailboxes: limit reached") + + # FIXME: aliases, domain admins diff --git a/modoboa/limits/tests/test_user_limits.py b/modoboa/limits/tests/test_user_limits.py new file mode 100644 index 000000000..b00ace977 --- /dev/null +++ b/modoboa/limits/tests/test_user_limits.py @@ -0,0 +1,422 @@ +# coding: utf-8 +"""Test cases for the limits extension.""" + +from django.core.urlresolvers import reverse + +from modoboa.admin.factories import populate_database +from modoboa.admin.models import Alias, Domain +from modoboa.core.factories import UserFactory +from modoboa.core.models import User +from modoboa.lib import parameters +from modoboa.lib import tests as lib_tests + +from .. import utils + + +class PermissionsTestCase(lib_tests.ModoTestCase): + + @classmethod + def setUpTestData(cls): + """Create test data.""" + super(PermissionsTestCase, cls).setUpTestData() + populate_database() + + def test_domainadmin_deletes_reseller(self): + """Check if a domain admin can delete a reseller. + + Expected result: no. + """ + values = dict( + username="reseller@test.com", first_name="Reseller", last_name="", + password1="Toto1234", password2="Toto1234", role="Resellers", + is_active=True, email="reseller@test.com", stepid='step2' + ) + self.ajax_post(reverse("admin:account_add"), values) + account = User.objects.get(username="reseller@test.com") + self.client.logout() + self.client.login(username="admin@test.com", password="toto") + resp = self.ajax_post( + reverse("admin:account_delete", args=[account.id]), + {}, status=403 + ) + self.assertEqual(resp, "Permission denied") + + +class ResourceTestCase(lib_tests.ModoTestCase): + + @classmethod + def setUpTestData(cls): + """Custom setUpTestData method.""" + super(ResourceTestCase, cls).setUpTestData() + for name, tpl in utils.get_user_limit_templates(): + parameters.save_admin( + "DEFLT_USER_{0}_LIMIT".format(name.upper()), 2, + app="limits" + ) + populate_database() + + def _create_account(self, username, role='SimpleUsers', status=200): + values = dict( + username=username, first_name="Tester", last_name="Toto", + password1="Toto1234", password2="Toto1234", role=role, + quota_act=True, + is_active=True, email=username, stepid='step2', + ) + return self.ajax_post( + reverse("admin:account_add"), values, status + ) + + def _create_alias(self, email, rcpt='user@test.com', status=200): + values = dict( + address=email, recipients=rcpt, enabled=True + ) + return self.ajax_post( + reverse("admin:alias_add"), values, status + ) + + def _create_domain(self, name, status=200, withtpl=False): + values = { + "name": name, "quota": 100, "create_dom_admin": "no", + "create_aliases": "no", "stepid": 'step3', "type": "domain" + } + if withtpl: + values['create_dom_admin'] = 'yes' + values['dom_admin_username'] = 'admin' + values['create_aliases'] = 'yes' + response = self.ajax_post( + reverse("admin:domain_add"), values, status + ) + return response + + def _domain_alias_operation(self, optype, domain, name, status=200): + dom = Domain.objects.get(name=domain) + values = { + 'name': dom.name, 'quota': dom.quota, 'enabled': dom.enabled, + "type": "domain" + } + aliases = [alias.name for alias in dom.domainalias_set.all()] + if optype == 'add': + aliases.append(name) + else: + aliases.remove(name) + for cpt, alias in enumerate(aliases): + fname = 'aliases' if not cpt else 'aliases_%d' % cpt + values[fname] = alias + self.ajax_post( + reverse("admin:domain_change", args=[dom.id]), + values, status + ) + + def _check_limit(self, name, curvalue, maxvalue): + l = self.user.userobjectlimit_set.get(name=name) + self.assertEqual(l.current_value, curvalue) + self.assertEqual(l.max_value, maxvalue) + + +class DomainAdminTestCase(ResourceTestCase): + + @classmethod + def setUpTestData(cls): + """Create test data.""" + super(DomainAdminTestCase, cls).setUpTestData() + cls.user = User.objects.get(username='admin@test.com') + cls.user.userobjectlimit_set.filter( + name__in=["mailboxes", "mailbox_aliases"]).update(max_value=2) + + def setUp(self): + """Test initialization.""" + super(DomainAdminTestCase, self).setUp() + self.client.logout() + self.client.login(username='admin@test.com', password='toto') + + def test_mailboxes_limit(self): + self._create_account('tester1@test.com') + self._check_limit('mailboxes', 1, 2) + self._create_account('tester2@test.com') + self._check_limit('mailboxes', 2, 2) + self._create_account('tester3@test.com', status=403) + self._check_limit('mailboxes', 2, 2) + self.ajax_post( + reverse('admin:account_delete', + args=[User.objects.get(username='tester2@test.com').id]), + {} + ) + self._check_limit('mailboxes', 1, 2) + + def test_aliases_limit(self): + self._create_alias('alias1@test.com') + self._check_limit('mailbox_aliases', 1, 2) + self._create_alias('alias2@test.com') + self._check_limit('mailbox_aliases', 2, 2) + self._create_alias('alias3@test.com', status=403) + self._check_limit('mailbox_aliases', 2, 2) + self.ajax_post( + reverse('admin:alias_delete') + '?selection=%d' + % Alias.objects.get(address='alias2@test.com').id, + {} + ) + self._check_limit('mailbox_aliases', 1, 2) + + def test_aliases_limit_through_account_form(self): + user = User.objects.get(username='user@test.com') + values = dict( + username=user.username, role=user.group, + is_active=user.is_active, email=user.email, quota_act=True, + aliases="alias1@test.com", aliases_1="alias2@test.com" + ) + self.ajax_post( + reverse("admin:account_change", args=[user.id]), + values + ) + Alias.objects.get(address='alias1@test.com') + self._check_limit('mailbox_aliases', 2, 2) + + +class ResellerTestCase(ResourceTestCase): + + @classmethod + def setUpTestData(cls): + """Create test data.""" + super(ResellerTestCase, cls).setUpTestData() + cls.user = UserFactory( + username='reseller', groups=('Resellers',) + ) + + def setUp(self): + """Test initialization.""" + super(ResellerTestCase, self).setUp() + self.client.logout() + self.client.login(username='reseller', password='toto') + + def test_domains_limit(self): + response = self.client.get(reverse("admin:domain_list")) + self.assertContains(response, "Domains (0%)") + self.assertContains(response, "Domain aliases (0%)") + self._create_domain('domain1.tld') + self._check_limit('domains', 1, 2) + self._create_domain('domain2.tld') + self._check_limit('domains', 2, 2) + self._create_domain('domain3.tld', 403) + self._check_limit('domains', 2, 2) + self.ajax_post( + reverse('admin:domain_delete', + args=[Domain.objects.get(name='domain2.tld').id]), + {} + ) + self._check_limit('domains', 1, 2) + + def test_domain_aliases_limit(self): + self._create_domain('pouet.com') + self._domain_alias_operation('add', 'pouet.com', 'domain-alias1.tld') + self._check_limit('domain_aliases', 1, 2) + self._domain_alias_operation('add', 'pouet.com', 'domain-alias2.tld') + self._check_limit('domain_aliases', 2, 2) + self._domain_alias_operation( + 'add', 'pouet.com', 'domain-alias3.tld', 403 + ) + self._check_limit('domain_aliases', 2, 2) + self._domain_alias_operation( + 'delete', 'pouet.com', 'domain-alias2.tld') + self._check_limit('domain_aliases', 1, 2) + + def test_domain_admins_limit(self): + response = self.client.get(reverse("admin:identity_list")) + self.assertContains(response, "Domain admins (0%)") + self.assertContains(response, "Mailboxes (0%)") + self.assertContains(response, "Mailbox aliases (0%)") + + self._create_domain('domain.tld') + self._create_account('admin1@domain.tld', role='DomainAdmins') + self._check_limit('domain_admins', 1, 2) + self._create_account('admin2@domain.tld', role='DomainAdmins') + self._check_limit('domain_admins', 2, 2) + resp = self._create_account( + 'admin3@domain.tld', + role='DomainAdmins', + status=400) + self.assertEqual( + resp['form_errors']['role'][0], + 'Select a valid choice. DomainAdmins is not one of the available ' + 'choices.' + ) + self._check_limit('domain_admins', 2, 2) + + self.user.userobjectlimit_set.filter( + name="mailboxes").update(max_value=3) + self._create_account('user1@domain.tld') + user = User.objects.get(username='user1@domain.tld') + values = { + 'username': user.username, 'role': 'DomainAdmins', + 'quota_act': True, 'is_active': user.is_active, + 'email': user.email + } + resp = self.ajax_post( + reverse("admin:account_change", args=[user.id]), + values, status=400 + ) + self.assertEqual( + resp['form_errors']['role'][0], + 'Select a valid choice. DomainAdmins is not one of the available ' + 'choices.' + ) + self._check_limit('domain_admins', 2, 2) + + def test_domain_admin_resource_are_empty(self): + self._create_domain('domain.tld') + self._create_account('admin1@domain.tld', role='DomainAdmins') + domadmin = User.objects.get(username='admin1@domain.tld') + for l in ['mailboxes', 'mailbox_aliases']: + self.assertEqual( + domadmin.userobjectlimit_set.get(name=l).max_value, 0 + ) + + def test_domain_admins_limit_from_domain_tpl(self): + self.user.userobjectlimit_set.filter( + name="domains").update(max_value=3) + self._create_domain('domain1.tld', withtpl=True) + self._create_domain('domain2.tld', withtpl=True) + self._check_limit('domain_admins', 2, 2) + self._check_limit('domains', 2, 3) + self._create_domain('domain3.tld', status=200, withtpl=True) + self._check_limit('domain_admins', 2, 2) + self._check_limit('domains', 3, 3) + + def test_reseller_deletes_domain(self): + """Check if all resources are restored after the deletion. + """ + self._create_domain('domain.tld', withtpl=True) + dom = Domain.objects.get(name="domain.tld") + self.ajax_post( + reverse("admin:domain_delete", args=[dom.id]), + {} + ) + self._check_limit('domains', 0, 2) + self._check_limit('domain_admins', 1, 2) + self._check_limit('mailboxes', 0, 2) + self._check_limit('mailbox_aliases', 0, 2) + + def test_sadmin_removes_ownership(self): + self._create_domain('domain.tld', withtpl=True) + dom = Domain.objects.get(name="domain.tld") + self.client.logout() + self.client.login(username='admin', password='password') + self.ajax_get( + "{0}?domid={1}&daid={2}".format( + reverse('admin:permission_remove'), + dom.id, self.user.id + ), {} + ) + self._check_limit('domains', 0, 2) + self._check_limit('domain_admins', 0, 2) + self._check_limit('mailboxes', 0, 2) + self._check_limit('mailbox_aliases', 0, 2) + + def test_allocate_from_pool(self): + self._create_domain('domain.tld') + self._create_account('admin1@domain.tld', role='DomainAdmins') + user = User.objects.get(username='admin1@domain.tld') + + # Give 1 mailbox and 2 aliases to the admin -> should work + values = { + 'username': user.username, 'role': user.group, 'quota_act': True, + 'is_active': user.is_active, 'email': user.email, + 'mailboxes_limit': 1, 'mailbox_aliases_limit': 2 + } + self.ajax_post( + reverse("admin:account_change", args=[user.id]), + values + ) + self._check_limit('mailboxes', 1, 1) + self._check_limit('mailbox_aliases', 0, 0) + + # Delete the admin -> resources should go back to the + # reseller's pool + self.ajax_post( + reverse("admin:account_delete", args=[user.id]), + {} + ) + self._check_limit('mailboxes', 0, 2) + self._check_limit('mailbox_aliases', 0, 2) + + def test_restore_resources(self): + """Give resource to a domain admin and restore them.""" + self._create_domain('domain.tld') + dom = Domain.objects.get(name='domain.tld') + self._create_account('admin1@domain.tld', role='DomainAdmins') + user = User.objects.get(username='admin1@domain.tld') + values = { + 'username': user.username, 'role': user.group, 'quota_act': True, + 'is_active': user.is_active, 'email': user.email, + 'mailboxes_limit': 1, 'mailbox_aliases_limit': 2 + } + self.ajax_post( + reverse("admin:account_change", args=[user.id]), + values + ) + dom.add_admin(user) + self.client.logout() + self.client.login(username='admin1@domain.tld', password='Toto1234') + self._create_account('user1@domain.tld') + self._create_alias('alias1@domain.tld', 'user1@domain.tld') + self._create_alias('alias2@domain.tld', 'user1@domain.tld') + self.client.logout() + self.client.login(username='reseller', password='toto') + # Delete the admin -> resources should go back to the + # reseller's pool + self.ajax_post( + reverse("admin:account_delete", args=[user.id]), + {} + ) + self._check_limit('mailboxes', 1, 2) + self._check_limit('mailbox_aliases', 2, 2) + + def test_change_role(self): + self._create_domain('domain.tld') + self._create_account('admin1@domain.tld', role='DomainAdmins') + user = User.objects.get(username='admin1@domain.tld') + + # Give 1 mailbox and 2 aliases to the admin -> should work + values = { + 'username': user.username, 'role': user.group, 'quota_act': True, + 'is_active': user.is_active, 'email': user.email, + 'mailboxes_limit': 1, 'mailbox_aliases_limit': 2 + } + self.ajax_post( + reverse("admin:account_change", args=[user.id]), + values + ) + self._check_limit('mailboxes', 1, 1) + self._check_limit('mailbox_aliases', 0, 0) + + # Change admin role to SimpleUser -> resources should go back + # to the reseller. + values = { + 'username': user.username, 'role': 'SimpleUsers', + 'quota_act': True, + 'is_active': user.is_active, 'email': user.email, + } + self.ajax_post( + reverse("admin:account_change", args=[user.id]), + values + ) + self._check_limit('mailboxes', 1, 2) + self._check_limit('mailbox_aliases', 0, 2) + + def test_allocate_too_much(self): + self._create_domain('domain.tld') + self._create_account('admin1@domain.tld', role='DomainAdmins') + user = User.objects.get(username='admin1@domain.tld') + + # Give 2 mailboxes and 3 aliases to the admin -> should fail. + values = { + 'username': user.username, 'role': user.group, 'quota_act': True, + 'is_active': user.is_active, 'email': user.email, + 'mailboxes_limit': 2, 'mailbox_aliases_limit': 3 + } + resp = self.ajax_post( + reverse("admin:account_change", args=[user.id]), + values, 424 + ) + self.assertEqual(resp, 'Not enough resources') + self._check_limit('mailboxes', 1, 2) + self._check_limit('mailbox_aliases', 0, 2) From b3b407fc3a30fffde171d06291f59a5a08de0df9 Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Tue, 26 Apr 2016 19:38:02 +0200 Subject: [PATCH 11/15] Fix unit test. see #799 --- modoboa/admin/tests/test_import_.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/modoboa/admin/tests/test_import_.py b/modoboa/admin/tests/test_import_.py index b917959ad..8e0610f60 100644 --- a/modoboa/admin/tests/test_import_.py +++ b/modoboa/admin/tests/test_import_.py @@ -3,6 +3,7 @@ from django.core.files.base import ContentFile from django.core.urlresolvers import reverse from modoboa.core.models import User +from modoboa.lib import parameters from modoboa.lib.tests import ModoTestCase from .. import factories @@ -15,6 +16,7 @@ class ImportTestCase(ModoTestCase): def setUpTestData(cls): """Create test data.""" super(ImportTestCase, cls).setUpTestData() + parameters.save_admin("ENABLE_ADMIN_LIMITS", "no", app="limits") factories.populate_database() def test_domains_import(self): From 90e46a85a7bfdd8159d0772bb2f656a5ebb07cb2 Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Wed, 27 Apr 2016 09:49:16 +0200 Subject: [PATCH 12/15] Add unit tests. see #799 --- modoboa/limits/tests/test_import.py | 54 ++++++++++++++++++++++++++++- 1 file changed, 53 insertions(+), 1 deletion(-) diff --git a/modoboa/limits/tests/test_import.py b/modoboa/limits/tests/test_import.py index a36114d58..8ca078d00 100644 --- a/modoboa/limits/tests/test_import.py +++ b/modoboa/limits/tests/test_import.py @@ -3,6 +3,7 @@ from django.core.files.base import ContentFile from django.core.urlresolvers import reverse from modoboa.admin import factories as admin_factories +from modoboa.admin import models as admin_models from modoboa.core import factories as core_factories from modoboa.core import models as core_models from modoboa.lib import parameters @@ -75,6 +76,33 @@ domainalias; domalias2.com; test.com; True ) self.assertContains(response, "Domain aliases: limit reached") + def test_domain_admins_import(self): + """Check domain admins limit.""" + self.client.login( + username=self.reseller.username, password="toto") + domain = admin_models.Domain.objects.get(name="test.com") + domain.add_admin(self.reseller) + limit = self.reseller.userobjectlimit_set.get(name="domain_admins") + self.assertFalse(limit.is_exceeded()) + f = ContentFile(b"""account; admin1@test.com; toto; User; One; True; DomainAdmins; user1@test.com; 5; test.com +account; admin2@test.com; toto; René; Truc; True; DomainAdmins; truc@test.com; 5; test.com +""", name="domain_admins.csv") + self.client.post( + reverse("admin:identity_import"), { + "sourcefile": f + } + ) + self.assertTrue(limit.is_exceeded()) + + f = ContentFile(b"""account; admin3@test.com; toto; User; One; True; DomainAdmins; admin3@test.com; 5; test.com +""", name="domain_admins.csv") + response = self.client.post( + reverse("admin:identity_import"), { + "sourcefile": f + } + ) + self.assertContains(response, "Permission denied") + def test_mailboxes_import(self): """Check mailboxes limit.""" self.client.login( @@ -101,4 +129,28 @@ account; user3@test.com; toto; User; One; True; SimpleUsers; user3@test.com; 5 ) self.assertContains(response, "Mailboxes: limit reached") - # FIXME: aliases, domain admins + def test_mailbox_aliases_import(self): + """Check mailbox aliases limit.""" + self.client.login( + username=self.dadmin.username, password="toto") + limit = self.dadmin.userobjectlimit_set.get(name="mailbox_aliases") + self.assertFalse(limit.is_exceeded()) + f = ContentFile(b"""alias; alias1@test.com; True; user@test.com +alias; alias2@test.com; True; user@test.com +""", name="aliases.csv") + self.client.post( + reverse("admin:identity_import"), { + "sourcefile": f + } + ) + self.assertTrue(limit.is_exceeded()) + + f = ContentFile(""" +alias; alias3@test.com; True; user@test.com +""", name="aliases.csv") + response = self.client.post( + reverse("admin:identity_import"), { + "sourcefile": f + } + ) + self.assertContains(response, "Mailbox aliases: limit reached") From cd6b30b2edbb5280bd0f06fa30792e43e25c0213 Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Wed, 27 Apr 2016 10:19:20 +0200 Subject: [PATCH 13/15] Add unit tests. see #799 --- modoboa/admin/models/domain_alias.py | 8 +- modoboa/limits/tests/test_import.py | 243 +++++++++++++++++---------- 2 files changed, 157 insertions(+), 94 deletions(-) diff --git a/modoboa/admin/models/domain_alias.py b/modoboa/admin/models/domain_alias.py index 52bed916d..1acf2ff92 100644 --- a/modoboa/admin/models/domain_alias.py +++ b/modoboa/admin/models/domain_alias.py @@ -8,10 +8,12 @@ from django.contrib.contenttypes import generic import reversion +from modoboa.core import models as core_models +from modoboa.core import signals as core_signals +from modoboa.lib.exceptions import BadRequest, Conflict + from .base import AdminObject from .domain import Domain -from modoboa.core import models as core_models -from modoboa.lib.exceptions import BadRequest, Conflict class DomainAliasManager(models.Manager): @@ -76,6 +78,8 @@ class DomainAlias(AdminObject): self.target = Domain.objects.get(name=domname) except Domain.DoesNotExist: raise BadRequest(_("Unknown domain %s") % domname) + core_signals.can_create_object.send( + sender="import", context=self.target, object_type="domain_aliases") self.enabled = row[3].strip() in ["True", "1", "yes", "y"] self.save(creator=user) diff --git a/modoboa/limits/tests/test_import.py b/modoboa/limits/tests/test_import.py index 8ca078d00..d88d72483 100644 --- a/modoboa/limits/tests/test_import.py +++ b/modoboa/limits/tests/test_import.py @@ -12,19 +12,128 @@ from modoboa.lib.tests import ModoTestCase from .. import utils -class UserLimitImportTestCase(ModoTestCase): +class LimitImportTestCase(ModoTestCase): + """Base class to test limits.""" @classmethod def setUpTestData(cls): """Create test data.""" - super(UserLimitImportTestCase, cls).setUpTestData() - for name, tpl in utils.get_user_limit_templates(): - parameters.save_admin( - "DEFLT_USER_{}_LIMIT".format(name.upper()), 2) + super(LimitImportTestCase, cls).setUpTestData() admin_factories.populate_database() cls.reseller = core_factories.UserFactory( username="reseller", groups=("Resellers", )) cls.dadmin = core_models.User.objects.get(username="admin@test.com") + cls.domain = admin_models.Domain.objects.get(name="test.com") + + def _test_domain_alias_import(self, limit): + """Check domain aliases limit.""" + self.client.login( + username=self.reseller.username, password="toto") + self.assertFalse(limit.is_exceeded()) + f = ContentFile(b"""domainalias; domalias1.com; {domain}; True +domainalias; domalias2.com; {domain}; True +""".format(domain=self.domain), name="domains.csv") + self.client.post( + reverse("admin:domain_import"), { + "sourcefile": f + } + ) + self.assertTrue(limit.is_exceeded()) + + f = ContentFile( + "domainalias; domalias3.com; {}; True".format(self.domain), + name="domains.csv") + response = self.client.post( + reverse("admin:domain_import"), { + "sourcefile": f + } + ) + self.assertContains(response, "Domain aliases: limit reached") + + def _test_domain_admins_import(self, limit): + """Check domain admins limit.""" + self.client.login( + username=self.reseller.username, password="toto") + self.assertFalse(limit.is_exceeded()) + f = ContentFile(b"""account; admin1@{domain}; toto; User; One; True; DomainAdmins; user1@{domain}; 5; {domain} +account; admin2@{domain}; toto; René; Truc; True; DomainAdmins; truc@{domain}; 5; {domain} +""".format(domain=self.domain), name="domain_admins.csv") + self.client.post( + reverse("admin:identity_import"), { + "sourcefile": f + } + ) + self.assertTrue(limit.is_exceeded()) + + f = ContentFile(b"""account; admin3@{domain}; toto; User; One; True; DomainAdmins; admin3@{domain}; 5; {domain} +""".format(domain=self.domain), name="domain_admins.csv") + response = self.client.post( + reverse("admin:identity_import"), { + "sourcefile": f + } + ) + self.assertContains(response, "Permission denied") + + def _test_mailboxes_import(self, limit): + """Check mailboxes limit.""" + self.client.login( + username=self.dadmin.username, password="toto") + self.assertFalse(limit.is_exceeded()) + f = ContentFile(b"""account; user1@{domain}; toto; User; One; True; SimpleUsers; user1@{domain}; 5 +account; truc@{domain}; toto; René; Truc; True; SimpleUsers; truc@{domain}; 5 +""".format(domain=self.domain), name="domains.csv") + response = self.client.post( + reverse("admin:identity_import"), { + "sourcefile": f + } + ) + self.assertTrue(limit.is_exceeded()) + + f = ContentFile(""" +account; user3@{domain}; toto; User; One; True; SimpleUsers; user3@{domain}; 5 +""".format(domain=self.domain), name="domains.csv") + response = self.client.post( + reverse("admin:identity_import"), { + "sourcefile": f + } + ) + self.assertContains(response, "Mailboxes: limit reached") + + def _test_mailbox_aliases_import(self, limit): + """Check mailbox aliases limit.""" + self.client.login( + username=self.dadmin.username, password="toto") + self.assertFalse(limit.is_exceeded()) + f = ContentFile(b"""alias; alias1@{domain}; True; user@{domain} +alias; alias2@{domain}; True; user@{domain} +""".format(domain=self.domain), name="aliases.csv") + response = self.client.post( + reverse("admin:identity_import"), { + "sourcefile": f + } + ) + self.assertTrue(limit.is_exceeded()) + + f = ContentFile(""" +alias; alias3@{domain}; True; user@{domain} +""".format(domain=self.domain), name="aliases.csv") + response = self.client.post( + reverse("admin:identity_import"), { + "sourcefile": f + } + ) + self.assertContains(response, "Mailbox aliases: limit reached") + + +class UserLimitImportTestCase(LimitImportTestCase): + + @classmethod + def setUpTestData(cls): + """Create test data.""" + for name, tpl in utils.get_user_limit_templates(): + parameters.save_admin( + "DEFLT_USER_{}_LIMIT".format(name.upper()), 2) + super(UserLimitImportTestCase, cls).setUpTestData() def test_domains_import(self): """Check domains limit.""" @@ -53,104 +162,54 @@ domain; domain2.com; 200; False def test_domain_alias_import(self): """Check domain aliases limit.""" - self.client.login( - username=self.reseller.username, password="toto") limit = self.reseller.userobjectlimit_set.get(name="domain_aliases") - self.assertFalse(limit.is_exceeded()) - f = ContentFile(b"""domainalias; domalias1.com; test.com; True -domainalias; domalias2.com; test.com; True -""", name="domains.csv") - self.client.post( - reverse("admin:domain_import"), { - "sourcefile": f - } - ) - self.assertTrue(limit.is_exceeded()) - - f = ContentFile( - "domainalias; domalias3.com; test.com; True", name="domains.csv") - response = self.client.post( - reverse("admin:domain_import"), { - "sourcefile": f - } - ) - self.assertContains(response, "Domain aliases: limit reached") + self._test_domain_alias_import(limit) def test_domain_admins_import(self): """Check domain admins limit.""" - self.client.login( - username=self.reseller.username, password="toto") - domain = admin_models.Domain.objects.get(name="test.com") - domain.add_admin(self.reseller) + self.domain.add_admin(self.reseller) limit = self.reseller.userobjectlimit_set.get(name="domain_admins") - self.assertFalse(limit.is_exceeded()) - f = ContentFile(b"""account; admin1@test.com; toto; User; One; True; DomainAdmins; user1@test.com; 5; test.com -account; admin2@test.com; toto; René; Truc; True; DomainAdmins; truc@test.com; 5; test.com -""", name="domain_admins.csv") - self.client.post( - reverse("admin:identity_import"), { - "sourcefile": f - } - ) - self.assertTrue(limit.is_exceeded()) - - f = ContentFile(b"""account; admin3@test.com; toto; User; One; True; DomainAdmins; admin3@test.com; 5; test.com -""", name="domain_admins.csv") - response = self.client.post( - reverse("admin:identity_import"), { - "sourcefile": f - } - ) - self.assertContains(response, "Permission denied") + self._test_domain_admins_import(limit) def test_mailboxes_import(self): """Check mailboxes limit.""" - self.client.login( - username=self.dadmin.username, password="toto") limit = self.dadmin.userobjectlimit_set.get(name="mailboxes") - self.assertFalse(limit.is_exceeded()) - f = ContentFile(b"""account; user1@test.com; toto; User; One; True; SimpleUsers; user1@test.com; 5 -account; truc@test.com; toto; René; Truc; True; SimpleUsers; truc@test.com; 5 -""", name="domains.csv") - self.client.post( - reverse("admin:identity_import"), { - "sourcefile": f - } - ) - self.assertTrue(limit.is_exceeded()) - - f = ContentFile(""" -account; user3@test.com; toto; User; One; True; SimpleUsers; user3@test.com; 5 -""", name="domains.csv") - response = self.client.post( - reverse("admin:identity_import"), { - "sourcefile": f - } - ) - self.assertContains(response, "Mailboxes: limit reached") + self._test_mailboxes_import(limit) def test_mailbox_aliases_import(self): """Check mailbox aliases limit.""" - self.client.login( - username=self.dadmin.username, password="toto") limit = self.dadmin.userobjectlimit_set.get(name="mailbox_aliases") - self.assertFalse(limit.is_exceeded()) - f = ContentFile(b"""alias; alias1@test.com; True; user@test.com -alias; alias2@test.com; True; user@test.com -""", name="aliases.csv") - self.client.post( - reverse("admin:identity_import"), { - "sourcefile": f - } - ) - self.assertTrue(limit.is_exceeded()) + self._test_mailbox_aliases_import(limit) - f = ContentFile(""" -alias; alias3@test.com; True; user@test.com -""", name="aliases.csv") - response = self.client.post( - reverse("admin:identity_import"), { - "sourcefile": f - } - ) - self.assertContains(response, "Mailbox aliases: limit reached") + +class DomainLimitImportTestCase(LimitImportTestCase): + + @classmethod + def setUpTestData(cls): + """Create test data.""" + parameters.save_admin("ENABLE_DOMAIN_LIMITS", "yes") + parameters.save_admin("ENABLE_ADMIN_LIMITS", "no") + for name, tpl in utils.get_domain_limit_templates(): + parameters.save_admin( + "DEFLT_DOMAIN_{}_LIMIT".format(name.upper()), 2) + super(DomainLimitImportTestCase, cls).setUpTestData() + mb = admin_factories.MailboxFactory( + user__username="user@test4.com", + domain__name="test4.com", address="user") + cls.domain = mb.domain + cls.domain.add_admin(cls.dadmin) + + def test_domain_alias_import(self): + """Check domain aliases limit.""" + limit = self.domain.domainobjectlimit_set.get(name="domain_aliases") + self._test_domain_alias_import(limit) + + def test_mailboxes_import(self): + """Check mailboxes limit.""" + limit = self.domain.domainobjectlimit_set.get(name="mailboxes") + self._test_mailboxes_import(limit) + + def test_mailbox_aliases_import(self): + """Check mailbox aliases limit.""" + limit = self.domain.domainobjectlimit_set.get(name="mailbox_aliases") + self._test_mailbox_aliases_import(limit) From d3c26b0f48f908a7de0a7157521739d3ef3b1d9e Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Wed, 27 Apr 2016 10:20:47 +0200 Subject: [PATCH 14/15] Remove useless file. see #799 --- modoboa/core/serializers.py | 41 ---------------------- modoboa/limits/tests/test_domain_limits.py | 4 --- 2 files changed, 45 deletions(-) delete mode 100644 modoboa/core/serializers.py diff --git a/modoboa/core/serializers.py b/modoboa/core/serializers.py deleted file mode 100644 index e81354c30..000000000 --- a/modoboa/core/serializers.py +++ /dev/null @@ -1,41 +0,0 @@ -"""Modoboa core serializers.""" - -from django.core.exceptions import ValidationError - -from rest_framework import serializers -from passwords import validators - -from . import models - - -class UserPasswordSerializer(serializers.ModelSerializer): - - """A serializer used to change a user password.""" - - new_password = serializers.CharField() - - class Meta: - model = models.User - fields = ( - "password", "new_password", ) - - def validate_password(self, value): - """Check password.""" - if not self.instance.check_password(value): - raise serializers.ValidationError("Password not correct") - return value - - def validate_new_password(self, value): - """Check new password.""" - try: - validators.validate_length(value) - validators.complexity(value) - except ValidationError as exc: - raise serializers.ValidationError(exc.messages[0]) - return value - - def update(self, instance, validated_data): - """Set new password.""" - instance.set_password(validated_data["new_password"]) - instance.save() - return instance diff --git a/modoboa/limits/tests/test_domain_limits.py b/modoboa/limits/tests/test_domain_limits.py index be539ef25..4c18078af 100644 --- a/modoboa/limits/tests/test_domain_limits.py +++ b/modoboa/limits/tests/test_domain_limits.py @@ -50,7 +50,6 @@ class DomainLimitsTestCase(lib_tests.ModoTestCase): def test_domain_aliases_limit(self): """Try to exceed defined limit.""" - # Import domain = Domain.objects.get(name="test.com") limit = domain.domainobjectlimit_set.get(name="domain_aliases") self.assertFalse(limit.is_exceeded()) @@ -74,8 +73,6 @@ class DomainLimitsTestCase(lib_tests.ModoTestCase): def test_mailboxes_limit(self): """Try to exceed defined limits.""" - # Import - # LDAP ? domain = Domain.objects.get(name="test.com") domain.domainobjectlimit_set.filter(name="mailboxes").update( max_value=3) @@ -97,7 +94,6 @@ class DomainLimitsTestCase(lib_tests.ModoTestCase): def test_mailbox_aliases_limit(self): """Try to exceed defined limits.""" - # Import domain = Domain.objects.get(name="test.com") user = User.objects.get(username="user@test.com") limit = domain.domainobjectlimit_set.get(name="mailbox_aliases") From f1863e61a63d4957236713e9b519c5b8b426a233 Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Wed, 27 Apr 2016 11:32:48 +0200 Subject: [PATCH 15/15] Domain admins limit added. see #799 --- modoboa/admin/models/domain.py | 17 +++++++--- modoboa/limits/app_settings.py | 25 ++++++++------- modoboa/limits/constants.py | 4 +++ modoboa/limits/controls.py | 7 ++-- modoboa/limits/general_callbacks.py | 18 ++++++++--- modoboa/limits/tests/test_domain_limits.py | 37 +++++++++++++++++++--- modoboa/limits/tests/test_import.py | 18 +++++++---- 7 files changed, 94 insertions(+), 32 deletions(-) diff --git a/modoboa/admin/models/domain.py b/modoboa/admin/models/domain.py index cd4d425d8..bf5197adf 100644 --- a/modoboa/admin/models/domain.py +++ b/modoboa/admin/models/domain.py @@ -8,11 +8,13 @@ from django.utils.translation import ugettext as _, ugettext_lazy import reversion -from .base import AdminObject from modoboa.core.models import User, ObjectAccess +from modoboa.core import signals as core_signals from modoboa.lib import events, parameters from modoboa.lib.exceptions import BadRequest, Conflict +from .base import AdminObject + class DomainManager(Manager): @@ -85,23 +87,28 @@ class Domain(AdminObject): @property def admins(self): - """Return the domain administrators of this domain + """Return the domain administrators of this domain. :return: a list of User objects """ - return [oa.user for oa in self.owners.filter(user__is_superuser=False)] + return User.objects.filter( + is_superuser=False, + objectaccess__content_type__model="domain", + objectaccess__object_id=self.pk) @property def aliases(self): return self.domainalias_set def add_admin(self, account): - """Add a new administrator for this domain + """Add a new administrator to this domain. - :param User account: the administrotor to add + :param User account: the administrator """ from modoboa.lib.permissions import grant_access_to_object + core_signals.can_create_object.send( + sender=self.__class__, context=self, object_type="domain_admins") grant_access_to_object(account, self) for mb in self.mailbox_set.all(): if mb.user.has_perm("admin.add_domain"): diff --git a/modoboa/limits/app_settings.py b/modoboa/limits/app_settings.py index 3e5ba0dfa..ff729a668 100644 --- a/modoboa/limits/app_settings.py +++ b/modoboa/limits/app_settings.py @@ -8,6 +8,8 @@ from modoboa.lib.parameters import AdminParametersForm class ParametersForm(AdminParametersForm): + """Available admin parameters.""" + app = "limits" defv_sep = SeparatorField(label=_("Default per-admin limits")) @@ -79,17 +81,17 @@ class ParametersForm(AdminParametersForm): help_text=_("Enable or disable per-domain limits") ) - # deflt_domain_domain_admins_limit = forms.IntegerField( - # label=_("Domain admins"), - # initial=0, - # help_text=_( - # "Maximum number of allowed domain administrators for a new " - # "domain. (0 to deny any creation, -1 to allow unlimited " - # "creations)" - # ), - # widget=forms.widgets.TextInput( - # attrs={"class": "col-md-1 form-control"}) - # ) + deflt_domain_domain_admins_limit = forms.IntegerField( + label=_("Domain admins"), + initial=0, + help_text=_( + "Maximum number of allowed domain administrators for a new " + "domain. (0 to deny any creation, -1 to allow unlimited " + "creations)" + ), + widget=forms.widgets.TextInput( + attrs={"class": "col-md-1 form-control"}) + ) deflt_domain_domain_aliases_limit = forms.IntegerField( label=_("Domain aliases"), initial=0, @@ -131,6 +133,7 @@ class ParametersForm(AdminParametersForm): "deflt_domain_mailboxes_limit": "enable_domain_limits=yes", "deflt_domain_mailbox_aliases_limit": "enable_domain_limits=yes", "deflt_domain_domain_aliases_limit": "enable_domain_limits=yes", + "deflt_domain_domain_admins_limit": "enable_domain_limits=yes", } def __init__(self, *args, **kwargs): diff --git a/modoboa/limits/constants.py b/modoboa/limits/constants.py index 4150911df..7bdb3f38d 100644 --- a/modoboa/limits/constants.py +++ b/modoboa/limits/constants.py @@ -43,5 +43,9 @@ DEFAULT_DOMAIN_LIMITS = collections.OrderedDict(( "help": _( "Maximum number of mailbox aliases allowed for this domain."), "extra_filters": {"internal": False} + }), + ("domain_admins", { + "relation": "admins", "label": _("Domain admins"), + "help": _("Maximum number of domain admins allowed for this domain."), }) )) diff --git a/modoboa/limits/controls.py b/modoboa/limits/controls.py index bec50673e..fa914072f 100644 --- a/modoboa/limits/controls.py +++ b/modoboa/limits/controls.py @@ -5,7 +5,7 @@ """ -from modoboa.lib import events +from modoboa.lib import events, parameters from modoboa.lib.permissions import get_object_owner from . import utils @@ -39,7 +39,10 @@ def user_can_set_role(user, role, account=None): :param ``User`` account: account modified (None on creation) :param str newrole: role to check """ - if role != "DomainAdmins": + condition = ( + parameters.get_admin("ENABLE_ADMIN_LIMITS") == "no" or + role != "DomainAdmins") + if condition: return [True] lname = "domain_admins" condition = ( diff --git a/modoboa/limits/general_callbacks.py b/modoboa/limits/general_callbacks.py index 068b9abd9..3d7ab6780 100644 --- a/modoboa/limits/general_callbacks.py +++ b/modoboa/limits/general_callbacks.py @@ -12,7 +12,10 @@ from . import utils def display_pool_usage(user, target, currentpage): from django.template.loader import render_to_string - if target != "leftcol" or user.is_superuser: + condition = ( + parameters.get_admin("ENABLE_ADMIN_LIMITS") == "no" or + target != "leftcol" or user.is_superuser) + if condition: return [] if currentpage == "identities": names = ["mailboxes", "mailbox_aliases"] @@ -38,6 +41,8 @@ def display_pool_usage(user, target, currentpage): @events.observe("ExtraAccountForm") def extra_account_form(user, account=None): + if parameters.get_admin("ENABLE_ADMIN_LIMITS") == "no": + return [] if user.group not in ["SuperAdmins", "Resellers"]: return [] if account is not None and \ @@ -85,16 +90,21 @@ def check_form_access(account, form): @events.observe("FillAccountInstances") def fill_account_instances(user, account, instances): - if not user.is_superuser and not user.belongs_to_group("Resellers"): + condition = ( + parameters.get_admin("ENABLE_ADMIN_LIMITS") == "no" or + (not user.is_superuser and user.group != "Resellers") + ) + if condition: return - if not account.belongs_to_group("Resellers") and \ - not account.belongs_to_group("DomainAdmins"): + if account.group not in ["Resellers", "DomainAdmins"]: return instances["resources"] = account @events.observe("GetStaticContent") def get_static_content(caller, st_type, user): + if parameters.get_admin("ENABLE_ADMIN_LIMITS") == "no": + return [] if caller not in ['domains', 'identities']: return [] if user.group == "SimpleUsers": diff --git a/modoboa/limits/tests/test_domain_limits.py b/modoboa/limits/tests/test_domain_limits.py index 4c18078af..12be480f2 100644 --- a/modoboa/limits/tests/test_domain_limits.py +++ b/modoboa/limits/tests/test_domain_limits.py @@ -3,8 +3,9 @@ from django.core.urlresolvers import reverse -from modoboa.admin.factories import populate_database +from modoboa.admin import factories as admin_factories from modoboa.admin.models import Domain +from modoboa.core import factories as core_factories from modoboa.core.models import User from modoboa.lib import parameters from modoboa.lib import tests as lib_tests @@ -19,11 +20,12 @@ class DomainLimitsTestCase(lib_tests.ModoTestCase): def setUpTestData(cls): """Create test data.""" super(DomainLimitsTestCase, cls).setUpTestData() + parameters.save_admin("ENABLE_ADMIN_LIMITS", "no") parameters.save_admin("ENABLE_DOMAIN_LIMITS", "yes") for name, tpl in utils.get_domain_limit_templates(): parameters.save_admin( "DEFLT_DOMAIN_{}_LIMIT".format(name.upper()), 2) - populate_database() + admin_factories.populate_database() def test_set_limits(self): """Try to set limits for a given domain.""" @@ -32,7 +34,7 @@ class DomainLimitsTestCase(lib_tests.ModoTestCase): "name": domain.name, "quota": domain.quota, "enabled": domain.enabled, "type": "domain", "mailboxes_limit": 3, "mailbox_aliases_limit": 3, - "domain_aliases_limit": 3 + "domain_aliases_limit": 3, "domain_admins_limit": 3 } self.ajax_post( reverse("admin:domain_change", args=[domain.id]), @@ -47,6 +49,9 @@ class DomainLimitsTestCase(lib_tests.ModoTestCase): self.assertEqual( domain.domainobjectlimit_set.get( name="domain_aliases").max_value, 3) + self.assertEqual( + domain.domainobjectlimit_set.get( + name="domain_admins").max_value, 3) def test_domain_aliases_limit(self): """Try to exceed defined limit.""" @@ -57,7 +62,7 @@ class DomainLimitsTestCase(lib_tests.ModoTestCase): "name": domain.name, "quota": domain.quota, "enabled": domain.enabled, "type": "domain", "mailboxes_limit": 2, "mailbox_aliases_limit": 2, - "domain_aliases_limit": 2, + "domain_aliases_limit": 2, "domain_admins_limit": 2, "aliases": "alias1.com", "aliases_1": "alias2.com" } self.ajax_post( @@ -71,6 +76,30 @@ class DomainLimitsTestCase(lib_tests.ModoTestCase): values, 403 ) + def test_domain_admins_limit(self): + """Try to exceed defined limit.""" + domain = Domain.objects.get(name="test.com") + limit = domain.domainobjectlimit_set.get(name="domain_admins") + self.assertFalse(limit.is_exceeded()) + user = User.objects.get(username="admin@test2.com") + values = { + "username": user.username, "role": user.group, + "is_active": user.is_active, "email": user.email, + "quota_act": True, "domains": "test2.com", + "domains_1": "test.com" + } + self.ajax_post( + reverse("admin:account_change", args=[user.id]), + values + ) + self.assertTrue(limit.is_exceeded()) + user = core_factories.UserFactory( + username="admin1000@test.com", groups=("DomainAdmins", )) + self.ajax_post( + reverse("admin:account_change", args=[user.id]), + values, 400 + ) + def test_mailboxes_limit(self): """Try to exceed defined limits.""" domain = Domain.objects.get(name="test.com") diff --git a/modoboa/limits/tests/test_import.py b/modoboa/limits/tests/test_import.py index d88d72483..e58139a1b 100644 --- a/modoboa/limits/tests/test_import.py +++ b/modoboa/limits/tests/test_import.py @@ -50,15 +50,20 @@ domainalias; domalias2.com; {domain}; True ) self.assertContains(response, "Domain aliases: limit reached") - def _test_domain_admins_import(self, limit): + def _test_domain_admins_import(self, limit, initial_count=0): """Check domain admins limit.""" self.client.login( username=self.reseller.username, password="toto") self.assertFalse(limit.is_exceeded()) - f = ContentFile(b"""account; admin1@{domain}; toto; User; One; True; DomainAdmins; user1@{domain}; 5; {domain} -account; admin2@{domain}; toto; René; Truc; True; DomainAdmins; truc@{domain}; 5; {domain} -""".format(domain=self.domain), name="domain_admins.csv") - self.client.post( + content = "" + for cpt in xrange(initial_count, 2): + content += ( + "account; admin{cpt}@{domain}; toto; User; One; True; " + "DomainAdmins; user{cpt}@{domain}; 5; {domain}\n" + .format(domain=self.domain, cpt=cpt) + ) + f = ContentFile(content, name="domain_admins.csv") + response = self.client.post( reverse("admin:identity_import"), { "sourcefile": f } @@ -197,7 +202,6 @@ class DomainLimitImportTestCase(LimitImportTestCase): user__username="user@test4.com", domain__name="test4.com", address="user") cls.domain = mb.domain - cls.domain.add_admin(cls.dadmin) def test_domain_alias_import(self): """Check domain aliases limit.""" @@ -206,10 +210,12 @@ class DomainLimitImportTestCase(LimitImportTestCase): def test_mailboxes_import(self): """Check mailboxes limit.""" + self.domain.add_admin(self.dadmin) limit = self.domain.domainobjectlimit_set.get(name="mailboxes") self._test_mailboxes_import(limit) def test_mailbox_aliases_import(self): """Check mailbox aliases limit.""" + self.domain.add_admin(self.dadmin) limit = self.domain.domainobjectlimit_set.get(name="mailbox_aliases") self._test_mailbox_aliases_import(limit)