Is It Possible To Implement A "change Password At Next Logon" Type Feature In The Django Admin?
Solution 1:
I'm actually in the process of doing this myself. You need three components: a user profile (if not already in use on your site), a middleware component, and a pre_save signal.
My code for this is in an app named 'accounts'.
# myproject/accounts/models.pyfrom django.db import models
from django.db.models import signals
from django.contrib.auth.models import User
classUserProfile(models.Model):
user = models.ForeignKey(User, unique=True)
force_password_change = models.BooleanField(default=False)
defcreate_user_profile_signal(sender, instance, created, **kwargs):
if created:
UserProfile.objects.create(user=instance)
defpassword_change_signal(sender, instance, **kwargs):
try:
user = User.objects.get(username=instance.username)
ifnot user.password == instance.password:
profile = user.get_profile()
profile.force_password_change = False
profile.save()
except User.DoesNotExist:
pass
signals.pre_save.connect(password_change_signal, sender=User, dispatch_uid='accounts.models')
signals.post_save.connect(create_user_profile_signal, sender=User, dispatch_uid='accounts.models')
First, we create a UserProfile with a foreign key to User. The force_password_change
boolean will, as its name describes, be set to true for a user whenever you want to force them to change their password. You could do anything here though. In my organization, we also chose to implement a mandatory change every 90 days, so I also have a DateTimeField that stores the last time a user changed their password. You then set that in the pre_save signal, password_changed_signal
.
Second, we have the create_user_profile_signal
. This is mostly added just for completeness. If you're just now adding user profiles into your project, you'll need a post_save signal that will create a UserProfile every time a User is created. This accomplishes that task.
Third, we have the password_changed_signal
. This is a pre_save signal because at this point in the process the actual row in the User table hasn't be updated. Therefore, we can access both the previous password and the new password about to be saved. If the two don't match, that means the user has changed their password, and we can then reset the force_password_change boolean. This would be the point, also where you would take care of any other things you've added such as setting the DateTimeField previously mentioned.
The last two lines attach the two functions to their appropriate signals.
If you haven't already, you will also need to add the following line to your project's settings.py
(changing the app label and model name to match your setup):
AUTH_PROFILE_MODULE = 'accounts.UserProfile'
That covers the basics. Now we need a middleware component to check the status of our force_password_change
flag (and any other necessary checks).
# myproject/accounts/middleware.pyfrom django.http import HttpResponseRedirect
import re
classPasswordChangeMiddleware:
defprocess_request(self, request):
if request.user.is_authenticated() and \
re.match(r'^/admin/?', request.path) and \
not re.match(r'^/admin/password_change/?', request.path):
profile = request.user.get_profile()
if profile.force_password_change:
return HttpResponseRedirect('/admin/password_change/')
This very simple middleware hooks into the process_request
stage of the page loading process. It checks that 1) the user has already logged in, 2) they are trying to access some page in the admin, and 3) the page they are accessing is not the password change page itself (otherwise, you'd get an infinite loop of redirects). If all of these are true and the force_password_change
flag has been set to True
, then the user is redirected to the password change page. They will not be able to navigate anywhere else until they change their password (firing the pre_save signal discussed previously).
Finally, you just need to add this middleware to your project's settings.py
(again, changing the import path as necessary):
MIDDLEWARE_CLASSES = (
# Other middleware here'myproject.accounts.middleware.PasswordChangeMiddleware',
)
Solution 2:
I have used Chris Pratt's solution, with a little change: instead of using a middleware, that'd be executed for every page with the consequent resource use, I figured I'd just intercept the login view.
In my urls.py I have added this to my urlpatterns:
url(r'^accounts/login/$', 'userbase.views.force_pwd_login'),
then I added the following to userbase.views:
defforce_pwd_login(request, *args, **kwargs):
response = auth_views.login(request, *args, **kwargs)
if response.status_code == 302:
#We have a usertry:
if request.user.get_profile().force_password_change:
return redirect('django.contrib.auth.views.password_change')
except AttributeError: #No profile?passreturn response
It seems to work flawlessly on Django 1.2, but I have no reason to believe 1.3+ should have problems with it.
Solution 3:
This is the middleware I use with Django 1.11 :
# myproject/accounts/middleware.pyfrom django.http import HttpResponseRedirect
from django.urls import reverse
classPasswordChangeMiddleware:
def__init__(self, get_response):
self.get_response = get_response
def__call__(self, request):
response = self.get_response(request)
next = reverse('client:password-update')
if request.user.is_authenticated() and request.path != next:
if request.user.account.force_password_change:
return HttpResponseRedirect(next)
return response
Still adding it to the settings middleware list :
MIDDLEWARE_CLASSES = (
# Other middleware here'myproject.accounts.middleware.PasswordChangeMiddleware',
)
Solution 4:
I spent 2 days on this issue recently, and a new solution came out. Hopefully it's useful.
Just as above said, a new user model created.
newuser/models.py
classUsers(AbstractUser):
default_pwd_updated = models.NullBooleanField(default=None, editable=False)
pwd_update_time = models.DateTimeField(editable=False, null=True, default=None) # reserved column to support further interval password (such as 60 days) update policydefset_password(self, raw_password):
if self.default_pwd_updated isNone:
self.default_pwd_updated = Falseelifnot self.default_pwd_updated:
self.default_pwd_updated = True
self.pwd_update_time = timezone.now()
else:
self.pwd_update_time = timezone.now()
super().set_password(raw_password)
Set this model as the AUTH_USER_MODEL.
[project]/settings.py
AUTH_USER_MODEL = 'newuser.Users'
Now you just need to customize LoginView and some methods in AdminSite.
[project]/admin.py
from django.contrib.admin import AdminSite
from django.contrib.auth.views import LoginView
from django.utils.translation import gettext as _, gettext_lazy
from django.urls import reverse
from django.views.decorators.cache import never_cache
from django.contrib.auth import REDIRECT_FIELD_NAME
from django.http import HttpResponseRedirect
classNewLoginView(LoginView):
defget_redirect_url(self):
if self.request.method == "POST"and self.request.user.get_username()\
andnot self.request.user.default_pwd_updated:
redirect_to = reverse("admin:password_change")
else:
redirect_to = self.request.POST.get(
self.redirect_field_name,
self.request.GET.get(self.redirect_field_name, '')
)
return redirect_to
classNewAdminSite(AdminSite):
site_header = site_title = gettext_lazy("Customized Admin Site")
def__init__(self, name="admin"):
super().__init__(name)
@never_cachedeflogin(self, request, extra_context=None):
"""
Display the login form for the given HttpRequest.
"""if request.method == 'GET'and self.has_permission(request):
# Already logged-in, redirect to admin indexif request.user.get_username() andnot request.user.default_pwd_updated:
# default password not changed, force to password_change view
path = reverse('admin:password_change', current_app=self.name)
else:
path = reverse('admin:index', current_app=self.name)
return HttpResponseRedirect(path)
from django.contrib.auth.views import LoginView
from django.contrib.admin.forms import AdminAuthenticationForm
context = {
**self.each_context(request),
'title': _('Log in'),
'app_path': request.get_full_path(),
'username': request.user.get_username(),
}
if (REDIRECT_FIELD_NAME notin request.GET and
REDIRECT_FIELD_NAME notin request.POST):
context[REDIRECT_FIELD_NAME] = reverse('admin:index', current_app=self.name)
context.update(extra_context or {})
defaults = {
'extra_context': context,
'authentication_form': self.login_form or AdminAuthenticationForm,
'template_name': self.login_template or'admin/login.html',
}
request.current_app = self.name
return NewLoginView.as_view(**defaults)(request) # use NewLoginView @never_cachedefindex(self, request, extra_context=None):
if request.user.get_username() andnot request.user.default_pwd_updated:
# if default password not updated, force to password_change page
context = self.each_context(request)
context.update(extra_context or {})
return self.password_change(request, context)
returnsuper().index(request, extra_context)
admin_site = NewAdminSite(name="admin")
NOTE: if you intend to use custom template for changing default password, you could override each_context method and then determine which template should be used up to the flag force_pwd_change.
[project]/admin.py
defusing_default_password(self, request):
if self.has_permission(request) and request.user.get_username() andnot request.user.default_pwd_updated:
returnTruereturnFalsedefeach_context(self, request):
context = super().each_context(request)
context["force_pwd_change"] = self.using_default_password(request)
return context
Solution 5:
From a thread on the Django Users mailing list:
This isn't ideal, but it should work (or prompt someone to propose something better).
Add a one-to-one table for the user, with a field containing the initial password (encrypted, of course, so it looks like the password in the
auth_user
table).When the user logs in, have the login page check to see if the passwords match. If they do, redirect to the password change page instead of the normal redirect page.
Post a Comment for "Is It Possible To Implement A "change Password At Next Logon" Type Feature In The Django Admin?"