Skip to content

Commit

Permalink
✨ Change password at first login (#650)
Browse files Browse the repository at this point in the history
* Add PASSWORD_CHANGE_FIRST_LOGIN setting.

* Add change_password_first_login property to Userdata model.

* Create custom G3WSetPasswordForm and custom G3WLoginView to use for change password at first login workflow.

* Start testing.

* Testing.

* Split change password at first login from reset password workflow.

* Create a custom view for confirm reset password to split User rest password workflow from Change password at first login worflow.

* Override settings fix.

---------

Co-authored-by: wlorenzetti <[email protected]>
  • Loading branch information
wlorenzetti and wlorenzetti authored Nov 13, 2023
1 parent c01ccc3 commit ca5ba4e
Show file tree
Hide file tree
Showing 8 changed files with 201 additions and 11 deletions.
4 changes: 4 additions & 0 deletions g3w-admin/base/settings/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -331,6 +331,10 @@
# must be done by the administrator
REGISTRATION_ACTIVE_BY_ADMIN = False

# CHANGE PASSWORD AT FIRST LOGIN
# ------------------------------
PASSWORD_CHANGE_FIRST_LOGIN = False


# QPLOTLY DEFAULT SETTINGS
# ------------------------
Expand Down
6 changes: 6 additions & 0 deletions g3w-admin/base/settings/tests_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,3 +45,9 @@
'worker_type': 'process',
},
}

# LOGIN/REGISTRATION/RESET USER/PASSWORD
# ======================================
# Add this setting here because test override_settings decorator
# does not work at django bootstrap i.e. inside main urls.py module.
PASSWORD_CHANGE_FIRST_LOGIN = True
35 changes: 30 additions & 5 deletions g3w-admin/base/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,16 @@
from usersmanage.forms import (
G3WAuthenticationForm,
G3WResetPasswordForm,
G3WRegistrationForm
G3WRegistrationForm,
G3WSetPasswordForm
)
from usersmanage.views import (
G3WUserRegistrationView,
G3WUsernameRecoveryView,
G3WUsernameRecoveryDoneView,
G3WPasswordResetView
G3WPasswordResetView,
G3WLoginView,
G3WPasswordChangeFirstLoginConfirmView
)

from ajax_select import urls as ajax_select_urls
Expand Down Expand Up @@ -100,7 +103,7 @@
),
path(
'login/',
auth.views.LoginView.as_view(
G3WLoginView.as_view(
template_name='login.html',
form_class=G3WAuthenticationForm,
extra_context=extra_context_login_page
Expand All @@ -122,11 +125,11 @@
include(ajax_select_urls)
)
]

#############################################################
# REGISTRATION USERS
#############################################################

#path('accounts/', include('django_registration.backends.activation.urls')),
urlpatterns += [
path(
"accounts/activate/complete/",
Expand Down Expand Up @@ -198,7 +201,9 @@
),
path(
'reset/<uidb64>/<token>/',
auth.views.PasswordResetConfirmView.as_view(extra_context=extra_context_login_page),
auth.views.PasswordResetConfirmView.as_view(
extra_context=extra_context_login_page,
form_class=G3WSetPasswordForm),
name='password_reset_confirm'
),
path(
Expand All @@ -220,6 +225,26 @@
),
]

#############################################################
# CHANGE PASSWORD FIRST LOGIN
#############################################################
if settings.PASSWORD_CHANGE_FIRST_LOGIN:
urlpatterns += [
path(
'changepassword/<uidb64>/<token>/',
G3WPasswordChangeFirstLoginConfirmView.as_view(
extra_context=extra_context_login_page,
form_class=G3WSetPasswordForm),
name='change_password_first_login_confirm'
),
path(
'changepassword/done/',
auth.views.PasswordResetCompleteView.as_view(
extra_context=extra_context_login_page),
name='change_password_first_login_complete'
),
]

#############################################################
# API URLs
#############################################################
Expand Down
19 changes: 18 additions & 1 deletion g3w-admin/usersmanage/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@
UserCreationForm,
ReadOnlyPasswordHashField,
AuthenticationForm,
PasswordResetForm
PasswordResetForm,
SetPasswordForm
)
from django.contrib.auth import (
password_validation,
Expand Down Expand Up @@ -923,4 +924,20 @@ def clean_email(self):
return self.cleaned_data['email']


class G3WSetPasswordForm(SetPasswordForm):
"""
Custom SetPasswordForm for G3W-SUITE
"""

def save(self, commit=True):

if (settings.PASSWORD_CHANGE_FIRST_LOGIN and
not self.user.is_superuser and
not self.user.userdata.change_password_first_login and
not self.user.userdata.registered):
self.user.userdata.change_password_first_login = True
self.user.userdata.save()

return super().save(commit=commit)


Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Generated by Django 3.2.23 on 2023-11-10 08:56

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('usersmanage', '0013_auto_20230922_0815'),
]

operations = [
migrations.AddField(
model_name='userdata',
name='change_password_first_login',
field=models.BooleanField(blank=True, default=False, null=True, verbose_name='User changed password at first login'),
),
]
2 changes: 2 additions & 0 deletions g3w-admin/usersmanage/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ class Userdata(models.Model):
registered = models.BooleanField(_('Registered user'), default=False, blank=True, null=True)
activated_by_admin = models.BooleanField(_('User activated by administrator'), default=False, null=True,
blank=True)
change_password_first_login = models.BooleanField(_('User changed password at first login'), default=False,
null=True, blank=True)


