add login and password change form for mailbox owners

This commit is contained in:
j3d1 2021-10-18 00:32:02 +02:00
parent 9e40022aca
commit 4a2a0be3a4
9 changed files with 191 additions and 28 deletions

View file

@ -43,6 +43,7 @@ AUTH_LDAP_USER_SEARCH = LDAPSearch(
AUTHENTICATION_BACKENDS = [ AUTHENTICATION_BACKENDS = [
"django_auth_ldap.backend.LDAPBackend", "django_auth_ldap.backend.LDAPBackend",
"django.contrib.auth.backends.ModelBackend", "django.contrib.auth.backends.ModelBackend",
"multimail.auth_backends.MailboxBackend",
] ]
LOGIN_REDIRECT_URL = '/' LOGIN_REDIRECT_URL = '/'

View file

@ -1,13 +1,13 @@
from django.http import HttpResponseRedirect, Http404 from django.http import HttpResponseRedirect, Http404
from django.urls import reverse 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 django.contrib.auth import logout as auth_logout
from .models import Domain, Mailbox, Alias 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): def delete_domain(request, domain_id):
try: try:
user = user_from_request(request) user = user_from_request(request)
@ -19,7 +19,7 @@ def delete_domain(request, domain_id):
return HttpResponseRedirect(reverse('multimail:domains')) 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): def delete_mailbox(request, mailbox_id):
try: try:
user = user_from_request(request) user = user_from_request(request)
@ -31,7 +31,7 @@ def delete_mailbox(request, mailbox_id):
return HttpResponseRedirect(reverse('multimail:mailboxes')) 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): def delete_alias(request, alias_id):
try: try:
user = user_from_request(request) user = user_from_request(request)

View file

@ -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

View file

@ -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.forms import ModelForm, CharField, PasswordInput
from django.http import HttpResponseRedirect, Http404 from django.http import HttpResponseRedirect, Http404
from django.shortcuts import render from django.shortcuts import render
from django.db import IntegrityError from django.db import IntegrityError
from multimail import owner
from multimail.models import Domain, Mailbox, Alias 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): class DomainForm(ModelForm):
@ -35,7 +36,23 @@ class AliasForm(ModelForm):
fields = '__all__' 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): def edit_domain(request, domain_id):
try: try:
user = user_from_request(request) user = user_from_request(request)
@ -58,7 +75,7 @@ def edit_domain(request, domain_id):
return render(request, 'multimail/edit.html', {'form': form}) 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): def new_domain(request):
user = user_from_request(request) user = user_from_request(request)
if request.method == 'POST': if request.method == 'POST':
@ -78,7 +95,7 @@ def new_domain(request):
return render(request, 'multimail/edit.html', {'form': form}) 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): def edit_mailbox(request, mailbox_id):
try: try:
user = user_from_request(request) user = user_from_request(request)
@ -102,7 +119,7 @@ def edit_mailbox(request, mailbox_id):
return render(request, 'multimail/edit.html', {'form': form}) 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): def new_mailbox(request):
if request.method == 'POST': if request.method == 'POST':
user = user_from_request(request) user = user_from_request(request)
@ -122,7 +139,7 @@ def new_mailbox(request):
return render(request, 'multimail/edit.html', {'form': form}) 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): def edit_alias(request, alias_id):
try: try:
user = user_from_request(request) user = user_from_request(request)
@ -146,7 +163,7 @@ def edit_alias(request, alias_id):
return render(request, 'multimail/edit.html', {'form': form}) 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): def new_alias(request):
if request.method == 'POST': if request.method == 'POST':
user = user_from_request(request) user = user_from_request(request)
@ -164,3 +181,24 @@ def new_alias(request):
form = AliasForm() form = AliasForm()
return render(request, 'multimail/edit.html', {'form': form}) 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})

View file

@ -1,4 +1,7 @@
import crypt 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 from django.db import models
@ -48,6 +51,13 @@ class Mailbox(models.Model):
def set_password(self, password): def set_password(self, password):
self.password = '{SHA512-CRYPT}' + crypt.crypt(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): def __str__(self):
return self.username + '@' + self.domain return self.username + '@' + self.domain

