From 4a2a0be3a402dfdf4e896bfac250d050c9e309bb Mon Sep 17 00:00:00 2001 From: jedi Date: Mon, 18 Oct 2021 00:32:02 +0200 Subject: [PATCH] add login and password change form for mailbox owners --- backend/backend/settings.py | 1 + backend/multimail/actions.py | 10 ++-- backend/multimail/auth_backends.py | 47 ++++++++++++++++ backend/multimail/forms.py | 54 ++++++++++++++++--- backend/multimail/models.py | 10 ++++ backend/multimail/owner.py | 19 +++++-- .../templates/multimail/mailboxindex.html | 40 ++++++++++++++ backend/multimail/urls.py | 2 +- backend/multimail/views.py | 36 +++++++++---- 9 files changed, 191 insertions(+), 28 deletions(-) create mode 100644 backend/multimail/auth_backends.py create mode 100644 backend/multimail/templates/multimail/mailboxindex.html diff --git a/backend/backend/settings.py b/backend/backend/settings.py index 5226316..bf7e141 100644 --- a/backend/backend/settings.py +++ b/backend/backend/settings.py @@ -43,6 +43,7 @@ AUTH_LDAP_USER_SEARCH = LDAPSearch( AUTHENTICATION_BACKENDS = [ "django_auth_ldap.backend.LDAPBackend", "django.contrib.auth.backends.ModelBackend", + "multimail.auth_backends.MailboxBackend", ] LOGIN_REDIRECT_URL = '/' diff --git a/backend/multimail/actions.py b/backend/multimail/actions.py index 75b92f0..dfb4911 100644 --- a/backend/multimail/actions.py +++ b/backend/multimail/actions.py @@ -1,13 +1,13 @@ from django.http import HttpResponseRedirect, Http404 from django.urls import reverse -from django.contrib.auth.decorators import login_required +from django.contrib.auth.decorators import user_passes_test from django.contrib.auth import logout as auth_logout from .models import Domain, Mailbox, Alias -from .owner import user_from_request +from .owner import user_from_request, may_own_domain -@login_required(login_url='/login/') +@user_passes_test(may_own_domain, login_url='/login/') def delete_domain(request, domain_id): try: user = user_from_request(request) @@ -19,7 +19,7 @@ def delete_domain(request, domain_id): return HttpResponseRedirect(reverse('multimail:domains')) -@login_required(login_url='/login/') +@user_passes_test(may_own_domain, login_url='/login/') def delete_mailbox(request, mailbox_id): try: user = user_from_request(request) @@ -31,7 +31,7 @@ def delete_mailbox(request, mailbox_id): return HttpResponseRedirect(reverse('multimail:mailboxes')) -@login_required(login_url='/login/') +@user_passes_test(may_own_domain, login_url='/login/') def delete_alias(request, alias_id): try: user = user_from_request(request) diff --git a/backend/multimail/auth_backends.py b/backend/multimail/auth_backends.py new file mode 100644 index 0000000..bbfb565 --- /dev/null +++ b/backend/multimail/auth_backends.py @@ -0,0 +1,47 @@ +from django.contrib.auth.backends import BaseBackend +from django.contrib.auth.models import AbstractUser +import re + +from multimail.models import Mailbox + + +class MailUser(AbstractUser): + def __init__(self, *args, **kwargs): + m = kwargs['mailbox'] + super().__init__(username=m.username+"@"+m.domain) + self.id = m.id + self.mailbox = m + + def is_active(self): + return True + + def is_authenticated(self): + return True + + def save(self, *args, **kwargs): + pass + + +class MailboxBackend(BaseBackend): + + def authenticate(self, request, **kwargs): + m = re.match("([^@]+)@([^@]+)", kwargs['username']) + if not m: + return None + username, domain = m.groups() + password = kwargs['password'] + try: + mailbox = Mailbox.objects.get(username=username, domain=domain) + if mailbox.check_password(password) is True: + return MailUser(mailbox=mailbox) + else: + return None + except Mailbox.DoesNotExist: + return None + + def get_user(self, user_id): + try: + mailbox = Mailbox.objects.get(pk=user_id) + return MailUser(mailbox=mailbox) + except Mailbox.DoesNotExist: + return None \ No newline at end of file diff --git a/backend/multimail/forms.py b/backend/multimail/forms.py index fe0650b..d51fbcf 100644 --- a/backend/multimail/forms.py +++ b/backend/multimail/forms.py @@ -1,11 +1,12 @@ -from django.contrib.auth.decorators import login_required +from django.contrib.auth.decorators import login_required, user_passes_test from django.forms import ModelForm, CharField, PasswordInput from django.http import HttpResponseRedirect, Http404 from django.shortcuts import render from django.db import IntegrityError +from multimail import owner from multimail.models import Domain, Mailbox, Alias -from multimail.owner import user_from_request +from multimail.owner import user_from_request, may_own_domain class DomainForm(ModelForm): @@ -35,7 +36,23 @@ class AliasForm(ModelForm): fields = '__all__' -@login_required(login_url='/login/') +class PasswdForm(ModelForm): + plain_password = CharField(label='Password', required=False, widget=PasswordInput()) + + class Meta: + model = Mailbox + fields = ['plain_password'] + + def save(self, commit=True): + mailbox = super(PasswdForm, self).save(commit=False) + if not self.cleaned_data["plain_password"] == '': + mailbox.set_password(self.cleaned_data["plain_password"]) + if commit: + mailbox.save() + return mailbox + + +@user_passes_test(owner.may_own_domain, login_url='/login/') def edit_domain(request, domain_id): try: user = user_from_request(request) @@ -58,7 +75,7 @@ def edit_domain(request, domain_id): return render(request, 'multimail/edit.html', {'form': form}) -@login_required(login_url='/login/') +@user_passes_test(owner.may_own_domain, login_url='/login/') def new_domain(request): user = user_from_request(request) if request.method == 'POST': @@ -78,7 +95,7 @@ def new_domain(request): return render(request, 'multimail/edit.html', {'form': form}) -@login_required(login_url='/login/') +@user_passes_test(owner.may_own_domain, login_url='/login/') def edit_mailbox(request, mailbox_id): try: user = user_from_request(request) @@ -102,7 +119,7 @@ def edit_mailbox(request, mailbox_id): return render(request, 'multimail/edit.html', {'form': form}) -@login_required(login_url='/login/') +@user_passes_test(owner.may_own_domain, login_url='/login/') def new_mailbox(request): if request.method == 'POST': user = user_from_request(request) @@ -122,7 +139,7 @@ def new_mailbox(request): return render(request, 'multimail/edit.html', {'form': form}) -@login_required(login_url='/login/') +@user_passes_test(owner.may_own_domain, login_url='/login/') def edit_alias(request, alias_id): try: user = user_from_request(request) @@ -146,7 +163,7 @@ def edit_alias(request, alias_id): return render(request, 'multimail/edit.html', {'form': form}) -@login_required(login_url='/login/') +@user_passes_test(owner.may_own_domain, login_url='/login/') def new_alias(request): if request.method == 'POST': user = user_from_request(request) @@ -164,3 +181,24 @@ def new_alias(request): form = AliasForm() return render(request, 'multimail/edit.html', {'form': form}) + + +@login_required(login_url='/login/') +def mailbox_passwd(request): + if may_own_domain(request.user): + raise Http404 + mailbox = request.user.mailbox + if request.method == 'POST': + form = PasswdForm(request.POST, instance=mailbox) + try: + if form.is_valid(): + form.save() + return HttpResponseRedirect('') + + except IntegrityError as e: + form.add_error(None, e) + + else: + form = PasswdForm(instance=mailbox) + + return render(request, 'multimail/mailboxindex.html', {'form': form}) diff --git a/backend/multimail/models.py b/backend/multimail/models.py index 605f6b9..41a193c 100644 --- a/backend/multimail/models.py +++ b/backend/multimail/models.py @@ -1,4 +1,7 @@ import crypt +from hmac import compare_digest as compare_hash +from django.contrib.auth.models import AbstractBaseUser +from django.contrib.auth.models import PermissionsMixin from django.db import models @@ -48,6 +51,13 @@ class Mailbox(models.Model): def set_password(self, password): self.password = '{SHA512-CRYPT}' + crypt.crypt(password) + def check_password(self, password): + hash = crypt.crypt(password, self.password.lstrip('{SHA512-CRYPT}')) + return compare_hash(hash, self.password.lstrip('{SHA512-CRYPT}')) + + def is_active(self): + return bool(self.enabled) + def __str__(self): return self.username + '@' + self.domain diff --git a/backend/multimail/owner.py b/backend/multimail/owner.py index e2ddcef..110f971 100644 --- a/backend/multimail/owner.py +++ b/backend/multimail/owner.py @@ -1,7 +1,16 @@ +def classify_user(user): + if hasattr(user, 'ldap_user'): + return {'name': user.username, 'source': 'ldap'} + elif hasattr(user, 'mailbox'): + return {'name': user.username, 'source': 'mail'} + else: + return {'name': user.username, 'source': 'system'} + 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 + return classify_user(request.user) + + +def may_own_domain(user): + u = classify_user(user) + return u["source"] == "system" or u["source"] == "ldap" diff --git a/backend/multimail/templates/multimail/mailboxindex.html b/backend/multimail/templates/multimail/mailboxindex.html new file mode 100644 index 0000000..46d2781 --- /dev/null +++ b/backend/multimail/templates/multimail/mailboxindex.html @@ -0,0 +1,40 @@ +{% extends 'multimail/base.html' %} +{% load bootstrap4 %} + +{% block main %} + + +
+ + + +
+
+ + {% if error_message %}

{{ error_message }}

{% endif %} +
+ {% csrf_token %} + {% bootstrap_form form %} + {% buttons %} + + {% endbuttons %} +
+
+
+
+ +{% endblock %} \ No newline at end of file diff --git a/backend/multimail/urls.py b/backend/multimail/urls.py index 6c09ee0..7a0af18 100644 --- a/backend/multimail/urls.py +++ b/backend/multimail/urls.py @@ -6,7 +6,7 @@ from . import actions app_name = 'multimail' urlpatterns = [ - path('', views.IndexView.as_view(), name='index'), + path('', views.dispatchIndex, name='index'), path('domains/', views.DomainListView.as_view(), name='domains'), path('domain/new/', forms.new_domain, name='new_domain'), diff --git a/backend/multimail/views.py b/backend/multimail/views.py index 36e235b..95057fc 100644 --- a/backend/multimail/views.py +++ b/backend/multimail/views.py @@ -1,16 +1,27 @@ -from django.views import generic -from django.contrib.auth.mixins import LoginRequiredMixin +from django.shortcuts import redirect +from django.views.generic import ListView +from django.contrib.auth.mixins import UserPassesTestMixin from django.contrib.auth.views import LoginView +from .forms import mailbox_passwd from .models import Domain, Mailbox, Alias -from .owner import user_from_request +from .owner import user_from_request, may_own_domain + + +class MayOwnDomainMixin(UserPassesTestMixin): + + def test_func(self): + return may_own_domain(self.request.user) + + def handle_no_permission(self): + return redirect('multimail:login') class UserLoginView(LoginView): template_name = 'multimail/login.html' -class IndexView(LoginRequiredMixin, generic.ListView): +class IndexView(MayOwnDomainMixin, ListView): login_url = 'login/' template_name = 'multimail/index.html' context_object_name = 'domain_list' @@ -27,7 +38,14 @@ class IndexView(LoginRequiredMixin, generic.ListView): return self.fill_related(Domain.objects.filter(admin__admin=user['name'], admin__source=user['source'])) -class DomainListView(LoginRequiredMixin, generic.ListView): +def dispatchIndex(request): + if may_own_domain(request.user): + return IndexView.as_view()(request) + else: + return mailbox_passwd(request) + + +class DomainListView(MayOwnDomainMixin, ListView): login_url = 'login/' template_name = 'multimail/domains.html' context_object_name = 'domain_list' @@ -38,7 +56,7 @@ class DomainListView(LoginRequiredMixin, generic.ListView): return Domain.objects.filter(admin__admin=user['name'], admin__source=user['source']) -class MailboxListView(LoginRequiredMixin, generic.ListView): +class MailboxListView(MayOwnDomainMixin, ListView): login_url = 'login/' template_name = 'multimail/mailboxes.html' context_object_name = 'mailbox_list' @@ -46,11 +64,11 @@ class MailboxListView(LoginRequiredMixin, generic.ListView): def get_queryset(self): """Return the last five published questions.""" user = user_from_request(self.request) - domains = [ o.domain for o in Domain.objects.filter(admin__admin=user['name'], admin__source=user['source'])] + domains = [o.domain for o in Domain.objects.filter(admin__admin=user['name'], admin__source=user['source'])] return Mailbox.objects.filter(domain__in=domains) -class AliasListView(LoginRequiredMixin, generic.ListView): +class AliasListView(MayOwnDomainMixin, ListView): login_url = 'login/' template_name = 'multimail/aliases.html' context_object_name = 'alias_list' @@ -58,5 +76,5 @@ class AliasListView(LoginRequiredMixin, generic.ListView): def get_queryset(self): """Return the last five published questions.""" user = user_from_request(self.request) - domains = [ o.domain for o in Domain.objects.filter(admin__admin=user['name'], admin__source=user['source'])] + domains = [o.domain for o in Domain.objects.filter(admin__admin=user['name'], admin__source=user['source'])] return Alias.objects.filter(source_domain__in=domains)