From eec44cb08e1dcfa116718b03aa59a5ec85e77a8f Mon Sep 17 00:00:00 2001 From: Magnus Hagander Date: Fri, 18 Sep 2020 09:10:51 +0200 Subject: [PATCH] Add admin function to initiate user password reset This will trigger the same reset-your-password email as a user initiated one, but it'll cut out one step and be a bit more user friendly... Also, if this is done with an OAuth connected account, it will be converted into a regular one (something we don't allow the end user to do, for support reasons) This also adds an entry to the user editor in the admin view that shows if the user *is* an oauth user or not, or if they might have an old "unmigrated" password. --- pgweb/account/admin.py | 24 ++++++++++ pgweb/core/forms.py | 4 ++ pgweb/core/views.py | 44 ++++++++++++++++++- pgweb/urls.py | 1 + .../widgets/community_auth_password_info.html | 9 ++++ templates/core/admin_reset_password.html | 31 +++++++++++++ 6 files changed, 112 insertions(+), 1 deletion(-) create mode 100644 pgweb/util/templates/forms/widgets/community_auth_password_info.html create mode 100644 templates/core/admin_reset_password.html diff --git a/pgweb/account/admin.py b/pgweb/account/admin.py index 864236e9..723b9f77 100644 --- a/pgweb/account/admin.py +++ b/pgweb/account/admin.py @@ -5,9 +5,11 @@ from django.contrib.auth.models import User from django import forms import base64 +import re from pgweb.util.widgets import TemplateRenderWidget from pgweb.util.db import exec_to_dict +from pgweb.account.views import OAUTH_PASSWORD_STORE from .models import CommunityAuthSite, CommunityAuthOrg @@ -46,6 +48,7 @@ class CommunityAuthSiteAdmin(admin.ModelAdmin): class PGUserChangeForm(UserChangeForm): + passwordinfo = forms.CharField(label="Password information", required=False) logininfo = forms.CharField(label="Community login history", required=False) def __init__(self, *args, **kwargs): @@ -57,6 +60,13 @@ class PGUserChangeForm(UserChangeForm): if self.fields.get('username'): del self.fields['username'] + self.fields['passwordinfo'].widget = TemplateRenderWidget( + template='forms/widgets/community_auth_password_info.html', + context={ + 'type': self.password_type(self.instance), + }, + ) + self.fields['logininfo'].widget = TemplateRenderWidget( template='forms/widgets/community_auth_usage_widget.html', context={ @@ -65,6 +75,18 @@ class PGUserChangeForm(UserChangeForm): }), }) + def password_type(self, obj): + if obj.password == OAUTH_PASSWORD_STORE: + return "OAuth integrated" + elif obj.password.startswith('pbkdf2_'): + return "Regular password" + elif obj.password.startswith('sha1_'): + return "Old SHA1 password" + elif re.match('^[a-z0-9]{64}'): + return "Old unknown hash" + else: + return "Unknown" + class PGUserAdmin(UserAdmin): """overrides default Django user admin""" @@ -82,6 +104,8 @@ class PGUserAdmin(UserAdmin): fs.append( ('Community authentication', {'fields': ('logininfo', )}), ) + if 'passwordinfo' not in fs[0][1]['fields']: + fs[0][1]['fields'] = list(fs[0][1]['fields']) + ['passwordinfo', ] return fs diff --git a/pgweb/core/forms.py b/pgweb/core/forms.py index b8178dfd..0d3e2bae 100644 --- a/pgweb/core/forms.py +++ b/pgweb/core/forms.py @@ -158,3 +158,7 @@ class ModerationForm(forms.Form): self.add_error('modnote', ("Moderation notices cannot be sent on first-moderator approvals for objects that require two moderators.")) return cleaned_data + + +class AdminResetPasswordForm(forms.Form): + confirm = forms.BooleanField(required=True, label="Confirm that you want to reset this password") diff --git a/pgweb/core/views.py b/pgweb/core/views.py index 66f78667..78bb4959 100644 --- a/pgweb/core/views.py +++ b/pgweb/core/views.py @@ -3,12 +3,16 @@ from django.http import HttpResponse, Http404, HttpResponseRedirect from django.http import HttpResponseNotModified from django.core.exceptions import PermissionDenied from django.template import TemplateDoesNotExist, loader +from django.contrib.auth.models import User from django.contrib.auth.decorators import user_passes_test +from django.contrib.auth.tokens import default_token_generator from pgweb.util.decorators import login_required, content_sources from django.contrib import messages from django.views.decorators.csrf import csrf_exempt from django.db import connection, transaction from django.utils.http import http_date, parse_http_date +from django.utils.http import urlsafe_base64_encode +from django.utils.encoding import force_bytes from django.conf import settings import django @@ -17,14 +21,17 @@ import os import re import urllib.parse import hashlib +import logging from pgweb.util.decorators import cache, nocache from pgweb.util.contexts import render_pgweb, get_nav_menu, PGWebContextProcessor from pgweb.util.helpers import PgXmlHelper from pgweb.util.moderation import get_all_pending_moderations, get_moderation_model, ModerationState from pgweb.util.misc import get_client_ip, varnish_purge, varnish_purge_expr, varnish_purge_xkey +from pgweb.util.misc import send_template_mail from pgweb.util.sitestruct import get_all_pages_struct from pgweb.mailqueue.util import send_simple_mail +from pgweb.account.views import OAUTH_PASSWORD_STORE # models needed for the pieces on the frontpage from pgweb.news.models import NewsArticle, NewsTag @@ -37,7 +44,9 @@ from pgweb.survey.models import Survey # models and forms needed for core objects from .models import Organisation -from .forms import MergeOrgsForm, ModerationForm +from .forms import MergeOrgsForm, ModerationForm, AdminResetPasswordForm + +log = logging.getLogger(__name__) # Front page view @@ -265,6 +274,39 @@ def sync_timestamp(request): return r +@login_required +@user_passes_test(lambda u: u.is_superuser) +def admin_resetpassword(request, userid): + user = get_object_or_404(User, pk=userid) + + if request.method == 'POST': + form = AdminResetPasswordForm(data=request.POST) + if form.is_valid(): + log.info("Admin {0} initiating password reset for {1}".format(request.user.username, user.email)) + token = default_token_generator.make_token(user) + send_template_mail( + settings.ACCOUNTS_NOREPLY_FROM, + user.email, + 'Password reset for your postgresql.org account', + 'account/password_reset_email.txt', + { + 'user': user, + 'uid': urlsafe_base64_encode(force_bytes(user.pk)), + 'token': token, + }, + ) + messages.info(request, "Password reset token sent.") + return HttpResponseRedirect("../") + else: + form = AdminResetPasswordForm() + + return render(request, 'core/admin_reset_password.html', { + 'is_oauth': user.password == OAUTH_PASSWORD_STORE, + 'user': user, + 'form': form, + }) + + # List of all unapproved objects, for the special admin page @login_required @user_passes_test(lambda u: u.is_staff) diff --git a/pgweb/urls.py b/pgweb/urls.py index b5a5755d..c3a364f1 100644 --- a/pgweb/urls.py +++ b/pgweb/urls.py @@ -149,6 +149,7 @@ urlpatterns = [ url(r'^admin/purge/$', pgweb.core.views.admin_purge), url(r'^admin/mergeorg/$', pgweb.core.views.admin_mergeorg), url(r'^admin/_moderate/(\w+)/(\d+)/$', pgweb.core.views.admin_moderate), + url(r'^admin/auth/user/(\d+)/change/resetpassword/$', pgweb.core.views.admin_resetpassword), # Uncomment the next line to enable the admin: url(r'^admin/', admin.site.urls), diff --git a/pgweb/util/templates/forms/widgets/community_auth_password_info.html b/pgweb/util/templates/forms/widgets/community_auth_password_info.html new file mode 100644 index 00000000..c297fad1 --- /dev/null +++ b/pgweb/util/templates/forms/widgets/community_auth_password_info.html @@ -0,0 +1,9 @@ + + + + + + + + +
Type:{{type}}
Reset password
diff --git a/templates/core/admin_reset_password.html b/templates/core/admin_reset_password.html new file mode 100644 index 00000000..ac49d940 --- /dev/null +++ b/templates/core/admin_reset_password.html @@ -0,0 +1,31 @@ +{%extends "admin/base_site.html"%} + +{%block breadcrumbs%} + +{%endblock%} + +{% block bodyclass %}change-list{% endblock %} +{% block coltype %}flex{% endblock %} + +{%block content%} +
+

Reset password

+

+ Resetting the password the password of a user will clear the current password and send a + password reset link to {{user.email}}. +

+{%if is_oauth%} +

+ WARNING! This is currently an OAuth integrated account. Resetting the password + of this account will turn it into a regular account with a local password!! +

+{%endif%} +
+{% csrf_token %} + +{{form.as_table}} +
+ +
+ +{%endblock%} -- 2.39.5