From 00837c8ef3582bbb4e8557ce69243f4e856625f7 Mon Sep 17 00:00:00 2001 From: piyush-kr Date: Thu, 1 Jun 2017 20:24:14 +0530 Subject: [PATCH 1/6] password expiration policy --- cloud/endagaweb/celery.py | 4 + cloud/endagaweb/forms/dashboard_forms.py | 13 ++ cloud/endagaweb/models.py | 5 +- cloud/endagaweb/settings/prod.py | 4 + cloud/endagaweb/tasks.py | 16 +- .../endagaweb/templates/dashboard/index.html | 48 ++++++ .../templates/dashboard/password_change.html | 38 +++++ cloud/endagaweb/urls.py | 2 + cloud/endagaweb/views/user.py | 144 +++++++++++++++++- 9 files changed, 265 insertions(+), 9 deletions(-) create mode 100644 cloud/endagaweb/templates/dashboard/password_change.html 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..0d09b67f 100644 --- a/cloud/endagaweb/forms/dashboard_forms.py +++ b/cloud/endagaweb/forms/dashboard_forms.py @@ -153,6 +153,19 @@ def __init__(self, *args, **kwargs): class ChangePasswordForm(PasswordChangeForm): """Change password form visible on user profile page.""" + """Updated to show password policy text. """ + old_password = forms.CharField(required=True, label='Old Password', + widget=forms.PasswordInput(attrs={ + 'title': 'Enter Old Password'}), + ) + new_password1 = forms.CharField(required=True, label='New Password', + widget=forms.PasswordInput(attrs={ + 'title': 'Password must contain at least 8 characters,contains ' + 'alphanumeric and one special character.'}), ) + new_password2 = forms.CharField(required=True, label='Confirm Password', + widget=forms.PasswordInput(attrs={ + 'title': 'Confirm Password'}), ) + def __init__(self, *args, **kwargs): super(ChangePasswordForm, self).__init__(*args, **kwargs) self.helper = FormHelper() diff --git a/cloud/endagaweb/models.py b/cloud/endagaweb/models.py index d2429f9d..10eee760 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 diff --git a/cloud/endagaweb/settings/prod.py b/cloud/endagaweb/settings/prod.py index 653f2f0e..933357ea 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", diff --git a/cloud/endagaweb/tasks.py b/cloud/endagaweb/tasks.py index 405ca698..18f564de 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 90 days + """ + six_month_ago = (django.utils.timezone.now() - + datetime.timedelta( + days=settings.ENDAGA['PASSWORD_EXPIRED_DAY'])) + user_profiles = UserProfile.objects.filter(last_pwd_update__lte=six_month_ago) + 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..208732d7 100644 --- a/cloud/endagaweb/templates/dashboard/index.html +++ b/cloud/endagaweb/templates/dashboard/index.html @@ -23,6 +23,54 @@ display: none; } + + + + + + +{% if messages %} + + + +{% 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..5e1be883 --- /dev/null +++ b/cloud/endagaweb/templates/dashboard/password_change.html @@ -0,0 +1,38 @@ +{% 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/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/views/user.py b/cloud/endagaweb/views/user.py index 8d7fc776..2e8e7d54 100644 --- a/cloud/endagaweb/views/user.py +++ b/cloud/endagaweb/views/user.py @@ -27,6 +27,9 @@ from endagaweb.models import UserProfile import logging +from django.utils import timezone +import urlparse +import re logger = logging.getLogger('endagaweb') @@ -103,11 +106,28 @@ def auth_and_login(request): 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_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']: + text = str(user) + ' , your account will be blocked in next '\ + + str(settings.ENDAGA['PASSWORD_EXPIRED_DAY'] - + (today - user_profile.last_pwd_update).days) + 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,13 +143,26 @@ 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 not validate_password_strength(request.POST['new_password1']): + text = 'Error: password must contain at least 8 characters,contains alphanumeric and one special character..' + tags = 'password alert alert-danger' + messages.info(request, text, extra_tags=tags) + return redirect(redirect_url) + if request.POST['old_password'] == request.POST['new_password1']: + text = 'Error: new password must not be old password.' + 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' @@ -142,10 +175,16 @@ def change_password(request): return redirect(redirect_url) # Everything checks out, change the password. request.user.set_password(request.POST['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) @@ -219,3 +258,94 @@ def update_notify_numbers(request): return redirect("/dashboard/profile") return HttpResponseBadRequest() + +@login_required(login_url='/login/') +def check_user(request): + if request.method == 'GET': + context = {} + if 'email' in request.GET: + if User.objects.filter(email=request.GET['email']).exists(): + context['email_available'] = False + else: + context['email_available'] = True + elif 'username' in request.GET: + if User.objects.filter(username=request.GET['username']).exists(): + context['username_available'] = False + else: + context['username_available'] = True + + return JsonResponse(context) + return HttpResponseBadRequest() + +# This view handles the password reset. +def reset(request): + return password_reset(request, + email_template_name= + 'dashboard/user_management/reset_email.html', + subject_template_name= + 'dashboard/user_management/reset_subject.txt', + post_reset_redirect=reverse('user-management')) + + +# This view handles the changing password to reset. +def reset_confirm(request, uidb64=None, token=None): + return password_reset_confirm(request, uidb64=uidb64, + template_name= + 'dashboard/user_management/reset_confirm.html', + token=token, post_reset_redirect= + reverse('success')) + + +def success(request): + return render(request, "dashboard/user_management/success.html") + + +@login_required(login_url='/login/') +def role_default_permissions(request): + if request.method == 'GET': + role = request.GET['role'] + permission_set = ['credit', 'graph', 'report', "smsbroadcast", "tower", + "bts", "subscriber", "network", + "notification", "usageevent"] + + business_analyst = ['view_graph', 'view_report', 'view_bts', + 'view_subscriber', 'view_network'] + + loader = ['view_graph', 'view_report', 'view_bts', 'view_subscriber', + 'view_network', 'change_subscriber', 'change_network', + 'add_subscriber', 'add_sms', 'add_credit', 'download_graph'] + + partner = ['view_graph', 'view_report', 'view_bts', 'view_subscriber', + 'view_network', 'edit_subscriber', 'edit_network', + 'add_subscriber', 'add_sms', 'download_graph'] + + content_type = ContentType.objects.filter(app_label='endagaweb', + model__in= + permission_set).values_list('id', flat=True) + permission = Permission.objects.filter( + content_type__in=content_type).values_list('id', flat=True) + role_permission = [] + if role == 'Business Analyst': + role_permission = Permission.objects.filter( + codename__in=business_analyst).values_list('id', flat=True) + elif role == 'Loader': + role_permission = Permission.objects.filter( + codename__in=loader).values_list('id', flat=True) + elif role == 'Partner': + role_permission = Permission.objects.filter( + codename__in=partner).values_list('id', flat=True) + else: + for i in permission: + role_permission.append(i) + + return JsonResponse({'permissions': list(role_permission)}) + return HttpResponseBadRequest() + +def validate_password_strength(value): + """Checks that a submitted value should match regex and return + boolean value + """ + + regex = "(?=.*[a-zA-Z])(?=.*\\d)(?=.*[!@#$%&*()_+=|<>?{}\\[\\]~-]).{8}" + pattern = re.compile(regex) + return bool(pattern.match(value)) From 1703300a21f5862e293ff32d36ecf0c51c6924a9 Mon Sep 17 00:00:00 2001 From: piyush-kr Date: Sat, 3 Jun 2017 01:16:35 +0530 Subject: [PATCH 2/6] review comments incorporated --- cloud/endagaweb/models.py | 3 +- cloud/endagaweb/tasks.py | 6 +- .../endagaweb/templates/dashboard/index.html | 4 +- cloud/endagaweb/tests/test_user_password.py | 230 ++++++++++++++++++ cloud/endagaweb/views/user.py | 145 ++++------- 5 files changed, 283 insertions(+), 105 deletions(-) create mode 100644 cloud/endagaweb/tests/test_user_password.py diff --git a/cloud/endagaweb/models.py b/cloud/endagaweb/models.py index 10eee760..133668c2 100644 --- a/cloud/endagaweb/models.py +++ b/cloud/endagaweb/models.py @@ -1469,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/tasks.py b/cloud/endagaweb/tasks.py index 18f564de..18dc1f12 100644 --- a/cloud/endagaweb/tasks.py +++ b/cloud/endagaweb/tasks.py @@ -443,12 +443,12 @@ def req_bts_log(self, obj, retry_delay=60*10, max_retries=432): @app.task(bind=True) def block_user(self): """ Block User if User password is not updated - for last 90 days + for last number of days which is configured in a settings . """ - six_month_ago = (django.utils.timezone.now() - + password_expired_duration = (django.utils.timezone.now() - datetime.timedelta( days=settings.ENDAGA['PASSWORD_EXPIRED_DAY'])) - user_profiles = UserProfile.objects.filter(last_pwd_update__lte=six_month_ago) + 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 diff --git a/cloud/endagaweb/templates/dashboard/index.html b/cloud/endagaweb/templates/dashboard/index.html index 208732d7..02860334 100644 --- a/cloud/endagaweb/templates/dashboard/index.html +++ b/cloud/endagaweb/templates/dashboard/index.html @@ -50,10 +50,10 @@