diff --git a/cloud/endagaweb/celery.py b/cloud/endagaweb/celery.py
index 8125f76f..c8731605 100644
--- a/cloud/endagaweb/celery.py
+++ b/cloud/endagaweb/celery.py
@@ -42,5 +42,9 @@
'task': 'endagaweb.tasks.usageevents_to_sftp',
# Run this at 15:00 UTC (10:00 PDT, 02:00 Papua time)
'schedule': crontab(minute=0, hour=17),
+ }, 'block-user': {
+ 'task': 'endagaweb.tasks.block_user',
+ # Run this at 15:00 UTC (10:00 PDT, 02:00 Papua time)
+ 'schedule': crontab(minute=0, hour=17),
}
})
diff --git a/cloud/endagaweb/forms/dashboard_forms.py b/cloud/endagaweb/forms/dashboard_forms.py
index 2f9a6ddc..50749e54 100644
--- a/cloud/endagaweb/forms/dashboard_forms.py
+++ b/cloud/endagaweb/forms/dashboard_forms.py
@@ -23,6 +23,7 @@
from ccm.common.currency import CURRENCIES
from endagaweb import models
from endagaweb.templatetags import apptags
+from django.contrib.auth import password_validation
class UpdateContactForm(forms.Form):
@@ -153,6 +154,10 @@ def __init__(self, *args, **kwargs):
class ChangePasswordForm(PasswordChangeForm):
"""Change password form visible on user profile page."""
+ """Updated for password validation. """
+
+ error_message = ''
+
def __init__(self, *args, **kwargs):
super(ChangePasswordForm, self).__init__(*args, **kwargs)
self.helper = FormHelper()
@@ -160,8 +165,26 @@ def __init__(self, *args, **kwargs):
self.helper.form_method = 'post'
self.helper.form_action = '/account/password/change'
self.helper.form_class = 'profile-form'
+ self.error_message = ''
self.helper.add_input(Submit('submit', 'Save'))
+ def clean_password1(self):
+ old_password = self.cleaned_data.get("old_password")
+ new_password1 = self.cleaned_data.get("new_password1")
+ if new_password1 and old_password and old_password == new_password1:
+ self.error_message = 'Error: new password must not be old password.'
+ raise forms.ValidationError(self.error_message)
+ password_validation.validate_password(new_password1)
+ return new_password1
+
+ def clean_new_password2(self):
+ password1 = self.cleaned_data.get('new_password1')
+ password2 = self.cleaned_data.get('new_password2')
+ if password1 and password2:
+ if password1 != password2:
+ self.error_message = 'Error:conform password does not match.'
+ raise forms.ValidationError(self.error_message)
+ return password2
class NotifyEmailsForm(forms.Form):
notify_emails = forms.CharField(required=False, label="",
diff --git a/cloud/endagaweb/models.py b/cloud/endagaweb/models.py
index d2429f9d..133668c2 100644
--- a/cloud/endagaweb/models.py
+++ b/cloud/endagaweb/models.py
@@ -77,7 +77,10 @@ class UserProfile(models.Model):
# because a user may have permissions on other Network instances.
# For example to get a list of networks the user can view:
# >>> get_objects_for_user(user_profile.user, 'view_network', klass=Network)
- network = models.ForeignKey('Network', null=True, on_delete=models.SET_NULL)
+ network = models.ForeignKey('Network', null=True,
+ on_delete=models.SET_NULL)
+ # Added for Password Expiry
+ last_pwd_update = models.DateTimeField(auto_now=True)
def __str__(self):
return "%s's profile" % self.user
@@ -1466,8 +1469,7 @@ class ConfigurationKey(models.Model):
Can be associated with many things.
"""
bts = models.ForeignKey(BTS, null=True, blank=True, on_delete=models.CASCADE)
- network = models.ForeignKey(Network, null=True, blank=True,
- on_delete=models.CASCADE)
+ network = models.ForeignKey(Network, null=True, blank=True, on_delete=models.CASCADE)
category = models.TextField() # "endaga", "openbts", etc..
key = models.TextField()
value = models.TextField()
diff --git a/cloud/endagaweb/settings/prod.py b/cloud/endagaweb/settings/prod.py
index 653f2f0e..045786f4 100644
--- a/cloud/endagaweb/settings/prod.py
+++ b/cloud/endagaweb/settings/prod.py
@@ -277,6 +277,10 @@
# Enable/disable billing for networks. If false, we ignore what's in the
# network's account balance.
'NW_BILLING': os.environ.get("NW_BILLING", "True").lower() == "true",
+ # Password Expiration day
+ 'PASSWORD_EXPIRED_DAY': 90,
+ # Password Expiry Alert
+ 'PASSSWORD_EXPIRED_LAST_SEVEN_DAYS': 83,
}
STRIPE_API_KEY = os.environ.get("STRIPE_API_KEY",
@@ -317,3 +321,11 @@
# Security middleware settings
SECURE_CONTENT_TYPE_NOSNIFF = True
+
+# Password validation
+AUTH_PASSWORD_VALIDATORS = [
+
+ { 'NAME': 'endagaweb.util.custom_password_validators.CustomPasswortValidator',
+ },
+
+]
\ No newline at end of file
diff --git a/cloud/endagaweb/tasks.py b/cloud/endagaweb/tasks.py
index 405ca698..18dc1f12 100644
--- a/cloud/endagaweb/tasks.py
+++ b/cloud/endagaweb/tasks.py
@@ -41,7 +41,7 @@
from endagaweb.models import Subscriber
from endagaweb.models import UsageEvent
from endagaweb.models import SystemEvent
-from endagaweb.models import TimeseriesStat
+from endagaweb.models import TimeseriesStat, UserProfile
from endagaweb.ic_providers.nexmo import NexmoProvider
@@ -439,3 +439,17 @@ def req_bts_log(self, obj, retry_delay=60*10, max_retries=432):
raise
finally:
obj.save()
+
+@app.task(bind=True)
+def block_user(self):
+ """ Block User if User password is not updated
+ for last number of days which is configured in a settings .
+ """
+ password_expired_duration = (django.utils.timezone.now() -
+ datetime.timedelta(
+ days=settings.ENDAGA['PASSWORD_EXPIRED_DAY']))
+ user_profiles = UserProfile.objects.filter(last_pwd_update__lte=password_expired_duration)
+ for user_profile in user_profiles:
+ user_profile.user.is_active = False
+ print '%s user is Blocked!' % user_profile.user.username
+ user_profile.user.save()
diff --git a/cloud/endagaweb/templates/dashboard/index.html b/cloud/endagaweb/templates/dashboard/index.html
index 417197b9..45fb034b 100644
--- a/cloud/endagaweb/templates/dashboard/index.html
+++ b/cloud/endagaweb/templates/dashboard/index.html
@@ -23,6 +23,56 @@
display: none;
}
+
+
+
+
+
+
+{% if messages %}
+
+
+
+
+
+ {% for message in messages %}
+
+ Hi {{ message }}
+
+ {% endfor %}
+
+
+ Please change your password.
+
+
+
+
+
+
+
+
+
+{% endif %}
+
+
+
{% endblock %}
diff --git a/cloud/endagaweb/templates/dashboard/password_change.html b/cloud/endagaweb/templates/dashboard/password_change.html
new file mode 100644
index 00000000..d79e4a34
--- /dev/null
+++ b/cloud/endagaweb/templates/dashboard/password_change.html
@@ -0,0 +1,37 @@
+{% extends "dashboard/layout.html" %}
+{% comment %}
+Copyright (c) 2016-present, Facebook, Inc.
+All rights reserved.
+
+This source code is licensed under the BSD-style license found in the
+LICENSE file in the root directory of this source tree. An additional grant
+of patent rights can be found in the PATENTS file in the same directory.
+{% endcomment %}
+{% load apptags %}
+{% load crispy_forms_tags %}
+
+{% block title %} {% tmpl_const "SITENAME" %} | Monitor Usage, Pay Bills {% endblock %}
+
+{% block content %}
+
+
+
+
+
+
+
+
+
Change Password
+ {% crispy change_pass_form %}
+ {% for message in messages %}
+ {% if 'password' in message.tags %}
+
{{ message }}
+ {% endif %}
+ {% endfor %}
+
+
+
+
+{% endblock %}
diff --git a/cloud/endagaweb/tests/test_user_password.py b/cloud/endagaweb/tests/test_user_password.py
new file mode 100644
index 00000000..716c0ebe
--- /dev/null
+++ b/cloud/endagaweb/tests/test_user_password.py
@@ -0,0 +1,223 @@
+"""Tests for models.Users password
+
+Copyright (c) 2016-present, Facebook, Inc.
+All rights reserved.
+
+This source code is licensed under the BSD-style license found in the
+LICENSE file in the root directory of this source tree. An additional grant
+of patent rights can be found in the PATENTS file in the same directory.
+"""
+
+from __future__ import absolute_import
+from __future__ import division
+from __future__ import print_function
+from __future__ import unicode_literals
+from django import test
+from django.test import TestCase
+from endagaweb import models
+
+
+class TestBase(TestCase):
+
+ @classmethod
+ def setUpClass(cls):
+ cls.username = 'testuser'
+ cls.password = 'testuser_abad'
+ cls.user = models.User(username=cls.username, email='y@l.com')
+ cls.user.set_password(cls.password)
+ cls.user.save()
+ cls.error_tag = 'password alert alert-danger error'
+ cls.success_tag = 'password alert alert-success success'
+ # Create a test client.
+ cls.client = test.Client()
+
+ @classmethod
+ def tearDownClass(cls):
+ cls.user.delete()
+
+ def tearDown(self):
+ self.logout()
+
+ def login(self):
+ """Log the client in."""
+ data = {
+ 'email': self.username,
+ 'password': self.password,
+ }
+ self.client.post('/auth/', data)
+
+ def logout(self):
+ """Log the client out."""
+ self.client.get('/logout')
+
+ @classmethod
+ def getmessage(cls, response):
+ """Helper method to return message from response """
+ for c in response.context:
+ message = [m for m in c.get('messages')][0]
+ if message:
+ return message
+
+
+class UserPasswordStrengthTests(TestBase):
+ """Testing strength of new password."""
+
+ def test_old_password(self):
+ """validate invalid old password."""
+ self.login()
+ data = {
+ 'old_password': 'testuser_abads',
+ 'new_password1': 'Admin_123',
+ 'new_password2': 'Admin_123'
+ }
+ response = self.client.post('/account/password/change/', data ,
+ follow=True, HTTP_REFERER='/password/change')
+ message = self.getmessage(response)
+ self.assertEqual(message.tags, self.error_tag)
+
+ def test_new_password_case1(self):
+ """validate new password must contain 8 length."""
+ self.login()
+ data = {
+ 'old_password': 'testuser_abad',
+ 'new_password1': 'test_12',
+ 'new_password2': 'test_12'
+ }
+ response = self.client.post('/account/password/change/', data,
+ follow=True,HTTP_REFERER='/password/change')
+ message = self.getmessage(response)
+ self.assertEqual(message.tags, self.error_tag)
+
+ def test_new_password_case2(self):
+ """validate new password must contain alphanumeric."""
+ self.login()
+ data = {
+ 'old_password': 'testuser_abad',
+ 'new_password1': 'user_abcde',
+ 'new_password2': 'user_abcde'
+ }
+ response = self.client.post('/account/password/change/', data,
+ follow=True, HTTP_REFERER='/password/change')
+ message = self.getmessage(response)
+ self.assertEqual(message.tags, self.error_tag)
+
+ def test_new_password_case3(self):
+ """validate new password must contain alphanumeric."""
+ self.login()
+ data = {
+ 'old_password': 'testuser_abad',
+ 'new_password1': '1988@1999',
+ 'new_password2': '1988@1999'
+ }
+ response = self.client.post('/account/password/change/', data,
+ follow=True, HTTP_REFERER='/password/change')
+ message = self.getmessage(response)
+ self.assertEqual(message.tags,self.error_tag)
+
+
+ def test_new_password_case4(self):
+ """validate new password must contain special character."""
+ self.login()
+ data = {
+ 'old_password': 'testuser_abad',
+ 'new_password1': '1988abcd',
+ 'new_password2': '1988abcd'
+ }
+ response = self.client.post('/account/password/change/', data,
+ follow=True, HTTP_REFERER='/password/change')
+ message = self.getmessage(response)
+ self.assertEqual(message.tags, self.error_tag)
+
+ def test_new_password_case5(self):
+ """validate new password must be minimum length of 8 character."""
+ self.login()
+ data = {
+ 'old_password': 'testuser_abad',
+ 'new_password1': 'sinha_1',
+ 'new_password2': 'sinha_1'
+ }
+ response = self.client.post('/account/password/change', data,
+ follow=True, HTTP_REFERER='/password/change')
+ message = self.getmessage(response)
+ self.assertEqual(message.tags, self.error_tag)
+
+ def test_new_password_case6(self):
+ """validate new password must have alhanumeric and special character."""
+ self.login()
+ data = {
+ 'old_password': 'testuser_abad',
+ 'new_password1': 'userA_183',
+ 'new_password2': 'userA_183'
+ }
+ response = self.client.post('/account/password/change', data,
+ follow=True, HTTP_REFERER='/password/change')
+ message = self.getmessage(response)
+ self.assertEqual(message.tags, self.success_tag)
+
+ def test_redirect_url_for_dashbboard_profile(self):
+ """validate if change password page come from /dashboard/profile ,
+ redirect to /dashboard/profile."""
+ self.login()
+ data = {
+ 'old_password': 'testuser_abad',
+ 'new_password1': 'userA_184',
+ 'new_password2': 'userA_184'
+ }
+ response = self.client.post('/account/password/change/', data,
+ HTTP_REFERER='/dashboard/profile')
+ self.assertEqual(response.url, '/dashboard/profile')
+ self.assertEqual(response.status_code,302)
+
+ def test_redirect_url_for_password_change(self):
+ """validate if change password page come from /password/change ,
+ redirect to /password/change."""
+ self.login()
+ data = {
+ 'old_password': 'testuser_abad',
+ 'new_password1': 'userA%185',
+ 'new_password2': 'userA%184'
+ }
+ response = self.client.post('/account/password/change', data,
+
+ HTTP_REFERER='/password/change')
+ self.assertEqual(response.url, '/password/change')
+ self.assertEqual(response.status_code, 302)
+
+ def test_new_password_case7(self):
+ """validate new password with special character."""
+ self.login()
+ data = {
+ 'old_password': 'testuser_abad',
+ 'new_password1': '185@auser1',
+ 'new_password2': '185@auser1'
+ }
+ response = self.client.post('/account/password/change', data,follow=True,
+ HTTP_REFERER='/password/change' )
+ message = self.getmessage(response)
+ self.assertEqual(message.tags, self.success_tag)
+
+ def test_new_password_case8(self):
+ """validate new password with special character."""
+ self.login()
+ data = {
+ 'old_password': 'testuser_abad',
+ 'new_password1': 'User*#4As',
+ 'new_password2': 'User*#4As'
+ }
+ response = self.client.post('/account/password/change', data,follow=True,
+ HTTP_REFERER='/password/change' )
+ message = self.getmessage(response)
+ self.assertEqual(message.tags, self.success_tag)
+
+ def test_new_password_case9(self):
+ """validate new password with special character."""
+ self.login()
+ data = {
+ 'old_password': 'testuser_abad',
+ 'new_password1': '4as8trtaabb^',
+ 'new_password2': '4as8trtaabb^'
+ }
+ response = self.client.post('/account/password/change', data,follow=True,
+ HTTP_REFERER='/password/change' )
+ message = self.getmessage(response)
+ self.assertEqual(message.tags, self.success_tag)
diff --git a/cloud/endagaweb/urls.py b/cloud/endagaweb/urls.py
index 4c7d2469..241541cd 100644
--- a/cloud/endagaweb/urls.py
+++ b/cloud/endagaweb/urls.py
@@ -83,6 +83,8 @@
url(r'^account/update', endagaweb.views.user.update_contact),
url(r'^account/', endagaweb.views.dashboard.dashboard_view),
url(r'^logout/$', django.contrib.auth.views.logout, {'next_page': '/'}),
+ # Added for ExpiredPassword
+ url(r'^password/change', endagaweb.views.user.change_expired_password),
# Dashboard.
url(r'^dashboard/card', endagaweb.views.dashboard.addcard),
diff --git a/cloud/endagaweb/util/custom_password_validators.py b/cloud/endagaweb/util/custom_password_validators.py
new file mode 100644
index 00000000..c6a2e4e0
--- /dev/null
+++ b/cloud/endagaweb/util/custom_password_validators.py
@@ -0,0 +1,24 @@
+from django.core.exceptions import ValidationError
+from django.utils.translation import ugettext as _
+
+class CustomPasswortValidator(object):
+
+ def __init__(self, min_length=1):
+ self.min_length = min_length
+
+ def validate(self, password, user=None):
+ special_characters = "[~\!@#\$%\^&\*\(\)_\+{}\":;'\[\]]"
+ min_charcter_length =8;
+ if len(password) < min_charcter_length:
+ raise ValidationError(_('Password must be at least {0} characters '
+ 'long.').format(min_charcter_length))
+
+ if not any(char.isdigit() for char in password):
+ raise ValidationError(_('Password must contain at least %(min_length)d digit.') % {'min_length': self.min_length})
+ if not any(char.isalpha() for char in password):
+ raise ValidationError(_('Password must contain at least %(min_length)d letter.') % {'min_length': self.min_length})
+ if not any(char in special_characters for char in password):
+ raise ValidationError(_('Password must contain at least %(min_length)d special character.') % {'min_length': self.min_length})
+
+ def get_help_text(self):
+ return 'Password must contain atlest 8 characters,contains alphanumeric and one special character.'
\ No newline at end of file
diff --git a/cloud/endagaweb/views/user.py b/cloud/endagaweb/views/user.py
index 8d7fc776..d89f8e9c 100644
--- a/cloud/endagaweb/views/user.py
+++ b/cloud/endagaweb/views/user.py
@@ -27,6 +27,13 @@
from endagaweb.models import UserProfile
import logging
+from django.utils import timezone
+import urlparse
+import re
+from guardian.shortcuts import get_objects_for_user
+from endagaweb.forms import dashboard_forms as dform
+from endagaweb import models
+from django.core import exceptions
logger = logging.getLogger('endagaweb')
@@ -102,12 +109,30 @@ def auth_and_login(request):
"""Handles POSTed credentials for login."""
user = authenticate(username=request.POST['email'],
password=request.POST['password'])
- if user:
- login(request, user)
- next_url = '/dashboard'
- if 'next' in request.POST and request.POST['next']:
- next_url = request.POST['next']
- return redirect(next_url)
+ if user is not None:
+ if user.is_active:
+ login(request, user)
+ user = User.objects.get(username=user)
+ today = timezone.now()
+ user_profile = UserProfile.objects.get(user=user)
+ next_url = '/dashboard'
+ if 'next' in request.POST and request.POST['next']:
+ next_url = request.POST['next']
+ if (today - user_profile.last_pwd_update).days >= \
+ settings.ENDAGA['PASSSWORD_EXPIRED_LAST_SEVEN_DAYS']:
+ password_expired_day_left = str(settings.ENDAGA['PASSWORD_EXPIRED_DAY']
+ - (today - user_profile.last_pwd_update).days)
+ text = '%s, your account will be blocked in next %s days unless' \
+ ' change your password' %(user, password_expired_day_left)
+ messages.error(request, text)
+ return redirect(next_url)
+ else:
+ return redirect(next_url)
+ else:
+ # Notification, if blocked user is trying to log in
+ text = "This user is blocked. Please contact admin."
+ messages.error(request, text)
+ return redirect('/login/')
else:
text = "Sorry, that email / password combination is not valid."
messages.error(request, text)
@@ -123,31 +148,69 @@ def change_password(request):
required_params = ('old_password', 'new_password1', 'new_password2')
if not all([param in request.POST for param in required_params]):
return HttpResponseBadRequest()
- # Validate
- redirect_url = '/dashboard/profile'
+ # Validate url for redirect
+ if urlparse.urlparse(request.META['HTTP_REFERER']
+ ).path != '/dashboard/profile':
+ redirect_url = '/password/change'
+ else:
+ redirect_url = '/dashboard/profile'
if not request.user.check_password(request.POST['old_password']):
text = 'Error: old password is incorrect.'
tags = 'password alert alert-danger'
messages.error(request, text, extra_tags=tags)
return redirect(redirect_url)
- if request.POST['new_password1'] != request.POST['new_password2']:
- text = 'Error: new passwords do not match.'
- tags = 'password alert alert-danger'
- messages.error(request, text, extra_tags=tags)
- return redirect(redirect_url)
- if request.POST['new_password1'] == '':
- text = 'Error: new password is not valid.'
+ try:
+ form = dform.ChangePasswordForm(user=request.user, data=request.POST)
+ if form.is_valid():
+ new_password1 =form.clean_password1()
+ form.save()
+ request.user.set_password(new_password1)
+ user_profile = UserProfile.objects.get(user=request.user)
+ user_profile.last_pwd_update = timezone.now()
+ user_profile.save()
+ request.user.save()
+ text = 'Password changed successfully.'
+ tags = 'password alert alert-success'
+ messages.success(request, text, extra_tags=tags)
+ if urlparse.urlparse(request.META['HTTP_REFERER']
+ ).path != '/dashboard/profile':
+ redirect_url = '/dashboard'
+ return redirect(redirect_url)
+ else:
+ return redirect(redirect_url)
+ else:
+ """if form is invalid in scenario if conform password not match with
+ new password, so firstly validate new_password strength and raise execption
+ if password strength is success then give error Error:conform password does
+ not match by default djnago called clean_password2()."""
+
+ form.clean_password1()
+ tags = 'password alert alert-danger'
+ messages.error(request, form.error_message, extra_tags=tags)
+ return redirect(redirect_url)
+ except exceptions.ValidationError as e:
tags = 'password alert alert-danger'
- messages.error(request, text, extra_tags=tags)
+ messages.error(request, ''.join(e.messages), extra_tags=tags)
return redirect(redirect_url)
- # Everything checks out, change the password.
- request.user.set_password(request.POST['new_password1'])
- request.user.save()
- text = 'Password changed successfully.'
- tags = 'password alert alert-success'
- messages.success(request, text, extra_tags=tags)
- return redirect(redirect_url)
+@login_required(login_url='/login/')
+def change_expired_password(request):
+ """Render password change template to change
+ password
+ """
+ user_profile = UserProfile.objects.get(user=request.user)
+ network = user_profile.network
+ context = {
+ 'networks': get_objects_for_user(request.user, 'view_network', klass=models.Network),
+ 'user_profile': user_profile,
+ 'network': network,
+ 'user_profile': user_profile,
+ 'change_pass_form': dform.ChangePasswordForm(request.user),
+
+ }
+ template = get_template("dashboard/password_change.html")
+ html = template.render(context, request)
+ return HttpResponse(html)
@login_required(login_url='/login/')
def update_contact(request):
@@ -219,3 +282,4 @@ def update_notify_numbers(request):
return redirect("/dashboard/profile")
return HttpResponseBadRequest()
+