diff --git a/modoboa/admin/api.py b/modoboa/admin/api.py index 24684c4fa..08facd697 100644 --- a/modoboa/admin/api.py +++ b/modoboa/admin/api.py @@ -75,10 +75,6 @@ class AccountViewSet(viewsets.ModelViewSet): queryset = queryset.filter(mailbox__domain__name=domain) return queryset - def perform_destroy(self, instance): - """Add custom args to delete call.""" - instance.delete(self.request.user) - @detail_route(methods=["put"]) def password(self, request, pk=None): """Change account password.""" diff --git a/modoboa/admin/app_settings.py b/modoboa/admin/app_settings.py index f695ee673..9f530be27 100644 --- a/modoboa/admin/app_settings.py +++ b/modoboa/admin/app_settings.py @@ -18,7 +18,7 @@ ADMIN_EVENTS = [ "DomainDeleted", "DomainOwnershipRemoved", "GetTagsForDomain", - "ExtraDomainMenuEntries", + "ExtraDomainFilters", "ExtraDomainQsetFilters", "ExtraDomainTypes", diff --git a/modoboa/admin/callbacks.py b/modoboa/admin/callbacks.py index 59e59313a..b092c2516 100644 --- a/modoboa/admin/callbacks.py +++ b/modoboa/admin/callbacks.py @@ -1,208 +1,7 @@ """Event callbacks for modoboa admin.""" -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 events -from modoboa.lib.email_utils import split_mailbox -from modoboa.lib.exceptions import PermDeniedException, BadRequest, Conflict -from modoboa.parameters import tools as param_tools from .models import ( Domain, DomainAlias, Mailbox, Alias ) - - -@events.observe("AdminMenuDisplay") -def admin_menu(target, user): - if target != "top_menu": - return [] - entries = [] - if user.has_perm("admin.view_domains"): - entries += [ - {"name": "domains", - "url": reverse("admin:domain_list"), - "label": _("Domains")} - ] - conditions = ( - user.has_perm("core.add_user"), - user.has_perm("admin.add_alias") - ) - if any(conditions): - entries += [ - {"name": "identities", - "url": reverse("admin:identity_list"), - "label": _("Identities")}, - ] - return entries - - -@events.observe("UserMenuDisplay") -def user_menu(target, user): - if target != "uprefs_menu": - return [] - if not hasattr(user, "mailbox"): - return [] - return [ - {"name": "forward", - "class": "ajaxnav", - "url": "forward/", - "label": ugettext_lazy("Forward")} - ] - - -@events.observe("RoleChanged") -def grant_access_to_all_objects(user, role): - from django.contrib.contenttypes.models import ContentType - from modoboa.lib.permissions import grant_access_to_objects - from modoboa.core.models import User - - if role != "SuperAdmins": - return - grant_access_to_objects( - user, User.objects.all(), - ContentType.objects.get_for_model(User) - ) - grant_access_to_objects( - user, Domain.objects.all(), - ContentType.objects.get_for_model(Domain) - ) - grant_access_to_objects( - user, DomainAlias.objects.all(), - ContentType.objects.get_for_model(DomainAlias) - ) - grant_access_to_objects( - user, Mailbox.objects.all(), - ContentType.objects.get_for_model(Mailbox) - ) - grant_access_to_objects( - user, Alias.objects.all(), - ContentType.objects.get_for_model(Alias) - ) - - -@events.observe("AccountExported") -def export_admin_domains(admin): - result = [admin.mailbox.quota] \ - if hasattr(admin, "mailbox") else [""] - if admin.role != "DomainAdmins": - return result - return result + [dom.name for dom in Domain.objects.get_for_admin(admin)] - - -@events.observe("AccountImported") -def import_account_mailbox(user, account, row): - """Handle extra fields when an account is imported. - - Expected fields: - - email address; quota; [domain; ...] - - :param User user: user importing the account - :param User account: account being imported - :param list rom: list of fields (strings) - """ - account.email = row[0].strip() - if account.email: - mailbox, domname = split_mailbox(account.email) - try: - domain = Domain.objects.get(name=domname) - except Domain.DoesNotExist: - raise BadRequest( - _("Account import failed (%s): domain does not exist" - % account.username) - ) - 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)) - if len(row) == 1: - quota = None - else: - try: - quota = int(row[1].strip()) - except ValueError: - raise BadRequest( - _("Account import failed (%s): wrong quota value" - % account.username) - ) - use_domain_quota = True if not quota else False - mb = Mailbox(address=mailbox, domain=domain, - user=account, use_domain_quota=use_domain_quota) - mb.set_quota( - quota, override_rules=user.has_perm("admin.change_domain") - ) - mb.save(creator=user) - if account.role == "DomainAdmins": - for domname in row[2:]: - try: - dom = Domain.objects.get(name=domname.strip()) - except Domain.DoesNotExist: - continue - dom.add_admin(account) - - -@events.observe("AccountAutoCreated") -def account_auto_created(user): - from modoboa.core.models import User - from modoboa.lib.permissions import grant_access_to_object - from .lib import check_if_domain_exists - - if not param_tools.get_global_parameter("auto_create_domain_and_mailbox"): - return - localpart, domname = split_mailbox(user.username) - if user.role != 'SimpleUsers' and domname is None: - return - sadmins = User.objects.filter(is_superuser=True) - try: - domain = Domain.objects.get(name=domname) - except Domain.DoesNotExist: - label = check_if_domain_exists( - domname, [(DomainAlias, _('domain alias'))]) - if label is not None: - return - domain = Domain(name=domname, enabled=True, quota=0) - domain.save(creator=sadmins[0]) - for su in sadmins[1:]: - grant_access_to_object(su, domain) - try: - mb = Mailbox.objects.get(domain=domain, address=localpart) - except Mailbox.DoesNotExist: - mb = Mailbox( - address=localpart, domain=domain, user=user, use_domain_quota=True - ) - mb.set_quota(override_rules=True) - mb.save(creator=sadmins[0]) - for su in sadmins[1:]: - grant_access_to_object(su, mb) - - -@events.observe("UserLogin") -def user_logged_in(request, username, password): - from modoboa.lib.cryptutils import encrypt - - if hasattr(request.user, "mailbox"): - request.session["password"] = encrypt(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 hasattr(account, "mailbox"): - return - mb = account.mailbox - if not byuser.can_access(mb): - raise PermDeniedException - keep_mb_dir = kwargs.get("keep_mb_dir", True) - mb.delete(keepdir=keep_mb_dir) diff --git a/modoboa/admin/forms/account.py b/modoboa/admin/forms/account.py index 920b0cd10..19afd9871 100644 --- a/modoboa/admin/forms/account.py +++ b/modoboa/admin/forms/account.py @@ -229,10 +229,14 @@ class AccountFormMail(forms.Form, DynamicForm): "quota": 3 } self.extra_fields = [] - result = events.raiseQueryEvent('ExtraFormFields', 'mailform', self.mb) - for fname, field in result: - self.fields[fname] = field - self.extra_fields.append(fname) + results = core_signals.extra_form_fields.send( + sender=self.__class__, form="mailform", instance=self.mb) + if results: + results = reduce( + lambda a, b: a + b, [result for result in results]) + for fname, field in results: + self.fields[fname] = field + self.extra_fields.append(fname) if self.mb is not None: self.fields["email"].required = True qset = self.mb.aliasrecipient_set.filter(alias__internal=False) @@ -380,9 +384,9 @@ class AccountFormMail(forms.Form, DynamicForm): self.cleaned_data["use_domain_quota"] = ( self.cleaned_data["quota_act"]) self.mb.update_from_dict(user, self.cleaned_data) - events.raiseEvent( - 'SaveExtraFormFields', 'mailform', self.mb, self.cleaned_data - ) + core_signals.extra_form_fields.send( + sender=self.__class__, form="mailform", instance=self.mb, + cleaned_data=self.cleaned_data) account.email = self.cleaned_data["email"] account.save() diff --git a/modoboa/admin/forms/domain.py b/modoboa/admin/forms/domain.py index a1c649526..445986f1e 100644 --- a/modoboa/admin/forms/domain.py +++ b/modoboa/admin/forms/domain.py @@ -193,8 +193,9 @@ class DomainFormOptions(forms.Form): def __init__(self, user, *args, **kwargs): super(DomainFormOptions, self).__init__(*args, **kwargs) - result = events.raiseQueryEvent("UserCanSetRole", user, "DomainAdmins") - if False in result: + results = core_signals.user_can_set_role.send( + sender=self.__class__, user=user, role="DomainAdmins") + if False in [result[1] for result in results]: self.fields = {} return diff --git a/modoboa/admin/handlers.py b/modoboa/admin/handlers.py index b7b6554e3..89bc051b6 100644 --- a/modoboa/admin/handlers.py +++ b/modoboa/admin/handlers.py @@ -1,10 +1,22 @@ """Django signal handlers for admin.""" +from django.core.urlresolvers import reverse from django.db.models import signals from django.dispatch import receiver +from django.utils.translation import ugettext as _ +from django.contrib.contenttypes.models import ContentType + +from modoboa.core import models as core_models from modoboa.core import signals as core_signals +from modoboa.lib.cryptutils import encrypt +from modoboa.lib.email_utils import split_mailbox +from modoboa.lib import exceptions +from modoboa.lib import permissions +from modoboa.lib import signals as lib_signals +from modoboa.parameters import tools as param_tools +from . import lib from . import models from . import postfix_maps @@ -74,7 +86,7 @@ def manage_alias_for_mailbox(sender, instance, **kwargs): @receiver(signals.pre_delete, sender=models.Mailbox) def mailbox_deleted_handler(sender, **kwargs): - """``Mailbox`` pre_delete signal receiver + """``Mailbox`` pre_delete signal receiver. In order to properly handle deletions (ie. we don't want to leave orphan records into the db), we define this custom receiver. @@ -85,7 +97,7 @@ def mailbox_deleted_handler(sender, **kwargs): from modoboa.lib import events from modoboa.lib.permissions import ungrant_access_to_object - mb = kwargs['instance'] + mb = kwargs["instance"] events.raiseEvent("MailboxDeleted", mb) ungrant_access_to_object(mb) for ralias in mb.aliasrecipient_set.select_related("alias"): @@ -93,6 +105,15 @@ def mailbox_deleted_handler(sender, **kwargs): ralias.delete() if not alias.aliasrecipient_set.exists(): alias.delete() + models.Quota.objects.filter(username=mb.full_address).delete() + request = lib_signals.get_request() + if not request.localconfig.parameters.get_value( + "handle_mailboxes", raise_exception=False): + return + keepdir = request.POST.get("keepdir", "false") == "true" + if keepdir: + return + mb.delete_dir() @receiver(signals.post_delete, sender=models.Mailbox) @@ -111,3 +132,166 @@ def register_postfix_maps(sender, **kwargs): postfix_maps.SenderLoginAliasMap, postfix_maps.SenderLoginMailboxMap, postfix_maps.SenderLoginMailboxExtraMap ] + + +@receiver(core_signals.account_auto_created) +def account_auto_created(sender, user, **kwargs): + """New account has been auto-created, build the rest.""" + if not param_tools.get_global_parameter("auto_create_domain_and_mailbox"): + return + localpart, domname = split_mailbox(user.username) + if user.role != 'SimpleUsers' and domname is None: + return + sadmins = core_models.User.objects.filter(is_superuser=True) + try: + domain = models.Domain.objects.get(name=domname) + except models.Domain.DoesNotExist: + label = lib.check_if_domain_exists( + domname, [(models.DomainAlias, _("domain alias"))]) + if label is not None: + return + domain = models.Domain(name=domname, enabled=True, quota=0) + domain.save(creator=sadmins[0]) + for su in sadmins[1:]: + permissions.grant_access_to_object(su, domain) + qset = models.Mailbox.objects.filter(domain=domain, address=localpart) + if not qset.exists(): + mb = models.Mailbox( + address=localpart, domain=domain, user=user, use_domain_quota=True + ) + mb.set_quota(override_rules=True) + mb.save(creator=sadmins[0]) + for su in sadmins[1:]: + permissions.grant_access_to_object(su, mb) + + +@receiver(core_signals.account_exported) +def export_admin_domains(sender, user, **kwargs): + """Export administered domains too.""" + result = [user.mailbox.quota] if hasattr(user, "mailbox") else [""] + if user.role != "DomainAdmins": + return result + return result + [ + dom.name for dom in models.Domain.objects.get_for_admin(user)] + + +@receiver(core_signals.account_imported) +def import_account_mailbox(sender, user, account, row, **kwargs): + """Handle extra fields when an account is imported. + + Expected fields: + + email address; quota; [domain; ...] + + :param User user: user importing the account + :param User account: account being imported + :param list rom: list of fields (strings) + """ + account.email = row[0].strip() + if account.email: + mailbox, domname = split_mailbox(account.email) + domain = models.Domain.objects.filter(name=domname).first() + if not domain: + raise exceptions.BadRequest( + _("Account import failed (%s): domain does not exist") + % account.username + ) + if not user.can_access(domain): + raise exceptions.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() + qset = models.Mailbox.objects.filter(address=mailbox, domain=domain) + if qset.exists(): + raise exceptions.Conflict( + _("Mailbox {} already exists").format(account.email)) + if len(row) == 1: + quota = None + else: + try: + quota = int(row[1].strip()) + except ValueError: + raise exceptions.BadRequest( + _("Account import failed (%s): wrong quota value") + % account.username + ) + use_domain_quota = True if not quota else False + mb = models.Mailbox( + address=mailbox, domain=domain, + user=account, use_domain_quota=use_domain_quota) + mb.set_quota( + quota, override_rules=user.has_perm("admin.change_domain") + ) + mb.save(creator=user) + if account.role == "DomainAdmins": + for domname in row[2:]: + try: + dom = models.Domain.objects.get(name=domname.strip()) + except models.Domain.DoesNotExist: + continue + dom.add_admin(account) + + +@receiver(core_signals.extra_admin_menu_entries) +def admin_menu(sender, location, user, **kwargs): + """Add extra menu entries for admins.""" + if location != "top_menu": + return [] + entries = [] + if user.has_perm("admin.view_domains"): + entries += [ + {"name": "domains", + "url": reverse("admin:domain_list"), + "label": _("Domains")} + ] + conditions = ( + user.has_perm("core.add_user"), + user.has_perm("admin.add_alias") + ) + if any(conditions): + entries += [ + {"name": "identities", + "url": reverse("admin:identity_list"), + "label": _("Identities")}, + ] + return entries + + +@receiver(core_signals.extra_user_menu_entries) +def user_menu(sender, location, user, **kwargs): + """Add extra menu entries for users.""" + if location != "uprefs_menu": + return [] + if not hasattr(user, "mailbox"): + return [] + return [ + {"name": "forward", + "class": "ajaxnav", + "url": "forward/", + "label": _("Forward")} + ] + + +@receiver(core_signals.user_login) +def user_logged_in(sender, username, password, **kwargs): + """Store user password in session.""" + request = lib_signals.get_request() + if hasattr(request.user, "mailbox"): + request.session["password"] = encrypt(password) + + +@receiver(core_signals.account_role_changed) +def grant_access_to_all_objects(sender, account, role, **kwargs): + """Grant all permissions if new role is SuperAdmin.""" + if role != "SuperAdmins": + return + perm_models = [ + core_models.User, models.Domain, models.DomainAlias, models.Mailbox, + models.Alias] + for model in perm_models: + permissions.grant_access_to_objects( + account, model.objects.all(), + ContentType.objects.get_for_model(model) + ) diff --git a/modoboa/admin/models/mailbox.py b/modoboa/admin/models/mailbox.py index 3e7c10f8f..14bc1d18e 100644 --- a/modoboa/admin/models/mailbox.py +++ b/modoboa/admin/models/mailbox.py @@ -75,7 +75,6 @@ class MailboxManager(Manager): @python_2_unicode_compatible class Mailbox(AdminObject): - """User mailbox.""" address = models.CharField( @@ -326,21 +325,6 @@ class Mailbox(AdminObject): username=self.full_address) super(Mailbox, self).save(*args, **kwargs) - def delete(self, keepdir=False): - """Custom delete method - - We try to delete the associated quota in the same time (it may - has already been removed if we're deleting a domain). - - :param bool keepdir: delete the mailbox home dir on the - filesystem or not - - """ - Quota.objects.filter(username=self.full_address).delete() - if not keepdir: - self.delete_dir() - super(Mailbox, self).delete() - reversion.register(Mailbox) diff --git a/modoboa/admin/signals.py b/modoboa/admin/signals.py index 7d9be0803..dae12a7e0 100644 --- a/modoboa/admin/signals.py +++ b/modoboa/admin/signals.py @@ -2,10 +2,14 @@ import django.dispatch -use_external_recipients = django.dispatch.Signal(providing_args=["recipients"]) +extra_admin_content = django.dispatch.Signal( + providing_args=["user", "location", "currentpage"]) extra_domain_actions = django.dispatch.Signal( providing_args=["user", "domain"]) extra_domain_dashboard_widgets = django.dispatch.Signal( providing_args=["user", "domain"]) +extra_domain_menu_entries = django.dispatch.Signal( + providing_args=["user"]) extra_account_dashboard_widgets = django.dispatch.Signal( providing_args=["user", "account"]) +use_external_recipients = django.dispatch.Signal(providing_args=["recipients"]) diff --git a/modoboa/admin/templatetags/admin_tags.py b/modoboa/admin/templatetags/admin_tags.py index 3cab73a96..a204105c4 100644 --- a/modoboa/admin/templatetags/admin_tags.py +++ b/modoboa/admin/templatetags/admin_tags.py @@ -6,7 +6,7 @@ from django.template.loader import render_to_string from django.utils.safestring import mark_safe from django.utils.translation import ugettext as _, ugettext_lazy -from modoboa.lib import events +from modoboa.core import signals as core_signals from modoboa.lib.templatetags.lib_tags import render_link from modoboa.lib.web_utils import render_actions @@ -47,7 +47,10 @@ def domains_menu(selection, user, ajax_mode=True): "url": reverse("admin:domain_statistics")} ] if user.has_perm("admin.add_domain"): - entries += events.raiseQueryEvent("ExtraDomainMenuEntries", user) + extra_entries = signals.extra_domain_menu_entries.send( + sender="domains_menu", user=user) + for entry in extra_entries: + entries += entry[1] entries += [ {"name": "import", "label": _("Import"), @@ -157,7 +160,11 @@ def identity_actions(user, ident): name = ident.__class__.__name__ objid = ident.id if name == "User": - actions = events.raiseQueryEvent("ExtraAccountActions", ident) + actions = [] + result = core_signals.extra_account_actions.send( + sender="identity_actions", account=ident) + for action in result: + actions += result[1] url = ( reverse("admin:account_change", args=[objid]) + "?active_tab=default" @@ -265,7 +272,10 @@ def gender(value, target): @register.simple_tag def get_extra_admin_content(user, target, currentpage): - res = events.raiseQueryEvent( - "ExtraAdminContent", user, target, currentpage - ) - return mark_safe("".join(res)) + results = signals.extra_admin_content.send( + sender="get_extra_admin_content", + user=user, location=target, currentpage=currentpage) + if not results: + return "" + results = reduce(lambda a, b: a + b, [result[1] for result in results]) + return mark_safe("".join(results)) diff --git a/modoboa/admin/views/identity.py b/modoboa/admin/views/identity.py index 466ced291..f2401bceb 100644 --- a/modoboa/admin/views/identity.py +++ b/modoboa/admin/views/identity.py @@ -188,8 +188,7 @@ def editaccount(request, pk): @login_required @permission_required("core.delete_user") def delaccount(request, pk): - keepdir = True if request.POST.get("keepdir", "false") == "true" else False - User.objects.get(pk=pk).delete(request.user, keep_mb_dir=keepdir) + User.objects.get(pk=pk).delete() return render_to_json_response( ungettext("Account deleted", "Accounts deleted", 1) ) diff --git a/modoboa/core/__init__.py b/modoboa/core/__init__.py index 95ccfde83..0f5234285 100644 --- a/modoboa/core/__init__.py +++ b/modoboa/core/__init__.py @@ -1,67 +1,3 @@ - -from django.core.urlresolvers import reverse -from django.utils.translation import ugettext as _ - -from modoboa.lib import events - - -PERMISSIONS = { - "SimpleUsers": [], - "DomainAdmins": [ - ["core", "user", "add_user"], - ["core", "user", "change_user"], - ["core", "user", "delete_user"], - ["admin", "domain", "view_domains"], - ["admin", "domain", "view_domain"], - ["admin", "mailbox", "add_mailbox"], - ["admin", "mailbox", "change_mailbox"], - ["admin", "mailbox", "delete_mailbox"], - ["admin", "alias", "add_alias"], - ["admin", "alias", "change_alias"], - ["admin", "alias", "delete_alias"], - ["admin", "mailbox", "view_mailboxes"], - ["admin", "alias", "view_aliases"], - ], - "Resellers": [ - ["core", "user", "add_user"], - ["core", "user", "change_user"], - ["core", "user", "delete_user"], - ["admin", "domain", "view_domains"], - ["admin", "domain", "view_domain"], - ["admin", "mailbox", "add_mailbox"], - ["admin", "mailbox", "change_mailbox"], - ["admin", "mailbox", "delete_mailbox"], - ["admin", "alias", "add_alias"], - ["admin", "alias", "change_alias"], - ["admin", "alias", "delete_alias"], - ["admin", "mailbox", "view_mailboxes"], - ["admin", "alias", "view_aliases"], - ["admin", "domain", "view_domains"], - ["admin", "domain", "add_domain"], - ["admin", "domain", "change_domain"], - ["admin", "domain", "delete_domain"], - ["admin", "domainalias", "add_domainalias"], - ["admin", "domainalias", "change_domainalias"], - ["admin", "domainalias", "delete_domainalias"], - ] -} - - -@events.observe("TopNotifications") -def check_for_new_versions(request, include_all): - """Check if new versions are available.""" - from modoboa.core.utils import check_for_updates - - if not request.user.is_superuser: - return [] - status, extensions = check_for_updates(request) - if not status: - return [{"id": "newversionavailable"}] if include_all else [] - return [{ - "id": "newversionavailable", - "url": reverse("core:index") + "#info/", - "text": _("One or more updates are available"), - "level": "info", - }] +"""App config for core.""" default_app_config = "modoboa.core.apps.CoreConfig" diff --git a/modoboa/core/apps.py b/modoboa/core/apps.py index df124bd32..0dca0a28f 100644 --- a/modoboa/core/apps.py +++ b/modoboa/core/apps.py @@ -6,38 +6,8 @@ from django.utils.translation import ugettext_lazy BASE_EVENTS = [ "AccountCreated", - "AccountAutoCreated", + "AccountModified", - "AccountDeleted", - "AccountExported", - "AccountImported", - "PasswordUpdated", - "ExtraAccountActions", - "RoleChanged", - "GetExtraRoles", - "GetExtraRolePermissions", - "PasswordChange", - "UserCanSetRole", - - "InitialDataLoaded", - - "UserMenuDisplay", - "AdminMenuDisplay", - "GetStaticContent", - - "UserLogin", - "UserLogout", - - "GetAnnouncement", - - "TopNotifications", - "ExtraAdminContent", - - "ExtraUprefsRoutes", - "ExtraUprefsJS", - - "ExtraFormFields", - "SaveExtraFormFields", ] @@ -57,7 +27,6 @@ def load_core_settings(): class CoreConfig(AppConfig): - """App configuration.""" name = "modoboa.core" diff --git a/modoboa/core/constants.py b/modoboa/core/constants.py index 8adce607f..6a9d2be6e 100644 --- a/modoboa/core/constants.py +++ b/modoboa/core/constants.py @@ -42,3 +42,44 @@ LDAP_GROUP_TYPES = ( ("posixgroup", "PosixGroup"), ("groupofnames", "GroupOfNames"), ) + +PERMISSIONS = { + "SimpleUsers": [], + "DomainAdmins": [ + ["core", "user", "add_user"], + ["core", "user", "change_user"], + ["core", "user", "delete_user"], + ["admin", "domain", "view_domains"], + ["admin", "domain", "view_domain"], + ["admin", "mailbox", "add_mailbox"], + ["admin", "mailbox", "change_mailbox"], + ["admin", "mailbox", "delete_mailbox"], + ["admin", "alias", "add_alias"], + ["admin", "alias", "change_alias"], + ["admin", "alias", "delete_alias"], + ["admin", "mailbox", "view_mailboxes"], + ["admin", "alias", "view_aliases"], + ], + "Resellers": [ + ["core", "user", "add_user"], + ["core", "user", "change_user"], + ["core", "user", "delete_user"], + ["admin", "domain", "view_domains"], + ["admin", "domain", "view_domain"], + ["admin", "mailbox", "add_mailbox"], + ["admin", "mailbox", "change_mailbox"], + ["admin", "mailbox", "delete_mailbox"], + ["admin", "alias", "add_alias"], + ["admin", "alias", "change_alias"], + ["admin", "alias", "delete_alias"], + ["admin", "mailbox", "view_mailboxes"], + ["admin", "alias", "view_aliases"], + ["admin", "domain", "view_domains"], + ["admin", "domain", "add_domain"], + ["admin", "domain", "change_domain"], + ["admin", "domain", "delete_domain"], + ["admin", "domainalias", "add_domainalias"], + ["admin", "domainalias", "change_domainalias"], + ["admin", "domainalias", "delete_domainalias"], + ] +} diff --git a/modoboa/core/context_processors.py b/modoboa/core/context_processors.py index 69e3a338a..d3e3d3690 100644 --- a/modoboa/core/context_processors.py +++ b/modoboa/core/context_processors.py @@ -1,6 +1,6 @@ """Custom context processors.""" -from modoboa.lib import events +from . import signals def top_notifications(request): @@ -9,8 +9,11 @@ def top_notifications(request): return {} interval = request.localconfig.parameters.get_value( "top_notifications_check_interval") + notifications = signals.get_top_notifications.send( + sender="top_notifications", include_all=False) + notifications = reduce( + lambda a, b: a + b, [notif[1] for notif in notifications]) return { "notifications_check_interval": interval * 1000, - "top_notifications": events.raiseQueryEvent( - "TopNotifications", request, False) + "top_notifications": notifications } diff --git a/modoboa/core/handlers.py b/modoboa/core/handlers.py index 576ed3798..477795b9b 100644 --- a/modoboa/core/handlers.py +++ b/modoboa/core/handlers.py @@ -2,7 +2,8 @@ import logging -from django.db.models.signals import post_delete +from django.core.urlresolvers import reverse +from django.db.models import signals from django.dispatch import receiver from django.utils.translation import ugettext as _ @@ -10,7 +11,13 @@ from django.contrib.sites import models as sites_models from reversion import revisions as reversion +from modoboa.lib import exceptions +from modoboa.lib import permissions +from modoboa.lib.signals import get_request + from . import models +from . import signals as core_signals +from . import utils @receiver(reversion.post_revision_commit) @@ -44,14 +51,13 @@ def post_revision_commit(sender, **kwargs): getattr(logger, level)(message) -@receiver(post_delete) +@receiver(signals.post_delete) def log_object_removal(sender, instance, **kwargs): """Custom post-delete hook. We want to know who was responsible for an object deletion. """ from reversion.models import Version - from modoboa.lib.signals import get_request if not reversion.is_registered(sender): return @@ -75,3 +81,50 @@ def create_local_config(sender, **kwargs): return models.LocalConfig.objects.create( site=sites_models.Site.objects.get_current()) + + +@receiver(signals.pre_delete, sender=models.User) +def update_permissions(sender, instance, **kwargs): + """Permissions cleanup.""" + from_user = get_request().user + if from_user == instance: + raise exceptions.PermDeniedException( + _("You can't delete your own account") + ) + + if not from_user.can_access(instance): + raise exceptions.PermDeniedException + + # We send an additional signal before permissions are removed + core_signals.account_deleted.send( + sender="update_permissions", user=instance) + owner = permissions.get_object_owner(instance) + if owner == instance: + # The default admin is being removed... + owner = from_user + # Change ownership of existing objects + for ooentry in instance.objectaccess_set.filter(is_owner=True): + if ooentry.content_object is not None: + permissions.grant_access_to_object( + owner, ooentry.content_object, True) + permissions.ungrant_access_to_object( + ooentry.content_object, instance) + # Remove existing permissions on this user + permissions.ungrant_access_to_object(instance) + + +@receiver(core_signals.get_top_notifications) +def check_for_new_versions(sender, include_all, **kwargs): + """Check if new versions are available.""" + request = get_request() + if not request.user.is_superuser: + return [] + status, extensions = utils.check_for_updates(request) + if not status: + return [{"id": "newversionavailable"}] if include_all else [] + return [{ + "id": "newversionavailable", + "url": reverse("core:index") + "#info/", + "text": _("One or more updates are available"), + "level": "info", + }] diff --git a/modoboa/core/management/commands/load_initial_data.py b/modoboa/core/management/commands/load_initial_data.py index 09dbcd7bb..d9df3d803 100644 --- a/modoboa/core/management/commands/load_initial_data.py +++ b/modoboa/core/management/commands/load_initial_data.py @@ -8,14 +8,14 @@ from django.contrib.auth.models import Group from django.core.management.base import BaseCommand -from modoboa.core import PERMISSIONS -from modoboa.core.extensions import exts_pool from modoboa.lib.cryptutils import random_key -from modoboa.lib import events from modoboa.lib.permissions import add_permissions_to_group import modoboa.relaydomains.models as relay_models +from ... import constants +from ... import extensions from ... import models +from ... import signals class Command(BaseCommand): @@ -54,27 +54,26 @@ class Command(BaseCommand): for service_name in ["relay", "smtp"]: relay_models.Service.objects.get_or_create(name=service_name) - exts_pool.load_all() + extensions.exts_pool.load_all() - superadmin = models.User.objects.filter(is_superuser=True).first() - groups = PERMISSIONS.keys() + [ - role[0] for role - in events.raiseQueryEvent("GetExtraRoles", superadmin, None) - ] + groups = constants.PERMISSIONS.keys() for groupname in groups: group, created = Group.objects.get_or_create(name=groupname) + results = signals.extra_role_permissions.send( + sender=self.__class__, role=groupname) permissions = ( - PERMISSIONS.get(groupname, []) + - events.raiseQueryEvent("GetExtraRolePermissions", groupname) + constants.PERMISSIONS.get(groupname, []) + + reduce(lambda a, b: a + b, [result[1] for result in results]) ) if not permissions: continue add_permissions_to_group(group, permissions) - for extname in exts_pool.extensions.keys(): - extension = exts_pool.get_extension(extname) + for extname in extensions.exts_pool.extensions.keys(): + extension = extensions.exts_pool.get_extension(extname) extension.load_initial_data() - events.raiseEvent("InitialDataLoaded", extname) + signals.initial_data_loaded.send( + sender=self.__class__, extname=extname) if options["extra_fixtures"]: from modoboa.admin import factories diff --git a/modoboa/core/models.py b/modoboa/core/models.py index 5e4c1a7f6..29617c4ed 100644 --- a/modoboa/core/models.py +++ b/modoboa/core/models.py @@ -27,6 +27,7 @@ from modoboa.lib.exceptions import ( from modoboa.parameters import tools as param_tools from . import constants +from . import signals try: @@ -103,41 +104,6 @@ class User(PermissionsMixin): super(User, self).__init__(*args, **kwargs) self.parameters = param_tools.Manager("user", self._parameters) - def delete(self, fromuser, *args, **kwargs): - """Custom delete method - - To check permissions properly, we need to make a distinction - between 2 cases: - - * If the user owns a mailbox, the check is made on that object - (useful for domain admins) - - * Otherwise, the check is made on the user - """ - from modoboa.lib.permissions import \ - get_object_owner, grant_access_to_object, ungrant_access_to_object - - if fromuser == self: - raise PermDeniedException( - _("You can't delete your own account") - ) - - if not fromuser.can_access(self): - raise PermDeniedException - - owner = get_object_owner(self) - if owner == self: - # The default admin is being removed... - owner = fromuser - for ooentry in self.objectaccess_set.filter(is_owner=True): - if ooentry.content_object is not None: - grant_access_to_object(owner, ooentry.content_object, True) - ungrant_access_to_object(ooentry.content_object, self) - - events.raiseEvent("AccountDeleted", self, fromuser, **kwargs) - ungrant_access_to_object(self) - super(User, self).delete() - def _crypt_password(self, raw_value): """Crypt the local password using the appropriate scheme. @@ -177,9 +143,9 @@ class User(PermissionsMixin): LDAPAuthBackend().update_user_password( self.username, curvalue, raw_value ) - events.raiseEvent( - "PasswordUpdated", self, raw_value, self.pk is None - ) + signals.account_password_updated.send( + sender=self.__class__, + account=self, password=raw_value, created=self.pk is None) def check_password(self, raw_value): """Compare raw_value to current password.""" @@ -329,7 +295,8 @@ class User(PermissionsMixin): """ if role is None or self.role == role: return - events.raiseEvent("RoleChanged", self, role) + signals.account_role_changed.send( + sender=self.__class__, account=self, role=role) self.groups.clear() if role == "SuperAdmins": self.is_superuser = True @@ -378,7 +345,7 @@ class User(PermissionsMixin): "account", loginname, password, first name, last name, enabled, role - Additional fields can be added using the *AccountImported* event. + Additional fields can be added using the *account_imported* signal. :param user: a ``core.User`` instance :param row: a list containing the expected information @@ -431,7 +398,8 @@ class User(PermissionsMixin): self.post_create(user) if len(row) < 8: return - events.raiseEvent("AccountImported", user, self, row[7:]) + signals.account_imported.send( + sender=self.__class__, user=user, account=self, row=row[7:]) def to_csv(self, csvwriter): """Export this account. @@ -450,7 +418,10 @@ class User(PermissionsMixin): self.role, self.email.encode("utf-8") ] - row += events.raiseQueryEvent("AccountExported", self) + results = signals.account_exported.send( + sender=self.__class__, user=self) + for result in results: + row += result[1] csvwriter.writerow(row) reversion.register(User) @@ -473,7 +444,8 @@ def populate_callback(user, group="SimpleUsers"): user.post_create(sadmins[0]) for su in sadmins[1:]: grant_access_to_object(su, user) - events.raiseEvent("AccountAutoCreated", user) + signals.account_auto_created.send( + sender="populate_callback", user=user) class ObjectAccess(models.Model): diff --git a/modoboa/core/signals.py b/modoboa/core/signals.py index 3169a6313..c6d406739 100644 --- a/modoboa/core/signals.py +++ b/modoboa/core/signals.py @@ -2,6 +2,37 @@ import django.dispatch +account_auto_created = django.dispatch.Signal(providing_args=["user"]) +account_exported = django.dispatch.Signal(providing_args=["account"]) +account_imported = django.dispatch.Signal( + providing_args=["user", "account", "row"]) +account_deleted = django.dispatch.Signal(providing_args=["user"]) +account_role_changed = django.dispatch.Signal( + providing_args=["account", "role"]) +account_password_updated = django.dispatch.Signal( + providing_args=["account", "password", "created"]) +allow_password_change = django.dispatch.Signal(providing_args=["user"]) can_create_object = django.dispatch.Signal( providing_args=["context", "object_type", "count"]) +extra_account_actions = django.dispatch.Signal(providing_args=["account"]) +extra_admin_menu_entries = django.dispatch.Signal( + providing_args=["location", "user"]) +extra_form_fields = django.dispatch.Signal( + providing_args=["form", "instance"]) +extra_role_permissions = django.dispatch.Signal(providing_args=["role"]) +extra_static_content = django.dispatch.Signal( + providing_args=["caller", "st_type", "user"]) +extra_uprefs_routes = django.dispatch.Signal() +extra_user_menu_entries = django.dispatch.Signal( + providing_args=["location", "user"]) +get_announcements = django.dispatch.Signal(providing_args=["location"]) +get_top_notifications = django.dispatch.Signal( + providing_args=["include_all"]) +initial_data_loaded = django.dispatch.Signal(providing_args=["extname"]) register_postfix_maps = django.dispatch.Signal() +save_extra_form_fields = django.dispatch.Signal( + providing_args=["form", "instance", "cleaned_data"]) +user_can_set_role = django.dispatch.Signal( + providing_args=["user", "role", "account"]) +user_login = django.dispatch.Signal( + providing_args=["username", "password"]) diff --git a/modoboa/core/templates/core/user_index.html b/modoboa/core/templates/core/user_index.html index db4896e44..bf38bfb46 100644 --- a/modoboa/core/templates/core/user_index.html +++ b/modoboa/core/templates/core/user_index.html @@ -12,7 +12,7 @@ -"""] diff --git a/modoboa/limits/handlers.py b/modoboa/limits/handlers.py index b2ffad27d..75657ed29 100644 --- a/modoboa/limits/handlers.py +++ b/modoboa/limits/handlers.py @@ -2,6 +2,7 @@ from django.db.models import signals from django.dispatch import receiver +from django.template.loader import render_to_string from django.contrib.contenttypes.models import ContentType @@ -10,6 +11,7 @@ from modoboa.admin import signals as admin_signals from modoboa.core import models as core_models from modoboa.core import signals as core_signals from modoboa.lib import signals as lib_signals +from modoboa.lib import permissions from modoboa.parameters import tools as param_tools from . import lib @@ -99,3 +101,117 @@ def display_admin_limits(sender, user, account, **kwargs): account.userobjectlimit_set.select_related("content_type")) } }] + + +@receiver(core_signals.account_deleted) +def move_resource(sender, user, **kwargs): + """Move remaining resource to another user.""" + owner = permissions.get_object_owner(user) + if owner.is_superuser or owner.role != "Resellers": + return + utils.move_pool_resource(owner, user) + + +@receiver(core_signals.user_can_set_role) +def user_can_set_role(sender, user, role, account=None, **kwargs): + """Check if the user can still set this role. + + The only interesting case concerns resellers defining new domain + administrators. We want to check if they are allowed to do this + operation before any modification is made to :keyword:`account`. + + :param ``User`` user: connected user + :param str role: role to check + :param ``User`` account: account modified (None on creation) + """ + condition = ( + not param_tools.get_global_parameter("enable_admin_limits") or + role != "DomainAdmins") + if condition: + return True + lname = "domain_admins" + condition = ( + user.is_superuser or + not user.userobjectlimit_set.get(name=lname).is_exceeded() + ) + if condition: + return True + if account is not None and account.role == role: + return True + return False + + +@receiver(core_signals.extra_static_content) +def get_static_content(sender, caller, st_type, user, **kwargs): + """Add extra static content.""" + condition = ( + not param_tools.get_global_parameter("enable_admin_limits") or + caller not in ["domains", "identities"] or + user.role in ["SuperAdmins", "SimpleUsers"] + ) + if condition: + return [] + if st_type == "css": + return [""" +"""] + return [""" + +"""] + + +@receiver(admin_signals.extra_admin_content) +def display_pool_usage(sender, user, location, currentpage, **kwargs): + """Display current usage.""" + condition = ( + not param_tools.get_global_parameter("enable_admin_limits") or + location != "leftcol" or user.is_superuser) + if condition: + return [] + if currentpage == "identities": + names = ["mailboxes", "mailbox_aliases"] + if user.has_perm("admin.add_domain"): + names += ["domain_admins"] + else: + exceptions = ["domain_admins", "mailboxes", "mailbox_aliases"] + names = [ + 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.role) + ] + + limits = user.userobjectlimit_set.filter(name__in=names, max_value__gt=0) + if len(limits) == 0: + return [] + return [ + render_to_string("limits/poolusage.html", + dict(limits=limits)) + ] + + +@receiver(core_signals.account_role_changed) +def move_pool_resource(sender, account, role, **kwargs): + """Move remaining resource to owner if needed.""" + owner = permissions.get_object_owner(account) + if not owner or owner.is_superuser or owner.role != "Resellers": + # Domain admins can't change the role so nothing to check. + return + + if role not in ["DomainAdmins", "Resellers"]: + utils.move_pool_resource(owner, account) diff --git a/modoboa/limits/utils.py b/modoboa/limits/utils.py index 6cbf3d1b0..25cb45629 100644 --- a/modoboa/limits/utils.py +++ b/modoboa/limits/utils.py @@ -12,3 +12,18 @@ def get_user_limit_templates(): def get_domain_limit_templates(): """Return defined templates.""" return constants.DEFAULT_DOMAIN_LIMITS.items() + + +def move_pool_resource(owner, user): + """Move resource from one pool to another. + + When an account doesn't need a pool anymore, we move the + associated resource to the pool of its owner. + """ + for name, ltpl in get_user_limit_templates(): + l = user.userobjectlimit_set.get(name=name) + if l.max_value < 0: + continue + ol = owner.userobjectlimit_set.get(name=name) + ol.max_value += l.max_value + ol.save() diff --git a/modoboa/parameters/tools.py b/modoboa/parameters/tools.py index b7c173ae3..1be26d468 100644 --- a/modoboa/parameters/tools.py +++ b/modoboa/parameters/tools.py @@ -174,7 +174,6 @@ def get_global_parameter(name, app=None, **kwargs): :param name: the parameter's name :param app: the application owning the parameter :return: the corresponding value - """ if app is None: app = guess_extension_name() diff --git a/modoboa/relaydomains/constants.py b/modoboa/relaydomains/constants.py new file mode 100644 index 000000000..951bfa1af --- /dev/null +++ b/modoboa/relaydomains/constants.py @@ -0,0 +1,12 @@ +"""Relaydomains app constants.""" + +PERMISSIONS = { + "Resellers": [ + ("relaydomains", "relaydomain", "add_relaydomain"), + ("relaydomains", "relaydomain", "change_relaydomain"), + ("relaydomains", "relaydomain", "delete_relaydomain"), + ("relaydomains", "service", "add_service"), + ("relaydomains", "service", "change_service"), + ("relaydomains", "service", "delete_service") + ] +} diff --git a/modoboa/relaydomains/general_callbacks.py b/modoboa/relaydomains/general_callbacks.py index f1342d564..56f3e7de3 100644 --- a/modoboa/relaydomains/general_callbacks.py +++ b/modoboa/relaydomains/general_callbacks.py @@ -1,46 +1,11 @@ """General event callbacks.""" -from django.conf import settings -from django.template import Template, Context from django.utils.translation import ugettext_lazy, ugettext as _ from modoboa.lib import events from .forms import RelayDomainWizardStep, RelayDomainFormGeneral -PERMISSIONS = { - "Resellers": [ - ("relaydomains", "relaydomain", "add_relaydomain"), - ("relaydomains", "relaydomain", "change_relaydomain"), - ("relaydomains", "relaydomain", "delete_relaydomain"), - ("relaydomains", "service", "add_service"), - ("relaydomains", "service", "change_service"), - ("relaydomains", "service", "delete_service") - ] -} - - -@events.observe("GetExtraRolePermissions") -def extra_permissions(rolename): - """Return extra permissions for Resellers.""" - return PERMISSIONS.get(rolename, []) - - -@events.observe('GetStaticContent') -def static_content(caller, st_type, user): - if caller != 'domains' or st_type != 'js': - return [] - - t = Template(""" - -""") - return [t.render(Context({'STATIC_URL': settings.STATIC_URL}))] - @events.observe('ExtraDomainFilters') def extra_domain_filters(): diff --git a/modoboa/relaydomains/handlers.py b/modoboa/relaydomains/handlers.py index bd326199e..c2d5adcee 100644 --- a/modoboa/relaydomains/handlers.py +++ b/modoboa/relaydomains/handlers.py @@ -1,13 +1,16 @@ """Django signal handlers for relaydomains.""" +from django.conf import settings from django.db.models import signals from django.dispatch import receiver +from django.template import Template, Context from modoboa.admin import models as admin_models from modoboa.admin.signals import use_external_recipients from modoboa.core import signals as core_signals from modoboa.lib.email_utils import split_mailbox +from . import constants from . import models from . import postfix_maps @@ -52,3 +55,26 @@ def register_postfix_maps(sender, **kwargs): postfix_maps.SplitedDomainsTransportMap, postfix_maps.RelayRecipientVerification ] + + +@receiver(core_signals.extra_role_permissions) +def extra_role_permissions(sender, role, **kwargs): + """Add permissions to the Resellers group.""" + return constants.PERMISSIONS.get(role, []) + + +@receiver(core_signals.extra_static_content) +def static_content(sender, caller, st_type, user, **kwargs): + """Add extra static content.""" + if caller != "domains" or st_type != "js": + return [] + + t = Template(""" + +""") + return [t.render(Context({"STATIC_URL": settings.STATIC_URL}))] diff --git a/modoboa/urls.py b/modoboa/urls.py index 8568c5558..e01bc7268 100644 --- a/modoboa/urls.py +++ b/modoboa/urls.py @@ -2,7 +2,7 @@ from django.conf import settings from django.conf.urls import patterns, include, url from modoboa.core.extensions import exts_pool -from modoboa.lib import events +from modoboa.core import signals as core_signals urlpatterns = patterns( @@ -22,10 +22,14 @@ urlpatterns += patterns( *exts_pool.load_all() ) -urlpatterns += patterns( - '', - *events.raiseQueryEvent("ExtraUprefsRoutes") -) +extra_routes = core_signals.extra_uprefs_routes.send(sender="urls") +if extra_routes: + extra_routes = reduce( + lambda a, b: a + b, [route for route in extra_routes]) + urlpatterns += patterns( + '', + *extra_routes + ) # API urls urlpatterns += patterns(