USER_BACKEND_TYPES = G3WChoices(
Expand Down
86 changes: 84 additions & 2 deletions g3w-admin/usersmanage/tests/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,20 @@
__date__ = '2020-04-14'
__copyright__ = 'Copyright 2015 - 2020, Gis3w'

from django.test import override_settings
from django.test import Client
from django.urls import reverse
from django.urls import reverse, resolve
from django.core.exceptions import ObjectDoesNotExist
from django.contrib.auth.models import Group as AuthGroup, User
from usersmanage.models import Userdata
from .base import BaseUsermanageTestCase
from .utils import setup_testing_user_relations, assign_perm, G3W_EDITOR1
from .utils import (
setup_testing_user_relations,
assign_perm, G3W_EDITOR1,
G3W_VIEWER1,
USER_BACKEND_DEFAULT,
Userbackend
)


class UsermanageViewsTest(BaseUsermanageTestCase):
Expand Down Expand Up @@ -270,6 +278,80 @@ def test_user_groups_delete(self):
self.assertEqual(response.status_code, 404)
self.client.logout()

def test_change_password_first_login(self):
""" Test for change password first login workflow"""

# Create new user
new_user_data = {
'username': 'new_user',
'password': 'new_user'
}
new_user = User.objects.create_user(**new_user_data)
new_user.save()
new_user.groups.add(self.main_roles[G3W_VIEWER1])
Userbackend(user=new_user, backend=USER_BACKEND_DEFAULT).save()
Userdata.objects.create(user=new_user).save()


login_url = reverse('login')

# Test login
res = self.client.post(login_url, data=new_user_data)

# Redirect to reset_password page
self.assertEqual(res.status_code, 302)
self.assertTrue(resolve(res.url).view_name, 'change_password_first_login_confirm')

# with /set-password/
res = self.client.get(res.url)
self.assertEqual(res.status_code, 302)
self.assertTrue(resolve(res.url).view_name, 'change_password_first_login_confirm')

# Reset password
new_password_data = {
'new_password1': 'jaskjT678873u5@#',
'new_password2': 'jaskjT678873u5@#'
}


res = self.client.post(res.url, data=new_password_data)
self.assertEqual(res.status_code, 302)
self.assertEqual(resolve(res.url).view_name, "change_password_first_login_complete")

# Check userdata of user
new_user.refresh_from_db()

self.assertEqual(new_user.userdata.change_password_first_login, True)

# Login again with new password
new_user_data = {
'username': 'new_user',
'password': 'jaskjT678873u5@#'
}

res = self.client.post(login_url, data=new_user_data)

self.assertEqual(res.status_code, 302)
self.assertEqual(res.url, "/")

# Admin user not redirect
# Create new user
new_admin_user_data = {
'username': 'new_admin_user',
'password': 'new_admin_user'
}
new_admin_user = User.objects.create_user(**new_admin_user_data)
new_admin_user.is_superuser = True
new_admin_user.save()
Userbackend(user=new_admin_user, backend=USER_BACKEND_DEFAULT).save()

res = self.client.post(login_url, data=new_admin_user_data)

self.assertEqual(res.status_code, 302)
self.assertEqual(res.url, "/")






Expand Down
42 changes: 39 additions & 3 deletions g3w-admin/usersmanage/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,24 +6,32 @@
DetailView,
View
)
from django.http.response import JsonResponse, Http404
from django.http.response import JsonResponse, Http404, HttpResponseRedirect
from django.views.generic.detail import SingleObjectMixin
from django.utils.decorators import method_decorator
from django.utils.translation import ugettext_lazy as _
from django.utils.encoding import force_bytes
from django.utils.http import urlsafe_base64_encode
from django.core.mail import send_mail
from django.contrib.auth.tokens import default_token_generator
from django.contrib.auth.decorators import permission_required
from django.contrib.auth.models import Group
from django.contrib.auth.views import (
PasswordResetView,
PasswordResetDoneView,
LoginView,
PasswordResetConfirmView
)
from django.contrib.sites.shortcuts import get_current_site
from django.template.loader import render_to_string
from django.contrib.auth.views import PasswordResetView, PasswordResetDoneView
from django.urls import reverse_lazy
from guardian.shortcuts import assign_perm, get_objects_for_user
from guardian.decorators import permission_required_or_403
from django_registration.backends.activation import views as registration_views
from core.mixins.views import G3WRequestViewMixin, G3WAjaxDeleteViewMixin
from core.models import GeneralSuiteData
from .decorators import permission_required_by_backend_or_403
from .utils import getUserGroups, get_user_groups
from .utils import getUserGroups, get_user_groups, userHasGroups
from .configs import *
from .forms import *
import json
Expand Down Expand Up @@ -381,6 +389,13 @@ class G3WPasswordResetView(G3WUserPasswordRecoveryMixin, PasswordResetView):
"""
pass

class G3WPasswordChangeFirstLoginConfirmView(G3WUserPasswordRecoveryMixin, PasswordResetConfirmView):
"""
Class for G3W-SUITE for Password change at first login
"""

success_url = reverse_lazy('change_password_first_login_complete')


class G3WUsernameRecoveryView(G3WUserPasswordRecoveryMixin, PasswordResetView):
"""
Expand All @@ -400,3 +415,24 @@ class G3WUsernameRecoveryDoneView(PasswordResetDoneView):
"""
template_name = 'registration/username_recovery_done.html'
title = _('Username sent')

class G3WLoginView(LoginView):
"""
Custom Login View for G3W-SUITE
"""

def form_valid(self, form):

# Check password at first login is active
# If true redirect to reset password form
user = form.get_user()
if (settings.PASSWORD_CHANGE_FIRST_LOGIN and
not user.is_superuser and
not user.userdata.change_password_first_login and
not user.userdata.registered):
return HttpResponseRedirect(reverse('change_password_first_login_confirm', args=[
urlsafe_base64_encode(force_bytes(user.pk)),
default_token_generator.make_token(user)
]))

return super().form_valid(form)

0 comments on commit ca5ba4e

Please sign in to comment.