View file

@ -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): def user_from_request(request):
if hasattr(request.user, 'ldap_user'): return classify_user(request.user)
# print(request.user.ldap_user.attrs.data)
return {'name': request.user.username, 'source': 'ldap'}
else: def may_own_domain(user):
return {'name': request.user.username, 'source': 'system'} u = classify_user(user)
return u["source"] == "system" or u["source"] == "ldap"

View file

@ -0,0 +1,40 @@
{% extends 'multimail/base.html' %}
{% load bootstrap4 %}
{% block main %}
<div id="page-content-wrapper">
<nav class="navbar navbar-expand-lg navbar-dark bg-dark border-bottom">
<div class="collapse navbar-collapse" id="navbarSupportedContent">
<ul class="navbar-nav ml-auto mt-2 mt-lg-0">
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button"
data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
{{ user }}
</a>
<div class="dropdown-menu dropdown-menu-right" aria-labelledby="navbarDropdown">
<a href="{% url 'multimail:logout' %}" class="dropdown-item">Logout</a>
</div>
</li>
</ul>
</div>
</nav>
<div class="container">
<div class="pt-3">
{% if error_message %}<p><strong>{{ error_message }}</strong></p>{% endif %}
<form action="" method="post" class="form">
{% csrf_token %}
{% bootstrap_form form %}
{% buttons %}
<button type="submit" class="btn btn-primary">Submit</button>
{% endbuttons %}
</form>
</div>
</div>
</div>
{% endblock %}

View file

@ -6,7 +6,7 @@ from . import actions
app_name = 'multimail' app_name = 'multimail'
urlpatterns = [ urlpatterns = [
path('', views.IndexView.as_view(), name='index'), path('', views.dispatchIndex, name='index'),
path('domains/', views.DomainListView.as_view(), name='domains'), path('domains/', views.DomainListView.as_view(), name='domains'),
path('domain/new/', forms.new_domain, name='new_domain'), path('domain/new/', forms.new_domain, name='new_domain'),

View file

@ -1,16 +1,27 @@
from django.views import generic from django.shortcuts import redirect
from django.contrib.auth.mixins import LoginRequiredMixin from django.views.generic import ListView
from django.contrib.auth.mixins import UserPassesTestMixin
from django.contrib.auth.views import LoginView from django.contrib.auth.views import LoginView
from .forms import mailbox_passwd
from .models import Domain, Mailbox, Alias 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): class UserLoginView(LoginView):
template_name = 'multimail/login.html' template_name = 'multimail/login.html'
class IndexView(LoginRequiredMixin, generic.ListView): class IndexView(MayOwnDomainMixin, ListView):
login_url = 'login/' login_url = 'login/'
template_name = 'multimail/index.html' template_name = 'multimail/index.html'
context_object_name = 'domain_list' 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'])) 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/' login_url = 'login/'
template_name = 'multimail/domains.html' template_name = 'multimail/domains.html'
context_object_name = 'domain_list' 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']) return Domain.objects.filter(admin__admin=user['name'], admin__source=user['source'])
class MailboxListView(LoginRequiredMixin, generic.ListView): class MailboxListView(MayOwnDomainMixin, ListView):
login_url = 'login/' login_url = 'login/'
template_name = 'multimail/mailboxes.html' template_name = 'multimail/mailboxes.html'
context_object_name = 'mailbox_list' context_object_name = 'mailbox_list'
@ -50,7 +68,7 @@ class MailboxListView(LoginRequiredMixin, generic.ListView):
return Mailbox.objects.filter(domain__in=domains) return Mailbox.objects.filter(domain__in=domains)
class AliasListView(LoginRequiredMixin, generic.ListView): class AliasListView(MayOwnDomainMixin, ListView):
login_url = 'login/' login_url = 'login/'
template_name = 'multimail/aliases.html' template_name = 'multimail/aliases.html'
context_object_name = 'alias_list' context_object_name = 'alias_list'