From a11e69074ddf950d0ae3c9a130ed59654b4324b5 Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Thu, 31 Oct 2013 20:35:59 +0100 Subject: [PATCH] wip: relay domains. Fixed various issues about: * Permissions * Limits Unit tests were also added. see #491 --- modoboa/core/__init__.py | 9 +- modoboa/core/extensions.py | 29 ++- modoboa/extensions/admin/__init__.py | 6 + modoboa/extensions/admin/forms/account.py | 16 +- modoboa/extensions/admin/models/domain.py | 29 ++- modoboa/extensions/admin/models/mailbox.py | 2 +- .../extensions/admin/static/admin/js/admin.js | 7 +- modoboa/extensions/limits/__init__.py | 3 +- modoboa/extensions/limits/controls.py | 45 +++- modoboa/extensions/limits/tests.py | 52 ++++- .../postfix_relay_domains/__init__.py | 1 + .../postfix_relay_domains/factories.py | 23 ++ .../extensions/postfix_relay_domains/tests.py | 196 +++++++++++++++++- modoboa/lib/permissions.py | 41 +++- modoboa/lib/tests.py | 8 +- modoboa/static/js/creation_wizard.js | 5 +- modoboa/static/js/global.js | 12 +- modoboa/urls.py | 4 +- 18 files changed, 400 insertions(+), 88 deletions(-) create mode 100644 modoboa/extensions/postfix_relay_domains/factories.py diff --git a/modoboa/core/__init__.py b/modoboa/core/__init__.py index 8e81e7145..751e84e47 100644 --- a/modoboa/core/__init__.py +++ b/modoboa/core/__init__.py @@ -39,8 +39,13 @@ base_events = [ ] -def load_settings(): - from .app_settings import GeneralParametersForm, UserSettings +def load_core_settings(): + """Load core settings. + + This function must be manually called (see :file:`urls.py`) in + order to load base settings. + """ + from modoboa.core.app_settings import GeneralParametersForm, UserSettings parameters.register(GeneralParametersForm, ugettext_lazy("General")) parameters.register(UserSettings, ugettext_lazy("General")) diff --git a/modoboa/core/extensions.py b/modoboa/core/extensions.py index ecd65de4a..b1f81e9d8 100644 --- a/modoboa/core/extensions.py +++ b/modoboa/core/extensions.py @@ -37,7 +37,7 @@ class ExtensionsPool(object): """The extensions manager""" def __init__(self): - self.extensions = dict() + self.extensions = {} def register_extension(self, ext, show=True): self.extensions[ext.name] = dict(cls=ext, show=show) @@ -63,6 +63,16 @@ class ExtensionsPool(object): return instance.infos() def load_all(self): + """Load all enabled extensions. + + Each extension must be loaded in order to integrate with + Modoboa. Only enabled and special extensions are loaded but + urls are always returned. The reason is urls are imported only + once so must know all of them when the python process + starts. Otherwise, it would lead to unexpected 404 errors :p + + :return: a list of url maps + """ from modoboa.core.models import Extension for ext in settings.MODOBOA_APPS: @@ -70,6 +80,14 @@ class ExtensionsPool(object): result = [] for extname in self.extensions.keys(): extinstance = self.get_extension(extname) + try: + baseurl = extinstance.url \ + if extinstance.url is not None else extname + result += [(r'^%s/' % (baseurl), + include("%s.urls" % extinstance.__module__))] + except ImportError: + # No urls for this extension + pass if not extinstance.always_active: try: ext = Extension.objects.get(name=extname) @@ -78,16 +96,11 @@ class ExtensionsPool(object): except Extension.DoesNotExist: continue extinstance.load() - try: - baseurl = extinstance.url if extinstance.url is not None else extname - result += [(r'^%s/' % (baseurl), - include("%s.urls" % extinstance.__module__))] - except ImportError: - # No urls for this extension - pass return result def list_all(self): + """List all defined extensions. + """ result = [] for extname, extdef in self.extensions.iteritems(): if not extdef["show"]: diff --git a/modoboa/extensions/admin/__init__.py b/modoboa/extensions/admin/__init__.py index 281fac420..0589b630b 100644 --- a/modoboa/extensions/admin/__init__.py +++ b/modoboa/extensions/admin/__init__.py @@ -13,6 +13,7 @@ admin_events = [ "DomainCreated", "DomainModified", "DomainDeleted", + "DomainOwnershipRemoved", "ExtraDomainEntries", "ExtraDomainMenuEntries", "ExtraDomainFilters", @@ -201,6 +202,11 @@ def user_logged_in(request, username, password): @events.observe("AccountDeleted") def account_deleted(account, byuser, **kwargs): + """'AccountDeleted' listener. + + When an account is deleted, we also need to remove its mailbox (if + any). + """ if not account.mailbox_set.count(): return mb = account.mailbox_set.all()[0] diff --git a/modoboa/extensions/admin/forms/account.py b/modoboa/extensions/admin/forms/account.py index c8afb5fae..a44ba884c 100644 --- a/modoboa/extensions/admin/forms/account.py +++ b/modoboa/extensions/admin/forms/account.py @@ -46,20 +46,20 @@ class AccountFormGeneral(forms.ModelForm): widget=forms.HiddenInput, required=False ) else: - self.fields["role"].choices = \ - [('', ugettext_lazy("Choose"))] + get_account_roles(user) + self.fields["role"].choices = [('', ugettext_lazy("Choose"))] + self.fields["role"].choices += \ + get_account_roles(user, kwargs['instance']) \ + if 'instance' in kwargs else get_account_roles(user) if "instance" in kwargs: - if len(args) \ + if args \ and (args[0].get("password1", "") == "" and args[0].get("password2", "") == ""): self.fields["password1"].required = False self.fields["password2"].required = False - - u = kwargs["instance"] - self.fields["role"].initial = u.group - - if not u.is_local \ + account = kwargs["instance"] + self.fields["role"].initial = account.group + if not account.is_local \ and parameters.get_admin("LDAP_AUTH_METHOD") == "directbind": del self.fields["password1"] del self.fields["password2"] diff --git a/modoboa/extensions/admin/models/domain.py b/modoboa/extensions/admin/models/domain.py index b0a2c474e..70166b9b4 100644 --- a/modoboa/extensions/admin/models/domain.py +++ b/modoboa/extensions/admin/models/domain.py @@ -23,6 +23,8 @@ class DomainManager(Manager): class Domain(AdminObject): + """Mail domain. + """ name = models.CharField(ugettext_lazy('name'), max_length=100, unique=True, help_text=ugettext_lazy("The domain name")) quota = models.IntegerField( @@ -84,11 +86,15 @@ class Domain(AdminObject): grant_access_to_object(account, al) def remove_admin(self, account): - """Remove an administrator for this domain + """Remove an administrator of this domain. - :param User account: the administrotor to remove + :param User account: administrator to remove """ - from modoboa.lib.permissions import ungrant_access_to_object + from modoboa.lib.permissions import \ + ungrant_access_to_object, get_object_owner + + if get_object_owner(self) == account: + events.raiseEvent('DomainOwnershipRemoved', account, self) ungrant_access_to_object(self, account) for mb in self.mailbox_set.all(): if mb.user.has_perm("admin.add_domain"): @@ -96,26 +102,27 @@ class Domain(AdminObject): ungrant_access_to_object(mb, account) ungrant_access_to_object(mb.user, account) for al in self.alias_set.all(): - ungrant_access_to_object(al, account) + ungrant_access_to_object(al, account) def delete(self, fromuser, keepdir=False): - from modoboa.lib.permissions import \ - ungrant_access_to_object, ungrant_access_to_objects + """Custom delete method. + """ + from modoboa.lib.permissions import ungrant_access_to_objects from .mailbox import Quota if self.domainalias_set.count(): events.raiseEvent("DomainAliasDeleted", self.domainalias_set.all()) ungrant_access_to_objects(self.domainalias_set.all()) - if self.mailbox_set.count(): - Quota.objects.filter(username__contains='@%s' % self.name).delete() - events.raiseEvent("DeleteMailbox", self.mailbox_set.all()) - ungrant_access_to_objects(self.mailbox_set.all()) if self.alias_set.count(): - events.raiseEvent("MailboxAliasDelete", self.alias_set.all()) + events.raiseEvent("MailboxAliasDeleted", self.alias_set.all()) ungrant_access_to_objects(self.alias_set.all()) if parameters.get_admin("AUTO_ACCOUNT_REMOVAL") == "yes": for account in User.objects.filter(mailbox__domain__name=self.name): account.delete(fromuser, keepdir) + elif self.mailbox_set.count(): + Quota.objects.filter(username__contains='@%s' % self.name).delete() + events.raiseEvent("MailboxDeleted", self.mailbox_set.all()) + ungrant_access_to_objects(self.mailbox_set.all()) super(Domain, self).delete() def __str__(self): diff --git a/modoboa/extensions/admin/models/mailbox.py b/modoboa/extensions/admin/models/mailbox.py index a16e478a0..ed61adcbe 100644 --- a/modoboa/extensions/admin/models/mailbox.py +++ b/modoboa/extensions/admin/models/mailbox.py @@ -238,7 +238,7 @@ def mailbox_deleted_handler(sender, **kwargs): from modoboa.lib.permissions import ungrant_access_to_object mb = kwargs['instance'] - events.raiseEvent("DeleteMailbox", mb) + events.raiseEvent("MailboxDeleted", mb) ungrant_access_to_object(mb) for alias in mb.alias_set.all(): alias.mboxes.remove(mb) diff --git a/modoboa/extensions/admin/static/admin/js/admin.js b/modoboa/extensions/admin/static/admin/js/admin.js index 57f27f9be..d0a5ef387 100644 --- a/modoboa/extensions/admin/static/admin/js/admin.js +++ b/modoboa/extensions/admin/static/admin/js/admin.js @@ -213,11 +213,11 @@ Domains.prototype = { domadminsform_init: function() { $("a[name=removeperm]").click(function(e) { - var $tr = $(this).parent().parent(); + var $tr = $(this).parents('tr'); simple_ajax_request.apply(this, [e, { ok_cb: function(resp) { $tr.remove(); - if (!$("#domadmins").children("tr").length) { + if (!$("#domadmins").find("tr").length) { $("#admins").html('
' + gettext("No domain administrator defined") + "
"); } @@ -235,7 +235,6 @@ Domains.prototype = { 1: this.optionsform_prefill }, error_callbacks: { - //1: this.generalform_init, 2: $.proxy(this.optionsform_init, this) }, success_callback: $.proxy(this.reload_listing, this) @@ -254,7 +253,7 @@ Domains.prototype = { }); }, this)); $(document).trigger('domform_init'); - }, + } }; Domains.prototype = $.extend({}, Admin.prototype, Domains.prototype); diff --git a/modoboa/extensions/limits/__init__.py b/modoboa/extensions/limits/__init__.py index 7f49c549f..61a6961fe 100644 --- a/modoboa/extensions/limits/__init__.py +++ b/modoboa/extensions/limits/__init__.py @@ -12,7 +12,6 @@ from modoboa.lib import events, parameters from modoboa.core.extensions import ModoExtension, exts_pool from .models import LimitTemplates from .forms import ResourcePoolForm -import controls levents = [ 'GetExtraLimitTemplates' @@ -53,6 +52,8 @@ class Limits(ModoExtension): def load(self): from modoboa.extensions.limits.app_settings import ParametersForm + from modoboa.extensions.limits import controls + parameters.register(ParametersForm, ugettext_lazy("Limits")) events.declare(levents) diff --git a/modoboa/extensions/limits/controls.py b/modoboa/extensions/limits/controls.py index 7ddd2f212..34ff09159 100644 --- a/modoboa/extensions/limits/controls.py +++ b/modoboa/extensions/limits/controls.py @@ -56,8 +56,9 @@ def inc_nb_domains(user, domain): @events.observe('DomainDeleted') -def dec_nb_domains(domain): - owner = get_object_owner(domain) +def dec_nb_domains(domain, owner=None): + if owner is None: + owner = get_object_owner(domain) dec_limit_usage(owner, 'domains_limit') for domalias in domain.domainalias_set.all(): dec_nb_domaliases(domalias) @@ -105,9 +106,14 @@ def inc_nb_mbaliases(user, mailboxalias): @events.observe('MailboxAliasDeleted') -def dec_nb_mbaliases(mailboxalias): - owner = get_object_owner(mailboxalias) - dec_limit_usage(owner, 'mailbox_aliases_limit') +def dec_nb_mbaliases(mailboxaliases): + from modoboa.extensions.admin.models import Alias + + if isinstance(mailboxaliases, Alias): + mailboxaliases = [mailboxaliases] + for alias in mailboxaliases: + owner = get_object_owner(alias) + dec_limit_usage(owner, 'mailbox_aliases_limit') @events.observe('CanCreate') @@ -118,21 +124,20 @@ def can_create_new_object(user, objtype, count=1): @events.observe("AccountCreated") def create_pool(user): owner = get_object_owner(user) - if not owner.is_superuser and \ - not owner.belongs_to_group("Resellers"): + if not owner.group in ['SuperAdmins', 'Resellers']: return - if user.belongs_to_group("DomainAdmins"): + if user.group == 'DomainAdmins': check_limit(owner, 'domain_admins_limit') inc_limit_usage(owner, 'domain_admins_limit') - if user.group in ["DomainAdmins", "Resellers"]: + if user.group in ['DomainAdmins', 'Resellers']: p, created = LimitsPool.objects.get_or_create(user=user) p.create_limits(owner) @events.observe("UserCanSetRole") -def user_can_set_role(user, role): +def user_can_set_role(user, role, account=None): """Check if the user can still set this role. The only interesting case concerns resellers defining new domain @@ -140,6 +145,7 @@ def user_can_set_role(user, role): operation before any modification is made to :keyword:`account`. :param ``User`` user: connected user + :param ``User`` account: account modified (None on creation) :param str newrole: role to check """ if role == 'DomainAdmins': @@ -148,6 +154,8 @@ def user_can_set_role(user, role): return [True] if user.is_superuser or not user.limitspool.will_be_reached(lname): return [True] + if account is not None and account.group == role: + return [True] return [False] @@ -192,3 +200,20 @@ def on_account_deleted(account, byuser, **kwargs): if account.group == "DomainAdmins": dec_limit_usage(owner, 'domain_admins_limit') + + +@events.observe('DomainOwnershipRemoved') +def domain_ownership_removed(reseller, domain): + """DomainOwnershipRemoved listener. + + The access :keyword:`reseller` had to :keyword:`domain` has been + removed by a super adminstrator. We must decrement all limit + usages. + + :param ``User`` reseller: reseller that created :keyword:`domain` + :param ``Domain`` domain: domain + """ + dec_nb_domains(domain, reseller) + for dadmin in domain.admins: + if reseller.is_owner(dadmin): + dec_limit_usage(reseller, 'domain_admins_limit') diff --git a/modoboa/extensions/limits/tests.py b/modoboa/extensions/limits/tests.py index 0bae19785..ccef3e3b6 100644 --- a/modoboa/extensions/limits/tests.py +++ b/modoboa/extensions/limits/tests.py @@ -12,16 +12,11 @@ from modoboa.extensions.limits.models import LimitTemplates class PermissionsTestCase(ExtTestCase): fixtures = ["initial_users.json"] - name = "limits" + names = ["limits"] def setUp(self): super(PermissionsTestCase, self).setUp() - dom = DomainFactory(name='test.com') - self.admin = UserFactory( - username='admin@test.com', groups=('DomainAdmins',) - ) - MailboxFactory(address='admin', domain=dom, user=self.admin) - dom.add_admin(self.admin) + populate_database() def test_domainadmin_deletes_reseller(self): """Check if a domain admin can delete a reseller. @@ -46,13 +41,21 @@ class PermissionsTestCase(ExtTestCase): class ResourceTestCase(ExtTestCase): fixtures = ["initial_users.json"] - name = "limits" + names = ["limits"] def setUp(self): + """Custom setUp method. + + The 'limits' is manually loaded to ensure extra parameters + provided by 'postfix_relay_domains' are properly received. + """ + from modoboa.core.extensions import exts_pool + super(ResourceTestCase, self).setUp() - populate_database() + exts_pool.get_extension('limits').load() for tpl in LimitTemplates().templates: parameters.save_admin('DEFLT_%s' % tpl[0].upper(), 2, app='limits') + populate_database() def _create_account(self, username, role='SimpleUsers', status=200): values = dict( @@ -255,6 +258,37 @@ class ResellerTestCase(ResourceTestCase): 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("modoboa.extensions.admin.views.domain.deldomain", + 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.clt.logout() + self.clt.login(username='admin', password='password') + self.ajax_get( + reverse( + 'modoboa.extensions.admin.views.identity.remove_permission' + ) + '?domid=%d&daid=%d' % (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') diff --git a/modoboa/extensions/postfix_relay_domains/__init__.py b/modoboa/extensions/postfix_relay_domains/__init__.py index 647615e24..da9bf2ea9 100644 --- a/modoboa/extensions/postfix_relay_domains/__init__.py +++ b/modoboa/extensions/postfix_relay_domains/__init__.py @@ -23,6 +23,7 @@ class PostfixRelayDomains(ModoExtension): label = "Postfix relay domains" version = "1.0" description = ugettext_lazy("Relay domains support for Postfix") + depends_on = ['limits'] def init(self): """Initialisation method. diff --git a/modoboa/extensions/postfix_relay_domains/factories.py b/modoboa/extensions/postfix_relay_domains/factories.py new file mode 100644 index 000000000..c8bc6e1c5 --- /dev/null +++ b/modoboa/extensions/postfix_relay_domains/factories.py @@ -0,0 +1,23 @@ +import factory +from modoboa.core.factories import PermissionFactory +from . import models + + +class ServiceFactory(factory.DjangoModelFactory): + FACTORY_FOR = models.Service + FACTORY_DJANGO_GET_OR_CREATE = ('name', ) + + name = 'dummy' + + +class RelayDomainFactory(PermissionFactory): + FACTORY_FOR = models.RelayDomain + + target_host = 'external.host.tld' + service = factory.SubFactory(ServiceFactory) + + +class RelayDomainAliasFactory(PermissionFactory): + FACTORY_FOR = models.RelayDomainAlias + + enabled = True diff --git a/modoboa/extensions/postfix_relay_domains/tests.py b/modoboa/extensions/postfix_relay_domains/tests.py index dab97161c..1361a757a 100644 --- a/modoboa/extensions/postfix_relay_domains/tests.py +++ b/modoboa/extensions/postfix_relay_domains/tests.py @@ -1,13 +1,201 @@ +from django.core.urlresolvers import reverse +from modoboa.core.factories import UserFactory from modoboa.lib.tests import ExtTestCase from modoboa.extensions.admin import factories +from modoboa.extensions.limits.tests import ResourceTestCase +from .models import RelayDomain, RelayDomainAlias, Service +from .factories import RelayDomainFactory, RelayDomainAliasFactory -class RelayDomainsTestCase(ExtTestCase): - fixtures = ["initial_users.json"] - name = "postfix_relay_domains" +class Operations(object): + + def _create_relay_domain(self, name, status=200): + srv, created = Service.objects.get_or_create(name='dummy') + values = { + 'name': name, 'target_host': 'external.host.tld', + 'service': srv.id, 'enabled': True + } + return self.ajax_post( + reverse("modoboa.extensions.postfix_relay_domains.views.create"), + values, status + ) + + def _relay_domain_alias_operation(self, optype, rdomain, name, status=200): + rdom = RelayDomain.objects.get(name=rdomain) + values = { + 'name': rdom.name, 'target_host': rdom.target_host, + 'service': rdom.service.id + } + aliases = [alias.name for alias in rdom.relaydomainalias_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 + return self.ajax_post( + reverse("modoboa.extensions.postfix_relay_domains.views.edit", + args=[rdom.id]), + values, status + ) + + +class RelayDomainsTestCase(ExtTestCase, Operations): + fixtures = ['initial_users.json'] + names = ['postfix_relay_domains'] def setUp(self): super(RelayDomainsTestCase, self).setUp() factories.populate_database() + self.rdom = RelayDomainFactory(name='relaydomain.tld') + RelayDomainAliasFactory(name='relaydomainalias.tld', target=self.rdom) - + def test_create_relaydomain(self): + """Test the creation of a relay domain. + + We also check that unique constraints are respected: domain, + relay domain alias. + + FIXME: add a check for domain alias. + """ + self._create_relay_domain('relaydomain1.tld') + rdom = RelayDomain.objects.get(name='relaydomain1.tld') + self.assertEqual(rdom.target_host, 'external.host.tld') + self.assertEqual(rdom.service.name, 'dummy') + self.assertEqual(rdom.enabled, True) + self.assertEqual(rdom.verify_recipients, False) + + resp = self._create_relay_domain('test.com', 400) + self.assertEqual(resp['form_errors']['name'][0], + 'A domain with this name already exists') + resp = self._create_relay_domain('relaydomainalias.tld', 400) + self.assertEqual( + resp['form_errors']['name'][0], + 'A relay domain alias with this name already exists' + ) + + def test_create_relaydomainalias(self): + """Test the creation of a relay domain alias. + + We also check that unique constraints are respected: domain, + relay domain. + + FIXME: add a check for domain alias. + """ + self._relay_domain_alias_operation( + 'add', self.rdom.name, 'relaydomainalias1.tld' + ) + resp = self._relay_domain_alias_operation( + 'add', self.rdom.name, 'test.com', 400 + ) + self.assertEqual( + resp['form_errors']['aliases_2'][0], + 'A domain with this name already exists' + ) + resp = self._relay_domain_alias_operation( + 'add', self.rdom.name, self.rdom.name, 400 + ) + self.assertEqual( + resp['form_errors']['aliases_2'][0], + 'A relay domain with this name already exists' + ) + + def test_edit_relaydomain(self): + """Test the modification of a relay domain. + + Rename 'relaydomain.tld' domain to 'relaydomain.org' + """ + values = { + 'name': 'relaydomain.org', 'target_host': self.rdom.target_host, + 'service': self.rdom.service.id + } + self.ajax_post( + reverse('modoboa.extensions.postfix_relay_domains.views.edit', + args=[self.rdom.id]), + values + ) + RelayDomain.objects.get(name='relaydomain.org') + + def test_edit_relaydomainalias(self): + """Test the modification of a relay domain alias. + + Rename 'relaydomainalias.tld' domain to 'relaydomainalias.net' + """ + values = { + 'name': 'relaydomain.org', 'target_host': self.rdom.target_host, + 'service': self.rdom.service.id, 'aliases': 'relaydomainalias.net' + } + self.ajax_post( + reverse('modoboa.extensions.postfix_relay_domains.views.edit', + args=[self.rdom.id]), + values + ) + RelayDomainAlias.objects.get(name='relaydomainalias.net') + with self.assertRaises(RelayDomainAlias.DoesNotExist): + RelayDomainAlias.objects.get(name='relaydomainalias.tld') + + def test_delete_relaydomain(self): + """Test the removal of a relay domain + """ + self.ajax_post( + reverse("modoboa.extensions.postfix_relay_domains.views.delete", + args=[self.rdom.id]), + {} + ) + with self.assertRaises(RelayDomain.DoesNotExist): + RelayDomain.objects.get(name='relaydomain.tld') + + def test_delete_relaydomainalias(self): + """Test the remove of a relay domain alias + """ + self._relay_domain_alias_operation( + 'del', self.rdom.name, 'relaydomainalias.tld' + ) + with self.assertRaises(RelayDomain.DoesNotExist): + RelayDomain.objects.get(name='relaydomainalias.tld') + + +class LimitsTestCase(ResourceTestCase, Operations): + names = ['limits', 'postfix_relay_domains'] + + def setUp(self): + super(LimitsTestCase, self).setUp() + self.user = UserFactory( + username='reseller', groups=('Resellers',) + ) + self.clt.logout() + self.clt.login(username='reseller', password='toto') + + def test_relay_domains_limit(self): + self._create_relay_domain('relaydomain1.tld') + self._check_limit('relay_domains', 1, 2) + self._create_relay_domain('relaydomain2.tld') + self._check_limit('relay_domains', 2, 2) + resp = self._create_relay_domain('relaydomain3.tld', 403) + self._check_limit('relay_domains', 2, 2) + self.ajax_post( + reverse('modoboa.extensions.postfix_relay_domains.views.delete', + args=[RelayDomain.objects.get(name='relaydomain2.tld').id]), + {} + ) + self._check_limit('relay_domains', 1, 2) + + def test_relay_domain_aliases_limit(self): + self._create_relay_domain('relaydomain1.tld') + self._relay_domain_alias_operation( + 'add', 'relaydomain1.tld', 'relay_domain_alias1.tld' + ) + self._check_limit('relay_domain_aliases', 1, 2) + self._relay_domain_alias_operation( + 'add', 'relaydomain1.tld', 'relay_domain_alias2.tld' + ) + self._check_limit('relay_domain_aliases', 2, 2) + self._relay_domain_alias_operation( + 'add', 'relaydomain1.tld', 'relay_domain_alias3.tld', 403 + ) + self._check_limit('relay_domain_aliases', 2, 2) + self._relay_domain_alias_operation( + 'delete', 'relaydomain1.tld', 'relay_domain_alias2.tld' + ) + self._check_limit('relay_domain_aliases', 1, 2) diff --git a/modoboa/lib/permissions.py b/modoboa/lib/permissions.py index 8b93a2d8c..5cf677aaf 100644 --- a/modoboa/lib/permissions.py +++ b/modoboa/lib/permissions.py @@ -2,23 +2,27 @@ from django.contrib.contenttypes.models import ContentType from django.utils.translation import ugettext as _ from django.db import IntegrityError -from modoboa.core.models import ObjectAccess -import events -from exceptions import ModoboaException +from modoboa.core.models import ObjectAccess, User +from modoboa.lib import events +from modoboa.lib.exceptions import ModoboaException -def get_account_roles(user): - """Return the list of supported account roles +def get_account_roles(user, account=None): + """Return the list of available account roles. - This list can be extended by extensions which listen to the - ``GetExtraRoles`` event. + This function is used to create or modify an account. The returned + list can be extended by listening to the ``GetExtraRoles`` event. + :param ``User`` user: connected user + :param ``User`` account: account beeing modified (None on creation) :return: list of strings """ std_roles = [("SimpleUsers", _("Simple user"))] if user.is_superuser: std_roles += [("SuperAdmins", _("Super administrator"))] - filters = events.raiseQueryEvent('UserCanSetRole', user, 'DomainAdmins') + filters = events.raiseQueryEvent( + 'UserCanSetRole', user, 'DomainAdmins', account + ) if user.has_perm("admin.add_domain") and \ (not filters or True in filters): std_roles += [("DomainAdmins", _("Domain administrator"))] @@ -92,17 +96,32 @@ def ungrant_access_to_object(obj, user=None): If no user is provided, all entries referencing this object are deleted from the database. + If a user is provided, we only remove his access. If it was the + owner, we give the ownership to the first super admin we find. + :param obj: an object inheriting from ``models.Model`` :param user: a ``User`` object """ ct = ContentType.objects.get_for_model(obj) - if user: + if user is not None: try: - ObjectAccess.objects.get(user=user, content_type=ct, object_id=obj.id).delete() + ObjectAccess.objects.get( + user=user, content_type=ct, object_id=obj.id + ).delete() except ObjectAccess.DoesNotExist: pass + try: + ObjectAccess.objects.get( + content_type=ct, object_id=obj.id, is_owner=True + ) + except ObjectAccess.DoesNotExist: + grant_access_to_object( + User.objects.filter(is_superuser=True)[0], obj, True + ) else: - ObjectAccess.objects.filter(content_type=ct, object_id=obj.id).delete() + ObjectAccess.objects.filter( + content_type=ct, object_id=obj.id + ).delete() def ungrant_access_to_objects(objects): diff --git a/modoboa/lib/tests.py b/modoboa/lib/tests.py index 8601f8682..3ad5f2007 100644 --- a/modoboa/lib/tests.py +++ b/modoboa/lib/tests.py @@ -27,13 +27,15 @@ class ModoTestCase(TestCase): class ExtTestCase(ModoTestCase): - name = None + names = [] def setUp(self, *args, **kwargs): super(ExtTestCase, self).setUp(*args, **kwargs) self.clt.get(reverse("modoboa.core.views.admin.viewextensions")) - self.clt.post(reverse("modoboa.core.views.admin.saveextensions"), - {"select_%s" % self.name : "1"}) + self.clt.post( + reverse("modoboa.core.views.admin.saveextensions"), + dict(("select_%s" % name, "1") for name in self.names) + ) class TestParams(parameters.AdminParametersForm): diff --git a/modoboa/static/js/creation_wizard.js b/modoboa/static/js/creation_wizard.js index e10985d64..2f1f5eabf 100644 --- a/modoboa/static/js/creation_wizard.js +++ b/modoboa/static/js/creation_wizard.js @@ -42,11 +42,10 @@ type: 'POST', url: $form.attr("action"), data: data, global: false }).done($.proxy(function(resp) { if (!last) { - var stepid = resp.stepid + 1; $('input:text:visible:first').focus(); this.set_current_title(resp.title); - if (this.options.transition_callbacks[stepid] != undefined) { - this.options.transition_callbacks[stepid](); + if (this.options.transition_callbacks[resp.stepid] != undefined) { + this.options.transition_callbacks[resp.stepid](); } this.$element.carousel('next'); } else { diff --git a/modoboa/static/js/global.js b/modoboa/static/js/global.js index b899731c0..6aaa76e11 100644 --- a/modoboa/static/js/global.js +++ b/modoboa/static/js/global.js @@ -35,15 +35,6 @@ function modalbox(e, css, defhref, defcb, defclosecb) { type: "GET", url: href }).done(function(data) { - if (typeof data === "object") { - /* - * Compatibility with the old way... - */ - if (data.status == "ko" && data.respmsg) { - $("body").notify("error", data.respmsg); - } - return; - } var $div = $('
', { id: "modalbox", 'class': "modal", html: data }); @@ -56,8 +47,7 @@ function modalbox(e, css, defhref, defcb, defclosecb) { if (modalcb != undefined) { if (typeof modalcb === "function") modalcb(); else eval(modalcb + '()'); } - }) - .on('hidden', function(e) { + }).on('hidden', function(e) { var $target = $(e.target); if (!$target.is($(this))) { diff --git a/modoboa/urls.py b/modoboa/urls.py index f100d17e1..28a89b86b 100644 --- a/modoboa/urls.py +++ b/modoboa/urls.py @@ -2,9 +2,9 @@ from django.conf.urls import patterns, include from django.conf import settings from modoboa.core.extensions import exts_pool from modoboa.lib import parameters, events -from modoboa.core import load_settings +from modoboa.core import load_core_settings -load_settings() +load_core_settings() urlpatterns = patterns( '',