diff --git a/admin/__init__.py b/admin/__init__.py index 0010e8ad6..db0c3c566 100644 --- a/admin/__init__.py +++ b/admin/__init__.py @@ -22,9 +22,11 @@ parameters.register_admin("MAILBOX_TYPE", type="list", deflt="maildir", help=_("Mailboxes storage format")) parameters.register_admin("MAILDIR_ROOT", type="string", deflt=".maildir", help=_("Sub-directory (inside the mailbox) where messages are stored when using the maildir format")) -parameters.register_admin("PASSWORD_SCHEME", type="list", deflt="crypt", - values=[("crypt", "crypt"), ("md5", "md5"), ("clear", "clear")], - help=_("Scheme used to crypt mailbox passwords")) +parameters.register_admin( + "PASSWORD_SCHEME", type="list", deflt="crypt", + values=[("crypt", "crypt"), ("md5", "md5"), ("sha256", "sha256"), ("clear", "clear")], + help=_("Scheme used to crypt mailbox passwords") + ) parameters.register_admin("ITEMS_PER_PAGE", type="int", deflt=30, help=_("Number of displayed items per page")) @@ -51,6 +53,7 @@ parameters.register_admin("DEFAULT_TOP_REDIRECTION", type="list", deflt="admin", values=enabled_applications(), help=_("The default redirection used when no application is specified")) +@events.observe("ExtDisabled") def unset_default_topredirection(extension): """ Simple callback to change the default redirection if the @@ -60,6 +63,7 @@ def unset_default_topredirection(extension): if topredirection == extension.name: parameters.save_admin("DEFAULT_TOP_REDIRECTION", "userprefs", app="general") +@events.observe("ExtDisabled", "ExtEnabled") def update_available_applications(extension): """Simple callback to update the list of available applications @@ -67,7 +71,3 @@ def update_available_applications(extension): """ parameters.update_admin("DEFAULT_TOP_REDIRECTION", app="general", values=enabled_applications()) - -events.register("ExtDisabled", unset_default_topredirection) -events.register("ExtDisabled", update_available_applications) -events.register("ExtEnabled", update_available_applications) diff --git a/admin/exceptions.py b/admin/exceptions.py new file mode 100644 index 000000000..cc89b4860 --- /dev/null +++ b/admin/exceptions.py @@ -0,0 +1,15 @@ +# coding: utf-8 +""" +:mod:`exceptions` --- Custom exceptions of the admin. application +----------------------------------------------------------------- +""" + +from modoboa.lib.exceptions import ModoboaException + +class AdminError(ModoboaException): + """ + Just a Custom exception to identify errors coming from the admin + application. + """ + pass + diff --git a/admin/fixtures/initial_data.json b/admin/fixtures/initial_data.json index 94985fe96..575aaf3b0 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": 2, "model": "auth.group", "fields": {"name": "SimpleUsers", "permissions": []}}] +[{"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": []}},{"pk": 1, "model": "auth.user", "fields": {"username": "admin", "first_name": "", "last_name": "", "is_active": true, "is_superuser": true, "is_staff": false, "last_login": "2012-01-26 15:19:25", "groups": [], "user_permissions": [], "password": "{CRYPT}dTTsGDkA5ZHKg", "email": "", "date_joined": "2012-01-26 15:19:25"}}] diff --git a/admin/forms.py b/admin/forms.py index 6cbfb8131..f0f1bc7c8 100644 --- a/admin/forms.py +++ b/admin/forms.py @@ -1,13 +1,10 @@ # coding: utf-8 from django import forms from modoboa.admin.models import * -from modoboa.admin.lib import is_domain_admin from django.utils.translation import ugettext as _, ugettext_noop -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, get_user_domains_qs class DomainForm(forms.ModelForm): class Meta: @@ -34,7 +31,7 @@ class ProxyForm(forms.ModelForm): super(ProxyForm, self).__init__(*args, **kwargs) if not user.is_superuser: - self._domains = get_user_domains_qs(user) + self._domains = user.get_domains() else: self._domains = None @@ -50,25 +47,23 @@ class DomainAliasForm(ProxyForm): self.fields["target"].queryset = self._domains class MailboxForm(ProxyForm): + name = forms.CharField( + label=ugettext_noop("Name"), max_length=100, + help_text=ugettext_noop("First name and last name of mailbox owner") + ) quota = forms.IntegerField(label=ugettext_noop("Quota"), required=False, help_text=ugettext_noop("Mailbox quota in MB (default to domain quota if blank)")) - password1 = forms.CharField(label=ugettext_noop("Password"), - widget=forms.PasswordInput(render_value=True), - help_text=ugettext_noop("Password used to log in")) - password2 = forms.CharField(label=ugettext_noop("Confirmation"), - widget=forms.PasswordInput(render_value=True), - help_text=ugettext_noop("Password confirmation")) enabled = forms.BooleanField(label=gender("Enabled", "f"), required=False, initial=True, help_text=ugettext_noop("Check to activate this mailbox")) class Meta: model = Mailbox - fields = ('domain', 'name', 'address') + fields = ('domain', 'address') def __init__(self, *args, **kwargs): super(MailboxForm, self).__init__(*args, **kwargs) - for f in ['name', 'address', 'quota', 'password1', 'password2']: + for f in ['name', 'address', 'quota']: self.fields[f].widget.attrs['size'] = 14 if self._domains is not None: self.fields["domain"].queryset = self._domains @@ -76,25 +71,21 @@ class MailboxForm(ProxyForm): #self.fields["domain"].initial = self._domain if kwargs.has_key("instance"): - self.fields['quota'].initial = kwargs["instance"].quota - self.fields['enabled'].initial = kwargs["instance"].user.is_active - self.fields['password1'].initial = "é" - self.fields['password2'].initial = "é" + mb = kwargs["instance"] + self.fields['name'].initial = unicode(mb.user) + self.fields['quota'].initial = mb.quota + self.fields['enabled'].initial = mb.user.is_active def clean_address(self): if self.cleaned_data["address"].find('@') != -1: return self.cleaned_data["address"].rsplit("@", 1)[0] return self.cleaned_data["address"] - def clean_password2(self): - if self.cleaned_data["password1"] != self.cleaned_data["password2"]: - raise forms.ValidationError(_("Passwords mismatch")) - return self.cleaned_data["password2"] - - def commit_save(self, mb): - mb.save(enabled=self.cleaned_data["enabled"], - password=self.cleaned_data["password1"], - quota=self.cleaned_data["quota"]) + def commit_save(self, mb, **kwargs): + mb.save(name=self.cleaned_data["name"], + enabled=self.cleaned_data["enabled"], + quota=self.cleaned_data["quota"], + **kwargs) def save(self, force_insert=False, force_update=False, commit=True): m = super(MailboxForm, self).save(commit=False) @@ -102,6 +93,31 @@ class MailboxForm(ProxyForm): self.commit_save(m) return m +class MailboxWithPasswordForm(MailboxForm): + password1 = forms.CharField(label=ugettext_noop("Password"), + widget=forms.PasswordInput(render_value=True), + help_text=ugettext_noop("Password used to log in")) + password2 = forms.CharField(label=ugettext_noop("Confirmation"), + widget=forms.PasswordInput(render_value=True), + help_text=ugettext_noop("Password confirmation")) + + def __init__(self, *args, **kwargs): + super(MailboxWithPasswordForm, self).__init__(*args, **kwargs) + for f in ['password1', 'password2']: + self.fields[f].widget.attrs['size'] = 14 + self.fields.keyOrder = ['name', 'domain', 'address', 'quota', + 'password1', 'password2', 'enabled'] + + def clean_password2(self): + if self.cleaned_data["password1"] != self.cleaned_data["password2"]: + raise forms.ValidationError(_("Passwords mismatch")) + return self.cleaned_data["password2"] + + def commit_save(self, mb): + super(MailboxWithPasswordForm, self).commit_save( + mb, password=self.cleaned_data["password1"] + ) + class AliasForm(ProxyForm): targets = forms.CharField(label=ugettext_noop("Target(s)"), required=False, help_text=ugettext_noop("Mailbox(es) this alias will point to")) @@ -152,8 +168,6 @@ class AliasForm(ProxyForm): 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 = [] @@ -168,7 +182,7 @@ class AliasForm(ProxyForm): except Domain.DoesNotExist: domain = None if domain: - if not is_object_owner(user, domain): + if not user.is_owner(domain): raise PermDeniedException(addr) try: mb = Mailbox.objects.get(domain=domain, address=local_part) @@ -274,9 +288,8 @@ class AssignDomainsForm(forms.Form): def __init__(self, user, domadmin, *args, **kwargs): super(AssignDomainsForm, self).__init__(*args, **kwargs) - self.fields["domains"].queryset = get_user_domains_qs(user) - self.fields["domains"].initial = get_user_domains_qs(domadmin) - + self.fields["domains"].queryset = user.get_domains() + self.fields["domains"].initial = domadmin.get_domains() class ImportDataForm(forms.Form): sourcefile = forms.FileField(label=_("Select a file")) diff --git a/admin/lib.py b/admin/lib.py index a42d989ed..3d02ad8cc 100644 --- a/admin/lib.py +++ b/admin/lib.py @@ -1,82 +1,12 @@ # coding: utf-8 from functools import wraps -from django.http import HttpResponse, HttpResponseRedirect -from django.contrib.auth.models import User, Group from django.contrib.contenttypes.models import ContentType 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): - if request.user.id == 1: - return _render_error(request, errortpl, { - "error" : _("Invalid action, %(user)s is a local user" \ - % {"user" : request.user.username}) - }) - return f(request, *args, **kwargs) - - wrapped_f.__name__ = f.__name__ - wrapped_f.__dict__ = f.__dict__ - wrapped_f.__doc__ = f.__doc__ - wrapped_f.__module__ = f.__module__ - return wrapped_f - return dec - -def is_domain_admin(user): - """Tell if a user is administrator for a domain of not - - Whatever the domain, we just want to know if the given user is - declared as a domain administrator. - - :param user: a User object - :return: True if the user is a domain administrator, False otherwise. - """ - grp = Group.objects.get(name="DomainAdmins") - return grp in user.groups.all() - -def good_domain(f): - """Custom permission decorator - - Check if a domain administrator is accessing the good domain. - - :param f: the original called function - """ - 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 - if request.GET.has_key("domid"): - dom_id = int(request.GET["domid"]) - if dom_id != mb.domain.id: - access = False - else: - q = request.GET.copy() - q["domid"] = mb.domain.id - request.GET = q - if access: - return f(request, **kwargs) - - from django.conf import settings - from django.utils.http import urlquote - path = urlquote(request.get_full_path()) - login_url = settings.LOGIN_URL - return HttpResponseRedirect("%s?next=%s" % (login_url, path)) - return dec +from exceptions import * def render_listing(request, objtype, tplname="admin/listing.html", **kwargs): @@ -162,53 +92,6 @@ def ungrant_access_to_object(obj, user=None): else: ObjectAccess.objects.filter(content_type=ct, object_id=obj.id).delete() -def can_access_object(user, obj): - """Check if a user can access a specific object - - This function is recursive : if the given user hasn't got direct - access to this object and if he has got access other ``User`` - objects, we check if one of those users owns the object. - - :param user: a ``User`` object - :param obj: a admin object - :return: a boolean - """ - if user.is_superuser: - return True - - ct = ContentType.objects.get_for_model(obj) - try: - ooentry = user.objectaccess_set.get(content_type=ct, object_id=obj.id) - except ObjectAccess.DoesNotExist: - pass - else: - return True - if ct.model == "user": - return False - if directowner: - return False - - ct = ContentType.objects.get_for_model(User) - qs = user.objectaccess_set.filter(content_type=ct) - for ooentry in qs.all(): - if is_object_owner(ooentry.content_object, obj): - return True - return False - -def is_object_owner(user, obj): - """Tell is the user is the unique owner of this object - - :param user: a ``User`` object - :param obj: an object inheriting from ``models.Model`` - :return: a boolean - """ - ct = ContentType.objects.get_for_model(obj) - try: - ooentry = user.objectaccess_set.get(content_type=ct, object_id=obj.id) - except ObjectAccess.DoesNotExist: - return False - return ooentry.is_owner - def get_object_owner(obj): """Return the unique owner of this object @@ -242,96 +125,8 @@ def check_domain_access(f): mbid = request.GET.get("mbid", None) if mbid: domain = Mailbox.objects.get(pk=mbid).domain - if domain and not can_access_object(request.user, domain): + if domain and not request.user.can_access(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() - - domct = ContentType.objects.get_for_model(Domain) - qs = user.objectaccess_set.filter(content_type=domct) - domains = [] - for entry in qs.all(): - domains += [entry.content_object] - - userct = ContentType.objects.get_for_model(User) - qs = user.objectaccess_set.filter(content_type=userct) - for entry in qs: - if not is_domain_admin(entry.content_object): - continue - domains += get_user_domains(entry.content_object) - return domains - -def get_user_domains_qs(user): - """Return the domains belonging to this user - - The result is a ``QuerySet`` object, so this function can be used - to fill ``ModelChoiceField`` objects. - - :param user: a ``User`` object - """ - if user.is_superuser: - return Domain.objects.all() - result = Domain.objects.filter(owners__user=user).distinct() - - userct = ContentType.objects.get_for_model(User) - for entry in user.objectaccess_set.filter(content_type=userct): - if not is_domain_admin(entry.content_object): - continue - result |= get_user_domains_qs(entry.content_object).distinct() - return result - -def get_user_domaliases(user): - """Return the domain aliases that belong to this user - - The result will contain the domain aliases defined for each domain - that user can see. - - :param user: a ``User`` object - :return: a list of ``DomainAlias`` objects - """ - if user.is_superuser: - return DomainAlias.objects.all() - domains = get_user_domains_qs(user) - domaliases = [] - for dom in domains: - domaliases += dom.domainalias_set.all() - return domaliases - -def get_user_mailboxes(user): - """Return the mailboxes that belong to this user - - The result will contain the mailboxes defined for each domain that - user can see. - - :param user: a ``User`` object - :return: a list of ``Mailbox`` objects - """ - if user.is_superuser: - return Mailbox.objects.all() - mboxes = [] - domains = get_user_domains_qs(user) - for dom in domains: - mboxes += dom.mailbox_set.all() - return mboxes - -def get_user_mbaliases(user): - """Return the mailbox aliases that belong to this user - - The result will contain the mailbox aliases defined for each - domain that user can see. - - :param user: a ``User`` object - :return: a list of ``Alias`` objects - """ - if user.is_superuser: - return Alias.objects.all() - domains = get_user_domains_qs(user) - mbaliases = [] - for dom in domains: - mbaliases += dom.alias_set.all() - return mbaliases diff --git a/admin/migrations/0015_move_passwords.py b/admin/migrations/0015_move_passwords.py new file mode 100644 index 000000000..1bd697e02 --- /dev/null +++ b/admin/migrations/0015_move_passwords.py @@ -0,0 +1,116 @@ +# encoding: utf-8 +import datetime +from south.db import db +from south.v2 import DataMigration +from django.db import models +from modoboa.lib.models import Parameter + +class Migration(DataMigration): + + def forwards(self, orm): + "Write your forwards methods here." + try: + s = Parameter.objects.get(name='PASSWORD_SCHEME').value + except Parameter.DoesNotExist: + s = "crypt" + + for mb in orm.Mailbox.objects.all(): + mb.user.password = '{%s}%s' % (s.upper(), mb.password) + mb.user.save() + + def backwards(self, orm): + "Write your backwards methods here." + raise RuntimeError("Cannot reverse this migration.") + + 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'}) + }, + '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/migrations/0015_auto__add_objectaccess__add_unique_objectaccess_user_content_type_obje.py b/admin/migrations/0016_auto__add_objectaccess__add_unique_objectaccess_user_content_type_obje.py similarity index 92% rename from admin/migrations/0015_auto__add_objectaccess__add_unique_objectaccess_user_content_type_obje.py rename to admin/migrations/0016_auto__add_objectaccess__add_unique_objectaccess_user_content_type_obje.py index de6bcfecc..860e18ef8 100644 --- a/admin/migrations/0015_auto__add_objectaccess__add_unique_objectaccess_user_content_type_obje.py +++ b/admin/migrations/0016_auto__add_objectaccess__add_unique_objectaccess_user_content_type_obje.py @@ -21,6 +21,12 @@ class Migration(SchemaMigration): # Adding unique constraint on 'ObjectAccess', fields ['user', 'content_type', 'object_id'] db.create_unique('admin_objectaccess', ['user_id', 'content_type_id', 'object_id']) + # Deleting field 'Mailbox.password' + db.delete_column('admin_mailbox', 'password') + + # Deleting field 'Mailbox.name' + db.delete_column('admin_mailbox', 'name') + def backwards(self, orm): @@ -30,6 +36,12 @@ class Migration(SchemaMigration): # Deleting model 'ObjectAccess' db.delete_table('admin_objectaccess') + # User chose to not deal with backwards NULL issues for 'Mailbox.password' + raise RuntimeError("Cannot reverse this migration. 'Mailbox.password' and its values cannot be restored.") + + # User chose to not deal with backwards NULL issues for 'Mailbox.name' + raise RuntimeError("Cannot reverse this migration. 'Mailbox.name' and its values cannot be restored.") + models = { 'admin.alias': { @@ -71,8 +83,6 @@ class Migration(SchemaMigration): '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', [], {}), @@ -92,6 +102,9 @@ class Migration(SchemaMigration): 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 'last_modification': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}) }, + 'admin.user': { + 'Meta': {'object_name': 'User', 'db_table': "'auth_user'", '_ormbases': ['auth.User'], 'proxy': 'True'} + }, 'auth.group': { 'Meta': {'object_name': 'Group'}, 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), diff --git a/admin/models.py b/admin/models.py index b0a246c45..ce7970aea 100644 --- a/admin/models.py +++ b/admin/models.py @@ -2,15 +2,20 @@ from django.db import models, IntegrityError from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes import generic -from django.contrib.auth.models import User, Group +from django.contrib.auth.models import User as DUser, UserManager, Group from django.utils.translation import ugettext as _, ugettext_noop +from django.utils.crypto import constant_time_compare from django.conf import settings from modoboa.lib import parameters, events +from modoboa.lib.exceptions import PermDeniedException from modoboa.lib.sysutils import exec_cmd, exec_as_vuser from modoboa.lib.emailutils import split_mailbox -from modoboa.auth.lib import crypt_password +from exceptions import * import os import pwd +import re +import crypt, hashlib, string, base64 +from random import Random try: from modoboa.lib.ldaputils import * @@ -18,6 +23,205 @@ try: except ImportError: ldap_available = False +class User(DUser): + """Proxy for the ``User`` model. + + It overloads the way passwords are stored into the database. The + main reason to change this mechanism is to ensure the + compatibility with the way Dovecot stores passwords. + + It also adds new attributes and methods. + """ + class Meta: + proxy = True + + password_expr = re.compile(r'\{(\w+)\}(.+)') + + def __unicode__(self): + return u"%s %s" % (self.first_name, self.last_name) + + def _crypt_password(self, raw_value): + scheme = parameters.get_admin("PASSWORD_SCHEME") + if scheme == "crypt": + salt = ''.join(Random().sample(string.letters + string.digits, 2)) + result = crypt.crypt(raw_value, salt) + elif scheme == "md5": + obj = hashlib.md5(raw_value) + result = obj.hexdigest() + elif scheme == "sha256": + obj = hashlib.sha256(raw_value) + result = base64.b64encode(obj.digest()) + else: + scheme = "plain" + result = raw_value + return "{%s}%s" % (scheme.upper(), result) + + def set_password(self, raw_value, curvalue=None): + """Password update + + Update the current mailbox's password with the given clear + value. This value is encrypted according to the defined method + before it is saved. + + :param raw_value: the new password's value + :param curvalue: the current password (for LDAP authentication) + """ + if parameters.get_admin("AUTHENTICATION_TYPE") == "local": + self.password = self._crypt_password(raw_value) + return + + if not ldap_available: + raise AdminError(_("Failed to update password: LDAP module not installed")) + + ab = LDAPAuthBackend() + try: + ab.update_user_password(self.username, curvalue, raw_value) + except LDAPException, e: + raise AdminError(_("Failed to update password: %s" % str(e))) + + def check_password(self, raw_value): + m = self.password_expr.match(self.password) + if m is None: + return False + scheme = m.group(1).lower() + val2 = m.group(2) + if scheme == "crypt": + val1 = crypt.crypt(raw_value, m.group(2)) + elif scheme == "md5": + val1 = hashlib.md5(raw_value).hexdigest() + elif scheme == "sha256": + val1 = base64.b64encode(hashlib.sha256(raw_value).digest()) + else: + val1 = raw_value + return constant_time_compare(val1, val2) + + @property + def has_mailbox(self): + return len(self.mailbox_set.all()) != 0 + + def belongs_to_group(self, name): + """Simple shortcut to check if this user is a member of a + specific group. + + :param name: the group's name + :return: a boolean + """ + try: + self.groups.get(name=name) + except Group.DoesNotExist: + return False + return True + + def get_domains(self): + """Return the domains belonging to this user + + The result is a ``QuerySet`` object, so this function can be used + to fill ``ModelChoiceField`` objects. + + :param user: a ``User`` object + """ + if self.is_superuser: + return Domain.objects.all() + result = Domain.objects.filter(owners__user=self).distinct() + + userct = ContentType.objects.get_for_model(User) + for entry in self.objectaccess_set.filter(content_type=userct): + if not entry.content_object.belongs_to_group('DomainAdmins'): + continue + result |= entry.content_object.get_domains().distinct() + return result + + def get_domaliases(self): + """Return the domain aliases that belong to this user + + The result will contain the domain aliases defined for each domain + that user can see. + + :return: a list of ``DomainAlias`` objects + """ + if self.is_superuser: + return DomainAlias.objects.all() + domains = self.get_domains() + domaliases = [] + for dom in domains: + domaliases += dom.domainalias_set.all() + return domaliases + + def get_mailboxes(self): + """Return the mailboxes that belong to this user + + The result will contain the mailboxes defined for each domain that + user can see. + + :return: a list of ``Mailbox`` objects + """ + if self.is_superuser: + return Mailbox.objects.all() + mboxes = [] + domains = self.get_domains() + for dom in domains: + mboxes += dom.mailbox_set.all() + return mboxes + + def get_mbaliases(self): + """Return the mailbox aliases that belong to this user + + The result will contain the mailbox aliases defined for each + domain that user can see. + + :return: a list of ``Alias`` objects + """ + if self.is_superuser: + return Alias.objects.all() + domains = self.get_domains() + mbaliases = [] + for dom in domains: + mbaliases += dom.alias_set.all() + return mbaliases + + def is_owner(self, obj): + """Tell is the user is the unique owner of this object + + :param obj: an object inheriting from ``models.Model`` + :return: a boolean + """ + ct = ContentType.objects.get_for_model(obj) + try: + ooentry = self.objectaccess_set.get(content_type=ct, object_id=obj.id) + except ObjectAccess.DoesNotExist: + return False + return ooentry.is_owner + + def can_access(self, obj): + """Check if the user can access a specific object + + This function is recursive : if the given user hasn't got direct + access to this object and if he has got access other ``User`` + objects, we check if one of those users owns the object. + + :param obj: a admin object + :return: a boolean + """ + if self.is_superuser: + return True + + ct = ContentType.objects.get_for_model(obj) + try: + ooentry = self.objectaccess_set.get(content_type=ct, object_id=obj.id) + except ObjectAccess.DoesNotExist: + pass + else: + return True + if ct.model == "user": + return False + + ct = ContentType.objects.get_for_model(User) + qs = self.objectaccess_set.filter(content_type=ct) + for ooentry in qs.all(): + if ooentry.content_object.is_owner(obj): + return True + return False + class ObjectAccess(models.Model): user = models.ForeignKey(User) content_type = models.ForeignKey(ContentType) @@ -27,7 +231,7 @@ class ObjectAccess(models.Model): class Meta: unique_together = (("user", "content_type", "object_id"),) - + class ObjectDates(models.Model): """Dates recording for admin objects @@ -154,11 +358,10 @@ class DomainAlias(DatesAware): ) class Mailbox(DatesAware): - name = models.CharField(ugettext_noop('name'), max_length=100, - help_text=ugettext_noop("First name and last name of mailbox owner")) - address = models.CharField(ugettext_noop('address'), max_length=100, - help_text=ugettext_noop("Mailbox address (without the @domain.tld part)")) - password = models.CharField(ugettext_noop('password'), max_length=100) + address = models.CharField( + ugettext_noop('address'), max_length=100, + help_text=ugettext_noop("Mailbox address (without the @domain.tld part)") + ) quota = models.IntegerField() uid = models.IntegerField() gid = models.IntegerField() @@ -251,16 +454,17 @@ class Mailbox(DatesAware): except User.DoesNotExist: user = User() user.username = user.email = "%s@%s" % (self.address, self.domain.name) - user.set_unusable_password() if kwargs.has_key("enabled"): user.is_active = kwargs["enabled"] try: - fname, lname = self.name.split() + fname, lname = kwargs["name"].split() except ValueError: - fname = self.name + fname = kwargs["name"] lname = "" user.first_name = fname user.last_name = lname + if kwargs.has_key("password"): + user.set_password(kwargs["password"]) try: user.save() except IntegrityError: @@ -271,8 +475,6 @@ class Mailbox(DatesAware): 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 v_uid = parameters.get_admin("VIRTUAL_UID"); if v_uid.isdigit(): @@ -291,35 +493,13 @@ class Mailbox(DatesAware): self.quota = kwargs["quota"] else: self.quota = self.domain.quota - for kw in ["enabled", "password", "quota"]: + for kw in ["name", "enabled", "password", "quota"]: try: del kwargs[kw] except KeyError: pass super(Mailbox, self).save(*args, **kwargs) - def set_password(self, curvalue, newvalue): - """Password update - - Update the current mailbox's password with the given clear - value. This value is encrypted according to the defined method - before it is saved. - - :param value: the new password's value - """ - if parameters.get_admin("AUTHENTICATION_TYPE") == "local": - 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: - ab.update_user_password(self.user.username, curvalue, newvalue) - except LDAPException, e: - raise AdminError(_("Failed to update password: %s" % str(e))) - def save_from_user(self, localpart, domain, user): """Simple save method called for automatic creations @@ -348,7 +528,7 @@ class Mailbox(DatesAware): self.quota = self.domain.quota super(Mailbox, self).save() - def create_from_csv(self, line): + def create_from_csv(self, user, line): """Create a new mailbox from a CSV file entry The expected order is the following:: @@ -365,8 +545,11 @@ class Mailbox(DatesAware): self.domain = Domain.objects.get(name=domname) except Domain.DoesNotExist: raise AdminError(_("Cannot import this mailbox because the associated domain does not exist")) - self.name = "%s %s" % (line[2], line[3]) - self.save(password=line[1], enabled=True) + if not user.is_owner(self.domain): + raise PermDeniedException(_("You don't have access to this domain")) + + name = "%s %s" % (line[2], line[3]) + self.save(name=name, password=line[1], enabled=True) def delete(self, *args, **kwargs): keepdir = False @@ -382,14 +565,6 @@ class Mailbox(DatesAware): if not keepdir: self.delete_dir() - def tohash(self): - return { - "id" : self.id, - "domain" : self.domain.name, - "full_name" : self.name, - "date_joined" : self.user.date_joined, - "enabled" : self.user.is_active - } class Alias(DatesAware): address = models.CharField(ugettext_noop('address'), max_length=254, @@ -464,12 +639,10 @@ 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 for mb in self.mboxes.all(): - if not is_object_owner(user, mb.domain): + if not user.is_owner(mb.domain): return True return False diff --git a/admin/permissions.py b/admin/permissions.py index ac212d49f..34b74311f 100644 --- a/admin/permissions.py +++ b/admin/permissions.py @@ -2,10 +2,11 @@ import datetime from django.utils.translation import ugettext as _, ugettext_noop -from django.contrib.auth.models import User, Group +from django.contrib.auth.models import Group from modoboa.lib.webutils import _render, getctx, _render_to_string from modoboa.lib import events from forms import * +from models import User from tables import SuperAdminsTable, DomainAdminsTable from lib import grant_access_to_object, ungrant_access_to_object @@ -50,6 +51,7 @@ class SuperAdminsPerms(Permissions): user = User.objects.get(pk=request.POST["user"]) user.is_superuser = True user.date_joined = datetime.datetime.now() + events.raiseEvent("SuperAdminPromotion", user) user.groups.clear() user.save() return True, None @@ -61,7 +63,6 @@ class SuperAdminsPerms(Permissions): for s in selection: user = User.objects.get(pk=int(s)) if user.username == "admin": - #messages.error(request, _("admin is intouchable!!"), fail_silently=True) continue user.is_superuser = False user.save() @@ -89,22 +90,6 @@ class DomainAdminsPerms(Permissions): def get_add_form(self, request): form = DomainAdminForm() return self._render_form(request, form) - - def add(self, request): - form = DomainAdminForm(request.POST) - if request.POST.get("user", "") != "": - mboxid = request.POST['user'] - mb = Mailbox.objects.get(pk=mboxid) - form.fields["user"].choices = [(mboxid, mb),] - if form.is_valid(): - mb.user.is_superuser = False - 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 = self._render_form(request, form, True) - return False, getctx("ko", content=content) def delete(self, selection): dagrp = Group.objects.get(name="DomainAdmins") @@ -121,11 +106,8 @@ class DomainAdminsPerms(Permissions): u.groups.add(sugrp) def get(self, request): - #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()] + domainadmins = filter(lambda da: request.user.is_owner(da), domainadmins) return DomainAdminsTable(request, domainadmins).render() def get_perms_class(user, role): diff --git a/admin/tables.py b/admin/tables.py index d5523435c..bcd15cd2a 100644 --- a/admin/tables.py +++ b/admin/tables.py @@ -68,7 +68,7 @@ class MailboxesTable(tables.Table): idkey = "id" address = tables.Column("address", label=ugettext_noop("Address")) - name = tables.Column("name", label=ugettext_noop("Name")) + name = tables.Column("user", label=ugettext_noop("Name")) creation = tables.Column("creation", label=ugettext_noop("Created"), width="160px") modified = tables.Column("last_modification", label=ugettext_noop("Last modified"), @@ -95,6 +95,9 @@ class MailboxesTable(tables.Table): def parse_enabled(self, value): return _("yes") if value else _("no") + def parse_name(self, user): + return "%s %s" % (user.first_name, user.last_name) + class MbaliasesTable(tables.Table): tableid = "objects_table" idkey = "id" diff --git a/admin/templatetags/admin_extras.py b/admin/templatetags/admin_extras.py index 0609c3bd9..90f819ea4 100644 --- a/admin/templatetags/admin_extras.py +++ b/admin/templatetags/admin_extras.py @@ -2,7 +2,6 @@ from django import template from django.template.loader import render_to_string from django.utils.translation import ugettext as _, ugettext_noop from django.core.urlresolvers import reverse -from django.contrib.auth.models import User from modoboa import admin from modoboa.lib import events from modoboa.lib.webutils import static_url, render_actions @@ -202,6 +201,8 @@ def mailbox_actions(user, mboxid): @register.simple_tag def domain_admin_actions(user, daid): + from modoboa.admin.models import User + actions = [ {"name" : "editdomainadmin", "url" : reverse(admin.views.edit_domain_admin, args=[daid]), diff --git a/admin/urls.py b/admin/urls.py index 4a1de7f50..fe48c7bd7 100644 --- a/admin/urls.py +++ b/admin/urls.py @@ -25,8 +25,6 @@ urlpatterns = patterns( (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'), diff --git a/admin/views.py b/admin/views.py index 88d13755e..0580e7fb9 100644 --- a/admin/views.py +++ b/admin/views.py @@ -6,7 +6,7 @@ from django.core.urlresolvers import reverse from django.contrib.auth.decorators \ import login_required, permission_required, user_passes_test from django.db.models import Q -from django.contrib.auth.models import User, Group +from django.contrib.auth.models import Group from django.contrib import messages from django.db import transaction, IntegrityError, transaction @@ -17,7 +17,6 @@ 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 from modoboa.lib.emailutils import split_mailbox @@ -32,7 +31,7 @@ def domains(request): return HttpResponseRedirect(reverse(userprefs.views.preferences)) - domains = get_user_domains_qs(request.user) + domains = request.user.get_domains() for dom in domains: dom.mbalias_counter = len(Alias.objects.filter(domain=dom.id)) deloptions = {"keepdir" : _("Do not delete domain directory")} @@ -93,7 +92,7 @@ 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): + if not request.user.is_owner(domain): raise PermDeniedException(_("You can't edit this domain")) commonctx = {"title" : _("Domain editing"), @@ -121,7 +120,7 @@ def deldomain(request): mb = None for dom in Domain.objects.filter(id__in=selection): - if not is_object_owner(request.user, dom): + if not request.user.is_owner(dom): raise PermDeniedException(_("You can't delete this domain")) if mb and mb.domain == dom: raise AdminError(_("You can't delete your own domain")) @@ -143,7 +142,7 @@ def domaliases(request): domaliases = DomainAlias.objects.filter(target=request.GET["domid"]) else: domain = None - domaliases = get_user_domaliases(request.user) + domaliases = request.user.get_domaliases() return render_listing(request, "domaliases", title=_("Domain aliases"), rel="310 190", @@ -190,7 +189,7 @@ def newdomalias(request, tplname="admin/adminform.html"): @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): + if not request.user.is_owner(domalias.target): raise PermDeniedException(_("You do not have access to this domain")) commonctx = {"title" : _("Domain alias editing"), @@ -227,7 +226,7 @@ def deldomalias(request): selection = request.GET["selection"].split(",") for daid in selection: domalias = DomainAlias.objects.get(pk=daid) - if not is_object_owner(request.user, domalias.target): + if not request.user.is_owner(domalias.target): raise PermDeniedException(_("You don't have acces to this domain")) events.raiseEvent("DomainAliasDeleted", domalias) ungrant_access_to_object(domalias) @@ -246,7 +245,7 @@ def mailboxes(request): mboxes = Mailbox.objects.filter(domain=domain.id) title += " (%s)" % domain.name else: - mboxes = get_user_mailboxes(request.user) + mboxes = request.user.get_mailboxes() domain = None deloptions = {"keepdir" : _("Do not delete mailbox directory")} return render_listing(request, "mailboxes", @@ -254,20 +253,6 @@ def mailboxes(request): rel="310 320", objects=mboxes, domain=domain, deloptions=deloptions) -@login_required -@good_domain -@permission_required("admin.view_mailboxes") -def mailboxes_raw(request, dom_id=None): - target = request.GET.has_key("target") and request.GET["target"] or "permissions" - if target == "permissions": - mailboxes = Mailbox.objects.filter(domain=dom_id).exclude(user__id=request.user.id) - else: - mailboxes = Mailbox.objects.filter(domain=dom_id) - return _render(request, 'admin/mailboxes_raw.html', { - "mailboxes" : mailboxes, - "target" : target - }) - @login_required @permission_required("admin.view_mailboxes") @check_domain_access @@ -281,7 +266,7 @@ def mailboxes_search(request): 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 + query = Q(domain__in=request.user.get_domains()) & query mboxes = Mailbox.objects.filter(query) result = map(lambda mb: mb.full_address, mboxes) return ajax_simple_response(result) @@ -296,11 +281,11 @@ def newmailbox(request, tplname="admin/adminform.html"): "action" : reverse(newmailbox), "formid" : "mboxform"} if request.method == "POST": - form = MailboxForm(request.user, request.POST) + form = MailboxWithPasswordForm(request.user, request.POST) error = None if form.is_valid(): if not request.user.is_superuser: - if not can_access_object(request.user, form.cleaned_data["domain"]): + if not request.user.can_access(form.cleaned_data["domain"]): raise PermDeniedException(_("You do not have access to this domain")) try: @@ -319,7 +304,7 @@ def newmailbox(request, tplname="admin/adminform.html"): commonctx["error"] = error return ajax_response(request, status="ko", template=tplname, **commonctx) - form = MailboxForm(request.user) + form = MailboxWithPasswordForm(request.user) commonctx["form"] = form return _render(request, tplname, commonctx) @@ -327,7 +312,7 @@ def newmailbox(request, tplname="admin/adminform.html"): @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): + if not request.user.is_owner(mb.domain): raise PermDeniedException(_("You do not have access to this domain")) commonctx = {"title" : _("Mailbox editing"), @@ -335,8 +320,13 @@ def editmailbox(request, mbox_id=None, tplname="admin/adminform.html"): "action" : reverse(editmailbox, args=[mbox_id]), "formid" : "mboxform"} if request.method == "POST": + if request.POST.get("password1", "") != "" or \ + request.POST.get("password2", "") != "": + formclass = MailboxWithPasswordForm + else: + formclass = MailboxForm oldmb = copy.deepcopy(mb) - form = MailboxForm(request.user, request.POST, instance=mb) + form = formclass(request.user, request.POST, instance=mb) error = None if form.is_valid(): try: @@ -356,7 +346,7 @@ def editmailbox(request, mbox_id=None, tplname="admin/adminform.html"): commonctx["error"] = error return ajax_response(request, status="ko", template=tplname, **commonctx) - form = MailboxForm(request.user, instance=mb) + form = MailboxWithPasswordForm(request.user, instance=mb) commonctx["form"] = form return _render(request, tplname, commonctx) @@ -371,7 +361,7 @@ def delmailbox(request): if len(request.user.mailbox_set.all()) else None for mb in Mailbox.objects.filter(id__in=selection): - if not can_access_object(request.user, mb.domain): + if not request.user.can_access(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")) @@ -393,7 +383,7 @@ def mbaliases(request): elif request.GET.has_key("mbid"): aliases = Alias.objects.filter(mboxes__id=request.GET["mbid"]).distinct() else: - aliases = get_user_mbaliases(request.user) + aliases = request.user.get_mbaliases() return render_listing(request, "mbaliases", title=_("Mailbox aliases"), @@ -456,7 +446,7 @@ def newmbalias(request, tplname="admin/mbaliasform.html"): @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): + if not request.user.is_owner(alias.domain): raise PermDeniedException(_("You do not have access to this domain")) commonctx = {"title" : _("Mailbox alias editing"), "submit_label" : _("Update"), @@ -479,7 +469,7 @@ def delmbalias(request): selection = request.GET["selection"].split(",") for mbaid in selection: mbalias = Alias.objects.get(pk=mbaid) - if not is_object_owner(request.user, mbalias.domain): + if not request.user.is_owner(mbalias.domain): raise PermDeniedException(_("You do not have access to this domain")) events.raiseEvent("MailboxAliasDeleted", mbalias) ungrant_access_to_object(mbalias) @@ -590,7 +580,7 @@ def domain_admin_promotion(request): u = User.objects.get(username=form.cleaned_data["name"]) u.groups.add(Group.objects.get(name="DomainAdmins")) u.save() - if not can_access_object(request.user, u): + if not request.user.can_access(u): # The only case where we need to grant access to the # object is when ``u`` has been create by a super # admin @@ -615,7 +605,7 @@ def assign_domains_to_admin(request, da_id, tplname="admin/adminform.html"): if request.method == "POST": form = AssignDomainsForm(request.user, da, request.POST) if form.is_valid(): - current_domains = get_user_domains(da) + current_domains = da.get_domains() for domain in form.cleaned_data["domains"]: if not domain in current_domains: grant_access_to_object(da, domain) @@ -744,7 +734,8 @@ def saveextensions(request): return HttpResponseRedirect(reverse(admin.views.viewextensions)) @login_required -@user_passes_test(lambda u: u.is_superuser) +@permission_required("admin.add_mailbox") +@transaction.commit_on_success def importdata(request, tplname="admin/import.html"): if request.method == "POST": error = None @@ -753,19 +744,19 @@ def importdata(request, tplname="admin/import.html"): import csv reader = csv.reader(request.FILES['sourcefile'], delimiter=form.cleaned_data['sepcar']) - cpt = 0 try: + cpt = 0 for row in reader: mb = Mailbox() - mb.create_from_csv(row) + mb.create_from_csv(request.user, row) + events.raiseEvent("CreateMailbox", request.user, mb) cpt += 1 - except AdminError, e: - error = str(e) - else: messages.info(request, _("%d mailboxes imported successfully" % cpt)) return _render(request, "admin/import_done.html", { "status" : "ok", "msg" : "" }) + except ModoboaException, e: + error = str(e) return _render(request, "admin/import_done.html", { "status" : "ko", "msg" : error diff --git a/auth/lib.py b/auth/lib.py index 33b541601..1a830c09b 100644 --- a/auth/lib.py +++ b/auth/lib.py @@ -25,21 +25,3 @@ def decrypt(ciph): def get_password(request): return decrypt(request.session["password"]) - -def _check_password(password, crypted): - scheme = parameters.get_admin("PASSWORD_SCHEME", app="admin") - if scheme == "crypt": - return crypt.crypt(password, crypted) == crypted - if scheme == "md5": - return hashlib.md5(password).hexdigest() == crypted - return password - -def crypt_password(password, hexdigest=True): - scheme = parameters.get_admin("PASSWORD_SCHEME", app="admin") - if scheme == "crypt": - salt = ''.join(Random().sample(string.letters + string.digits, 2)) - return crypt.crypt(password, salt) - if scheme == "md5": - obj = hashlib.md5(password) - return obj.hexdigest() if hexdigest else obj.digest() - return password diff --git a/demo/__init__.py b/demo/__init__.py index 20ad29592..a8be7ca30 100755 --- a/demo/__init__.py +++ b/demo/__init__.py @@ -6,10 +6,12 @@ from django.template.loader import render_to_string from modoboa.lib import events from modoboa.lib.webutils import static_url -def menu(**kwargs): +def menu(target, user): import views - if kwargs["target"] != "top_menu_middle": + if target != "top_menu_middle": + return [] + if not user.has_mailbox: return [] return [ {"name" : "demo", @@ -29,8 +31,8 @@ def menu(**kwargs): } ] -def announcement(**kwargs): - if kwargs["target"] == "loginpage": +def announcement(target): + if target == "loginpage": txt = render_to_string("demo/login_announcement.html") return [txt] return "" diff --git a/demo/views.py b/demo/views.py index 4a1aabf37..5b10f7f09 100755 --- a/demo/views.py +++ b/demo/views.py @@ -2,12 +2,12 @@ import os from django.conf import settings from django.contrib.auth.decorators import login_required -from modoboa.admin.lib import is_not_localadmin from modoboa.lib.webutils import ajax_response from modoboa.lib.emailutils import sendmail_simple, sendmail_fromfile +from modoboa.lib.decorators import needs_mailbox @login_required -@is_not_localadmin() +@needs_mailbox() def send_virus(request): status, error = sendmail_fromfile("virus@example.net", request.user.username, os.path.join(settings.MODOBOA_DIR, @@ -17,7 +17,7 @@ def send_virus(request): return ajax_response(request, "ko", respmsg=error) @login_required -@is_not_localadmin() +@needs_mailbox() def send_spam(request): status, error = sendmail_simple("spam@example.net", request.user.username, """ This is the GTUBE, the diff --git a/extensions/amavis_quarantine/cleanup.py b/extensions/amavis_quarantine/cleanup.py index 235be0789..4020fb6ac 100755 --- a/extensions/amavis_quarantine/cleanup.py +++ b/extensions/amavis_quarantine/cleanup.py @@ -20,7 +20,7 @@ if __name__ == "__main__": l.setLevel(logging.DEBUG) l.addHandler(logging.StreamHandler()) - main.init() + main.load() max_messages_age = int(parameters.get_admin("MAX_MESSAGES_AGE", app="amavis_quarantine")) diff --git a/extensions/amavis_quarantine/main.py b/extensions/amavis_quarantine/main.py index 01f1605bc..11dac1047 100644 --- a/extensions/amavis_quarantine/main.py +++ b/extensions/amavis_quarantine/main.py @@ -1,4 +1,4 @@ -# -*- coding: utf-8 -*- +# coding: utf-8 """ """ @@ -16,13 +16,16 @@ def infos(): "url" : "quarantine" } -def init(): - events.register("UserMenuDisplay", menu) - parameters.register_admin("MAX_MESSAGES_AGE", type="int", - deflt=14, - help=_("Quarantine messages maximum age (in days) before deletion")) - parameters.register_admin("RELEASED_MSGS_CLEANUP", type="list_yesno", deflt="no", - help=_("Remove messages marked as released while cleaning up the database")) +def load(): + parameters.register_admin( + "MAX_MESSAGES_AGE", type="int", + deflt=14, + help=_("Quarantine messages maximum age (in days) before deletion") + ) + parameters.register_admin( + "RELEASED_MSGS_CLEANUP", type="list_yesno", deflt="no", + help=_("Remove messages marked as released while cleaning up the database") + ) parameters.register_admin("AM_PDP_MODE", type="list", deflt="unix", values=[("inet", "inet"), ("unix", "unix")], @@ -37,9 +40,11 @@ def init(): deflt=_("/var/amavis/amavisd.sock"), help=_("Path to the PDP server socket (if unix mode)")) - parameters.register_user("MESSAGES_PER_PAGE", type="int", deflt=40, - label="Number of displayed emails per page", - help=_("Sets the maximum number of messages displayed in a page")) + parameters.register_user( + "MESSAGES_PER_PAGE", type="int", deflt=40, + label="Number of displayed emails per page", + help=_("Sets the maximum number of messages displayed in a page") + ) def destroy(): events.unregister("UserMenuDisplay", menu) @@ -49,6 +54,7 @@ def urls(prefix): return (r'^%squarantine/' % prefix, include('modoboa.extensions.amavis_quarantine.urls')) +@events.observe("UserMenuDisplay") def menu(target, menu): import views diff --git a/extensions/amavis_quarantine/sql_listing.py b/extensions/amavis_quarantine/sql_listing.py index 340da068b..a1deb238c 100644 --- a/extensions/amavis_quarantine/sql_listing.py +++ b/extensions/amavis_quarantine/sql_listing.py @@ -7,7 +7,6 @@ from modoboa.lib import tables from modoboa.lib.webutils import static_url from modoboa.lib.email_listing import MBconnector, EmailListing from modoboa.lib.emailutils import * -from modoboa.admin.lib import is_domain_admin from models import * class Qtable(tables.Table): @@ -84,7 +83,7 @@ class SQLlisting(EmailListing): reset_wm_url = True def __init__(self, user, msgs, filter, **kwargs): - if not user.is_superuser and not is_domain_admin(user): + if user.belongs_to_group('SimpleUsers'): Qtable.cols_order = ['type', 'rstatus', 'from_', 'subject', 'time'] else: Qtable.cols_order = ['type', 'rstatus', 'to', 'from_', 'subject', 'time'] diff --git a/extensions/amavis_quarantine/templatetags/amextras.py b/extensions/amavis_quarantine/templatetags/amextras.py index 27998f04e..8f8bb0c4e 100644 --- a/extensions/amavis_quarantine/templatetags/amextras.py +++ b/extensions/amavis_quarantine/templatetags/amextras.py @@ -5,7 +5,6 @@ from django.core.urlresolvers import reverse from django.conf import settings from modoboa.lib.webutils import static_url from modoboa.extensions import amavis_quarantine -from modoboa.admin.lib import is_domain_admin register = template.Library() @@ -90,7 +89,7 @@ def quar_menu(selection, user): menu = render_to_string('common/menu.html', {"selection" : selection, "entries" : entries}) - if user.is_superuser or is_domain_admin(user): + if not user.belongs_to_group('SimpleUsers'): extraopts = [{"name" : "to", "label" : _("To")}] else: extraopts = [] diff --git a/extensions/amavis_quarantine/views.py b/extensions/amavis_quarantine/views.py index 2b3a73210..009c2442a 100644 --- a/extensions/amavis_quarantine/views.py +++ b/extensions/amavis_quarantine/views.py @@ -16,7 +16,6 @@ from lib import AMrelease from templatetags.amextras import * from modoboa.lib.email_listing import parse_search_parameters from sql_listing import * -from modoboa.admin.lib import is_domain_admin def __get_current_url(request): if request.session.has_key("page"): @@ -28,7 +27,13 @@ def __get_current_url(request): if params != "": res += "?%s" % (params) return res - + +def empty_quarantine(request): + content = "%s" % _("Empty quarantine") + ctx = getctx("ok", level=2, listing=content, navbar="", + menu=quar_menu("", request.user)) + return HttpResponse(simplejson.dumps(ctx), mimetype="application/json") + @login_required def _listing(request): filter = None @@ -59,12 +64,16 @@ def _listing(request): q = ~Q(rs='D') if not request.user.is_superuser: - mb = Mailbox.objects.get(user=request.user.id) - if is_domain_admin(request.user): - q &= Q(rid__email__contains=mb.domain.name) + if not request.user.belongs_to_group('SimpleUsers'): + doms = request.user.get_domains() + if not len(doms): + return empty_quarantine(request) + regexp = "(%s)" % '|'.join(map(lambda dom: dom.name, doms)) + q &= Q(rid__email__regex=regexp) else: - q &= Q(rid__email=mb.full_address) - if (request.user.is_superuser or is_domain_admin(request.user)) \ + q &= Q(rid__email=request.user.email) + + if not request.user.belongs_to_group('SimpleUsers') \ and rcptfilter is not None: q &= Q(rid__email__contains=rcptfilter) msgs = Msgrcpt.objects.filter(q).values("mail_id") @@ -82,12 +91,11 @@ def _listing(request): elems_per_page=int(parameters.get_user(request.user, "MESSAGES_PER_PAGE"))) page = lst.paginator.getpage(pageid) - if page: - content = lst.fetch(request, page.id_start, page.id_stop) - navbar = lst.render_navbar(page) - else: - content = "%s" % _("Empty quarantine") - navbar = "" + if not page: + return empty_quarantine(request) + + content = lst.fetch(request, page.id_start, page.id_stop) + navbar = lst.render_navbar(page) ctx = getctx("ok", listing=content, navbar=navbar, menu=quar_menu("", request.user)) return HttpResponse(simplejson.dumps(ctx), mimetype="application/json") @@ -115,7 +123,7 @@ def getmailcontent(request, mail_id): @login_required def viewmail(request, mail_id): - if request.user.is_superuser or is_domain_admin(request.user): + if not request.user.belongs_to_group('SimpleUsers'): rcpt = request.GET["rcpt"] else: mb = Mailbox.objects.get(user=request.user) @@ -158,7 +166,7 @@ def check_mail_id(request, mail_id): @login_required def delete(request, mail_id): mail_id = check_mail_id(request, mail_id) - if not request.user.is_superuser and not is_domain_admin(request.user): + if request.user.belongs_to_group('SimpleUsers'): mb = Mailbox.objects.get(user=request.user) msgrcpts = Msgrcpt.objects.filter(mail__in=mail_id, rid__email=mb.full_address) msgrcpts.update(rs='D') @@ -178,7 +186,7 @@ def delete(request, mail_id): @login_required def release(request, mail_id): mail_id = check_mail_id(request, mail_id) - if not request.user.is_superuser and not is_domain_admin(request.user): + if request.user.belongs_to_group('SimpleUsers'): mb = Mailbox.objects.get(user=request.user) msgrcpts = Msgrcpt.objects.filter(mail__in=mail_id, rid__email=mb.full_address) else: diff --git a/extensions/limits/controls.py b/extensions/limits/controls.py index ff49f2b2f..130050f58 100644 --- a/extensions/limits/controls.py +++ b/extensions/limits/controls.py @@ -6,7 +6,7 @@ """ from modoboa.admin.models import Mailbox from modoboa.admin.lib import \ - get_object_owner, grant_access_to_object, is_object_owner, \ + get_object_owner, grant_access_to_object, \ ungrant_access_to_object from modoboa.lib import events from lib import * @@ -104,20 +104,20 @@ def create_pool(user): p.save() p.create_limits() -@events.observe("DomainAdminDeleted") -def move_pool_resource(domadmin): - owner = get_object_owner(domadmin) +@events.observe("DomainAdminDeleted", "SuperAdminPromotion") +def move_pool_resource(user): + owner = get_object_owner(user) if not owner.is_superuser: - for ooentry in domadmin.objectaccess_set.all(): + for ooentry in user.objectaccess_set.all(): if ooentry.is_owner: grant_access_to_object(owner, ooentry.content_object, True) ooentry.delete() for lname in reseller_limits_tpl: - l = domadmin.limitspool.get_limit(lname) + l = user.limitspool.get_limit(lname) ol = owner.limitspool.get_limit(lname) ol.curvalue += l.curvalue ol.maxvalue += l.maxvalue ol.save() - domadmin.limitspool.delete() + user.limitspool.delete() diff --git a/extensions/limits/main.py b/extensions/limits/main.py index 74582e465..d313d8e17 100644 --- a/extensions/limits/main.py +++ b/extensions/limits/main.py @@ -6,7 +6,8 @@ The *limits* extension """ -from django.contrib.auth.models import Permission, Group +from django.contrib.auth.models import Permission, User, Group +from django.db import IntegrityError from django.contrib.contenttypes.models import ContentType from django.utils.translation import ugettext_noop as _, ugettext from django.core.urlresolvers import reverse @@ -28,6 +29,12 @@ def init(): grp.permissions.add(perm) grp.save() + for user in User.objects.filter(groups__name='DomainAdmins'): + try: + controls.create_pool(user) + except IntegrityError: + pass + def destroy(): ct = ContentType.objects.get(app_label="admin", model="domain") grp = Group.objects.get(name="DomainAdmins") @@ -50,6 +57,8 @@ def urls(prefix): @events.observe('AdminFooterDisplay') def display_pool_usage(user, objtype): + if user.id == 1: + return [] if objtype == "domaliases": objtype = "domain_aliases" elif objtype == "mbaliases": diff --git a/extensions/limits/migrations/0001_initial.py b/extensions/limits/migrations/0001_initial.py new file mode 100644 index 000000000..adf61f1ac --- /dev/null +++ b/extensions/limits/migrations/0001_initial.py @@ -0,0 +1,100 @@ +# 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 'LimitsPool' + db.create_table('limits_limitspool', ( + ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('user', self.gf('django.db.models.fields.related.OneToOneField')(to=orm['admin.User'], unique=True)), + )) + db.send_create_signal('limits', ['LimitsPool']) + + # Adding model 'Limit' + db.create_table('limits_limit', ( + ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('name', self.gf('django.db.models.fields.CharField')(max_length=255)), + ('curvalue', self.gf('django.db.models.fields.IntegerField')(default=0)), + ('maxvalue', self.gf('django.db.models.fields.IntegerField')(default=-2)), + ('pool', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['limits.LimitsPool'])), + )) + db.send_create_signal('limits', ['Limit']) + + # Adding unique constraint on 'Limit', fields ['name', 'pool'] + db.create_unique('limits_limit', ['name', 'pool_id']) + + + def backwards(self, orm): + + # Removing unique constraint on 'Limit', fields ['name', 'pool'] + db.delete_unique('limits_limit', ['name', 'pool_id']) + + # Deleting model 'LimitsPool' + db.delete_table('limits_limitspool') + + # Deleting model 'Limit' + db.delete_table('limits_limit') + + + models = { + 'admin.user': { + 'Meta': {'object_name': 'User', '_ormbases': ['auth.User']}, + 'user_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['auth.User']", 'unique': 'True', 'primary_key': 'True'}) + }, + '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'}) + }, + 'limits.limit': { + 'Meta': {'unique_together': "(('name', 'pool'),)", 'object_name': 'Limit'}, + 'curvalue': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'maxvalue': ('django.db.models.fields.IntegerField', [], {'default': '-2'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'pool': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['limits.LimitsPool']"}) + }, + 'limits.limitspool': { + 'Meta': {'object_name': 'LimitsPool'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'user': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['admin.User']", 'unique': 'True'}) + } + } + + complete_apps = ['limits'] diff --git a/extensions/limits/migrations/__init__.py b/extensions/limits/migrations/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/extensions/limits/models.py b/extensions/limits/models.py index 7c6d5e85a..ed2386905 100644 --- a/extensions/limits/models.py +++ b/extensions/limits/models.py @@ -3,8 +3,8 @@ from django.db import models from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes import generic -from django.contrib.auth.models import User from django.utils.translation import ugettext as _, ugettext_noop +from modoboa.admin.models import User reseller_limits_tpl = [ "domains_limit", diff --git a/extensions/limits/permissions.py b/extensions/limits/permissions.py index 52349171f..a16b3939c 100644 --- a/extensions/limits/permissions.py +++ b/extensions/limits/permissions.py @@ -54,7 +54,6 @@ def get_resellers_table(request): @events.observe("PermsGetClass") def get_perms_class(role): - print role if role != "resellers": return [] return [ResellersPerms] diff --git a/extensions/postfix_autoreply/main.py b/extensions/postfix_autoreply/main.py index 3d82fa7c5..2fff53989 100644 --- a/extensions/postfix_autoreply/main.py +++ b/extensions/postfix_autoreply/main.py @@ -22,17 +22,12 @@ def infos(): "url" : "postfix_autoreply" } -def init(): - events.register("CreateDomain", onCreateDomain) - events.register("DeleteDomain", onDeleteDomain) - events.register("CreateMailbox", onCreateMailbox) - events.register("DeleteMailbox", onDeleteMailbox) - events.register("ModifyMailbox", onModifyMailbox) - events.register("UserMenuDisplay", menu) - - parameters.register_admin("AUTOREPLIES_TIMEOUT", - type="int", deflt=86400, - help=_("Timeout in seconds between two auto-replies to the same recipient")) +def load(): + parameters.register_admin( + "AUTOREPLIES_TIMEOUT", + type="int", deflt=86400, + help=_("Timeout in seconds between two auto-replies to the same recipient") + ) def destroy(): events.unregister("CreateDomain", onCreateDomain) @@ -47,11 +42,14 @@ def urls(prefix): return (r'^%spostfix_autoreply/' % prefix, include('modoboa.extensions.postfix_autoreply.urls')) +@events.observe("UserMenuDisplay") def menu(target, user): import views if target != "uprefs_menu": return [] + if not user.has_mailbox: + return [] return [ {"name" : "autoreply", "url" : reverse(views.autoreply), @@ -61,41 +59,40 @@ def menu(target, user): "label" : ugettext("Auto-reply message")} ] -def onCreateDomain(**kwargs): - dom = kwargs["dom"] +@events.observe("CreateDomain") +def onCreateDomain(user, domain): transport = Transport() - transport.domain = "autoreply.%s" % dom.name + transport.domain = "autoreply.%s" % domain.name transport.method = "autoreply:" transport.save() -def onDeleteDomain(**kwargs): - dom = kwargs["dom"] - trans = Transport.objects.get(domain="autoreply.%s" % dom.name) +@events.observe("DeleteDomain") +def onDeleteDomain(domain): + trans = Transport.objects.get(domain="autoreply.%s" % domain.name) trans.delete() -def onCreateMailbox(**kwargs): - mbox = kwargs["mbox"] +@events.observe("CreateMailbox") +def onCreateMailbox(user, mailbox): alias = Alias() - alias.full_address = mbox.full_address + alias.full_address = mailbox.full_address alias.autoreply_address = \ - "%s@autoreply.%s" % (mbox.full_address, mbox.domain.name) + "%s@autoreply.%s" % (mailbox.full_address, mailbox.domain.name) alias.save() -def onDeleteMailbox(**kwargs): - mbox = kwargs["mbox"] +@events.observe("DeleteMailbox") +def onDeleteMailbox(mailbox): try: - alias = Alias.objects.get(full_address=mbox.full_address) + alias = Alias.objects.get(full_address=mailbox.full_address) alias.delete() except Alias.DoesNotExist: pass -def onModifyMailbox(**kwargs): - mbox = kwargs["mbox"] - oldmbox = kwargs["oldmbox"] - if oldmbox.full_address == mbox.full_address: +@events.observe("ModifyMailbox") +def onModifyMailbox(mailbox, oldmailbox): + if oldmailbox.full_address == mailbox.full_address: return - alias = Alias.objects.get(full_address=oldmbox.full_address) - alias.full_address = mbox.full_address + alias = Alias.objects.get(full_address=oldmailbox.full_address) + alias.full_address = mailbox.full_address alias.autoreply_address = \ - "%s@autoreply.%s" % (mbox.full_address, mbox.domain.name) + "%s@autoreply.%s" % (mailbox.full_address, mailbox.domain.name) alias.save() diff --git a/extensions/postfix_autoreply/scripts/autoreply.py b/extensions/postfix_autoreply/scripts/autoreply.py index 01f771500..db1e3f393 100755 --- a/extensions/postfix_autoreply/scripts/autoreply.py +++ b/extensions/postfix_autoreply/scripts/autoreply.py @@ -20,7 +20,7 @@ def send_autoreply(sender, mailbox, armessage): try: lastar = ARhistoric.objects.get(armessage=armessage.id, sender=sender) - main.init() + main.load() timeout = parameters.get_admin("AUTOREPLIES_TIMEOUT", app="postfix_autoreply") delta = datetime.timedelta(seconds=int(timeout)) diff --git a/extensions/postfix_autoreply/scripts/upgrade_autoreply.py b/extensions/postfix_autoreply/scripts/upgrade_autoreply.py index 856c1408b..780fa54e3 100755 --- a/extensions/postfix_autoreply/scripts/upgrade_autoreply.py +++ b/extensions/postfix_autoreply/scripts/upgrade_autoreply.py @@ -13,7 +13,7 @@ if __name__ == "__main__": import modoboa.extensions.postfix_autoreply.main as par from modoboa.extensions.postfix_autoreply.models import Transport, Alias - par.init() + par.load() doms = Domain.objects.all() for dom in doms: try: diff --git a/extensions/postfix_autoreply/views.py b/extensions/postfix_autoreply/views.py index abd67f25f..907a4ba40 100644 --- a/extensions/postfix_autoreply/views.py +++ b/extensions/postfix_autoreply/views.py @@ -8,12 +8,12 @@ from django.contrib import messages from django.core.urlresolvers import reverse from django.utils.translation import ugettext as _ from modoboa.lib.webutils import _render, ajax_response -from modoboa.admin.lib import is_not_localadmin +from modoboa.lib.decorators import needs_mailbox from forms import * from models import * @login_required -@is_not_localadmin('boxerror') +@needs_mailbox() def autoreply(request, tplname="postfix_autoreply/autoreply.html"): mb = Mailbox.objects.get(user=request.user.id) try: diff --git a/extensions/sievefilters/main.py b/extensions/sievefilters/main.py index 58e79606d..7609a008f 100644 --- a/extensions/sievefilters/main.py +++ b/extensions/sievefilters/main.py @@ -6,10 +6,7 @@ from modoboa.lib import events, parameters from modoboa.lib.webutils import static_url from sievelib.managesieve import SUPPORTED_AUTH_MECHS -def init(): - events.register("UserMenuDisplay", menu) - events.register("UserLogout", userlogout) - +def load(): parameters.register_admin("SERVER", type="string", deflt="127.0.0.1", help=_("Address of your MANAGESIEVE server")) @@ -47,11 +44,14 @@ def urls(prefix): return (r'^%ssfilters/' % prefix, include('modoboa.extensions.sievefilters.urls')) +@events.observe("UserMenuDisplay") def menu(target, user): import views if target != "options_menu": return [] + if not user.has_mailbox: + return [] return [ {"name" : "sievefilters", "label" : ugettext("Message filters"), @@ -59,14 +59,15 @@ def menu(target, user): "img" : static_url("pics/filters.png")} ] -def userlogout(**kwargs): +@events.observe("Userlogout") +def userlogout(request): from lib import SieveClient - if kwargs["request"].user.id == 1: + if not request.user.has_mailbox: return try: - sc = SieveClient(user=kwargs["request"].user.username, - password=kwargs["request"].session["password"]) + sc = SieveClient(user=request.user.username, + password=request.session["password"]) except Exception, e: pass else: diff --git a/extensions/sievefilters/views.py b/extensions/sievefilters/views.py index 9613e2d75..59774e488 100644 --- a/extensions/sievefilters/views.py +++ b/extensions/sievefilters/views.py @@ -7,14 +7,14 @@ from django.core.urlresolvers import reverse from modoboa.lib import parameters from modoboa.lib.webutils import _render, _render_to_string, _render_error, \ ajax_response, ajax_simple_response -from modoboa.admin.lib import is_not_localadmin +from modoboa.lib.decorators import needs_mailbox from modoboa.lib.connections import ConnectionError from modoboa.auth.lib import get_password from lib import * from forms import * @login_required -@is_not_localadmin() +@needs_mailbox() def index(request, tplname="sievefilters/index.html"): try: sc = SieveClient(user=request.user.username, @@ -38,14 +38,14 @@ def index(request, tplname="sievefilters/index.html"): }) @login_required -@is_not_localadmin() +@needs_mailbox() def get_templates(request, ftype): if ftype == "condition": return ajax_simple_response(FilterForm([], [], request).cond_templates) return ajax_simple_response(FilterForm([], [], request).action_templates) @login_required -@is_not_localadmin() +@needs_mailbox() def get_user_folders(request): from modoboa.extensions.webmail.lib import IMAPconnector @@ -55,7 +55,7 @@ def get_user_folders(request): return ajax_simple_response(ret) @login_required -@is_not_localadmin() +@needs_mailbox() def getfs(request, name): sc = SieveClient(user=request.user.username, password=request.session["password"]) @@ -109,7 +109,7 @@ def submitfilter(request, setname, okmsg, tplname, tplctx, update=False, sc=None return ajax_response(request, status="ko", template=tplname, **tplctx) @login_required -@is_not_localadmin() +@needs_mailbox() def newfilter(request, setname, tplname="sievefilters/filter.html"): ctx = {"title" : _("New filter"), "actionurl" : reverse(newfilter, args=[setname])} @@ -123,7 +123,7 @@ def newfilter(request, setname, tplname="sievefilters/filter.html"): return _render(request, tplname, ctx) @login_required -@is_not_localadmin() +@needs_mailbox() def editfilter(request, setname, fname, tplname="sievefilters/filter.html"): ctx = {"title" : _("Edit filter"), "actionurl" : reverse(editfilter, args=[setname, fname])} @@ -143,7 +143,7 @@ def editfilter(request, setname, fname, tplname="sievefilters/filter.html"): return _render(request, tplname, ctx) @login_required -@is_not_localadmin() +@needs_mailbox() def removefilter(request, setname, fname): sc = SieveClient(user=request.user.username, password=request.session["password"]) @@ -154,7 +154,7 @@ def removefilter(request, setname, fname): return ajax_response(request, "ko", respmsg=_("Failed to remove filter")) @login_required -@is_not_localadmin() +@needs_mailbox() def savefs(request, name): if not request.POST.has_key("scriptcontent"): return @@ -168,7 +168,7 @@ def savefs(request, name): return ajax_response(request, respmsg=_("Filters set saved")) @login_required -@is_not_localadmin() +@needs_mailbox() def new_filters_set(request, tplname="sievefilters/newfiltersset.html"): ctx = {"title" : _("Create a new filters set"), "fname" : "newfiltersset", @@ -199,7 +199,7 @@ def new_filters_set(request, tplname="sievefilters/newfiltersset.html"): return _render(request, tplname, ctx) @login_required -@is_not_localadmin() +@needs_mailbox() def remove_filters_set(request, name): sc = SieveClient(user=request.user.username, password=request.session["password"]) @@ -210,7 +210,7 @@ def remove_filters_set(request, name): return ajax_response(request, respmsg=_("Filters set deleted")) @login_required -@is_not_localadmin() +@needs_mailbox() def activate_filters_set(request, name): sc = SieveClient(user=request.user.username, password=request.session["password"]) @@ -221,7 +221,7 @@ def activate_filters_set(request, name): return ajax_response(request, respmsg=_("Filters set activated")) @login_required -@is_not_localadmin() +@needs_mailbox() def download_filters_set(request, name): sc = SieveClient(user=request.user.username, password=request.session["password"]) @@ -237,7 +237,7 @@ def download_filters_set(request, name): return resp @login_required -@is_not_localadmin() +@needs_mailbox() def toggle_filter_state(request, setname, fname): sc = SieveClient(user=request.user.username, password=request.session["password"]) @@ -275,11 +275,11 @@ def move_filter(request, setname, fname, direction): return ajax_response(request, template="sievefilters/filters.html", fs=fset) @login_required -@is_not_localadmin() +@needs_mailbox() def move_filter_up(request, setname, fname): return move_filter(request, setname, fname, "up") @login_required -@is_not_localadmin() +@needs_mailbox() def move_filter_down(request, setname, fname): return move_filter(request, setname, fname, "down") diff --git a/extensions/stats/grapher.py b/extensions/stats/grapher.py index ad52da760..2dfd5d3e7 100644 --- a/extensions/stats/grapher.py +++ b/extensions/stats/grapher.py @@ -3,7 +3,7 @@ import os import time import datetime -import rrdtool +#import rrdtool from django.utils.translation import ugettext as _ from modoboa.lib import parameters diff --git a/extensions/stats/main.py b/extensions/stats/main.py index f805e4717..ec87417a6 100644 --- a/extensions/stats/main.py +++ b/extensions/stats/main.py @@ -21,8 +21,7 @@ def infos(): "url" : "stats" } -def init(): - events.register("AdminMenuDisplay", menu) +def load(): parameters.register_admin("LOGFILE", type="string", deflt="/var/log/mail.log", help=_("Path to log file used to collect statistics")) @@ -40,6 +39,7 @@ def urls(prefix): return (r'^%sstats/' % prefix, include('modoboa.extensions.stats.urls')) +@events.observe("AdminMenuDisplay") def menu(target, user): if target == "admin_menu_box": return [ diff --git a/extensions/stats/scripts/logparser.py b/extensions/stats/scripts/logparser.py index 135ad8fa4..f6b2111e0 100755 --- a/extensions/stats/scripts/logparser.py +++ b/extensions/stats/scripts/logparser.py @@ -259,7 +259,7 @@ class LogParser(object): if __name__ == "__main__": from modoboa.extensions.stats import main - main.init() + main.load() log_file = parameters.get_admin("LOGFILE", app="stats") rrd_rootdir = parameters.get_admin("RRD_ROOTDIR", app="stats") diff --git a/extensions/stats/templates/stats/index.html b/extensions/stats/templates/stats/index.html index bd041abbd..83e9ac203 100644 --- a/extensions/stats/templates/stats/index.html +++ b/extensions/stats/templates/stats/index.html @@ -58,11 +58,7 @@ window.addEvent("domready", function() { }; get_graph = function(evt) { - {% if user.is_superuser %} var url = "{% url getgraph_root %}" + $("domain").value + "/"; - {% else %} - var url = "{% url getgraph domain.id %}"; - {% endif %} var p = get_period(); if (p == "") { @@ -72,9 +68,7 @@ window.addEvent("domready", function() { onSuccess : getgraph_callback}).get(); }; - {% if user.is_superuser %} $("domain").addEvent("change", get_graph); - {% endif %} $("period").addEvent("change", get_graph); $("customsend").addEvent("click", get_graph); }); @@ -84,17 +78,18 @@ window.addEvent("domready", function() { {% block pagetitle %}{% trans "Statistics" %}{% endblock %} {% block subcontent %} - {% if user.is_superuser %} {% trans "Domain:" %} + {% if user.is_superuser %} {% trans "Global" %} + {% endif %} {% for d in domains %} {{ d.name }} {% endfor %} - {% endif %} + {% trans "Period:" %} diff --git a/extensions/stats/views.py b/extensions/stats/views.py index adb64a436..47acb78e1 100644 --- a/extensions/stats/views.py +++ b/extensions/stats/views.py @@ -5,7 +5,7 @@ from django.core.urlresolvers import reverse from django.utils.translation import ugettext as _, ugettext_noop from django.utils import simplejson from modoboa.lib.webutils import _render, _render_to_string, getctx -from modoboa.admin.views import good_domain +from modoboa.admin.lib import check_domain_access from modoboa.admin.models import Domain, Mailbox from django.contrib.auth.decorators \ import login_required, user_passes_test, permission_required @@ -22,21 +22,17 @@ periods = [{"name" : "day", "label" : ugettext_noop("Day")}, {"name" : "custom", "label" : ugettext_noop("Custom")}] @login_required -@good_domain @permission_required("admin.view_mailboxes") +@check_domain_access def index(request): domains = None - period = request.GET.has_key("period") and request.GET["period"] or "day" - domid = request.GET.has_key("domid") and request.GET["domid"] or "" - if request.user.is_superuser: - domains = Domain.objects.all() - domain = "global" - else: - domain = Domain.objects.get(pk=domid) + period = request.GET.get("period", "day") + domid = request.GET.get("domid", "") + + domains = request.user.get_domains() return _render(request, 'stats/index.html', { "domains" : domains, - "domain" : domain, "graphs" : graph_list, "periods" : periods, "period" : period, @@ -45,16 +41,20 @@ def index(request): }) @login_required -@good_domain @permission_required("admin.view_mailboxes") def getgraph(request, dom_id): - period = request.GET.has_key("period") and request.GET["period"] or "day" + period = request.GET.get("period", "day") if dom_id == "global": + if not request.user.is_superuser: + raise PermDeniedError(_("you're not allowed to see those statistics")) domain = dom_id else: - domain = Domain.objects.get(pk=dom_id).name + domain = Domain.objects.get(pk=dom_id) + if not request.user.can_access(domain): + raise PermDeniedError(_("You don't have access to this domain")) + ctx = None - tplvars = {"graphs" : graph_list, "period" : period, "domain" : domain} + tplvars = {"graphs" : graph_list, "period" : period, "domain" : domain.name} if period == "custom": if not request.GET.has_key("start") or not request.GET.has_key("end"): ctx = getctx("ko", error=_("Bad custom period")) @@ -64,7 +64,7 @@ def getgraph(request, dom_id): G = Grapher() period_name = "%s_%s" % (start.replace('-',''), end.replace('-','')) for tpl_name in graph_list: - G.process(domain, + G.process(domain.name, period_name, str2Time(*start.split('-')), str2Time(*end.split('-')), diff --git a/extensions/webmail/lib.py b/extensions/webmail/lib.py index 658d71aa6..02d32b0a2 100644 --- a/extensions/webmail/lib.py +++ b/extensions/webmail/lib.py @@ -735,7 +735,6 @@ def send_mail(request, origmsg=None, posturl=None): from email.mime.multipart import MIMEMultipart from forms import ComposeMailForm from modoboa.lib.webutils import getctx, ajax_simple_response - from modoboa.admin.models import Mailbox from modoboa.auth.lib import get_password form = ComposeMailForm(request.POST) @@ -772,11 +771,11 @@ def send_mail(request, origmsg=None, posturl=None): msg.attach(create_mail_attachment(attdef)) msg["Subject"] = form.cleaned_data["subject"] - address, domain = split_mailbox(form.cleaned_data["from_"]) - try: - mb = Mailbox.objects.get(address=address, domain__name=domain) - msg["From"] = "%s <%s>" % (mb.name, form.cleaned_data["from_"]) - except Mailbox.DoesNotExist: + if request.user.first_name != "" or request.user.last_name != "": + msg["From"] = "%s %s <%s>" % (request.user.first_name, + request.user.last_name, + form.cleaned_data["from_"]) + else: msg["From"] = form.cleaned_data["from_"] msg["To"] = form.cleaned_data["to"] msg["Message-ID"] = make_msgid() diff --git a/extensions/webmail/main.py b/extensions/webmail/main.py index b49ac14de..4a476cb4c 100644 --- a/extensions/webmail/main.py +++ b/extensions/webmail/main.py @@ -5,10 +5,7 @@ from django.conf.urls.defaults import include from modoboa.lib import events, parameters from modoboa.lib.webutils import static_url -def init(): - events.register("UserMenuDisplay", menu) - events.register("UserLogout", userlogout) - +def load(): parameters.register_admin("IMAP_SERVER", type="string", deflt="127.0.0.1", help=_("Address of your IMAP server")) @@ -81,11 +78,14 @@ def urls(prefix): return (r'^%swebmail/' % prefix, include('modoboa.extensions.webmail.urls')) +@events.observe("UserMenuDisplay") def menu(target, user): import views if target != "top_menu": return [] + if not user.has_mailbox: + return [] return [ {"name" : "webmail", "label" : _("Webmail"), @@ -93,14 +93,15 @@ def menu(target, user): "img" : static_url("pics/webmail.png")} ] -def userlogout(**kwargs): +@events.observe("Userlogout") +def userlogout(request): from lib import IMAPconnector - if kwargs["request"].user.id == 1: + if not request.user.has_mailbox: return try: - m = IMAPconnector(user=kwargs["request"].user.username, - password=kwargs["request"].session["password"]) + m = IMAPconnector(user=request.user.username, + password=request.session["password"]) except Exception, e: pass else: diff --git a/extensions/webmail/views.py b/extensions/webmail/views.py index 1a76a3a9e..0a36cf982 100644 --- a/extensions/webmail/views.py +++ b/extensions/webmail/views.py @@ -9,13 +9,12 @@ from django.utils.translation import ugettext as _ from django.core.urlresolvers import reverse from django.contrib.auth.decorators import login_required from django.contrib import messages -from modoboa.admin.models import Mailbox from modoboa.lib import parameters from modoboa.lib.webutils import _render, _render_error, \ getctx, _render_to_string, ajax_response from modoboa.lib.emailutils import split_mailbox from modoboa.lib.email_listing import parse_search_parameters, Paginator -from modoboa.admin.lib import is_not_localadmin +from modoboa.lib.decorators import needs_mailbox from modoboa.auth.lib import * from lib import * from forms import * @@ -78,7 +77,7 @@ def __render_common_components(request, folder_name, lst=None, content=None, men @login_required -@is_not_localadmin() +@needs_mailbox() def folder(request, name, updatenav=True): if not name: name = "INBOX" @@ -112,7 +111,7 @@ def folder(request, name, updatenav=True): return HttpResponse(simplejson.dumps(ctx), mimetype="application/json") @login_required -@is_not_localadmin() +@needs_mailbox() def index(request): try: navp = request.session.has_key("navparams") \ @@ -134,7 +133,7 @@ def fetchmail(request, folder, mail_id, all=False): return None @login_required -@is_not_localadmin() +@needs_mailbox() def viewmail(request, folder, mail_id=None): from templatetags.webextras import viewm_menu @@ -155,7 +154,7 @@ def viewmail(request, folder, mail_id=None): return HttpResponse(simplejson.dumps(ctx), mimetype="application/json") @login_required -@is_not_localadmin() +@needs_mailbox() def getmailcontent(request, folder, mail_id): msg = fetchmail(request, folder, mail_id, True) if "class" in msg.keys() and msg["class"] == "unseen": @@ -172,7 +171,7 @@ def getmailcontent(request, folder, mail_id): }) @login_required -@is_not_localadmin() +@needs_mailbox() def getattachment(request, folder, mail_id): if request.GET.has_key("partnumber"): headers = {"Content-Type" : "text/plain", @@ -207,7 +206,7 @@ def getattachment(request, folder, mail_id): raise Http404 @login_required -@is_not_localadmin() +@needs_mailbox() def move(request): for arg in ["msgset", "to"]: if not request.GET.has_key(arg): @@ -218,7 +217,7 @@ def move(request): return folder(request, request.session["folder"], False) @login_required -@is_not_localadmin() +@needs_mailbox() def delete(request, fdname, mail_id): mbc = IMAPconnector(user=request.user.username, password=request.session["password"]) @@ -227,7 +226,7 @@ def delete(request, fdname, mail_id): return HttpResponse(simplejson.dumps(ctx), mimetype="application/json") @login_required -@is_not_localadmin() +@needs_mailbox() def mark(request, name): if not request.GET.has_key("status") or not request.GET.has_key("ids"): return @@ -240,7 +239,7 @@ def mark(request, name): return folder(request, name, False) @login_required -@is_not_localadmin() +@needs_mailbox() def empty(request, name): if name == parameters.get_user(request.user, "TRASH_FOLDER"): mbc = IMAPconnector(user=request.user.username, @@ -249,7 +248,7 @@ def empty(request, name): return folder(request, name, False) @login_required -@is_not_localadmin() +@needs_mailbox() def compact(request, name): mbc = IMAPconnector(user=request.user.username, password=request.session["password"]) @@ -301,7 +300,7 @@ def render_compose(request, form, posturl, email=None, insert_signature=False): return HttpResponse(simplejson.dumps(ctx), mimetype="application/json") @login_required -@is_not_localadmin() +@needs_mailbox() def compose(request): if request.method == "POST": status, resp = send_mail(request, posturl=reverse(compose)) @@ -312,7 +311,7 @@ def compose(request): return render_compose(request, form, reverse(compose), insert_signature=True) @login_required -@is_not_localadmin() +@needs_mailbox() def reply(request, folder, mail_id): msg = fetchmail(request, folder, mail_id, True) if request.method == "POST": @@ -331,7 +330,7 @@ def reply(request, folder, mail_id): email) @login_required -@is_not_localadmin() +@needs_mailbox() def forward(request, folder, mail_id): if request.method == "POST": status, response = send_mail(request, @@ -362,7 +361,7 @@ def separate_folder(fullname, sep="."): return fullname, None @login_required -@is_not_localadmin() +@needs_mailbox() def newfolder(request, tplname="webmail/folder.html"): mbc = IMAPconnector(user=request.user.username, password=request.session["password"]) @@ -389,7 +388,7 @@ def newfolder(request, tplname="webmail/folder.html"): return _render(request, tplname, ctx) @login_required -@is_not_localadmin() +@needs_mailbox() def editfolder(request, tplname="webmail/folder.html"): mbc = IMAPconnector(user=request.user.username, password=request.session["password"]) @@ -430,7 +429,7 @@ def editfolder(request, tplname="webmail/folder.html"): return _render(request, tplname, ctx) @login_required -@is_not_localadmin() +@needs_mailbox() def delfolder(request): if not request.GET.has_key("name") or request.GET["name"] == "": return @@ -440,7 +439,7 @@ def delfolder(request): return ajax_response(request) @login_required -@is_not_localadmin() +@needs_mailbox() def attachments(request, tplname="webmail/attachments.html"): if request.method == "POST": csuploader = AttachmentUploadHandler() @@ -475,7 +474,7 @@ def attachments(request, tplname="webmail/attachments.html"): return _render(request, tplname, ctx) @login_required -@is_not_localadmin() +@needs_mailbox() def delattachment(request): if not request.session.has_key("compose_mail") \ or not request.GET.has_key("name") \ diff --git a/lib/authbackends.py b/lib/authbackends.py index d49235ccd..aa1f81f4f 100644 --- a/lib/authbackends.py +++ b/lib/authbackends.py @@ -1,6 +1,4 @@ -from django.contrib.auth.models import User -from modoboa.admin.models import Mailbox -from modoboa.auth.lib import _check_password +from modoboa.admin.models import User class SimpleBackend: @@ -13,11 +11,7 @@ class SimpleBackend: user = User.objects.get(username=username) except User.DoesNotExist: return None - try: - mb = user.mailbox_set.all()[0] - except Exception: - return None - if not _check_password(password, mb.password): + if not user.check_password(password): return None return user @@ -27,3 +21,33 @@ class SimpleBackend: except User.DoesNotExist: return None +try: + from django_auth_ldap.backend import LDAPBackend as orig_LDAPBackend, _LDAPUser + from modoboa.admin.models import User + + class LDAPBackend(orig_LDAPBackend): + + def get_or_create_user(self, username, ldap_user): + """ + This must return a (User, created) 2-tuple for the given LDAP user. + username is the Django-friendly username of the user. ldap_user.dn is + the user's DN and ldap_user.attrs contains all of their LDAP attributes. + """ + return User.objects.get_or_create( + username__iexact=username, + defaults={'username': username.lower()} + ) + + def get_user(self, user_id): + user = None + + try: + user = User.objects.get(pk=user_id) + _LDAPUser(self, user=user) # This sets user.ldap_user + except User.DoesNotExist: + pass + + return user + +except ImportError: + pass diff --git a/lib/decorators.py b/lib/decorators.py new file mode 100644 index 000000000..4dba8f235 --- /dev/null +++ b/lib/decorators.py @@ -0,0 +1,22 @@ +# coding: utf-8 +""" +:mod:`decorators` --- common decorators +--------------------------------------- +""" +from functools import wraps +from exceptions import NeedsMailboxException + +def needs_mailbox(): + """Check if the current user owns at least one mailbox + + Some applications (the webmail for example) need a mailbox to + work. + """ + def decorator(f): + @wraps(f) + def wrapped_f(request, *args, **kwargs): + if request.user.has_mailbox: + return f(request, *args, **kwargs) + raise NeedsMailboxException() + return wrapped_f + return decorator diff --git a/lib/events.py b/lib/events.py index c2de6f32c..71e780fdd 100644 --- a/lib/events.py +++ b/lib/events.py @@ -13,6 +13,8 @@ import inspect import re events = [ + "SuperAdminPromotion", + "CanCreateDomain", "CreateDomain", "DeleteDomain", @@ -87,8 +89,8 @@ class observe(object): :param evtname: the event's name """ - def __init__(self, evtname): - self.evtname = evtname + def __init__(self, *evtnames): + self.evtnames = evtnames def __guess_extension_name(self, modname): if modname.startswith('modoboa.extensions'): @@ -112,7 +114,7 @@ class observe(object): return [] return f(*args) - register(self.evtname, wrapped_f) + map(lambda evt: register(evt, wrapped_f), self.evtnames) return wrapped_f def unregister(event, callback): diff --git a/lib/exceptions.py b/lib/exceptions.py index 381064da9..d2d4bd1b4 100644 --- a/lib/exceptions.py +++ b/lib/exceptions.py @@ -16,3 +16,6 @@ class PermDeniedException(ModoboaException): def __str__(self): return _("Permission denied: %s" % self.msg) +class NeedsMailboxException(ModoboaException): + pass + diff --git a/lib/ldaputils.py b/lib/ldaputils.py index aa1fff912..c16120b9e 100644 --- a/lib/ldaputils.py +++ b/lib/ldaputils.py @@ -36,9 +36,9 @@ Extracted from `this blog import sys import ldap import re -import base64 +import base64, hashlib, string, crypt +from random import Random from django.conf import settings -from modoboa.auth.lib import crypt_password from modoboa.lib import parameters class LDAPException(Exception): pass @@ -81,7 +81,7 @@ class LDAPAuthBackend(object): return None return dn - def _crypt_password(self, clearpassword): + def _crypt_password(self, user, clearpassword): """Overidding of the crypt_password function (LDAP compliant) The crypted password in base64 encoded and we prepend the used @@ -91,10 +91,18 @@ class LDAPAuthBackend(object): :return: the encrypted password """ scheme = parameters.get_admin("PASSWORD_SCHEME", app="admin") - if scheme == "clear": + if scheme == "crypt": + salt = ''.join(Random().sample(string.letters + string.digits, 2)) + result = crypt.crypt(clearpassword, salt) + elif scheme == "md5": + obj = hashlib.md5(clearpassword) + result = obj.digest() + elif scheme == "sha256": + obj = hashlib.sha256(clearpassword) + result = obj.digest() + else: return str(clearpassword) - crypted = crypt_password(clearpassword, False) - return str("{%s}%s" % (scheme.upper(), base64.encodestring(crypted))) + return str("{%s}%s" % (scheme.upper(), base64.b64encode(result))) def update_user_password(self, user, password, newpassword): try: @@ -104,7 +112,7 @@ class LDAPAuthBackend(object): newpassword = ('"%s"' % newpassword).encode('utf-16').lstrip('\377\376') ldif = [(ldap.MOD_REPLACE, self.pwd_attr, - self._crypt_password(newpassword))] + self._crypt_password(user, newpassword))] conn.modify_s(dn, ldif) except ldap.LDAPError, e: raise LDAPException(str(e)) diff --git a/lib/webutils.py b/lib/webutils.py index eac0b8fc5..ef99251eb 100644 --- a/lib/webutils.py +++ b/lib/webutils.py @@ -155,7 +155,6 @@ def topredirection(request): """ from modoboa.lib import parameters from modoboa.extensions import get_extension_infos - from modoboa.admin.lib import is_domain_admin topredir = parameters.get_admin("DEFAULT_TOP_REDIRECTION", app="general") if not topredir in ["admin", "userprefs"]: @@ -165,7 +164,7 @@ def topredirection(request): path = topredir if topredir in ["admin", "stats"] and \ - (not request.user.is_superuser and not is_domain_admin(request.user)): + request.user.belongs_to_group('SimpleUsers'): path = "userprefs" return HttpResponseRedirect(path) diff --git a/templates/userprefs/chpassword.html b/templates/userprefs/chpassword.html index 056f7968a..883a8d7d4 100644 --- a/templates/userprefs/chpassword.html +++ b/templates/userprefs/chpassword.html @@ -1,6 +1,11 @@ {% extends "layout_simple.html" %} {% block html_head %} + {% endblock %} {% block content %} {% load i18n %} diff --git a/userprefs/forms.py b/userprefs/forms.py index 0d8a061b3..f5de949e5 100644 --- a/userprefs/forms.py +++ b/userprefs/forms.py @@ -1,7 +1,5 @@ from django import forms from modoboa.admin.models import Domain, Mailbox -from modoboa.auth.lib import _check_password -from django.contrib.auth.models import check_password from django.utils.translation import ugettext as _, ugettext_noop from modoboa.lib.emailutils import split_mailbox from modoboa.lib import parameters @@ -25,12 +23,7 @@ class ChangePasswordForm(forms.Form): if parameters.get_admin("AUTHENTICATION_TYPE", app="admin") != "local": return self.cleaned_data["oldpassword"] - if not isinstance(self.target, Mailbox): - func = check_password - else: - func = _check_password - if not func(self.cleaned_data["oldpassword"], - self.target.password): + if not self.target.check_password(self.cleaned_data["oldpassword"]): raise forms.ValidationError(_("Old password mismatchs")) return self.cleaned_data["oldpassword"] diff --git a/userprefs/templatetags/uprefs_extras.py b/userprefs/templatetags/uprefs_extras.py index e341ef4dd..a75914496 100644 --- a/userprefs/templatetags/uprefs_extras.py +++ b/userprefs/templatetags/uprefs_extras.py @@ -5,7 +5,6 @@ from django.core.urlresolvers import reverse from modoboa import userprefs from modoboa.lib import events from modoboa.lib.webutils import static_url -from modoboa.admin.lib import is_domain_admin register = template.Library() @@ -31,7 +30,7 @@ def options_menu(user): ] }, ] - if not user.is_superuser and not is_domain_admin(user): + if user.belongs_to_group('SimpleUsers'): entries[0]["menu"] += [{ "name" : "setforwards", "url" : reverse(userprefs.views.setforward), @@ -41,7 +40,7 @@ def options_menu(user): "rel" : "{handler:'iframe',size:{x:360,y:350},closeBtn:true}" }] - entries[0]["menu"] += events.raiseQueryEvent("UserMenuDisplay", "options_menu") + entries[0]["menu"] += events.raiseQueryEvent("UserMenuDisplay", "options_menu", user) return render_to_string('common/menulist.html', {"entries" : entries, "user" : user}) @@ -49,7 +48,7 @@ def options_menu(user): @register.simple_tag def uprefs_menu(user): entries = [] - entries += events.raiseQueryEvent("UserMenuDisplay", "uprefs_menu") + entries += events.raiseQueryEvent("UserMenuDisplay", "uprefs_menu", user) return render_to_string('common/menulist.html', {"entries" : entries, "user" : user}) diff --git a/userprefs/views.py b/userprefs/views.py index a9b2a5917..0f996a8d6 100644 --- a/userprefs/views.py +++ b/userprefs/views.py @@ -9,7 +9,7 @@ from modoboa.lib import parameters, events from modoboa.lib.webutils import _render, _render_error, ajax_response from forms import * from modoboa.admin.models import Mailbox, Alias -from modoboa.admin.lib import is_domain_admin, AdminError +from modoboa.admin.lib import AdminError from modoboa.auth.lib import encrypt @login_required @@ -18,42 +18,36 @@ def index(request): @login_required def changepassword(request, tplname="userprefs/chpassword.html"): - if request.user.id == 1: - target = request.user - else: - target = Mailbox.objects.get(user=request.user.id) - res = events.raiseQueryEvent("PasswordChange", user=request.user) + res = events.raiseQueryEvent("PasswordChange", request.user) if True in res: ctx = dict(error=_("Password change is disabled for this user")) return _render_error(request, errortpl="error_simple", user_context=ctx) error = None if request.method == "POST": - form = ChangePasswordForm(target, request.POST) + form = ChangePasswordForm(request.user, request.POST) if form.is_valid(): - if request.user.id != 1: - try: - target.set_password(form.cleaned_data["oldpassword"], - form.cleaned_data["confirmation"]) - except AdminError, e: - error = str(e) - request.session["password"] = encrypt(request.POST["confirmation"]) + try: + request.user.set_password(form.cleaned_data["confirmation"], + form.cleaned_data["oldpassword"]) + except AdminError, e: + error = str(e) else: - target.set_password(request.POST["confirmation"]) - target.save() - + request.session["password"] = encrypt(request.POST["confirmation"]) + request.user.save() if error is None: return ajax_response(request, respmsg=_("Password changed")) + return ajax_response(request, status="ko", template=tplname, form=form, error=error) - form = ChangePasswordForm(target) + form = ChangePasswordForm(request.user) return _render(request, 'userprefs/chpassword.html', { "form" : form }) @login_required -@user_passes_test(lambda u: not u.is_superuser and not is_domain_admin(u)) +@user_passes_test(lambda u: u.belongs_to_group('SimpleUsers')) def setforward(request, tplname="userprefs/setforward.html"): mb = Mailbox.objects.get(user=request.user.id) try: