diff options
author | Magnus Hagander | 2025-06-02 12:54:46 +0000 |
---|---|---|
committer | Magnus Hagander | 2025-06-02 12:54:46 +0000 |
commit | 197fb34fa458178d9bde9f11f304c60f02579243 (patch) | |
tree | 82e31cd86b808b712bab151f4f3b26603ac869cc | |
parent | c16a71968443c094312334641da11290bef51d83 (diff) |
Add community account to mailboxes, and self service password change
Instead of just having pattern based permissions, also make it possible
to attach a community account to an individual mailbox. If this is done,
that account can then manage their own password without having pattern
based access, as a simple self-service. The only thing they can change
is the password, other attributes are still managed by the admins.
-rw-r--r-- | pgmailmgr/mailmgr/forms.py | 64 | ||||
-rw-r--r-- | pgmailmgr/mailmgr/models.py | 5 | ||||
-rw-r--r-- | pgmailmgr/mailmgr/templates/home.html | 22 | ||||
-rw-r--r-- | pgmailmgr/mailmgr/templates/mailbox.html | 26 | ||||
-rw-r--r-- | pgmailmgr/mailmgr/views.py | 34 | ||||
-rw-r--r-- | pgmailmgr/urls.py | 3 |
6 files changed, 151 insertions, 3 deletions
diff --git a/pgmailmgr/mailmgr/forms.py b/pgmailmgr/mailmgr/forms.py index b541291..2b6521a 100644 --- a/pgmailmgr/mailmgr/forms.py +++ b/pgmailmgr/mailmgr/forms.py @@ -1,19 +1,67 @@ from django import forms from django.forms import ValidationError from django.db import connection +from django.contrib.auth.password_validation import validate_password, MinimumLengthValidator, UserAttributeSimilarityValidator +from django.contrib.auth.password_validation import CommonPasswordValidator, NumericPasswordValidator + +from pgmailmgr.auth import user_search, user_import from .models import * +class AccountEmailWidget(forms.TextInput): + def __init__(self, account): + self.account = account + super().__init__() + + def get_context(self, name, value, attrs): + c = super().get_context(name, value, attrs) + c['widget']['value'] = self.account and self.account.email or '' + c['widget']['attrs']['placeholder'] = 'Enter email address of a community account to connect to' + return c + + +class password_validator: + def __init__(self, mailbox): + self.mailbox = mailbox + + def __call__(self, password): + return validate_password(password, self.mailbox, password_validators=[ + MinimumLengthValidator(10), + CommonPasswordValidator(), + NumericPasswordValidator(), + UserAttributeSimilarityValidator(user_attributes=['local_part', 'full_name']), + ]) + + +class PasswordChangeForm(forms.Form): + password = forms.CharField(widget=forms.PasswordInput) + verify_password = forms.CharField(widget=forms.PasswordInput) + + def __init__(self, *args, **kwargs): + mailbox = kwargs.pop('mailbox') + super().__init__(*args, **kwargs) + + self.fields['password'].validators = [password_validator(mailbox)] + + def clean(self): + d = super().clean() + if 'password' in d and d.get('password') != d.get('verify_password', None): + self.add_error('verify_password', "Passwords don't match") + return d + + class VirtualUserForm(forms.ModelForm): class Meta: model = VirtualUser - fields = ('local_domain', 'local_part', 'mail_quota', 'passwd', 'full_name') + fields = ('local_domain', 'local_part', 'mail_quota', 'passwd', 'full_name', 'account') def __init__(self, data=None, instance=None, user=None): super(VirtualUserForm, self).__init__(data=data, instance=instance) self.user = user + self.fields['account'] = forms.CharField(max_length=200, widget=AccountEmailWidget(instance.account), required=False) + def clean_local_domain(self): if not self.instance.pk: return self.cleaned_data['local_domain'] @@ -44,6 +92,20 @@ class VirtualUserForm(forms.ModelForm): return self.cleaned_data['passwd'] + def clean_account(self): + a = self.cleaned_data['account'] + if a == '': + return None + + try: + return User.objects.get(email=a) + except User.DoesNotExist: + # Import the user if we can + users = user_search(searchterm=a) + if len(users) == 1 and users[0]['e'] == a: + return user_import(users[0]['u']) + raise ValidationError("User not found") + def clean(self): if 'local_part' not in self.cleaned_data: return {} diff --git a/pgmailmgr/mailmgr/models.py b/pgmailmgr/mailmgr/models.py index e8d944f..61c1cbd 100644 --- a/pgmailmgr/mailmgr/models.py +++ b/pgmailmgr/mailmgr/models.py @@ -45,10 +45,15 @@ class VirtualUser(models.Model): mail_quota = models.IntegerField(null=False, default=500) passwd = models.CharField(max_length=100, null=False, blank=False, verbose_name="Password") full_name = models.CharField(max_length=200, null=False, blank=True) + account = models.ForeignKey(User, null=True, blank=True, verbose_name="Connected user", on_delete=models.SET_NULL) def __str__(self): return "%s@%s (%s)" % (self.local_part, self.local_domain.domain_name, self.full_name or '') + @property + def fulladdr(self): + return "%s@%s" % (self.local_part, self.local_domain.domain_name) + trigger_update = True class Meta: diff --git a/pgmailmgr/mailmgr/templates/home.html b/pgmailmgr/mailmgr/templates/home.html new file mode 100644 index 0000000..8dff238 --- /dev/null +++ b/pgmailmgr/mailmgr/templates/home.html @@ -0,0 +1,22 @@ +{%extends "base.html" %} + +{%block content%} + +<h2>Mailboxes</h2> +{% if mailboxes %} +<ul> +{% for m in mailboxes %} + <li><a href="{{m.virtual_user_id}}/">{{m.fulladdr}}</a></li> +{% endfor %} +</ul> +{% else %} +<p> + You have no mailbox here. +</p> +{% endif %} + +{%if admperm %} +<a href="/https/git.postgresql.org/adm/" class="btn btn-primary">Administer other users and forwarders</a> +{%endif%} + +{%endblock%} diff --git a/pgmailmgr/mailmgr/templates/mailbox.html b/pgmailmgr/mailmgr/templates/mailbox.html new file mode 100644 index 0000000..28a0235 --- /dev/null +++ b/pgmailmgr/mailmgr/templates/mailbox.html @@ -0,0 +1,26 @@ +{%extends "base.html" %} +{%block content%} +<h1>{{mailbox.fulladdr}}</h1> + +<h2>Change password</h2> + +<form method="post" action="."> +{% csrf_token %} +{%if form.non_field_errors%} + <div class="alert alert-danger">{{form.non_field_errors}}</div> +{%endif%} +{%for field in form%} +{%include "form_field.html"%} +{%endfor%} + + <div class="form-group"> + <div class="col-lg-12"> + <div class="control"> + <input type="submit" name="submit" class="col-md-2 mb-2 mr-2 btn btn-primary" value="Change password"> + <a class="col-md-2 mb-2 mr-2 btn btn-secondary" href="/">Cancel</a> + </div> + </div> + </div> + +</form> +{%endblock%} diff --git a/pgmailmgr/mailmgr/views.py b/pgmailmgr/mailmgr/views.py index 41b6df6..343bdb1 100644 --- a/pgmailmgr/mailmgr/views.py +++ b/pgmailmgr/mailmgr/views.py @@ -18,7 +18,39 @@ def log(user, what): @login_required def home(request): - return None + admperm = VirtualUser.objects.extra(where=["EXISTS (SELECT 1 FROM mailmgr_userpermissions p WHERE p.user_id=%s AND p.domain_id = local_domain_id AND local_part ~* ('^'||p.pattern||'$'))" % request.user.id]).exists() or \ + Forwarder.objects.extra(where=["EXISTS (SELECT 1 FROM mailmgr_userpermissions p WHERE p.user_id=%s AND p.domain_id = local_domain_id AND local_part ~* ('^'||p.pattern||'$'))" % request.user.id]).exists() + mailboxes = VirtualUser.objects.filter(account=request.user.id) + + return render(request, 'home.html', { + 'admperm': admperm, + 'mailboxes': mailboxes, + }) + + +@login_required +def mailbox(request, virtualid): + mailbox = get_object_or_404(VirtualUser, virtual_user_id=virtualid, account=request.user.id) + + if request.method == 'POST': + form = PasswordChangeForm(data=request.POST, mailbox=mailbox) + if form.is_valid(): + with transaction.atomic(): + curs = connection.cursor() + curs.execute("UPDATE mail.virtual_user SET passwd=public.crypt(%(pwd)s, public.gen_salt('md5')) WHERE virtual_user_id=%(id)s", { + 'id': mailbox.virtual_user_id, + 'pwd': form.cleaned_data['password'], + }) + log(request.user, "Changed password for own mailbox {}".format(mailbox.fulladdr)) + messages.info(request, "Changed password of {}".format(mailbox.fulladdr)) + return HttpResponseRedirect('/') + else: + form = PasswordChangeForm(mailbox=mailbox) + + return render(request, 'mailbox.html', { + 'mailbox': mailbox, + 'form': form, + }) @login_required diff --git a/pgmailmgr/urls.py b/pgmailmgr/urls.py index 8876ef2..882cf21 100644 --- a/pgmailmgr/urls.py +++ b/pgmailmgr/urls.py @@ -8,7 +8,8 @@ from django.contrib import admin admin.autodiscover() urlpatterns = [ - re_path(r'^/$', views.home), + re_path(r'^$', views.home), + re_path(r'^(\d+)/$', views.mailbox), re_path(r'^adm/$', views.adm_home), re_path(r'^adm/user/(\d+|add)/$', views.userform), re_path(r'^adm/forwarder/(\d+|add)/$', views.forwarderform), |