From 252ac918cc4657ac02b1137c22d196114a3264de Mon Sep 17 00:00:00 2001 From: jedi Date: Sat, 7 Nov 2020 17:57:00 +0100 Subject: [PATCH] only allow owner to handle domains, mailboxes and aliases --- backend/backend/settings.py | 155 ++++++++++++++++++ backend/multimail/actions.py | 26 ++- backend/multimail/admin.py | 28 ++-- backend/multimail/forms.py | 87 +++++++++- backend/multimail/migrations/0001_initial.py | 65 +++++++- ...2_aliases_domains_mailboxes_tlspolicies.py | 68 -------- backend/multimail/models.py | 30 +--- backend/multimail/owner.py | 7 + .../templates/multimail/aliases.html | 16 +- .../multimail/templates/multimail/base.html | 6 +- .../templates/multimail/domains.html | 8 + .../templates/multimail/mailboxes.html | 8 + backend/multimail/urls.py | 3 + backend/multimail/views.py | 24 +-- 14 files changed, 382 insertions(+), 149 deletions(-) create mode 100644 backend/backend/settings.py delete mode 100644 backend/multimail/migrations/0002_aliases_domains_mailboxes_tlspolicies.py create mode 100644 backend/multimail/owner.py diff --git a/backend/backend/settings.py b/backend/backend/settings.py new file mode 100644 index 0000000..da4f4ef --- /dev/null +++ b/backend/backend/settings.py @@ -0,0 +1,155 @@ +""" +Django settings for backend project. + +Generated by 'django-admin startproject' using Django 3.1.3. + +For more information on this file, see +https://docs.djangoproject.com/en/3.1/topics/settings/ + +For the full list of settings and their values, see +https://docs.djangoproject.com/en/3.1/ref/settings/ +""" + +from pathlib import Path + +# Build paths inside the project like this: BASE_DIR / 'subdir'. +import config.secret +import config.sql +import config.ldap + +BASE_DIR = Path(__file__).resolve().parent.parent + +# Quick-start development settings - unsuitable for production +# See https://docs.djangoproject.com/en/3.1/howto/deployment/checklist/ + +# SECURITY WARNING: keep the secret key used in production secret! +SECRET_KEY = config.secret.key + +# SECURITY WARNING: don't run with debug turned on in production! +DEBUG = True + +ALLOWED_HOSTS = [] + +import ldap +from django_auth_ldap.config import LDAPSearch + +AUTH_LDAP_SERVER_URI = config.ldap.uri +AUTH_LDAP_BIND_DN = config.ldap.bind_dn +AUTH_LDAP_BIND_PASSWORD = config.ldap.bind_pass +AUTH_LDAP_USER_SEARCH = LDAPSearch( + config.ldap.search_dn, ldap.SCOPE_SUBTREE, config.ldap.search_filter +) + +AUTHENTICATION_BACKENDS = [ + "django_auth_ldap.backend.LDAPBackend", + "django.contrib.auth.backends.ModelBackend", +] + +LOGIN_REDIRECT_URL = '/' + +# Application definition + +INSTALLED_APPS = [ + 'multimail.apps.MultimailConfig', + 'bootstrap4', + 'django_static_fontawesome', + 'django.contrib.admin', + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.staticfiles', +] + +MIDDLEWARE = [ + 'django.middleware.security.SecurityMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.common.CommonMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + 'django.middleware.clickjacking.XFrameOptionsMiddleware', +] + +ROOT_URLCONF = 'backend.urls' + +TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': [BASE_DIR / 'multimail/templates'] + , + 'APP_DIRS': True, + 'OPTIONS': { + 'context_processors': [ + 'django.template.context_processors.debug', + 'django.template.context_processors.request', + 'django.contrib.auth.context_processors.auth', + 'django.contrib.messages.context_processors.messages', + ], + }, + }, +] + +WSGI_APPLICATION = 'backend.wsgi.application' + +# Database +# https://docs.djangoproject.com/en/3.1/ref/settings/#databases + +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': BASE_DIR / 'db.sqlite3', + }, + 'mail': { + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': BASE_DIR / 'mail.sqlite3', + }, + 'ldap': { + 'ENGINE': 'ldapdb.backends.ldap', + 'NAME': config.ldap.uri, + 'USER': config.ldap.bind_dn, + 'PASSWORD': config.ldap.bind_pass, + }, + #'mysql': { + # 'NAME': 'user_data', + # 'ENGINE': 'django.db.backends.mysql', + # 'USER': 'mysql_user', + # 'PASSWORD': 'priv4te' + #} +} + +# Password validation +# https://docs.djangoproject.com/en/3.1/ref/settings/#auth-password-validators + +AUTH_PASSWORD_VALIDATORS = [ + { + 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', + }, +] + +# Internationalization +# https://docs.djangoproject.com/en/3.1/topics/i18n/ + +LANGUAGE_CODE = 'en-us' + +TIME_ZONE = 'UTC' + +USE_I18N = True + +USE_L10N = True + +USE_TZ = True + +# Static files (CSS, JavaScript, Images) +# https://docs.djangoproject.com/en/3.1/howto/static-files/ + +STATIC_URL = '/static/' diff --git a/backend/multimail/actions.py b/backend/multimail/actions.py index feaeb13..75b92f0 100644 --- a/backend/multimail/actions.py +++ b/backend/multimail/actions.py @@ -1,28 +1,44 @@ -from django.http import HttpResponseRedirect -from django.shortcuts import get_object_or_404 +from django.http import HttpResponseRedirect, Http404 from django.urls import reverse from django.contrib.auth.decorators import login_required from django.contrib.auth import logout as auth_logout from .models import Domain, Mailbox, Alias +from .owner import user_from_request + @login_required(login_url='/login/') def delete_domain(request, domain_id): - domain = get_object_or_404(Domain, pk=domain_id) + try: + user = user_from_request(request) + domain = Domain.objects.filter(admin__admin=user['name'], admin__source=user['source']).get(pk=domain_id) + except: + raise Http404 + # TODO this might fail due to foreign key constraint domain.delete() return HttpResponseRedirect(reverse('multimail:domains')) @login_required(login_url='/login/') def delete_mailbox(request, mailbox_id): - mailbox = get_object_or_404(Mailbox, pk=mailbox_id) + try: + user = user_from_request(request) + domains = [o.domain for o in Domain.objects.filter(admin__admin=user['name'], admin__source=user['source'])] + mailbox = Mailbox.objects.filter(domain__in=domains).get(pk=mailbox_id) + except: + raise Http404 mailbox.delete() return HttpResponseRedirect(reverse('multimail:mailboxes')) @login_required(login_url='/login/') def delete_alias(request, alias_id): - alias = get_object_or_404(Alias, pk=alias_id) + try: + user = user_from_request(request) + domains = [o.domain for o in Domain.objects.filter(admin__admin=user['name'], admin__source=user['source'])] + alias = Alias.objects.filter(source_domain__in=domains).get(pk=alias_id) + except: + raise Http404 alias.delete() return HttpResponseRedirect(reverse('multimail:aliases')) diff --git a/backend/multimail/admin.py b/backend/multimail/admin.py index b64b403..482fb66 100644 --- a/backend/multimail/admin.py +++ b/backend/multimail/admin.py @@ -1,32 +1,26 @@ from django.contrib import admin -from .models import Choice, Question -from .models import Alias, Domain, Mailbox, TLSPolicy +from .models import TLSPolicy, Domain, DomainOwner -class ChoiceInline(admin.TabularInline): - model = Choice +class AdminInline(admin.TabularInline): + model = DomainOwner extra = 1 -class QuestionAdmin(admin.ModelAdmin): - fieldsets = [ - (None, {'fields': ['question_text']}), - ('Date information', {'fields': ['pub_date'], 'classes': ['collapse']}), - ] - list_display = ('question_text', 'pub_date', 'was_published_recently') - inlines = [ChoiceInline] +class DomainAdmin(admin.ModelAdmin): + list_display = [('domain'), ('admins')] + inlines = [AdminInline] + + def admins(self, obj): + return "[" + ", ".join([x.source + ":" + x.admin for x in obj.admin.all()]) + "]" -admin.site.register(Question, QuestionAdmin) +admin.site.register(Domain, DomainAdmin) class TLSPolicyAdmin(admin.ModelAdmin): - list_display = ('domain', 'policy', 'params') + list_display = [('domain'), ('policy'), ('params')] admin.site.register(TLSPolicy, TLSPolicyAdmin) - -admin.site.register(Alias) -admin.site.register(Domain) -admin.site.register(Mailbox) diff --git a/backend/multimail/forms.py b/backend/multimail/forms.py index 7df34d9..98570f2 100644 --- a/backend/multimail/forms.py +++ b/backend/multimail/forms.py @@ -1,29 +1,38 @@ from django.contrib.auth.decorators import login_required from django.forms import ModelForm -from django.http import HttpResponseRedirect +from django.http import HttpResponseRedirect, Http404 from django.shortcuts import render from multimail.models import Domain, Mailbox, Alias +from multimail.owner import user_from_request class DomainForm(ModelForm): - class Meta: + class Meta: model = Domain - fields = ['domain'] + fields = '__all__' + #fields = ['domain'] + class MailboxForm(ModelForm): - class Meta: + class Meta: model = Mailbox fields = '__all__' + class AliasForm(ModelForm): - class Meta: + class Meta: model = Alias fields = '__all__' + @login_required(login_url='/login/') def edit_domain(request, domain_id): - domain = Domain.objects.get(id=domain_id) + try: + user = user_from_request(request) + domain = Domain.objects.filter(admin__admin=user['name'], admin__source=user['source']).get(pk=domain_id) + except: + raise Http404 if request.method == 'POST': form = DomainForm(request.POST, instance=domain) if form.is_valid(): @@ -35,9 +44,31 @@ def edit_domain(request, domain_id): return render(request, 'multimail/edit_domain.html', {'form': form}) + +@login_required(login_url='/login/') +def new_domain(request): + user = user_from_request(request) + if request.method == 'POST': + form = DomainForm(request.POST) + if form.is_valid(): + domain = form.save() + domain.admin.create(admin=user['name'], source=user['source']) + return HttpResponseRedirect('/domains/') + + else: + form = DomainForm() + + return render(request, 'multimail/edit_domain.html', {'form': form}) + + @login_required(login_url='/login/') def edit_mailbox(request, mailbox_id): - mailbox = Mailbox.objects.get(id=mailbox_id) + try: + user = user_from_request(request) + domains = [o.domain for o in Domain.objects.filter(admin__admin=user['name'], admin__source=user['source'])] + mailbox = Mailbox.objects.filter(domain__in=domains).get(pk=mailbox_id) + except: + raise Http404 if request.method == 'POST': form = MailboxForm(request.POST, instance=mailbox) if form.is_valid(): @@ -49,9 +80,31 @@ def edit_mailbox(request, mailbox_id): return render(request, 'multimail/edit_mailbox.html', {'form': form}) + +@login_required(login_url='/login/') +def new_mailbox(request): + if request.method == 'POST': + user = user_from_request(request) + domains = [o.domain for o in Domain.objects.filter(admin__admin=user['name'], admin__source=user['source'])] + form = MailboxForm(request.POST) + if form.is_valid() and form.domain in domains: + form.save() + return HttpResponseRedirect('/mailboxes/') + + else: + form = MailboxForm() + + return render(request, 'multimail/edit_mailbox.html', {'form': form}) + + @login_required(login_url='/login/') def edit_alias(request, alias_id): - alias = Alias.objects.get(id=alias_id) + try: + user = user_from_request(request) + domains = [o.domain for o in Domain.objects.filter(admin__admin=user['name'], admin__source=user['source'])] + alias = Alias.objects.filter(source_domain__in=domains).get(pk=alias_id) + except: + raise Http404 if request.method == 'POST': form = AliasForm(request.POST, instance=alias) if form.is_valid(): @@ -61,4 +114,20 @@ def edit_alias(request, alias_id): else: form = AliasForm(instance=alias) - return render(request, 'multimail/edit_alias.html', {'form': form}) \ No newline at end of file + return render(request, 'multimail/edit_alias.html', {'form': form}) + + +@login_required(login_url='/login/') +def new_alias(request): + if request.method == 'POST': + user = user_from_request(request) + domains = [o.domain for o in Domain.objects.filter(admin__admin=user['name'], admin__source=user['source'])] + form = AliasForm(request.POST) + if form.is_valid() and form.source_domain in domains: + form.save() + return HttpResponseRedirect('/aliases/') + + else: + form = AliasForm() + + return render(request, 'multimail/edit_alias.html', {'form': form}) diff --git a/backend/multimail/migrations/0001_initial.py b/backend/multimail/migrations/0001_initial.py index 2528c2e..39b76a2 100644 --- a/backend/multimail/migrations/0001_initial.py +++ b/backend/multimail/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 3.1.3 on 2020-11-02 18:55 +# Generated by Django 3.1.3 on 2020-11-07 16:49 from django.db import migrations, models import django.db.models.deletion @@ -13,20 +13,67 @@ class Migration(migrations.Migration): operations = [ migrations.CreateModel( - name='Question', + name='Alias', fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('question_text', models.CharField(max_length=200)), - ('pub_date', models.DateTimeField(verbose_name='date published')), + ('id', models.AutoField(primary_key=True, serialize=False)), + ('source_username', models.CharField(max_length=64)), + ('source_domain', models.CharField(max_length=255)), + ('destination_username', models.CharField(max_length=64)), + ('destination_domain', models.CharField(max_length=255)), + ('enabled', models.IntegerField(blank=True, null=True)), ], + options={ + 'db_table': 'aliases', + 'managed': False, + }, ), migrations.CreateModel( - name='Choice', + name='Domain', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('choice_text', models.CharField(max_length=200)), - ('votes', models.IntegerField(default=0)), - ('question', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='multimail.question')), + ('domain', models.CharField(max_length=255)), + ], + options={ + 'db_table': 'domains', + 'managed': False, + }, + ), + migrations.CreateModel( + name='Mailbox', + fields=[ + ('id', models.AutoField(primary_key=True, serialize=False)), + ('username', models.CharField(max_length=64)), + ('domain', models.CharField(max_length=255)), + ('password', models.CharField(max_length=255)), + ('quota', models.IntegerField(blank=True, null=True)), + ('enabled', models.IntegerField(blank=True, null=True)), + ('sendonly', models.IntegerField(blank=True, null=True)), + ], + options={ + 'db_table': 'mailboxes', + 'managed': False, + }, + ), + migrations.CreateModel( + name='TLSPolicy', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('domain', models.CharField(max_length=255)), + ('policy', models.TextField()), + ('params', models.CharField(blank=True, max_length=255, null=True)), + ], + options={ + 'db_table': 'tlspolicies', + 'managed': False, + }, + ), + migrations.CreateModel( + name='DomainOwner', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('admin', models.CharField(max_length=200)), + ('source', models.CharField(choices=[('system', 'system'), ('ldap', 'ldap'), ('mail', 'mail')], default=0, max_length=8)), + ('domain', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='admin', to='multimail.domain')), ], ), ] diff --git a/backend/multimail/migrations/0002_aliases_domains_mailboxes_tlspolicies.py b/backend/multimail/migrations/0002_aliases_domains_mailboxes_tlspolicies.py deleted file mode 100644 index b0a0ffb..0000000 --- a/backend/multimail/migrations/0002_aliases_domains_mailboxes_tlspolicies.py +++ /dev/null @@ -1,68 +0,0 @@ -# Generated by Django 3.1.3 on 2020-11-02 21:11 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('multimail', '0001_initial'), - ] - - operations = [ - migrations.CreateModel( - name='Aliases', - fields=[ - ('id', models.AutoField(primary_key=True, serialize=False)), - ('source_username', models.CharField(max_length=64)), - ('source_domain', models.CharField(max_length=255)), - ('destination_username', models.CharField(max_length=64)), - ('destination_domain', models.CharField(max_length=255)), - ('enabled', models.IntegerField(blank=True, null=True)), - ], - options={ - 'db_table': 'aliases', - 'managed': False, - }, - ), - migrations.CreateModel( - name='Domains', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('domain', models.CharField(max_length=255)), - ], - options={ - 'db_table': 'domains', - 'managed': False, - }, - ), - migrations.CreateModel( - name='Mailboxes', - fields=[ - ('id', models.AutoField(primary_key=True, serialize=False)), - ('username', models.CharField(max_length=64)), - ('domain', models.CharField(max_length=255)), - ('password', models.CharField(max_length=255)), - ('quota', models.IntegerField(blank=True, null=True)), - ('enabled', models.IntegerField(blank=True, null=True)), - ('sendonly', models.IntegerField(blank=True, null=True)), - ], - options={ - 'db_table': 'mailboxes', - 'managed': False, - }, - ), - migrations.CreateModel( - name='Tlspolicies', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('domain', models.CharField(max_length=255)), - ('policy', models.TextField()), - ('params', models.CharField(blank=True, max_length=255, null=True)), - ], - options={ - 'db_table': 'tlspolicies', - 'managed': False, - }, - ), - ] diff --git a/backend/multimail/models.py b/backend/multimail/models.py index 1bedeea..6813fc2 100644 --- a/backend/multimail/models.py +++ b/backend/multimail/models.py @@ -3,27 +3,6 @@ from django.db import models from django.utils import timezone -class Question(models.Model): - question_text = models.CharField(max_length=200) - pub_date = models.DateTimeField('date published') - - def __str__(self): - return self.question_text - - def was_published_recently(self): - now = timezone.now() - return now - datetime.timedelta(days=1) <= self.pub_date <= now - - -class Choice(models.Model): - question = models.ForeignKey(Question, on_delete=models.CASCADE) - choice_text = models.CharField(max_length=200) - votes = models.IntegerField(default=0) - - def __str__(self): - return self.choice_text - - class Alias(models.Model): id = models.AutoField(primary_key=True) source_username = models.CharField(max_length=64) @@ -67,3 +46,12 @@ class TLSPolicy(models.Model): class Meta: managed = False db_table = 'tlspolicies' + + +class DomainOwner(models.Model): + domain = models.ForeignKey(Domain, on_delete=models.CASCADE, related_name='admin') + admin = models.CharField(max_length=200) + source = models.CharField(max_length=8, choices=[('system','system'), ('ldap', 'ldap'), ('mail', 'mail')], default=0) + + def __str__(self): + return self.admin diff --git a/backend/multimail/owner.py b/backend/multimail/owner.py new file mode 100644 index 0000000..e2ddcef --- /dev/null +++ b/backend/multimail/owner.py @@ -0,0 +1,7 @@ + +def user_from_request(request): + if hasattr(request.user, 'ldap_user'): + # print(request.user.ldap_user.attrs.data) + return {'name': request.user.username, 'source': 'ldap'} + else: + return {'name': request.user.username, 'source': 'system'} \ No newline at end of file diff --git a/backend/multimail/templates/multimail/aliases.html b/backend/multimail/templates/multimail/aliases.html index d41052a..7797fe3 100644 --- a/backend/multimail/templates/multimail/aliases.html +++ b/backend/multimail/templates/multimail/aliases.html @@ -16,13 +16,25 @@ {% for alias in alias_list %} - {{ alias.source_username }}@{{ alias.source_domain }} + {{ alias.source_username }}@{{ alias.source_domain }} + {{ alias.destination_username }}@{{ alias.destination_domain }} - Delete + Delete {% endfor %} + + + + + + Add + + + {% else %}

You haven't set up any aliases yet.

diff --git a/backend/multimail/templates/multimail/base.html b/backend/multimail/templates/multimail/base.html index 2e71a3a..593a09b 100644 --- a/backend/multimail/templates/multimail/base.html +++ b/backend/multimail/templates/multimail/base.html @@ -39,7 +39,7 @@