This commit is contained in:
Antoine Nguyen
2016-11-19 16:45:09 +01:00
parent 5349b4f6a5
commit 9ee200ea5b
35 changed files with 645 additions and 640 deletions
-4
View File
@@ -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."""
+1 -1
View File
@@ -18,7 +18,7 @@ ADMIN_EVENTS = [
"DomainDeleted",
"DomainOwnershipRemoved",
"GetTagsForDomain",
"ExtraDomainMenuEntries",
"ExtraDomainFilters",
"ExtraDomainQsetFilters",
"ExtraDomainTypes",
-201
View File
@@ -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)
+11 -7
View File
@@ -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()
+3 -2
View File
@@ -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
+186 -2
View File
@@ -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)
)
-16
View File
@@ -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)
+5 -1
View File
@@ -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"])
+17 -7
View File
@@ -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))
+1 -2
View File
@@ -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)
)
+1 -65
View File
@@ -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"
+1 -32
View File
@@ -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"
+41
View File
@@ -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"],
]
}
+6 -3
View File
@@ -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
}
+56 -3
View File
@@ -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",
}]
@@ -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
+15 -43
View File
@@ -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):
+31
View File
@@ -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"])
+1 -1
View File
@@ -12,7 +12,7 @@
<script type="text/javascript" src="{% static 'js/listing.js' %}"></script>
<script type="text/javascript" src="{% static 'js/twocols_nav.js' %}"></script>
<script type="text/javascript">
{{ extrajs|safe }}
{% extra_static_content "user_index" "js" user %}
$(document).ready(function() {
var userprefs = new TwocolsNav({
deflocation: "profile/",
+33 -21
View File
@@ -1,17 +1,20 @@
"""Custom tags for Core application."""
import os
import pkg_resources
import re
from django import template
from django.contrib.sessions.models import Session
from django.conf import settings
from django.core.urlresolvers import reverse
from django.template.loader import render_to_string
from django.utils import timezone
from django.utils.safestring import mark_safe
from django.utils.translation import ugettext as _
from modoboa.lib import events
from django.contrib.sessions.models import Session
from .. import signals
register = template.Library()
@@ -19,8 +22,9 @@ register = template.Library()
@register.simple_tag
def core_menu(selection, user):
"""Build the top level menu."""
entries = (
events.raiseQueryEvent("AdminMenuDisplay", "top_menu", user))
entries = signals.extra_admin_menu_entries.send(
sender="core_menu", location="top_menu", user=user)
entries = reduce(lambda a, b: a + b, [entry[1] for entry in entries])
if user.is_superuser:
entries += [
{"name": "settings",
@@ -38,8 +42,10 @@ def core_menu(selection, user):
@register.simple_tag
def extensions_menu(selection, user):
menu = events.raiseQueryEvent("UserMenuDisplay", "top_menu", user)
return render_to_string('common/menulist.html', {
menu = signals.extra_user_menu_entries.send(
sender="core_menu", location="top_menu", user=user)
menu = reduce(lambda a, b: a + b, [entry[1] for entry in menu])
return render_to_string("common/menulist.html", {
"selection": selection, "entries": menu, "user": user
})
@@ -83,13 +89,16 @@ def user_menu(user, selection):
]}
]
entries[0]["menu"] += \
events.raiseQueryEvent("UserMenuDisplay", "options_menu", user) \
+ [{"name": "logout",
"url": reverse("core:logout"),
"label": _("Logout"),
"img": "fa fa-sign-out"}]
extra_entries = signals.extra_user_menu_entries.send(
sender="user_menu", location="options_menu", user=user)
extra_entries = reduce(
lambda a, b: a + b, [entry[1] for entry in extra_entries])
entries[0]["menu"] += (
extra_entries + [{"name": "logout",
"url": reverse("core:logout"),
"label": _("Logout"),
"img": "fa fa-sign-out"}]
)
return render_to_string("common/menulist.html", {
"selection": selection, "entries": entries, "user": user
})
@@ -114,9 +123,13 @@ def uprefs_menu(selection, user):
"url": "api/",
"label": _("API"),
})
entries += events.raiseQueryEvent("UserMenuDisplay", "uprefs_menu", user)
extra_entries = signals.extra_user_menu_entries.send(
sender="user_menu", location="uprefs_menu", user=user)
extra_entries = reduce(
lambda a, b: a + b, [entry[1] for entry in extra_entries])
entries += extra_entries
entries = sorted(entries, key=lambda e: e["label"])
return render_to_string('common/menu.html', {
return render_to_string("common/menu.html", {
"entries": entries,
"css": "nav nav-sidebar",
"selection": selection,
@@ -157,7 +170,6 @@ def visirule(field):
@register.simple_tag
def get_version():
import pkg_resources
return pkg_resources.get_distribution("modoboa").version
@@ -201,9 +213,6 @@ def connected_users(parser, token):
@register.simple_tag
def get_modoboa_logo():
import os
from django.conf import settings
try:
logo = settings.MODOBOA_CUSTOM_LOGO
except AttributeError:
@@ -215,9 +224,12 @@ def get_modoboa_logo():
@register.simple_tag
def load_optionalmenu(user):
menu = events.raiseQueryEvent("UserMenuDisplay", "top_menu_middle", user)
menu = signals.extra_user_menu_entries.send(
sender="user_menu", location="top_menu_middle", user=user)
menu = reduce(
lambda a, b: a + b, [entry[1] for entry in menu])
return template.loader.render_to_string(
'common/menulist.html',
"common/menulist.html",
{"entries": menu, "user": user}
)
+9 -7
View File
@@ -1,6 +1,5 @@
"""
Views available to super administrators only.
"""
"""Views available to super administrators only."""
from django.contrib.auth.decorators import (
login_required, user_passes_test
)
@@ -10,13 +9,14 @@ from django.utils.translation import ugettext as _
from modoboa.core.models import Log
from modoboa.core.utils import check_for_updates
from modoboa.lib import events
from modoboa.lib.listing import get_sort_order, get_listing_page
from modoboa.lib.web_utils import (
_render_to_string, render_to_json_response
)
from modoboa.parameters import tools as param_tools
from .. import signals
@login_required
@user_passes_test(lambda u: u.is_superuser)
@@ -117,6 +117,8 @@ def check_top_notifications(request):
"""
AJAX service to check for new top notifications to display.
"""
return render_to_json_response(
events.raiseQueryEvent("TopNotifications", request, True)
)
notifications = signals.get_top_notifications.send(
sender="top_notifications", include_all=True)
notifications = reduce(
lambda a, b: a + b, [notif[1] for notif in notifications])
return render_to_json_response(notifications)
+14 -9
View File
@@ -3,18 +3,20 @@
import logging
from django.contrib.auth import authenticate, login, logout
from django.core.urlresolvers import reverse
from django.http import HttpResponse, HttpResponseRedirect
from django.utils import translation
from django.utils.translation import ugettext as _
from django.views.decorators.cache import never_cache
from django.contrib.auth import authenticate, login, logout
from modoboa.core.forms import LoginForm
from modoboa.lib import events
from modoboa.lib.web_utils import _render_to_string
from .base import find_nextlocation
from .. import signals
logger = logging.getLogger("modoboa.auth")
@@ -40,9 +42,10 @@ def dologin(request):
logger.info(
_("User '%s' successfully logged in") % user.username
)
events.raiseEvent("UserLogin", request,
form.cleaned_data["username"],
form.cleaned_data["password"])
signals.user_login.send(
sender="dologin",
username=form.cleaned_data["username"],
password=form.cleaned_data["password"])
return HttpResponseRedirect(find_nextlocation(request, user))
error = _(
"Your username and password didn't match. Please try again.")
@@ -59,20 +62,22 @@ def dologin(request):
nextlocation = request.GET.get("next", None)
httpcode = 200
announcements = signals.get_announcements.send(
sender="login", location="loginpage")
announcements = [announcement[1] for announcement in announcements]
return HttpResponse(_render_to_string(request, "registration/login.html", {
"form": form, "error": error, "next": nextlocation,
"annoucements": events.raiseQueryEvent("GetAnnouncement", "loginpage")
"annoucements": announcements
}), status=httpcode)
dologin = never_cache(dologin)
def dologout(request):
"""Logout the current user.
"""
"""Logout current user."""
if not request.user.is_anonymous():
events.raiseEvent("UserLogout", request)
signals.user_logout.send(sender="dologout")
logger = logging.getLogger("modoboa.auth")
logger.info(_("User '%s' logged out") % request.user.username)
logger.info(_("User {} logged out").format(request.user.username))
logout(request)
return HttpResponseRedirect(reverse("core:login"))
+7 -6
View File
@@ -16,22 +16,23 @@ from modoboa.lib.web_utils import (
from modoboa.parameters import tools as param_tools
from ..forms import ProfileForm, APIAccessForm
from .. import signals
@login_required
def index(request, tplname="core/user_index.html"):
extrajs = events.raiseQueryEvent("ExtraUprefsJS", request.user)
return render(request, tplname, {
"selection": "user",
"extrajs": "".join(extrajs)
})
"""Render user index page."""
# FIXME: replace ExtraUprefsJS in autoreply
return render(request, tplname, {"selection": "user"})
@login_required
def profile(request, tplname='core/user_profile.html'):
"""Profile detail/update view."""
update_password = True
if True in events.raiseQueryEvent("PasswordChange", request.user):
results = signals.allow_password_change.send(
sender="profile", user=request.user)
if True in [result[1] for result in results]:
update_password = False
if request.method == "POST":
+9 -9
View File
@@ -5,31 +5,31 @@ from django.contrib.auth.models import Group, Permission
from django.contrib.contenttypes.models import ContentType
from modoboa.core import constants as core_constants
from modoboa.core import signals as core_signals
from modoboa.core.models import ObjectAccess, User
from modoboa.lib import events
def get_account_roles(user, account=None):
"""Return the list of available account roles.
This function is used to create or modify an account. The returned
list can be extended by listening to the ``GetExtraRoles`` event.
This function is used to create or modify an account.
:param ``User`` user: connected user
:param ``User`` account: account beeing modified (None on creation)
:return: list of strings
"""
result = [core_constants.SIMPLEUSERS_ROLE]
filters = events.raiseQueryEvent(
"UserCanSetRole", user, "DomainAdmins", account
)
if user.has_perm("admin.add_domain") and \
(not filters or True in filters):
filters = core_signals.user_can_set_role.send(
sender="get_account_roles", user=user, role="DomainAdmins",
account=account)
condition = (
user.has_perm("admin.add_domain") and
(not filters or True in [flt[1] for flt in filters]))
if condition:
result += [core_constants.DOMAINADMINS_ROLE]
if user.is_superuser:
result += [
core_constants.RESELLERS_ROLE, core_constants.SUPERADMINS_ROLE]
result += events.raiseQueryEvent("GetExtraRoles", user, account)
return sorted(result, key=lambda role: role[1])
+6 -5
View File
@@ -8,7 +8,7 @@ from django.template import Template, Context
from django.utils.safestring import mark_safe
from django.utils.translation import ugettext as _
from modoboa.lib import events
from modoboa.core import signals as core_signals
register = template.Library()
@@ -89,11 +89,12 @@ def extra_static_content(caller, st_type, user):
tpl = template.Template(
"{% for sc in static_content %}{{ sc|safe }}{% endfor %}"
)
static_content = core_signals.extra_static_content.send(
sender="extra_static_content",
caller=caller, st_type=st_type, user=user)
static_content = [result[1] for result in static_content]
return tpl.render(
template.Context({
'static_content': events.raiseQueryEvent(
"GetStaticContent", caller, st_type, user)
})
template.Context({"static_content": static_content})
)
-1
View File
@@ -16,7 +16,6 @@ def load_limits_settings():
param_tools.registry.add("global", ParametersForm, _("Limits"))
events.declare(EVENTS)
from . import controls
from . import general_callbacks
-75
View File
@@ -1,75 +0,0 @@
# coding: utf-8
"""Event handlers that check if limits are reached."""
from modoboa.lib import events
from modoboa.lib.permissions import get_object_owner
from modoboa.parameters import tools as param_tools
from . import utils
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.
"""
if not owner.is_superuser:
for name, ltpl in utils.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()
@events.observe("UserCanSetRole")
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
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 ``User`` account: account modified (None on creation)
:param str newrole: role to check
"""
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]
@events.observe("AccountModified")
def on_account_modified(old, new):
"""Update limits when roles are updated"""
owner = get_object_owner(old)
if owner.role not in ["SuperAdmins", "Resellers"]:
# Domain admins can't change the role so nothing to check.
return
if new.role not in ["DomainAdmins", "Resellers"]:
move_pool_resource(owner, new)
@events.observe("AccountDeleted")
def on_account_deleted(account, byuser, **kwargs):
owner = get_object_owner(account)
if owner.role not in ["SuperAdmins", "Resellers"]:
return
move_pool_resource(owner, account)
-62
View File
@@ -10,35 +10,6 @@ from . import forms
from . import utils
@events.observe("ExtraAdminContent")
def display_pool_usage(user, target, currentpage):
condition = (
not param_tools.get_global_parameter("enable_admin_limits") or
target != "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))
]
@events.observe("ExtraAccountForm")
def extra_account_form(user, account=None):
if not param_tools.get_global_parameter("enable_admin_limits"):
@@ -102,36 +73,3 @@ def fill_account_instances(user, account, instances):
if account.role not in ["Resellers", "DomainAdmins"]:
return
instances["resources"] = account
@events.observe("GetStaticContent")
def get_static_content(caller, st_type, user):
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 ["""<style>
.resource {
padding: 10px 15px;
}
.resource .progress {
margin-bottom: 0px;
}
.resource .progress .bar {
color: #000000;
}
</style>
"""]
return ["""
<script type="text/javascript">
$(document).ready(function() {
$(".progress").tooltip();
});
</script>
"""]
+116
View File
@@ -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 ["""<style>
.resource {
padding: 10px 15px;
}
.resource .progress {
margin-bottom: 0px;
}
.resource .progress .bar {
color: #000000;
}
</style>
"""]
return ["""
<script type="text/javascript">
$(document).ready(function() {
$(".progress").tooltip();
});
</script>
"""]
@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)
+15
View File
@@ -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()
-1
View File
@@ -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()
+12
View File
@@ -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")
]
}
-35
View File
@@ -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("""<script src="{{ STATIC_URL }}relaydomains/js/relay_domains.js" type="text/javascript"></script>
<script type="text/javascript">
var rdomain;
$(document).ready(function() {
rdomain = new RelayDomains({});
});
</script>
""")
return [t.render(Context({'STATIC_URL': settings.STATIC_URL}))]
@events.observe('ExtraDomainFilters')
def extra_domain_filters():
+26
View File
@@ -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("""<script src="{{ STATIC_URL }}relaydomains/js/relay_domains.js" type="text/javascript"></script>
<script type="text/javascript">
var rdomain;
$(document).ready(function() {
rdomain = new RelayDomains({});
});
</script>
""")
return [t.render(Context({"STATIC_URL": settings.STATIC_URL}))]
+9 -5
View File
@@ -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(