From 3681bb4198aaf7da96f38dd448ddfcffaeac5163 Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Fri, 20 Jan 2012 17:33:05 +0100 Subject: [PATCH] wip: resellers and limits I've finally started to rethink the database structure. The goal I'm trying to reach: only use User objects to manage permissions and accesses to the webui and associate other objects to them if needed (like mailboxes). see #105 --- admin/__init__.py | 4 +- admin/fixtures/initial_data.json | 2 +- admin/forms.py | 160 +++++++---- admin/lib.py | 112 ++++++++ .../migrations/0015_auto__add_objectowner.py | 125 +++++++++ admin/models.py | 41 +-- admin/permissions.py | 73 +++-- admin/tables.py | 21 +- admin/templatetags/admin_extras.py | 17 ++ admin/urls.py | 77 +++--- admin/views.py | 254 +++++++++++++----- extensions/limits/controls.py | 90 +++++-- extensions/limits/fixtures/initial_data.json | 2 +- extensions/limits/forms.py | 52 ++-- extensions/limits/lib.py | 7 +- extensions/limits/main.py | 44 +-- extensions/limits/models.py | 36 ++- extensions/limits/permissions.py | 50 ++++ extensions/limits/tables.py | 3 +- extensions/limits/templates/limits/pool.html | 8 +- extensions/limits/templatetags/limits_tags.py | 10 +- extensions/limits/urls.py | 3 - extensions/limits/views.py | 87 ++---- lib/events.py | 64 ++++- lib/exceptions.py | 18 ++ lib/ldaputils.py | 1 + lib/middleware.py | 13 + static/css/box.css | 27 +- static/css/default.css | 7 +- static/js/confirmation.js | 7 +- static/js/form.js | 250 +++++++++++++---- static/js/permissions.js | 4 + static/js/utils.js | 1 + templates/admin/add_domain_admin.html | 73 +++-- templates/admin/add_permission.html | 10 +- templates/admin/adminform.html | 7 +- templates/admin/create_domain_admin.html | 9 + templates/admin/domain_admin_promotion.html | 10 + templates/admin/listing.html | 3 +- userprefs/templatetags/uprefs_extras.py | 3 +- userprefs/views.py | 4 +- 41 files changed, 1311 insertions(+), 478 deletions(-) create mode 100644 admin/migrations/0015_auto__add_objectowner.py create mode 100644 extensions/limits/permissions.py create mode 100644 lib/exceptions.py create mode 100644 templates/admin/create_domain_admin.html create mode 100644 templates/admin/domain_admin_promotion.html diff --git a/admin/__init__.py b/admin/__init__.py index 20e90bee8..0010e8ad6 100644 --- a/admin/__init__.py +++ b/admin/__init__.py @@ -51,13 +51,13 @@ parameters.register_admin("DEFAULT_TOP_REDIRECTION", type="list", deflt="admin", values=enabled_applications(), help=_("The default redirection used when no application is specified")) -def unset_default_topredirection(**kwargs): +def unset_default_topredirection(extension): """ Simple callback to change the default redirection if the corresponding extension is being disabled. """ topredirection = parameters.get_admin("DEFAULT_TOP_REDIRECTION", app="general") - if topredirection == kwargs["ext"].name: + if topredirection == extension.name: parameters.save_admin("DEFAULT_TOP_REDIRECTION", "userprefs", app="general") def update_available_applications(extension): diff --git a/admin/fixtures/initial_data.json b/admin/fixtures/initial_data.json index c7d22a2a1..94985fe96 100644 --- a/admin/fixtures/initial_data.json +++ b/admin/fixtures/initial_data.json @@ -1 +1 @@ -[{"pk": 1, "model": "auth.group", "fields": {"name": "DomainAdmins", "permissions": [["add_mailbox", "admin", "mailbox"], ["change_mailbox", "admin", "mailbox"], ["delete_mailbox", "admin", "mailbox"], ["add_alias", "admin", "alias"], ["change_alias", "admin", "alias"], ["delete_alias", "admin", "alias"], ["view_domain", "admin", "domain"], ["view_mailboxes", "admin", "mailbox"], ["view_aliases", "admin", "alias"], ["view_domaliases", "admin", "domainalias"], ["add_domainalias", "admin", "domainalias"], ["change_domainalias", "admin", "domainalias"], ["delete_domainalias", "admin", "domainalias"]]}}] +[{"pk": 1, "model": "auth.group", "fields": {"name": "DomainAdmins", "permissions": [["add_mailbox", "admin", "mailbox"], ["change_mailbox", "admin", "mailbox"], ["delete_mailbox", "admin", "mailbox"], ["add_alias", "admin", "alias"], ["change_alias", "admin", "alias"], ["delete_alias", "admin", "alias"], ["view_domain", "admin", "domain"], ["view_mailboxes", "admin", "mailbox"], ["view_aliases", "admin", "alias"], ["view_domaliases", "admin", "domainalias"], ["add_domainalias", "admin", "domainalias"], ["change_domainalias", "admin", "domainalias"], ["delete_domainalias", "admin", "domainalias"]]}},{"pk": 2, "model": "auth.group", "fields": {"name": "SimpleUsers", "permissions": []}}] diff --git a/admin/forms.py b/admin/forms.py index 9567a7dc7..16c66c777 100644 --- a/admin/forms.py +++ b/admin/forms.py @@ -7,6 +7,7 @@ from django.contrib.auth.models import User from modoboa.admin.templatetags.admin_extras import gender from modoboa.lib import tables from modoboa.lib.emailutils import split_mailbox +from lib import get_user_domains class DomainForm(forms.ModelForm): class Meta: @@ -32,10 +33,10 @@ class ProxyForm(forms.ModelForm): def __init__(self, user, *args, **kwargs): super(ProxyForm, self).__init__(*args, **kwargs) - if user.is_superuser: - self._domain = None + if not user.is_superuser: + self._domains = Domain.objects.filter(owners__user=user) else: - self._domain = user.mailbox_set.all()[0].domain + self._domains = None class DomainAliasForm(ProxyForm): class Meta: @@ -44,8 +45,9 @@ class DomainAliasForm(ProxyForm): def __init__(self, *args, **kwargs): super(DomainAliasForm, self).__init__(*args, **kwargs) - if self._domain is not None: - self.fields["target"].queryset = Domain.objects.filter(pk=self._domain.id) + if self._domains is not None: + #self.fields["target"].queryset = Domain.objects.filter(pk=self._domain.id) + self.fields["target"].queryset = self._domains class MailboxForm(ProxyForm): quota = forms.IntegerField(label=ugettext_noop("Quota"), required=False, @@ -68,9 +70,10 @@ class MailboxForm(ProxyForm): super(MailboxForm, self).__init__(*args, **kwargs) for f in ['name', 'address', 'quota', 'password1', 'password2']: self.fields[f].widget.attrs['size'] = 14 - if self._domain is not None: - self.fields["domain"].queryset = Domain.objects.filter(pk=self._domain.id) - self.fields["domain"].initial = self._domain + if self._domains is not None: + self.fields["domain"].queryset = self._domains + #self.fields["domain"].queryset = Domain.objects.filter(pk=self._domain.id) + #self.fields["domain"].initial = self._domain if kwargs.has_key("instance"): self.fields['quota'].initial = kwargs["instance"].quota @@ -111,22 +114,26 @@ class AliasForm(ProxyForm): # member of Alias, we need to fill it manually. if # self._domain exists, it means a DomainAdmin is going to # create a new alias, we know how to fill the fields. - if self._domain is not None: - self.fields["domain"].queryset = Domain.objects.filter(pk=self._domain.id) + # if self._domain is not None: + # self.fields["domain"].queryset = Domain.objects.filter(pk=self._domain.id) + if self._domains is not None: + self.fields["domain"].queryset = self._domains + # if 'instance' exists, it means we are modifying an existing # record, we need to select the right value for 'domain' and # to fill the first "targets" input (because it is created by the # Form object, not by us). - domain = None - if self._domain: - domain = self._domain + #domain = None + # if self._domain: + # domain = self._domain if kwargs.has_key("instance"): - if self._domain is None: - domain = kwargs["instance"].domain + # if self._domain is None: + # domain = kwargs["instance"].domain self.fields["targets"].initial = kwargs["instance"].first_target() - if domain is not None: - self.fields["domain"].initial = domain + + # if domain is not None: + # self.fields["domain"].initial = domain self.fields.keyOrder = ['address', 'domain', 'enabled', 'targets'] @@ -140,33 +147,37 @@ class AliasForm(ProxyForm): return self.cleaned_data["address"] def set_targets(self, user, values): + """Targets dispatching + + We make a difference between 'local' targets (the ones hosted + by Modoboa) and 'external targets. + """ + from modoboa.admin.lib import is_object_owner + self.ext_targets = [] self.int_targets = [] - if user.id != 1: - umb = Mailbox.objects.get(user=user) + for addr in values: if addr == "": continue - local_part, domain = split_mailbox(addr) - if domain is None: + local_part, domname = split_mailbox(addr) + if domname is None: raise AdminError("%s %s" % (_("Invalid mailbox"), addr)) - if is_domain_admin(user) and umb.domain.name != domain: - try: - d = Domain.objects.get(name=domain) - except Domain.DoesNotExist: - pass - else: - raise AdminError("%s %s" % (_("Access denied for"), addr)) try: - mb = Mailbox.objects.get(address=local_part, domain__name=domain) - except Mailbox.DoesNotExist: - self.ext_targets += [addr] - else: - if not user.is_superuser: - usermb = Mailbox.objects.get(user=user.id) - if usermb.domain.id != mb.domain.id: - raise AdminError("%s %s" % (_("Permission denied on"), addr)) + domain = Domain.objects.get(name=domname) + except Domain.DoesNotExist: + domain = None + if domain: + if not is_object_owner(user, domain): + raise PermDeniedException(addr) + try: + mb = Mailbox.objects.get(domain=domain, address=local_part) + except Mailbox.DoesNotExist: + raise AdminError(_("Mailbox %s does not exist" % addr)) self.int_targets += [mb] + continue + + self.ext_targets += [addr] def save(self, force_insert=False, force_update=False, commit=True): a = super(AliasForm, self).save(commit=False) @@ -186,19 +197,72 @@ class SuperAdminForm(forms.Form): self.fields["user"].queryset = User.objects.exclude(pk__in=[1, user.id]) class DomainAdminForm(forms.Form): - def __init__(self, *args, **kwargs): - super(DomainAdminForm, self).__init__(*args, **kwargs) + createfrom = forms.ChoiceField( + choices=[("new", ugettext_noop("Create a new account")), + ("select", ugettext_noop("Promote an existing account"))], + widget=forms.RadioSelect) - self.fields["domain"] = \ - forms.ModelChoiceField(queryset=Domain.objects.all(), - label=_("Domain"), - required=True, - empty_label=_("Select a domain"), - help_text=_("Select a domain in the list")) - self.fields["user"] = \ - forms.ChoiceField(label=_("User"), required=True, - choices=[("", _("Empty"))], - help_text=_("Select a user in the list")) + domain = forms.ModelChoiceField( + queryset=Domain.objects.all(), + label=_("Domain"), + required=True, + empty_label=ugettext_noop("Select a domain"), + help_text=ugettext_noop("Select a domain in the list") + ) + + user = forms.ChoiceField( + label=ugettext_noop("User"), required=True, + choices=[("", ugettext_noop("Empty"))], + help_text=ugettext_noop("Select a user in the list")) + +class UserForm(forms.ModelForm): + createmb = forms.BooleanField( + label=ugettext_noop("Create mailbox"), required=False, + help_text=ugettext_noop("Create a mailbox for this user using the given address") + ) + + class Meta: + model = User + fields = ("username", "first_name", "last_name", "email") + + def save(self, commit=True, group=None): + user = super(UserForm, self).save(commit=False) + if self.cleaned_data.has_key("password1"): + user.set_password(self.cleaned_data["password1"]) + if commit: + user.save() + if group: + user.groups.add(Group.objects.get(name=group)) + user.save() + return user + +class UserWithPasswordForm(UserForm): + password1 = forms.CharField(label=_("Password"), widget=forms.PasswordInput) + password2 = forms.CharField( + label=ugettext_noop("Confirmation"), + widget=forms.PasswordInput, + help_text=ugettext_noop("Enter the same password as above, for verification.") + ) + + def clean_password2(self): + password1 = self.cleaned_data.get("password1", "") + password2 = self.cleaned_data["password2"] + if password1 != password2: + raise forms.ValidationError(_("The two password fields didn't match.")) + return password2 + +class DomainAdminPromotionForm(forms.Form): + name = forms.CharField( + label=ugettext_noop("Name"), + help_text=ugettext_noop("The username/email address/name of the account to promote") + ) + + def clean_name(self): + try: + u = User.objects.get(username=self.cleaned_data["name"]) + except User.DoesNotExist: + raise forms.ValidationError(_("Unknown user")) + return self.cleaned_data["name"] class ImportDataForm(forms.Form): sourcefile = forms.FileField(label=_("Select a file")) diff --git a/admin/lib.py b/admin/lib.py index c1cf1cc9d..1e631cb52 100644 --- a/admin/lib.py +++ b/admin/lib.py @@ -1,12 +1,21 @@ # coding: utf-8 +from functools import wraps from django.http import HttpResponse, HttpResponseRedirect from django.contrib.auth.models import User, Group from django.utils.translation import ugettext as _ from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger from models import * +from modoboa.lib.exceptions import ModoboaException from modoboa.lib.webutils import _render_error, _render from modoboa.admin.tables import * +class AdminError(ModoboaException): + """ + Just a Custom exception to identify errors coming from the admin + application. + """ + pass + def is_not_localadmin(errortpl="error"): def dec(f): def wrapped_f(request, *args, **kwargs): @@ -46,6 +55,7 @@ def good_domain(f): def dec(request, **kwargs): if request.user.is_superuser: return f(request, **kwargs) + if is_domain_admin(request.user): mb = Mailbox.objects.get(user=request.user.id) access = True @@ -67,6 +77,18 @@ def good_domain(f): return HttpResponseRedirect("%s?next=%s" % (login_url, path)) return dec +# def has_access_to_domain(user, domain): +# """ +# """ +# access = events.raiseQueryEvent("CanViewDomain", user, domain) +# if True in access: +# return True +# if is_domain_admin(user): +# mb = Mailbox.objects.get(user=request.user.id) +# if mb.domain == domain: +# return True +# return False + def render_listing(request, objtype, tplname="admin/listing.html", **kwargs): """Common function to render a listing @@ -103,3 +125,93 @@ def render_listing(request, objtype, tplname="admin/listing.html", kwargs["table"] = tblclass(request, kwargs["objects"].object_list) return _render(request, tplname, kwargs) + +def set_object_ownership(user, obj): + """Simple shorcut to associate an object to a user + + :param user: a ``User`` object + :param obj: an admin. object (Domain, Mailbox, ...) + """ + obj_owner = ObjectOwner(user=user, content_object=obj) + obj_owner.save() + +def is_object_owner(user, obj): + """Check if a user is the object's owner + + An object can have multiple owners but this function only + concerns one given user. + + :param user: a ``User`` object + :param obj: a admin object + :return: a boolean + """ + if user.is_superuser: + return True + + qs = user.objectowner_set.filter(content_type__app_label="admin", + content_type__model=obj._meta.module_name) + for entry in qs.all(): + if entry.content_object == obj: + return True + return False + +def check_domain_ownership(f): + @wraps(f) + def dec(request, **kwargs): + if not request.user.is_superuser: + domain = None + domid = request.GET.get("domid", None) + if domid: + domain = Domain.objects.get(pk=domid) + else: + mbid = request.GET.get("mbid", None) + if mbid: + domain = Mailbox.objects.get(pk=mbid).domain + if domain and not is_object_owner(request.user, domain): + raise PermDeniedError(_("You can't access this domain")) + + return f(request, **kwargs) + return dec + +def get_user_domains(user): + if user.is_superuser: + return Domain.objects.all() + + qs = user.objectowner_set.filter(content_type__app_label="admin", + content_type__model="domain") + domains = [] + for entry in qs.all(): + domains += [entry.content_object] + return domains + +def get_user_domaliases(user): + if user.is_superuser: + return DomainAlias.objects.all() + qs = user.objectowner_set.filter(content_type__app_label="admin", + content_type__model="domain") + domaliases = [] + for entry in qs.all(): + domaliases += entry.content_object.domainalias_set.all() + return domaliases + +def get_user_mailboxes(user): + if user.is_superuser: + return Mailbox.objects.all() + + qs = user.objectowner_set.filter(content_type__app_label="admin", + content_type__model="domain") + mboxes = [] + for ro in qs.all(): + mboxes += Mailbox.objects.filter(domain=ro.content_object) + return mboxes + +def get_user_mbaliases(user): + if user.is_superuser: + return Alias.objects.all() + + qs = user.objectowner_set.filter(content_type__app_label="admin", + content_type__model="domain") + mbaliases = [] + for ro in qs.all(): + mbaliases += Alias.objects.filter(domain=ro.content_object) + return mbaliases diff --git a/admin/migrations/0015_auto__add_objectowner.py b/admin/migrations/0015_auto__add_objectowner.py new file mode 100644 index 000000000..cc5fa4ad1 --- /dev/null +++ b/admin/migrations/0015_auto__add_objectowner.py @@ -0,0 +1,125 @@ +# encoding: utf-8 +import datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + +class Migration(SchemaMigration): + + def forwards(self, orm): + + # Adding model 'ObjectOwner' + db.create_table('admin_objectowner', ( + ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('user', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['auth.User'])), + ('content_type', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['contenttypes.ContentType'])), + ('object_id', self.gf('django.db.models.fields.PositiveIntegerField')()), + )) + db.send_create_signal('admin', ['ObjectOwner']) + + + def backwards(self, orm): + + # Deleting model 'ObjectOwner' + db.delete_table('admin_objectowner') + + + models = { + 'admin.alias': { + 'Meta': {'object_name': 'Alias'}, + 'address': ('django.db.models.fields.CharField', [], {'max_length': '254'}), + 'dates': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['admin.ObjectDates']"}), + 'domain': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['admin.Domain']"}), + 'enabled': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'extmboxes': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'mboxes': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['admin.Mailbox']", 'symmetrical': 'False'}) + }, + 'admin.domain': { + 'Meta': {'object_name': 'Domain'}, + 'dates': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['admin.ObjectDates']"}), + 'enabled': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '100'}), + 'quota': ('django.db.models.fields.IntegerField', [], {}) + }, + 'admin.domainalias': { + 'Meta': {'object_name': 'DomainAlias'}, + 'dates': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['admin.ObjectDates']"}), + 'enabled': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '100'}), + 'target': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['admin.Domain']"}) + }, + 'admin.extension': { + 'Meta': {'object_name': 'Extension'}, + 'enabled': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '150'}) + }, + 'admin.mailbox': { + 'Meta': {'object_name': 'Mailbox'}, + 'address': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'dates': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['admin.ObjectDates']"}), + 'domain': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['admin.Domain']"}), + 'gid': ('django.db.models.fields.IntegerField', [], {}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'password': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'path': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'quota': ('django.db.models.fields.IntegerField', [], {}), + 'uid': ('django.db.models.fields.IntegerField', [], {}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}) + }, + 'admin.objectdates': { + 'Meta': {'object_name': 'ObjectDates'}, + 'creation': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'last_modification': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}) + }, + 'admin.objectowner': { + 'Meta': {'object_name': 'ObjectOwner'}, + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'object_id': ('django.db.models.fields.PositiveIntegerField', [], {}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}) + }, + 'auth.group': { + 'Meta': {'object_name': 'Group'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), + 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) + }, + 'auth.permission': { + 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'}, + 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + 'auth.user': { + 'Meta': {'object_name': 'User'}, + 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) + }, + 'contenttypes.contenttype': { + 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, + 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + } + } + + complete_apps = ['admin'] diff --git a/admin/models.py b/admin/models.py index 98593d0c7..6dc9fbd42 100644 --- a/admin/models.py +++ b/admin/models.py @@ -1,31 +1,28 @@ # coding: utf-8 from django.db import models, IntegrityError -from django.contrib.auth.models import User +from django.contrib.contenttypes.models import ContentType +from django.contrib.contenttypes import generic +from django.contrib.auth.models import User, Group from django.utils.translation import ugettext as _, ugettext_noop from django.conf import settings from modoboa.lib import parameters, events from modoboa.lib.sysutils import exec_cmd, exec_as_vuser from modoboa.lib.emailutils import split_mailbox -from modoboa.lib.ldaputils import * from modoboa.auth.lib import crypt_password import os import pwd -class AdminError(Exception): - """Custom exception +try: + from modoboa.lib.ldaputils import * + ldap_available = True +except ImportError: + ldap_available = False - - """ - def __init__(self, value): - """Constructor - - :param value: the information contained in this exception. - """ - self.value = str(value) - - def __str__(self): - """String representation""" - return self.value +class ObjectOwner(models.Model): + user = models.ForeignKey(User) + content_type = models.ForeignKey(ContentType) + object_id = models.PositiveIntegerField() + content_object = generic.GenericForeignKey('content_type', 'object_id') class ObjectDates(models.Model): """Dates recording for admin objects @@ -80,6 +77,7 @@ class Domain(DatesAware): quota = models.IntegerField(help_text=ugettext_noop("Default quota in MB applied to mailboxes")) enabled = models.BooleanField(ugettext_noop('enabled'), help_text=ugettext_noop("Check to activate this domain")) + owners = generic.GenericRelation(ObjectOwner) class Meta: permissions = ( @@ -265,6 +263,10 @@ class Mailbox(DatesAware): raise AdminError(_("Mailbox with this address already exists")) self.user = user + if len(user.groups.all()) == 0: + user.groups.add(Group.objects.get(name="SimpleUsers")) + user.save() + if kwargs.has_key("password") and kwargs["password"] != u"é": self.password = crypt_password(kwargs["password"]) # If we ever had numerical uid, don't resolve it @@ -305,6 +307,8 @@ class Mailbox(DatesAware): self.password = crypt_password(newvalue) self.save() return + if not ldap_available: + raise AdminError(_("Failed to update password: LDAP module not installed")) ab = LDAPAuthBackend() try: @@ -456,11 +460,12 @@ class Alias(DatesAware): return "
".join(self.extmboxes.split(",")) def ui_disabled(self, user): + from modoboa.admin.lib import is_object_owner + if user.is_superuser: return False - usermb = Mailbox.objects.get(user=user.id) for mb in self.mboxes.all(): - if mb.domain.id != usermb.domain.id: + if not is_object_owner(user, mb.domain): return True return False diff --git a/admin/permissions.py b/admin/permissions.py index 6065061cb..fc6894edb 100644 --- a/admin/permissions.py +++ b/admin/permissions.py @@ -7,11 +7,15 @@ from modoboa.lib.webutils import _render, getctx, _render_to_string from modoboa.lib import events from forms import * from tables import SuperAdminsTable, DomainAdminsTable +from lib import set_object_ownership class Permissions(object): + submit_label = ugettext_noop("Add") + formtpl = 'admin/add_permission.html' + @staticmethod def is_authorized(user): - return True + return user.is_superuser def get_add_form(self, request): raise NotImplementedError @@ -25,21 +29,20 @@ class Permissions(object): def get(self, request): raise NotImplementedError + def _render_form(self, request, form, instring=False): + ctx = dict(form=form, title=self.title, submit_label=self.submit_label, + role=self.role) + if instring: + return _render_to_string(request, self.formtpl, ctx) + return _render(request, self.formtpl, ctx) + class SuperAdminsPerms(Permissions): title = ugettext_noop("Add super administrator") role = "super_admins" - - @staticmethod - def is_authorized(user): - return user.is_superuser def get_add_form(self, request): form = SuperAdminForm(request.user) - return _render(request, 'admin/add_permission.html', { - "form" : form, - "title" : _(self.title), - "role": self.role - }) + return self._render_form(request, form) def add(self, request): form = SuperAdminForm(request.user, request.POST) @@ -51,11 +54,7 @@ class SuperAdminsPerms(Permissions): user.save() return True, None - content = _render_to_string(request, "admin/add_permission.html", { - "form" : form, - "title" : _(self.title), - "role" : self.role - }) + content = self._render_form(request, form, True) return False, getctx("ko", content=content) def delete(self, selection): @@ -81,22 +80,19 @@ class SuperAdminsPerms(Permissions): class DomainAdminsPerms(Permissions): title = ugettext_noop("Add domain administrator") role = "domain_admins" - + formtpl = 'admin/add_domain_admin.html' + @staticmethod def is_authorized(user): return user.has_perm("admin.view_domains") def get_add_form(self, request): form = DomainAdminForm() - return _render(request, 'admin/add_domain_admin.html', { - "form" : form, - "title" : _(self.title), - "role" : self.role - }) + return self._render_form(request, form) def add(self, request): form = DomainAdminForm(request.POST) - if request.POST.has_key("user") and request.POST["user"] != "": + if request.POST.get("user", "") != "": mboxid = request.POST['user'] mb = Mailbox.objects.get(pk=mboxid) form.fields["user"].choices = [(mboxid, mb),] @@ -105,27 +101,28 @@ class DomainAdminsPerms(Permissions): mb.user.date_joined = datetime.datetime.now() mb.user.groups.add(Group.objects.get(name="DomainAdmins")) mb.user.save() + set_object_ownership(mb.user, mb.domain) return True, None - content = _render_to_string(request, "admin/add_domain_admin.html", { - "form" : form, - "title" : _(self.title), - "role" : self.role - }) + content = self._render_form(request, form, True) return False, getctx("ko", content=content) def delete(self, selection): - for s in selection: - mbox = Mailbox.objects.get(pk=int(s)) - grp = Group.objects.get(name="DomainAdmins") - mbox.user.groups.remove(grp) - mbox.user.save() + User.objects.filter(id__in=selection).delete() + # for s in selection: + # user.delete() + # try: + # mbox = Mailbox.objects.get(pk=int(s)) + # grp = Group.objects.get(name="DomainAdmins") + # mbox.user.groups.remove(grp) + # mbox.user.save() def get(self, request): - domadmins = Mailbox.objects.filter(user__groups__name="DomainAdmins") - domadmins_list = [] - for admin in domadmins: - domadmins_list += [admin.tohash()] - return DomainAdminsTable(request, domadmins_list).render() + #domadmins = Mailbox.objects.filter(user__groups__name="DomainAdmins") + domainadmins = User.objects.filter(groups__name="DomainAdmins") + # domadmins_list = [] + # for admin in domadmins: + # domadmins_list += [admin.tohash()] + return DomainAdminsTable(request, domainadmins).render() def get_perms_class(user, role): """Retrieves the class associated to a given role @@ -150,7 +147,7 @@ def get_perms_class(user, role): if globals()[cname].is_authorized(user): return globals()[cname] return None - res = events.raiseQueryEvent("PermsGetClass", role=role) + res = events.raiseQueryEvent("PermsGetClass", role) if not len(res): return None return res[0] diff --git a/admin/tables.py b/admin/tables.py index 3617cadb9..0648f94cd 100644 --- a/admin/tables.py +++ b/admin/tables.py @@ -142,11 +142,22 @@ class SuperAdminsTable(tables.Table): class DomainAdminsTable(tables.Table): idkey = "id" - selection = tables.SelectionColumn("selection", width="4%", first=True) - domain = tables.Column("domain", label=ugettext_noop("Domain")) - full_name = tables.Column("full_name", label=ugettext_noop("Full name")) + selection = tables.SelectionColumn("selection", width="4%") + username = tables.Column("username", label=ugettext_noop("User name")) + first_name = tables.Column("first_name", label=ugettext_noop("First name")) + last_name = tables.Column("last_name", label=ugettext_noop("Last name")) date_joined = tables.Column("date_joined", label=ugettext_noop("Defined")) - enabled = tables.Column("enabled", label=gender("Enabled", "m"), width="10%") + enabled = tables.Column("is_active", label=gender("Enabled", "m"), width="10%") + actions = tables.ActionColumn("actions", label=ugettext_noop("Actions"), + width="50px", align="center", + defvalue=domain_admin_actions) - cols_order = ["selection", "domain", "full_name", "date_joined", "enabled"] + cols_order = ["selection", "username", "first_name", + "last_name", "date_joined", "enabled", "actions"] + def __init__(self, request, users): + super(DomainAdminsTable, self).__init__(request) + self.populate(self._rows_from_model(users)) + + def parse_is_active(self, value): + return _("yes") if value else _("no") diff --git a/admin/templatetags/admin_extras.py b/admin/templatetags/admin_extras.py index 5796f6461..9d2c5ca66 100644 --- a/admin/templatetags/admin_extras.py +++ b/admin/templatetags/admin_extras.py @@ -199,6 +199,18 @@ def mailbox_actions(user, mboxid): ] return render_actions(actions) +@register.simple_tag +def domain_admin_actions(user, daid): + actions = [ + {"name" : "editdomainadmin", + "url" : reverse(admin.views.edit_domain_admin, args=[daid]), + "img" : static_url("pics/edit.png"), + "title" : _("Edit domain admin"), + "class" : "boxed", + "rel" : "{handler:'iframe',size:{x:350,y:320}}"}, + ] + return render_actions(actions) + @register.simple_tag def mbalias_actions(user, aliasid): from modoboa.admin.models import Alias @@ -274,3 +286,8 @@ def gender(value, target): if trans.find("_") == -1: return trans return value + +@register.simple_tag +def get_footer_content(user, objtype): + res = events.raiseQueryEvent("AdminFooterDisplay", user, objtype) + return "".join(res) diff --git a/admin/urls.py b/admin/urls.py index 51c7ebc35..ba5351fba 100644 --- a/admin/urls.py +++ b/admin/urls.py @@ -1,38 +1,43 @@ from django.conf.urls.defaults import * -urlpatterns = patterns('modoboa.admin.views', - url(r'^$', 'domains', name="index"), - url(r'^domains/$', 'domains', name="domains"), - (r'^domains/new/', 'newdomain'), - (r'^domains/(?P\d+)/edit/$', 'editdomain'), - (r'^domains/delete/$', 'deldomain'), - (r'^domains/import/$', 'importdata'), - - (r'^domaliases/$', "domaliases"), - (r'^domaliases/new/', 'newdomalias'), - (r'^domaliases/edit/(?P\d+)/', 'editdomalias'), - (r'^domaliases/delete/$', 'deldomalias'), - - (r'^mailboxes/$', 'mailboxes'), - (r'^mailboxes/new/$', 'newmailbox'), - (r'^mailboxes/edit/(?P\d+)/$', 'editmailbox'), - (r'^mailboxes/delete/$', 'delmailbox'), - (r'^mailboxes/search/$', 'mailboxes_search'), - - (r'^mbaliases/$', 'mbaliases'), - (r'^mbaliases/new/$', 'newmbalias'), - (r'^mbaliases/edit/(?P\d+)/$', 'editmbalias'), - (r'^mbaliases/delete/$', 'delmbalias'), - - (r'^domains/(?P\d+)/rawmboxes/$', 'mailboxes_raw'), - - (r'^settings/$', 'viewparameters'), - (r'^settings/parameters/$', 'viewparameters'), - (r'^settings/parameters/save/$', 'saveparameters'), - (r'^settings/extensions/$', 'viewextensions'), - (r'^settings/extensions/save/$', 'saveextensions'), - - (r'^permissions/$', 'permissions'), - (r'^permissions/add/$', 'add_permission'), - (r'^permissions/delete/$', 'delete_permissions'), - ) +urlpatterns = patterns( + 'modoboa.admin.views', + url(r'^$', 'domains', name="index"), + url(r'^domains/$', 'domains', name="domains"), + (r'^domains/new/', 'newdomain'), + (r'^domains/(?P\d+)/edit/$', 'editdomain'), + (r'^domains/delete/$', 'deldomain'), + (r'^domains/import/$', 'importdata'), + + (r'^domaliases/$', "domaliases"), + (r'^domaliases/new/', 'newdomalias'), + (r'^domaliases/edit/(?P\d+)/', 'editdomalias'), + (r'^domaliases/delete/$', 'deldomalias'), + + (r'^mailboxes/$', 'mailboxes'), + (r'^mailboxes/new/$', 'newmailbox'), + (r'^mailboxes/edit/(?P\d+)/$', 'editmailbox'), + (r'^mailboxes/delete/$', 'delmailbox'), + (r'^mailboxes/search/$', 'mailboxes_search'), + + (r'^mbaliases/$', 'mbaliases'), + (r'^mbaliases/new/$', 'newmbalias'), + (r'^mbaliases/edit/(?P\d+)/$', 'editmbalias'), + (r'^mbaliases/delete/$', 'delmbalias'), + + (r'^domains/(?P\d+)/rawmboxes/$', 'mailboxes_raw'), + + (r'^settings/$', 'viewparameters'), + (r'^settings/parameters/$', 'viewparameters'), + (r'^settings/parameters/save/$', 'saveparameters'), + (r'^settings/extensions/$', 'viewextensions'), + (r'^settings/extensions/save/$', 'saveextensions'), + + (r'^permissions/$', 'permissions'), + (r'^permissions/add/$', 'add_permission'), + (r'^permissions/domainadmin/new/$', 'create_domain_admin'), + (r'^permissions/domainadmin/edit/(?P\d+)/$', 'edit_domain_admin'), + (r'^permissions/domainadmin/promote/$', 'domain_admin_promotion'), + (r'^permissions/searchaccount/$', 'search_account'), + (r'^permissions/delete/$', 'delete_permissions'), + ) diff --git a/admin/views.py b/admin/views.py index f9a9aad9d..a7b554bd1 100644 --- a/admin/views.py +++ b/admin/views.py @@ -16,6 +16,7 @@ from models import * from modoboa.admin.permissions import * from modoboa.admin.tables import * from modoboa.lib import events, parameters +from modoboa.lib.exceptions import * from modoboa.auth.lib import crypt_password from modoboa.lib.webutils \ import _render, ajax_response, ajax_simple_response, getctx @@ -31,10 +32,7 @@ def domains(request): return HttpResponseRedirect(reverse(userprefs.views.preferences)) - if request.user.is_superuser: - domains = Domain.objects.all() - else: - domains = events.raiseQueryEvent("GetUserDomains", request.user) + domains = get_user_domains(request.user) for dom in domains: dom.mbalias_counter = len(Alias.objects.filter(domain=dom.id)) deloptions = {"keepdir" : _("Do not delete domain directory")} @@ -53,7 +51,6 @@ def _validate_domain(request, form, successmsg, commonctx, """ error = None if form.is_valid(): - events.raiseEvent("CanCreateDomain", request.user) try: domain = form.save() except AdminError, e: @@ -74,7 +71,9 @@ def _validate_domain(request, form, successmsg, commonctx, @permission_required("admin.add_domain") @transaction.commit_on_success def newdomain(request, tplname="admin/adminform.html"): + events.raiseEvent("CanCreateDomain", request.user) def newdomain_cb(user, domain): + set_object_ownership(user, domain) events.raiseEvent("CreateDomain", user, domain) commonctx = {"title" : _("New domain"), @@ -94,6 +93,9 @@ def newdomain(request, tplname="admin/adminform.html"): @permission_required("admin.change_domain") def editdomain(request, dom_id, tplname="admin/adminform.html"): domain = Domain.objects.get(pk=dom_id) + if not is_object_owner(request.user, domain): + raise PermDeniedException(_("You can't edit this domain")) + commonctx = {"title" : _("Domain editing"), "submit_label" : _("Update"), "action" : reverse(editdomain, args=[dom_id]), @@ -108,47 +110,47 @@ def editdomain(request, dom_id, tplname="admin/adminform.html"): @login_required @permission_required("admin.delete_domain") +@transaction.commit_on_success def deldomain(request): selection = request.GET["selection"].split(",") error = None + keepdir = True if request.GET.get("keepdir", "false") == "true" else False try: mb = Mailbox.objects.get(user__id=request.user.id) except Mailbox.DoesNotExist: - pass - else: - if str(mb.domain.id) in selection: - error = _("You can't delete your own domain") + mb = None - if error is None: - if request.GET.has_key("keepdir") and request.GET["keepdir"] == "true": - keepdir = True - else: - keepdir = False - for dom in Domain.objects.filter(id__in=selection): - events.raiseEvent("DeleteDomain", dom) - dom.delete(keepdir=keepdir) - msg = ungettext("Domain deleted", "Domains deleted", len(selection)) - messages.info(request, msg, fail_silently=True) - return ajax_response(request) - return ajax_response(request, status="ko", respmsg=error) + for dom in Domain.objects.filter(id__in=selection): + if not is_object_owner(request.user, dom): + raise PermDeniedException(_("You can't delete this domain")) + if mb and mb.domain == dom: + raise AdminError(_("You can't delete your own domain")) + + events.raiseEvent("DeleteDomain", request.user, dom) + dom.delete(keepdir=keepdir) + + msg = ungettext("Domain deleted", "Domains deleted", len(selection)) + messages.info(request, msg, fail_silently=True) + return ajax_response(request) @login_required @permission_required("admin.view_domaliases") +@check_domain_ownership def domaliases(request): if request.GET.has_key("domid"): domain = Domain.objects.get(pk=request.GET["domid"]) domaliases = DomainAlias.objects.filter(target=request.GET["domid"]) else: domain = None - domaliases = DomainAlias.objects.all() + domaliases = get_user_domaliases(request.user) return render_listing(request, "domaliases", title=_("Domain aliases"), rel="310 190", objects=domaliases) @login_required -@good_domain @permission_required("admin.add_domainalias") +@transaction.commit_on_success def newdomalias(request, tplname="admin/adminform.html"): commonctx = {"title" : _("New domain alias"), "submit_label" : _("Create"), @@ -158,6 +160,7 @@ def newdomalias(request, tplname="admin/adminform.html"): form = DomainAliasForm(request.user, request.POST) error = None if form.is_valid(): + events.raiseEvent("CanCreateDomainAlias", request.user) domalias = form.save(commit=False) if Domain.objects.filter(name=domalias.name): error = _("A domain with this name already exists") @@ -167,6 +170,7 @@ def newdomalias(request, tplname="admin/adminform.html"): except IntegrityError: error = _("Alias with this name already exists") else: + events.raiseEvent("DomainAliasCreated", request.user, domalias) messages.info(request, _("Domain alias created"), fail_silently=True) return ajax_response(request, url=reverse(admin.views.domaliases)) @@ -181,10 +185,12 @@ def newdomalias(request, tplname="admin/adminform.html"): return _render(request, tplname, commonctx) @login_required -@good_domain @permission_required("admin.change_domainalias") def editdomalias(request, alias_id, tplname="admin/adminform.html"): domalias = DomainAlias.objects.get(pk=alias_id) + if not is_object_owner(request.user, domalias.target): + raise PermDeniedException(_("You do not have access to this domain")) + commonctx = {"title" : _("Domain alias editing"), "submit_label" : _("Update"), "action" : reverse(editdomalias, args=[alias_id]), @@ -213,18 +219,23 @@ def editdomalias(request, alias_id, tplname="admin/adminform.html"): return _render(request, tplname, commonctx) @login_required -@good_domain @permission_required("admin.delete_domainalias") +@transaction.commit_on_success def deldomalias(request): selection = request.GET["selection"].split(",") - DomainAlias.objects.filter(id__in=selection).delete() + for daid in selection: + domalias = DomainAlias.objects.get(pk=daid) + if not is_object_owner(request.user, domalias.target): + raise PermDeniedException(_("You don't have acces to this domain")) + events.raiseEvent("DomainAliasDeleted", request.user, domalias) + domalias.delete() msg = ungettext("Domain alias deleted", "Domain aliases deleted", len(selection)) messages.info(request, msg, fail_silently=True) return ajax_response(request) @login_required -@good_domain @permission_required("admin.view_mailboxes") +@check_domain_ownership def mailboxes(request): title = _("Available mailboxes") if request.GET.has_key("domid"): @@ -232,7 +243,7 @@ def mailboxes(request): mboxes = Mailbox.objects.filter(domain=domain.id) title += " (%s)" % domain.name else: - mboxes = Mailbox.objects.all() + mboxes = get_user_mailboxes(request.user) domain = None deloptions = {"keepdir" : _("Do not delete mailbox directory")} return render_listing(request, "mailboxes", @@ -255,8 +266,8 @@ def mailboxes_raw(request, dom_id=None): }) @login_required -@good_domain @permission_required("admin.view_mailboxes") +@check_domain_ownership def mailboxes_search(request): if request.method != "POST": return @@ -266,13 +277,15 @@ def mailboxes_search(request): query &= Q(domain__name__startswith=domain) if request.GET.has_key("domid"): query = Q(domain=request.GET["domid"]) & query + elif not request.user.is_superuser: + query = Q(domain__in=get_user_domains(request.user)) & query mboxes = Mailbox.objects.filter(query) result = map(lambda mb: mb.full_address, mboxes) return ajax_simple_response(result) @login_required -@good_domain @permission_required("admin.add_mailbox") +@transaction.commit_on_success def newmailbox(request, tplname="admin/adminform.html"): commonctx = {"title" : _("New mailbox"), "submit_label" : _("Create"), @@ -282,12 +295,17 @@ def newmailbox(request, tplname="admin/adminform.html"): form = MailboxForm(request.user, request.POST) error = None if form.is_valid(): + if not request.user.is_superuser: + if not is_object_owner(request.user, form.cleaned_data["domain"]): + raise PermDeniedException(_("You do not have access to this domain")) + + events.raiseEvent("CanCreateMailbox", request.user) try: mb = form.save() except AdminError as e: error = str(e) else: - events.raiseEvent("CreateMailbox", mb) + events.raiseEvent("CreateMailbox", request.user, mb) messages.info(request, _("Mailbox created"), fail_silently=True) return ajax_response(request, url=reverse(admin.views.mailboxes) + "?domid=%d" % mb.domain.id) @@ -301,10 +319,12 @@ def newmailbox(request, tplname="admin/adminform.html"): return _render(request, tplname, commonctx) @login_required -@good_domain @permission_required("admin.change_mailbox") def editmailbox(request, mbox_id=None, tplname="admin/adminform.html"): mb = Mailbox.objects.get(pk=mbox_id) + if not is_object_owner(request.user, mb.domain): + raise PermDeniedException(_("You do not have access to this domain")) + commonctx = {"title" : _("Mailbox editing"), "submit_label" : _("Create"), "action" : reverse(editmailbox, args=[mbox_id]), @@ -336,66 +356,63 @@ def editmailbox(request, mbox_id=None, tplname="admin/adminform.html"): return _render(request, tplname, commonctx) @login_required -@good_domain @permission_required("admin.delete_mailbox") +@transaction.commit_on_success def delmailbox(request): selection = request.GET["selection"].split(",") error = None - if not request.user.is_superuser: - mb = Mailbox.objects.get(user__pk=request.user.id) - if str(mb.id) in selection: - error = _("You can't delete your own mailbox") + keepdir = True if request.GET.get("keepdir", "false") == "true" else False + usermb = request.user.mailbox_set.all()[0] \ + if len(request.user.mailbox_set.all()) else None - if error is None: - if request.GET.has_key("keepdir") and request.GET["keepdir"] == "true": - keepdir = True - else: - keepdir = False - for mb in Mailbox.objects.filter(id__in=selection): - events.raiseEvent("DeleteMailbox", mb) - mb.delete(keepdir=keepdir) - msg = ungettext("Mailbox deleted", "Mailboxes deleted", len(selection)) - messages.info(request, msg, fail_silently=True) - return ajax_response(request) + for mb in Mailbox.objects.filter(id__in=selection): + if not is_object_owner(request.user, mb.domain): + raise PermDeniedException(_("You don't have access to this domain")) + if usermb and usermb == mb: + raise AdminError(_("You can't delete your own mailbox")) + events.raiseEvent("DeleteMailbox", request.user, mb) + mb.delete(keepdir=keepdir) - return ajax_response(request, status="ko", error=error) + msg = ungettext("Mailbox deleted", "Mailboxes deleted", len(selection)) + messages.info(request, msg, fail_silently=True) + return ajax_response(request) @login_required -@good_domain @permission_required("admin.view_aliases") +@check_domain_ownership def mbaliases(request): if request.GET.has_key("domid"): aliases = Alias.objects.filter(domain=request.GET["domid"]) elif request.GET.has_key("mbid"): aliases = Alias.objects.filter(mboxes__id=request.GET["mbid"]).distinct() else: - aliases = Alias.objects.all() + aliases = get_user_mbaliases(request.user) return render_listing(request, "mbaliases", title=_("Mailbox aliases"), rel="320 300", objects=aliases) -def _validate_mbalias(request, form, successmsg, tplname, commonctx): +def _validate_mbalias(request, form, successmsg, tplname, commonctx, callback=None): """Mailbox alias validation Common function shared between creation and modification actions. """ error = None if form.is_valid(): + if not request.POST.has_key("targets"): + raise AdminError(_("No target defined")) + form.set_targets(request.user, request.POST.getlist("targets")) try: - if not request.POST.has_key("targets"): - raise AdminError(_("No target defined")) - form.set_targets(request.user, request.POST.getlist("targets")) - try: - alias = form.save() - except IntegrityError: - raise AdminError(_("Alias with this name already exists")) + alias = form.save() + except IntegrityError: + raise AdminError(_("Alias with this name already exists")) - messages.info(request, successmsg, fail_silently=True) - return ajax_response(request, url=reverse(admin.views.mbaliases)) - except AdminError, e: - error = str(e) + if callback: + callback(request.user, alias) + + messages.info(request, successmsg, fail_silently=True) + return ajax_response(request, url=reverse(admin.views.mbaliases)) if request.POST.has_key("targets"): targets = request.POST.getlist("targets") @@ -406,17 +423,21 @@ def _validate_mbalias(request, form, successmsg, tplname, commonctx): return ajax_response(request, status="ko", template=tplname, **commonctx) @login_required -@good_domain @permission_required("admin.add_alias") +@transaction.commit_on_success def newmbalias(request, tplname="admin/mbaliasform.html"): + events.raiseEvent("CanCreateMailboxAlias", request.user) commonctx = {"title" : _("New mailbox alias"), "submit_label" : _("Create"), "action" : reverse(newmbalias), "formid" : "mbaliasform"} if request.method == "POST": + def callback(user, mbalias): + events.raiseEvent("MailboxAliasCreated", user, mbalias) + form = AliasForm(request.user, request.POST) return _validate_mbalias(request, form, _("Mailbox alias created"), - tplname, commonctx) + tplname, commonctx, callback) form = AliasForm(request.user) commonctx["form"] = form @@ -424,10 +445,11 @@ def newmbalias(request, tplname="admin/mbaliasform.html"): return _render(request, tplname, commonctx) @login_required -@good_domain @permission_required("admin.change_alias") def editmbalias(request, alias_id, tplname="admin/mbaliasform.html"): alias = Alias.objects.get(pk=alias_id) + if not is_object_owner(request.user, alias.domain): + raise PermDeniedException(_("You do not have access to this domain")) commonctx = {"title" : _("Mailbox alias editing"), "submit_label" : _("Update"), "action" : reverse(editmbalias, args=[alias.id]), @@ -443,11 +465,17 @@ def editmbalias(request, alias_id, tplname="admin/mbaliasform.html"): return _render(request, tplname, commonctx) @login_required -@good_domain @permission_required("admin.delete_alias") +@transaction.commit_on_success def delmbalias(request): selection = request.GET["selection"].split(",") - Alias.objects.filter(id__in=selection).delete() + for mbaid in selection: + mbalias = Alias.objects.get(pk=mbaid) + if not is_object_owner(request.user, mbalias.domain): + raise PermDeniedException(_("You do not have access to this domain")) + events.raiseEvent("MailboxAliasDeleted", request.user, mbalias) + mbalias.delete() + msg = ungettext("Alias deleted", "Aliases deleted", len(selection)) messages.info(request, msg, fail_silently=True) return ajax_response(request) @@ -464,12 +492,12 @@ def permissions(request, tplname='admin/permissions.html'): "content" : SuperAdminsPerms().get(request)} ] - permtables += events.raiseQueryEvent("PermsGetTables", user=request.user) + permtables += events.raiseQueryEvent("PermsGetTables", request) permtables += [ {"id" : "domain_admins", "title" : _("Domain administrators"), - "rel" : "310 180", + "rel" : "350 250", "content" : DomainAdminsPerms().get(request)} ] @@ -492,10 +520,92 @@ def add_permission(request): status, data = pobj.add(request) if not status: return HttpResponse(simplejson.dumps(data), mimetype="application/json") - messages.info(request, _("Permission added."), - fail_silently=True) + try: + msg = pobj.add_success_msg + except AttributeError: + msg = _("Permission added") + messages.info(request, msg, fail_silently=True) return ajax_response(request, url=reverse(admin.views.permissions)) +@login_required +@permission_required("auth.view_permissions") +def create_domain_admin(request, tplname="admin/create_domain_admin.html"): + if request.method == "POST": + form = UserWithPasswordForm(request.POST) + if form.is_valid(): + user = form.save(group="DomainAdmins") + events.raiseEvent("DomainAdminCreated", user) + messages.info(request, _("Domain admin created"), fail_silently=True) + return ajax_response(request, url=reverse(admin.views.permissions)) + + return ajax_response(request, status="ko", template=tplname, form=form) + + form = UserWithPasswordForm() + return ajax_response(request, tplname, form=form) + +@login_required +@permission_required("auth.view_permissions") +def edit_domain_admin(request, da_id, tplname="admin/adminform.html"): + ctx = dict(title=_("Edit domain admin"), + action=reverse(edit_domain_admin, args=[da_id]), + formid="eda_form", + submit_label=_("Update")) + u = User.objects.get(pk=da_id, groups__name="DomainAdmins") + if request.method == "POST": + if request.POST.get("password1", "") != "" and \ + request.POST.get("password2", "") != "": + form = UserWithPasswordForm(request.POST, instance=u) + else: + form = UserForm(request.POST, instance=u) + if form.is_valid(): + form.save() + if form.cleaned_data["createmb"]: + pass + messages.info(request, _("Domain admin updated"), fail_silently=True) + return ajax_response(request, + url=reverse("modoboa.admin.views.permissions")) + ctx.update(form=form) + return ajax_response(request, status="ko", template=tplname, **ctx) + + form = UserWithPasswordForm(instance=u) + ctx.update(form=form) + return _render(request, tplname, ctx) + +@login_required +@permission_required("auth.view_permissions") +def domain_admin_promotion(request): + if request.method == "POST": + form = DomainAdminPromotionForm(request.POST) + if form.is_valid(): + u = User.objects.get(username=form.cleaned_data["name"]) + u.groups.add(Group.objects.get(name="DomainAdmins")) + u.save() + messages.info(request, _("Domain admin added"), fail_silently=True) + return ajax_response(request, url=reverse(admin.views.permissions)) + + form = DomainAdminPromotionForm() + return ajax_response(request, template="admin/domain_admin_promotion.html", + form=form) + +@login_required +@permission_required("admin.view_permissions") +@check_domain_ownership +def search_account(request): + search = request.POST.get("search", None) + if search is None: + return ajax_simple_response([]) + + query = Q(username__startswith=search) + query &= Q(is_superuser=False) + if not request.user.has_perm("admin.add_domain"): + query &= Q(groups__name="SimpleUsers") + else: + query &= ~Q(groups__name__in=["DomainAdmins"]) + + users = User.objects.filter(query) + result = map(lambda u: u.username, users.all()) + return ajax_simple_response(result) + @login_required def delete_permissions(request): pclass = get_perms_class(request.user, request.GET["role"]) diff --git a/extensions/limits/controls.py b/extensions/limits/controls.py index 067a224c2..4d9ac882f 100644 --- a/extensions/limits/controls.py +++ b/extensions/limits/controls.py @@ -1,15 +1,19 @@ # coding: utf-8 """ -:mod:`controls` --- provides event handlers to check if limits are reached --------------------------------------------------------------------------- +:mod:`controls` --- provides event handlers that check if limits are reached +---------------------------------------------------------------------------- """ - +from modoboa.admin.models import Mailbox +from modoboa.lib import events from lib import * from models import * -def check_domains_limit(user): - """Check if user has reached its limit for domains creation +def check_limit(user, lname): + """Check if a user has reached a defined limit + + We check if the user will not reach the given limit after this + creation. Raise a ``LimitReached`` exception if the limit is reached. @@ -17,28 +21,64 @@ def check_domains_limit(user): """ if not is_reseller(user): return - if not user.limitspool.inc_curvalue('domains_limit'): - raise LimitReached(user.limitspool.get_limit('domains_limit')) + try: + if user.limitspool.will_be_reached(lname): + raise LimitReached(user.limitspool.get_limit(lname)) + except LimitsPool.DoesNotExist: + pass -def associate_domain_to_reseller(user, domain): - if not is_reseller(user): - return - ro = ResellerObject(user=user, content_object=domain) - ro.save() +def inc_limit(user, lname): + try: + user.limitspool.inc_curvalue(lname) + except LimitsPool.DoesNotExist: + pass -def get_reseller_domains(user): - qs = user.resellerobject_set.filter(content_type__app_label="admin", - content_type__model="domain") - domains = [] - for ro in qs.all(): - domains += [ro.content_object] - return domains +def dec_limit(user, lname): + try: + user.limitspool.dec_curvalue(lname) + except LimitsPool.DoesNotExist: + pass -def check_domaliases_limit(user, domalias): - pass +@events.observe('CanCreateDomain') +def check_domains_limit(user): + check_limit(user, 'domains_limit') -def check_mboxes_limit(user, mbox): - pass +@events.observe('CreateDomain') +def inc_nb_domains(user, domain): + inc_limit(user, 'domains_limit') -def check_mbaliases_limit(user, mbalias): - pass +@events.observe('DeleteDomain') +def dec_nb_domains(user, domain): + dec_limit(user, 'domains_limit') + +@events.observe('CanCreateDomainAlias') +def check_domaliases_limit(user): + check_limit(user, 'domain_aliases_limit') + +@events.observe('DomainAliasCreated') +def inc_nb_domaliases(user, domalias): + inc_limit(user, 'domain_aliases_limit') + +@events.observe('DomainAliasDeleted') +def dec_nb_domaliases(user, domainalias): + dec_limit(user, 'domain_aliases_limit') + +@events.observe('CanCreateMailbox') +def check_mboxes_limit(user): + check_limit(user, 'mailboxes_limit') + +@events.observe('CreateMailbox') +def inc_nb_mailboxes(user, mailbox): + inc_limit(user, 'mailboxes_limit') + +@events.observe('DeleteMailbox') +def dec_nb_mailboxes(user, mailbox): + dec_limit(user, 'mailboxes_limit') + +@events.observe('CanCreateMailboxAlias') +def check_mbaliases_limit(user): + check_limit(user, 'mailbox_aliases_limit') + +@events.observe('MailboxAliasCreated') +def inc_nb_mbaliases(user, mailboxalias): + inc_limit(user, 'mailbox_aliases_limit') diff --git a/extensions/limits/fixtures/initial_data.json b/extensions/limits/fixtures/initial_data.json index 6bd24036c..7f05e0df4 100644 --- a/extensions/limits/fixtures/initial_data.json +++ b/extensions/limits/fixtures/initial_data.json @@ -1 +1 @@ -[{"pk": 2, "model": "auth.group", "fields": {"name": "Resellers", "permissions": [["add_domain", "admin", "domain"], ["change_domain", "admin", "domain"], ["delete_domain", "admin", "domain"], ["view_domains", "admin", "domain"], ["add_mailbox", "admin", "mailbox"], ["change_mailbox", "admin", "mailbox"], ["delete_mailbox", "admin", "mailbox"], ["add_alias", "admin", "alias"], ["change_alias", "admin", "alias"], ["delete_alias", "admin", "alias"], ["view_domain", "admin", "domain"], ["view_mailboxes", "admin", "mailbox"], ["view_aliases", "admin", "alias"], ["view_domaliases", "admin", "domainalias"], ["add_domainalias", "admin", "domainalias"], ["change_domainalias", "admin", "domainalias"], ["delete_domainalias", "admin", "domainalias"]]}}] +[{"pk": 3, "model": "auth.group", "fields": {"name": "Resellers", "permissions": [["add_domain", "admin", "domain"], ["change_domain", "admin", "domain"], ["delete_domain", "admin", "domain"], ["view_domains", "admin", "domain"], ["add_mailbox", "admin", "mailbox"], ["change_mailbox", "admin", "mailbox"], ["delete_mailbox", "admin", "mailbox"], ["add_alias", "admin", "alias"], ["change_alias", "admin", "alias"], ["delete_alias", "admin", "alias"], ["view_domain", "admin", "domain"], ["view_mailboxes", "admin", "mailbox"], ["view_aliases", "admin", "alias"], ["view_domaliases", "admin", "domainalias"], ["add_domainalias", "admin", "domainalias"], ["change_domainalias", "admin", "domainalias"], ["delete_domainalias", "admin", "domainalias"], ["view_permissions", "auth", "permission"]]}}] diff --git a/extensions/limits/forms.py b/extensions/limits/forms.py index 030b36d28..617fc7e54 100644 --- a/extensions/limits/forms.py +++ b/extensions/limits/forms.py @@ -3,28 +3,23 @@ from django import forms from django.utils.translation import ugettext_noop as _ from django.contrib.auth.models import User, Group +from modoboa.admin.forms import UserForm, UserWithPasswordForm from models import * -class ResellerForm(forms.ModelForm): - class Meta: - model = User - fields = ("username", "first_name", "last_name", "email") +class ResellerForm(UserForm): + def __init__(self, *args, **kwargs): + super(ResellerForm, self).__init__(*args, **kwargs) + del self.fields["createmb"] - def save(self, commit=True): - user = super(ResellerForm, self).save(commit=False) - if self.cleaned_data.has_key("password1"): - user.set_password(self.cleaned_data["password1"]) + def save(self, commit=True, group=None): + user = super(ResellerForm, self).save(commit, group) if commit: - user.save() - grp = Group.objects.get(name="Resellers") - if not grp in user.groups.all(): - user.groups.add(grp) - user.save() try: pool = user.limitspool except LimitsPool.DoesNotExist: pool = LimitsPool() pool.user = user + print pool.user pool.save() for lname in reseller_limits_tpl: @@ -35,18 +30,8 @@ class ResellerForm(forms.ModelForm): return user -class ResellerWithPasswordForm(ResellerForm): - password1 = forms.CharField(label=_("Password"), widget=forms.PasswordInput) - password2 = forms.CharField(label=_("Confirmation"), - widget=forms.PasswordInput, - help_text = _("Enter the same password as above, for verification.")) - - def clean_password2(self): - password1 = self.cleaned_data.get("password1", "") - password2 = self.cleaned_data["password2"] - if password1 != password2: - raise forms.ValidationError(_("The two password fields didn't match.")) - return password2 +class ResellerWithPasswordForm(ResellerForm, UserWithPasswordForm): + pass class ResellerPoolForm(forms.Form): domains_limit = forms.IntegerField(ugettext_noop("Max domains"), @@ -58,6 +43,23 @@ class ResellerPoolForm(forms.Form): mailbox_aliases_limit = forms.IntegerField(ugettext_noop("Max mailbox aliases"), help_text=ugettext_noop("Maximum number of mailbox aliases that can be created by this user")) + def check_limit_value(self, lname): + if self.cleaned_data[lname] < -1: + raise forms.ValidationError(_("Invalid limit")) + return self.cleaned_data[lname] + + def clean_domains_limit(self): + return self.check_limit_value("domains_limit") + + def clean_domain_aliases_limit(self): + return self.check_limit_value("domain_aliases_limit") + + def clean_mailboxes_limit(self): + return self.check_limit_value("mailboxes_limit") + + def clean_mailbox_aliases_limit(self): + return self.check_limit_value("mailbox_aliases_limit") + def load_from_user(self, user): for l in reseller_limits_tpl: self.fields[l].initial = user.limitspool.getmaxvalue(l) diff --git a/extensions/limits/lib.py b/extensions/limits/lib.py index 6a52e5c39..927979138 100644 --- a/extensions/limits/lib.py +++ b/extensions/limits/lib.py @@ -4,16 +4,19 @@ ------------------------------- """ - from django.contrib.auth.models import Group +from modoboa.lib.exceptions import ModoboaException -class LimitReached(Exception): +class LimitReached(ModoboaException): def __init__(self, limit): self.limit = limit def __str__(self): return "%s reached" % self.limit.name +class BadLimitValue(ModoboaException): + pass + def is_reseller(user): """Tell if a user is a reseller or not diff --git a/extensions/limits/main.py b/extensions/limits/main.py index 929d22e81..aaafb6090 100644 --- a/extensions/limits/main.py +++ b/extensions/limits/main.py @@ -11,18 +11,17 @@ from django.core.urlresolvers import reverse from django.conf.urls.defaults import include from modoboa.lib import events, parameters from modoboa.lib.webutils import static_url -from controls import * +from models import * +import permissions +import controls baseurl = "limits" def init(): - events.register("AdminMenuDisplay", menu) - events.register("CanCreateDomain", check_domains_limit) - events.register("CreateDomain", associate_domain_to_reseller) - events.register("GetUserDomains", get_reseller_domains) + pass def destroy(): - events.unregister("AdminMenuDisplay", menu) + pass def infos(): return { @@ -32,20 +31,25 @@ def infos(): "url" : baseurl } -def menu(target, user): - import views - - if target != "top_menu": - return [] - if not user.is_superuser: - return [] - return [ - {"name" : "resellers", - "label" : _("Resellers"), - "url" : reverse(views.resellers), - "img" : static_url("pics/resellers.png")} - ] - def urls(prefix): return (r'^%s%s/' % (prefix, baseurl), include('modoboa.extensions.limits.urls')) + +@events.observe('AdminFooterDisplay') +def display_pool_usage(user, objtype): + if objtype == "domaliases": + objtype = "domain_aliases" + elif objtype == "mbaliases": + objtype = "mailbox_aliases" + try: + l = user.limitspool.get_limit('%s_limit' % objtype) + except LimitsPool.DoesNotExist: + return [] + if l.maxvalue == -2: + label = _("undefined") + elif l.maxvalue == -1: + label = _("unlimited") + else: + label = str(l.maxvalue) + return [_("Pool usage: %s / %s") % (l.curvalue, label)] + diff --git a/extensions/limits/models.py b/extensions/limits/models.py index bcbe4693a..cbe76ada4 100644 --- a/extensions/limits/models.py +++ b/extensions/limits/models.py @@ -17,32 +17,42 @@ class LimitsPool(models.Model): user = models.OneToOneField(User) def getcurvalue(self, lname): - l = Limit.objects.get(name=lname) + l = self.limit_set.get(name=lname) return l.curvalue def getmaxvalue(self, lname): - l = Limit.objects.get(name=lname) + l = self.limit_set.get(name=lname) return l.maxvalue def inc_curvalue(self, lname, nb=1): - l = Limit.objects.get(name=lname) - if l.curvalue + nb > l.maxvalue: - return False + l = self.limit_set.get(name=lname) l.curvalue += nb l.save() - return True + + def dec_curvalue(self, lname, nb=1): + l = self.limit_set.get(name=lname) + if not l.curvalue: + return + l.curvalue -= nb + l.save() + + def will_be_reached(self, lname, nb=1): + l = self.limit_set.get(name=lname) + if l.maxvalue <= -1: + return False + if l.curvalue + nb > l.maxvalue: + return True + return False def get_limit(self, lname): - return Limit.objects.get(name=lname) + try: + return self.limit_set.get(name=lname) + except Limit.DoesNotExist: + return None class Limit(models.Model): name = models.CharField(max_length=255) curvalue = models.IntegerField(default=0) - maxvalue = models.IntegerField(default=0) + maxvalue = models.IntegerField(default=-2) pool = models.ForeignKey(LimitsPool) -class ResellerObject(models.Model): - user = models.ForeignKey(User) - content_type = models.ForeignKey(ContentType) - object_id = models.PositiveIntegerField() - content_object = generic.GenericForeignKey('content_type', 'object_id') diff --git a/extensions/limits/permissions.py b/extensions/limits/permissions.py new file mode 100644 index 000000000..ebdd4548a --- /dev/null +++ b/extensions/limits/permissions.py @@ -0,0 +1,50 @@ +# coding: utf-8 + +from django.utils.translation import ugettext as _, ugettext_noop +from django.contrib.auth.models import User +from modoboa.admin.permissions import Permissions +from modoboa.lib import events +from modoboa.lib.webutils import _render, _render_to_string +from tables import * +from forms import * + +class ResellersPerms(Permissions): + title = ugettext_noop("New reseller") + role = "resellers" + add_success_msg = ugettext_noop("Reseller added") + + def get_add_form(self, request): + form = ResellerWithPasswordForm() + return self._render_form(request, form) + + def add(self, request): + form = ResellerWithPasswordForm(request.POST) + error = None + if form.is_valid(): + form.save(group="Resellers") + return True, None + content = self._render_form(request, form, True) + return False, dict(status="ko", content=content) + + def delete(self, selection): + User.objects.filter(id__in=selection).delete() + + def get(self, request): + resellers = User.objects.filter(groups__name="Resellers") + return ResellersTable(request, resellers).render() + +@events.observe('PermsGetTables') +def get_resellers_table(request): + if not request.user.is_superuser: + return [] + return [{"id" : "resellers", + "title" : _("Resellers"), + "rel" : "350 300", + "content" : ResellersPerms().get(request)}] + +@events.observe("PermsGetClass") +def get_perms_class(role): + print role + if role != "resellers": + return [] + return [ResellersPerms] diff --git a/extensions/limits/tables.py b/extensions/limits/tables.py index 0ed3fd8b1..b4c7a2fce 100644 --- a/extensions/limits/tables.py +++ b/extensions/limits/tables.py @@ -8,6 +8,7 @@ class ResellersTable(tables.Table): tableid = "resellers_table" idkey = "id" + selection = tables.SelectionColumn("selection", width="4%") fname = tables.Column("first_name", label=ugettext_noop("First name")) lname = tables.Column("last_name", label=ugettext_noop("Last name")) username = tables.Column("username", label=ugettext_noop("Username")) @@ -16,7 +17,7 @@ class ResellersTable(tables.Table): width="50px", align="center", defvalue=reseller_actions) - cols_order = ["username", "fname", "lname", "created", "actions"] + cols_order = ["selection", "username", "fname", "lname", "created", "actions"] def __init__(self, request, users): super(ResellersTable, self).__init__(request) diff --git a/extensions/limits/templates/limits/pool.html b/extensions/limits/templates/limits/pool.html index f43491e1c..4f4156bf4 100644 --- a/extensions/limits/templates/limits/pool.html +++ b/extensions/limits/templates/limits/pool.html @@ -2,11 +2,17 @@ {% load i18n %} {% block html_head %} + {% endblock %} {% block content %}

{% trans "Limits pool for" %} {{ reseller.username }}

{% if error %}
{{ error }}
{% endif %} -
+ {% include "common/genericform.html" %}
diff --git a/extensions/limits/templatetags/limits_tags.py b/extensions/limits/templatetags/limits_tags.py index 739eb115b..e61597397 100644 --- a/extensions/limits/templatetags/limits_tags.py +++ b/extensions/limits/templatetags/limits_tags.py @@ -5,7 +5,7 @@ from django.utils.translation import ugettext as _ from django.template.loader import render_to_string from django.core.urlresolvers import reverse from modoboa.lib.webutils import static_url, render_actions -from modoboa.extensions import limits +from modoboa.extensions.limits import views register = template.Library() @@ -16,11 +16,11 @@ def resellers_menu(user): "img" : static_url("pics/add.png"), "class" : "boxed", "rel" : "{handler:'iframe',size:{x:350,y:300}}", - "url" : reverse(limits.views.new_reseller)}, + "url" : reverse(views.new_reseller)}, {"name" : "remove", "label" : _("Remove"), "img" : static_url("pics/remove.png"), - "url" : reverse(limits.views.delete_resellers) + "url" : reverse(views.delete_resellers) }] return render_to_string("common/menulist.html", {"entries" : entries, "user" : user}) @@ -29,13 +29,13 @@ def resellers_menu(user): def reseller_actions(user, resid): actions = [ {"name" : "editreseller", - "url" : reverse(limits.views.edit_reseller, args=[resid]), + "url" : reverse(views.edit_reseller, args=[resid]), "img" : static_url("pics/edit.png"), "title" : _("Edit reseller"), "class" : "boxed", "rel" : "{handler:'iframe',size:{x:350,y:300}}"}, {"name" : "editlimits", - "url" : reverse(limits.views.edit_limits_pool, args=[resid]), + "url" : reverse(views.edit_limits_pool, args=[resid]), "img" : static_url("pics/settings.png"), "title" : _("Edit limits allocated to this reseller"), "class" : "boxed", diff --git a/extensions/limits/urls.py b/extensions/limits/urls.py index cadf99fed..17f89d920 100644 --- a/extensions/limits/urls.py +++ b/extensions/limits/urls.py @@ -2,9 +2,6 @@ from django.conf.urls.defaults import * urlpatterns = patterns( 'modoboa.extensions.limits.views', - (r'^$', 'resellers'), - (r'reseller/new/$', 'new_reseller'), (r'reseller/edit/(?P\d+)/$', 'edit_reseller'), - (r'reseller/delete/$', 'delete_resellers'), (r'pool/edit/(?P\d+)/$', 'edit_limits_pool'), ) diff --git a/extensions/limits/views.py b/extensions/limits/views.py index 9c9475a4f..714c457e4 100644 --- a/extensions/limits/views.py +++ b/extensions/limits/views.py @@ -1,103 +1,58 @@ # coding: utf-8 -from django.utils.translation import ugettext as _, ungettext +from django.utils.translation import ugettext as _ from django.db import IntegrityError from django.contrib.auth.models import * from django.contrib import messages from django.contrib.auth.decorators \ import login_required, user_passes_test from django.core.urlresolvers import reverse -from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger -from tables import * from forms import * -from modoboa.lib import parameters from modoboa.lib.webutils import _render, ajax_response @login_required @user_passes_test(lambda u: u.is_superuser) -def resellers(request, tplname="limits/resellers.html"): - users = User.objects.filter(groups__name="Resellers") - paginator = Paginator(users, - int(parameters.get_admin("ITEMS_PER_PAGE", app="admin"))) - page = int(request.GET.get("page", "1")) - try: - subset = paginator.page(page) - except (EmptyPage, PageNotAnInteger): - subset = paginator.page(paginator.num_pages) - - ctx = dict(table=ResellersTable(request, subset.object_list), - page=subset, - paginator=paginator) - return _render(request, tplname, ctx) - -def validate_reseller(request, form, action, tplname="limits/reseller.html"): - error = None - if form.is_valid(): - try: - form.save() - except IntegrityError: - error = _("User with this Username already exists") - else: - msg = _("Reseller created") if action == "create" \ - else _("Reseller updated") - messages.info(request, msg, fail_silently=True) - return ajax_response(request, url=reverse(resellers)) - submit_label = _("Create") if action == "create" else _("Update") - ctx = dict(form=form, error=error, submit_label=submit_label) - return ajax_response(request, status="ko", template=tplname, **ctx) - -@login_required -@user_passes_test(lambda u: u.is_superuser) -def new_reseller(request, tplname="limits/reseller.html"): - if request.method == "POST": - form = ResellerWithPasswordForm(request.POST) - return validate_reseller(request, form, "create") - - form = ResellerWithPasswordForm() - ctx = dict(form=form, submit_label=_("Create")) - return _render(request, tplname, ctx) - -@login_required -@user_passes_test(lambda u: u.is_superuser) -def edit_reseller(request, resid, tplname="limits/reseller.html"): +def edit_reseller(request, resid, tplname="admin/add_permission.html"): reseller = User.objects.get(pk=resid) + ctx = dict(title=_("Edit reseller"), submit_label=_("Update"), + addurl=reverse(edit_reseller, args=[resid])) if request.method == "POST": if request.POST.get("password1", "") != "" and \ request.POST.get("password2", "") != "": form = ResellerWithPasswordForm(request.POST, instance=reseller) else: form = ResellerForm(request.POST, instance=reseller) - return validate_reseller(request, form, action="update") + if form.is_valid(): + try: + form.save() + except IntegrityError: + error = _("User with this Username already exists") + else: + messages.info(request, _("Reseller updated"), fail_silently=True) + return ajax_response(request, + url=reverse("modoboa.admin.views.permissions")) + + ctx.update(form=form, error=error) + return ajax_response(request, status="ko", template=tplname, **ctx) form = ResellerWithPasswordForm(instance=reseller) - ctx = dict(form=form, submit_label=_("Update")) + ctx.update(form=form) return _render(request, tplname, ctx) -@login_required -@user_passes_test(lambda u: u.is_superuser) -def delete_resellers(request): - selection = map(lambda idx: int(idx), request.GET.get("selection", "").split(",")) - if not len(selection): - raise Exception("") - - User.objects.filter(id__in=selection).delete() - msg = ungettext("Reselled deleted", "Resellers deleted", len(selection)) - messages.info(request, msg, fail_silently=True) - return ajax_response(request) - @login_required @user_passes_test(lambda u: u.is_superuser) def edit_limits_pool(request, resid, tplname="limits/pool.html"): reseller = User.objects.get(pk=resid) + ctx = dict(reseller=reseller) if request.method == "POST": form = ResellerPoolForm(request.POST) if form.is_valid(): form.save_new_limits(reseller.limitspool) messages.info(request, _("Pool updated"), fail_silently=True) - return ajax_response(request, url=reverse(resellers)) - ctx = dict(form=form) + return ajax_response(request, url=reverse("modoboa.admin.views.permissions")) + ctx.update(form=form) return ajax_response(request, status="ko", template=tplname, **ctx) form = ResellerPoolForm() form.load_from_user(reseller) - ctx = dict(form=form, reseller=reseller) + ctx.update(form=form) return _render(request, tplname, ctx) diff --git a/lib/events.py b/lib/events.py index 25d23004a..76e475d99 100644 --- a/lib/events.py +++ b/lib/events.py @@ -8,17 +8,30 @@ This module provides a simple way of managing events between Modoboa core application and additional components. """ +from functools import wraps +import inspect +import re events = [ "CanCreateDomain", "CreateDomain", "DeleteDomain", + + "CanCreateDomainAlias", + "DomainAliasCreated", + "DomainAliasDeleted", + + "CanCreateMailbox", "CreateMailbox", "DeleteMailbox", "ModifyMailbox", - "GetUserDomains", - + "CanCreateMailboxAlias", + "MailboxAliasCreated", + "MailboxAliasDeleted", + + "DomainAdminCreated", + "PasswordChange", "UserMenuDisplay", @@ -33,7 +46,9 @@ events = [ "UserLogin", "UserLogout", - "GetAnnouncement" + "GetAnnouncement", + + "AdminFooterDisplay", ] callbacks = {} @@ -54,6 +69,49 @@ def register(event, callback): callbacks[event].append(callback) return 1 +class observe(object): + """Event observing decorator + + Automatically register the decorated function to observe the given + event. If the decorated function is located into an extension, we + check before each call if the extension is enabled or not. If + that's not the case, the callback is not called. + + .. note:: + + That's not a really optimized behaviour but I haven't found + another solution to achieve that feature. + + :param evtname: the event's name + """ + def __init__(self, evtname): + self.evtname = evtname + + def __guess_extension_name(self, modname): + if modname.startswith('modoboa.extensions'): + m = re.match(r'modoboa\.extensions\.([^\.]+)', modname) + if m: + return m.group(1) + return None + + def __call__(self, f): + modname = inspect.getmodule(inspect.stack()[1][0]).__name__ + extname = self.__guess_extension_name(modname) + @wraps(f) + def wrapped_f(*args): + if extname: + from modoboa.admin.models import Extension + try: + ext = Extension.objects.get(name=extname) + except Extension.DoesNotExist: + return [] + if not ext.enabled: + return [] + return f(*args) + + register(self.evtname, wrapped_f) + return wrapped_f + def unregister(event, callback): """Unregister a callback for a specific event diff --git a/lib/exceptions.py b/lib/exceptions.py new file mode 100644 index 000000000..381064da9 --- /dev/null +++ b/lib/exceptions.py @@ -0,0 +1,18 @@ +# coding: utf-8 +""" +:mod:`exceptions` --- Custom Modoboa exceptions +----------------------------------------------- + +""" +from django.utils.translation import ugettext as _ + +class ModoboaException(Exception): + pass + +class PermDeniedException(ModoboaException): + def __init__(self, msg): + self.msg = msg + + def __str__(self): + return _("Permission denied: %s" % self.msg) + diff --git a/lib/ldaputils.py b/lib/ldaputils.py index 9d9f42143..aa1fff912 100644 --- a/lib/ldaputils.py +++ b/lib/ldaputils.py @@ -33,6 +33,7 @@ Extracted from `this blog one instance to another. """ +import sys import ldap import re import base64 diff --git a/lib/middleware.py b/lib/middleware.py index 7f74557e0..f4dab641f 100644 --- a/lib/middleware.py +++ b/lib/middleware.py @@ -3,6 +3,8 @@ import re from django.http import Http404, HttpResponseRedirect from modoboa.admin.models import Extension +from modoboa.lib.webutils import _render_error, ajax_response +from modoboa.lib.exceptions import ModoboaException class ExtControlMiddleware(object): def process_view(self, request, view, args, kwargs): @@ -22,3 +24,14 @@ class AjaxLoginRedirect(object): if type(response) == HttpResponseRedirect: response.status_code = 278 return response + +class CommonExceptionCatcher(object): + def process_exception(self, request, exception): + if not isinstance(exception, ModoboaException): + return None + + if not request.is_ajax(): + return _render_error(request, errortpl="error_simple", + user_context=dict(error=str(exception))) + + return ajax_response(request, status="ko", respmsg=str(exception)) diff --git a/static/css/box.css b/static/css/box.css index 9c0d49370..6c7c16e84 100644 --- a/static/css/box.css +++ b/static/css/box.css @@ -26,7 +26,7 @@ input { } input[type=text], input[type=password] { - width: 140px + width: 140px; } /*input:hover { @@ -102,6 +102,10 @@ td.even { font-weight: bold; } +.center { + text-align: center; +} + #id_username { width: 120px; } @@ -139,6 +143,15 @@ td.even { clear: both; } +ul { + list-style: none; + padding: 0; +} + +li { + display: inline; +} + /* * clearfix related */ @@ -164,3 +177,15 @@ html[xmlns] .clearfix { height: 1%; overflow: visible; } + +/* + * Domain admin form + */ +#createfrom { + margin: 10px auto; + width: auto; +} + +#permform > #id_name { + width: 180px; +} \ No newline at end of file diff --git a/static/css/default.css b/static/css/default.css index 6d5a5ed31..2df04d698 100644 --- a/static/css/default.css +++ b/static/css/default.css @@ -39,9 +39,14 @@ div#footer { font-size: 1em; } +#adminfo { + float: left; + padding: 5px; +} + #navbar { float: right; - padding: 0; + padding: 5px; margin: 0; text-align: right; } diff --git a/static/js/confirmation.js b/static/js/confirmation.js index cd3c9e75e..670e90cfa 100644 --- a/static/js/confirmation.js +++ b/static/js/confirmation.js @@ -66,7 +66,7 @@ var Confirmation = new Class({ } params += item.get("name") + "=true"; }); - new Request.JSON({ + new Request.JSON.mdb({ method: "get", url: Confirmation.url, onSuccess: function(res) { @@ -76,10 +76,7 @@ var Confirmation = new Class({ infobox.error(res.respmsg); infobox.hide(2); } - }, - onFailure: function(xhr) { - $(parent.document.body).set("html", xhr.responseText); - } + } }).send(params); }, diff --git a/static/js/form.js b/static/js/form.js index 7f7e62dac..f37a80356 100644 --- a/static/js/form.js +++ b/static/js/form.js @@ -1,3 +1,133 @@ +var Form = new Class({ + Implements: [Options], + + options: { + resptarget: null + }, + + callbacks: { + error: {}, + success: {}, + extra_data: {} + }, + + initialize: function(options) { + this.setOptions(options); + + $(document.body) + .removeEvents("submit") + .addEvent("submit", this.submit.bind(this)); + + this.init_help_messages(); + }, + + init_help_messages: function() { + $$("input[type!=submit]", "select").addEvents({ + focus: function(evt) { + var helpdiv = $(evt.target.name + "_helptext"); + if (helpdiv) { + var coords = evt.target.getCoordinates(); + helpdiv.setPosition({x: 10, y: coords['top'] + coords['height'] + 5}); + helpdiv.setStyle("display", "block"); + } + }, + blur: function(evt) { + var helpdiv = $(evt.target.name + "_helptext"); + if (helpdiv) { + helpdiv.setStyle("display", "none"); + } + } + }); + }, + + register_callback: function(category, name, callback) { + if (this.callbacks[category][name] == undefined) { + return false; + } + this.callbacks[category][name] = callback; + return true; + }, + + register_error_callback: function(name, callback) { + return this.register_callback("error", name, callback); + }, + + register_success_callback: function(name, callback) { + return this.register_callback("success", name, callback); + }, + + register_extra_data_callback: function(name, callback) { + return this.register_callback("extra_data", name, callback); + }, + + submit: function(event) { + var form = event.target; + var myid = form.get("id"); + var data = form.toQueryString(); + + console.log("submit"); + + event.stop(); + if (this.callbacks["extra_data"][myid] != undefined) { + if (data != "") { + data += "&"; + } + data += this.callbacks["extra_data"][myid](); + } + + new Request.JSON({ + url: form.get("action"), + method: form.get("method"), + data: data, + onSuccess: function(response) { + if (response.status == "ko") { + if (response.norefresh != undefined && response.norefresh == true) { + $$("div[class=error]").set("html", response.respmsg); + } else { + var target = (this.options.target) + ? $(this.options.target) : $(document.body); + + target.set("html", response.content); + this.init_help_messages(); + /*$(document.body).addEvent("submit", ajax_submit);*/ + if (this.callbacks["error"][myid] != undefined) { + this.callbacks["error"][myid](); + } + } + return; + } + if ($defined(response.respmsg)) { + if ($defined(parent.current_anchor)) { + parent.current_anchor.loading_message = response.respmsg; + parent.current_anchor.loading_color = "green"; + } else { + parent.infobox.info(response.respmsg); + parent.infobox.hide(1); + } + } + if ($defined(response.ajaxnav)) { + if ($defined(response.url)) { + parent.current_anchor.baseurl(response.url, 1); + } + parent.current_anchor.update(1); + } else if ($defined(response.url)) { + parent.location.href = response.url; + } + if (this.callbacks["success"][myid] != undefined) { + this.callbacks["success"][myid](response); + } + parent.SqueezeBox.close(); + }.bind(this), + onFailure: function(xhr) { + parent.document.getElement('iframe') + .setStyles({width: 800, height: 600}); + $(document.body).set("html", xhr.responseText); + } + }).send(); + } +}); + + var error_callbacks = { permform: function() { if ($defined($("id_domain"))) { @@ -36,70 +166,74 @@ function register_extra_data_callback(name, callback) { } function ajax_submit(event) { - var form = event.target; - var myid = form.get("id"); - var data = form.toQueryString(); + var form = event.target; + var myid = form.get("id"); + var data = form.toQueryString(); - event.stop(); - if ($defined(extra_data_callbacks[myid])) { - if (data != "") { - data += "&"; + event.stop(); + if ($defined(extra_data_callbacks[myid])) { + if (data != "") { + data += "&"; + } + data += extra_data_callbacks[myid](); } - data += extra_data_callbacks[myid](); - } - new Request({ - method: form.get("method"), - data: data, - onSuccess: function(responseText){ - var response = JSON.decode(responseText); + new Request.JSON({ + url: form.get("action"), + method: form.get("method"), + data: data, + onSuccess: function(response) { + if (response.status == "ko") { + if (response.norefresh != undefined && response.norefresh == true) { + $$("div[class=error]").set("html", response.respmsg); + } else { + var target = (resptarget != undefined) + ? $(resptarget) : $(document.body); - if (response.status == "ko") { - if ($defined(response.norefresh) && response.norefresh == true) { - $("error").set("html", response.respmsg); - } else { - $(document.body).set("html", response.content); - $(document.body).addEvent("submit", ajax_submit); - if ($defined(error_callbacks[myid])) { - error_callbacks[myid](); - } + target.set("html", response.content); + /*$(document.body).addEvent("submit", ajax_submit);*/ + if ($defined(error_callbacks[myid])) { + error_callbacks[myid](); + } + } + return; + } + if ($defined(response.respmsg)) { + if ($defined(parent.current_anchor)) { + parent.current_anchor.loading_message = response.respmsg; + parent.current_anchor.loading_color = "green"; + } else { + parent.infobox.info(response.respmsg); + parent.infobox.hide(1); + } + } + if ($defined(response.ajaxnav)) { + if ($defined(response.url)) { + parent.current_anchor.baseurl(response.url, 1); + } + parent.current_anchor.update(1); + } else if ($defined(response.url)) { + parent.location.href = response.url; + } + if ($defined(success_callbacks[myid])) { + success_callbacks[myid](response); + } + parent.SqueezeBox.close(); + }, + onFailure: function(xhr) { + parent.document.getElement('iframe').setStyles({width: 800, height: 600}); + $(document.body).set("html", xhr.responseText); } - } else { - if ($defined(response.respmsg)) { - if ($defined(parent.current_anchor)) { - parent.current_anchor.loading_message = response.respmsg; - parent.current_anchor.loading_color = "green"; - } else { - parent.infobox.info(response.respmsg); - parent.infobox.hide(1); - } - } - if ($defined(response.ajaxnav)) { - if ($defined(response.url)) { - parent.current_anchor.baseurl(response.url, 1); - } - parent.current_anchor.update(1); - } else if ($defined(response.url)) { - parent.location.href = response.url; - } - if ($defined(success_callbacks[myid])) { - success_callbacks[myid](response); - } - parent.SqueezeBox.close(); - } - }, - onFailure: function(xhr) { - parent.document.getElement('iframe').setStyles({width: 800, height: 600}); - $(document.body).set("html", xhr.responseText); - } - }).send(); + }).send(); } -window.addEvent("domready", function() { - $(document.body).addEvent("submit", ajax_submit); +function initform() { + $(document.body) + .removeEvents("submit") + .addEvent("submit", ajax_submit); $$("input[type!=submit]", "select").addEvents({ - focus: function(evt) { + focus: function(evt) { var helpdiv = $(evt.target.name + "_helptext"); if (helpdiv) { var coords = evt.target.getCoordinates(); @@ -114,4 +248,8 @@ window.addEvent("domready", function() { } } }); -}); +} + +/*window.addEvent("domready", function() { + initform(); +});*/ diff --git a/static/js/permissions.js b/static/js/permissions.js index 491845f8f..6b7c45080 100644 --- a/static/js/permissions.js +++ b/static/js/permissions.js @@ -16,6 +16,10 @@ window.addEvent('domready', function() { } }, $('accordion')); + SqueezeBox.assign($("content").getElements('a[class=boxed]'), { + parse: 'rel' + }); + $$("a[name=addperm]").addEvent("click", function(evt) { evt.stop(); var sizes = current_sizes.split(" "); diff --git a/static/js/utils.js b/static/js/utils.js index 48b11aaaf..e1ca1c490 100644 --- a/static/js/utils.js +++ b/static/js/utils.js @@ -25,6 +25,7 @@ Request.JSON.mdb = new Class({ Extends: Request.JSON, options: { + format: "json", onComplete: ajax_login_redirect, onFailure: function(xhr) { $(document.body).setStyle("overflow", "auto"); diff --git a/templates/admin/add_domain_admin.html b/templates/admin/add_domain_admin.html index 3d1bad5da..df5304d0c 100644 --- a/templates/admin/add_domain_admin.html +++ b/templates/admin/add_domain_admin.html @@ -1,28 +1,57 @@ -{% extends "admin/add_permission.html" %} +{% extends "layout_simple.html" %} +{% load i18n %} {% block html_head %} -{{ block.super }} + + + + + {% endblock %} +{% block content %} +

{{ title }}

+{% if error %}
{{ error }}
{% endif %} +
+ +
+
+{% endblock %} diff --git a/templates/admin/add_permission.html b/templates/admin/add_permission.html index 189f5d80b..8c14c9b8c 100644 --- a/templates/admin/add_permission.html +++ b/templates/admin/add_permission.html @@ -2,18 +2,24 @@ {% load i18n %} {% block html_head %} + {% endblock %} {% block content %}

{{ title }}

{% if error %}
{{ error }}
{% endif %} +{% if not addurl %} {% url admin.views.add_permission as addurl %} +{% endif %} {% include "common/genericform.html" %}
-
- +
{% endblock %} diff --git a/templates/admin/adminform.html b/templates/admin/adminform.html index ac06a6c27..102f345bb 100644 --- a/templates/admin/adminform.html +++ b/templates/admin/adminform.html @@ -1,10 +1,15 @@ {% extends "layout_simple.html" %} {% block html_head %} + {% endblock %} {% block content %}

{{ title }}

-{% if error %}
{{ error }}
{% endif %} +
{% if error %}{{ error }}{% endif %}
{% include "common/genericform.html" %} diff --git a/templates/admin/create_domain_admin.html b/templates/admin/create_domain_admin.html new file mode 100644 index 000000000..00d50f286 --- /dev/null +++ b/templates/admin/create_domain_admin.html @@ -0,0 +1,9 @@ +{% load i18n %} + + {% include "common/genericform.html" %} +
+
+ +
+
diff --git a/templates/admin/domain_admin_promotion.html b/templates/admin/domain_admin_promotion.html new file mode 100644 index 000000000..5be510a1c --- /dev/null +++ b/templates/admin/domain_admin_promotion.html @@ -0,0 +1,10 @@ +{% load i18n %} +
+ + {% include "common/genericform.html" %} +
+
+ +
+
diff --git a/templates/admin/listing.html b/templates/admin/listing.html index 79dd8bf9d..114f0ba19 100644 --- a/templates/admin/listing.html +++ b/templates/admin/listing.html @@ -1,5 +1,5 @@ {% extends "admin/domains_header.html" %} -{% load i18n %} +{% load i18n admin_extras %} {% block middletitle %}{{ title }}{% endblock %} {% block subcontent %}
@@ -7,6 +7,7 @@
{% endblock %} {% block footer %} +
{% get_footer_content user selection %}