Commit 5ccd5cb9 by ziafazal

Storing passoword history of user and validation for user's email and username

parent 16e7ea41
......@@ -4,6 +4,9 @@ Tests for session api with advance security features
import json
import uuid
import unittest
from datetime import datetime, timedelta
from freezegun import freeze_time
from pytz import UTC
from django.test import TestCase
from django.test.client import Client
......@@ -15,7 +18,8 @@ from student.tests.factories import UserFactory
TEST_API_KEY = str(uuid.uuid4())
@override_settings(EDX_API_KEY=TEST_API_KEY, ENABLE_MAX_FAILED_LOGIN_ATTEMPTS=True)
@override_settings(EDX_API_KEY=TEST_API_KEY)
@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
class SessionApiSecurityTest(TestCase):
"""
......@@ -23,7 +27,9 @@ class SessionApiSecurityTest(TestCase):
"""
def setUp(self):
# Create one user and save it to the database
"""
Create one user and save it to the database
"""
self.user = UserFactory.build(username='test', email='test@edx.org')
self.user.set_password('test_password')
self.user.save()
......@@ -31,44 +37,173 @@ class SessionApiSecurityTest(TestCase):
# Create the test client
self.client = Client()
cache.clear()
self.url = '/api/sessions/'
self.session_url = '/api/sessions'
self.user_url = '/api/users'
@override_settings(MAX_FAILED_LOGIN_ATTEMPTS_ALLOWED=10)
@override_settings(ENABLE_MAX_FAILED_LOGIN_ATTEMPTS=True, MAX_FAILED_LOGIN_ATTEMPTS_ALLOWED=10)
def test_login_ratelimited_success(self):
# Try (and fail) logging in with fewer attempts than the limit of 10
# and verify that you can still successfully log in afterwards.
"""
Try (and fail) logging in with fewer attempts than the limit of 10
and verify that you can still successfully log in afterwards.
"""
for i in xrange(9):
password = u'test_password{0}'.format(i)
response = self._login_response('test', password, secure=True)
response = self._do_post_request(self.session_url, 'test', password, secure=True)
self.assertEqual(response.status_code, 401)
# now try logging in with a valid password and check status
response = self._login_response('test', 'test_password', secure=True)
response = self._do_post_request(self.session_url, 'test', 'test_password', secure=True)
self._assert_response(response, status=201)
@override_settings(MAX_FAILED_LOGIN_ATTEMPTS_ALLOWED=10)
@override_settings(ENABLE_MAX_FAILED_LOGIN_ATTEMPTS=True, MAX_FAILED_LOGIN_ATTEMPTS_ALLOWED=10)
def test_login_blockout(self):
# Try (and fail) logging in with 10 attempts
# and verify that user is blocked out.
"""
Try (and fail) logging in with 10 attempts
and verify that user is blocked out.
"""
for i in xrange(10):
password = u'test_password{0}'.format(i)
response = self._login_response('test', password, secure=True)
response = self._do_post_request(self.session_url, 'test', password, secure=True)
self.assertEqual(response.status_code, 401)
# check to see if this response indicates blockout
response = self._login_response('test', 'test_password', secure=True)
response = self._do_post_request(self.session_url, 'test', 'test_password', secure=True)
message = _('This account has been temporarily locked due to excessive login failures. Try again later.')
self._assert_response(response, status=403, message=message)
def _login_response(self, username, password, secure=False):
@override_settings(ENABLE_MAX_FAILED_LOGIN_ATTEMPTS=True, MAX_FAILED_LOGIN_ATTEMPTS_ALLOWED=10,
MAX_FAILED_LOGIN_ATTEMPTS_LOCKOUT_PERIOD_SECS=1800)
def test_login_blockout_reset_time_period(self):
"""
Try logging in 10 times to block user and then login with right credentials(after 30 minutes)
to verify blocked out time expired and user can login successfully.
"""
for i in xrange(10):
password = u'test_password{0}'.format(i)
response = self._do_post_request(self.session_url, 'test', password, secure=True)
self.assertEqual(response.status_code, 401)
# check to see if this response indicates blockout
response = self._do_post_request(self.session_url, 'test', 'test_password', secure=True)
message = _('This account has been temporarily locked due to excessive login failures. Try again later.')
self._assert_response(response, status=403, message=message)
# now reset the time to 30 from now in future
reset_time = datetime.now(UTC) + timedelta(seconds=1800)
with freeze_time(reset_time):
response = self._do_post_request(self.session_url, 'test', 'test_password', secure=True)
self._assert_response(response, status=201)
@override_settings(ENFORCE_PASSWORD_POLICY=True, PASSWORD_MIN_LENGTH=4)
def test_with_short_password(self):
"""
Try (and fail) user creation with shorter password
"""
response = self._do_post_request(self.user_url, 'test', 'abc', email='test@edx.org',
first_name='John', last_name='Doe', secure=True)
message = _('Password: Invalid Length (must be 4 characters or more)')
self._assert_response(response, status=400, message=message)
@override_settings(ENFORCE_PASSWORD_POLICY=True, PASSWORD_MAX_LENGTH=12)
def test_with_long_password(self):
"""
Try (and fail) user creation with longer password
"""
response = self._do_post_request(self.user_url, 'test', 'test_password', email='test@edx.org',
first_name='John', last_name='Doe', secure=True)
message = _('Password: Invalid Length (must be 12 characters or less)')
self._assert_response(response, status=400, message=message)
@override_settings(ENFORCE_PASSWORD_POLICY=True,
PASSWORD_COMPLEXITY={'UPPER': 2, 'LOWER': 2, 'PUNCTUATION': 2, 'DIGITS': 2})
def test_password_without_uppercase(self):
"""
Try (and fail) user creation since password should have atleast 2 upper characters
"""
response = self._do_post_request(self.user_url, 'test', 'test.pa64!', email='test@edx.org',
first_name='John', last_name='Doe', secure=True)
message = _('Password: Must be more complex (must contain 2 or more uppercase characters)')
self._assert_response(response, status=400, message=message)
@override_settings(ENFORCE_PASSWORD_POLICY=True,
PASSWORD_COMPLEXITY={'UPPER': 2, 'LOWER': 2, 'PUNCTUATION': 2, 'DIGITS': 2})
def test_password_without_lowercase(self):
"""
Try (and fail) user creation without any numeric characters in password
"""
response = self._do_post_request(self.user_url, 'test', 'TEST.PA64!', email='test@edx.org',
first_name='John', last_name='Doe', secure=True)
message = _('Password: Must be more complex (must contain 2 or more lowercase characters)')
self._assert_response(response, status=400, message=message)
@override_settings(ENFORCE_PASSWORD_POLICY=True,
PASSWORD_COMPLEXITY={'UPPER': 2, 'LOWER': 2, 'PUNCTUATION': 2, 'DIGITS': 2})
def test_password_without_punctuation(self):
"""
Try (and fail) user creation without any punctuation in password
"""
response = self._do_post_request(self.user_url, 'test', 'test64SS', email='test@edx.org',
first_name='John', last_name='Doe', secure=True)
message = _('Password: Must be more complex (must contain 2 or more uppercase characters,'
' must contain 2 or more punctuation characters)')
self._assert_response(response, status=400, message=message)
@override_settings(ENFORCE_PASSWORD_POLICY=True,
PASSWORD_COMPLEXITY={'UPPER': 2, 'LOWER': 2, 'PUNCTUATION': 2, 'DIGITS': 2})
def test_password_without_numeric(self):
"""
Try (and fail) user creation without any numeric characters in password
"""
response = self._do_post_request(self.user_url, 'test', 'test.paSS!', email='test@edx.org',
first_name='John', last_name='Doe', secure=True)
message = _('Password: Must be more complex (must contain 2 or more uppercase characters,'
' must contain 2 or more digits)')
self._assert_response(response, status=400, message=message)
@override_settings(ENFORCE_PASSWORD_POLICY=True,
PASSWORD_COMPLEXITY={'UPPER': 2, 'LOWER': 2, 'PUNCTUATION': 2, 'DIGITS': 2})
def test_password_with_complexity(self):
"""
This should pass since it has everything needed for a complex password
"""
response = self._do_post_request(self.user_url, str(uuid.uuid4()), 'Test.Me64!', email='test@edx.org',
first_name='John', last_name='Doe', secure=True)
self._assert_response(response, status=201)
def test_user_with_invalid_email(self):
"""
Try (and fail) user creation with invalid email address
"""
response = self._do_post_request(self.user_url, 'test', 'Test.Me64!', email='test-edx.org',
first_name='John', last_name='Doe', secure=True)
message = _('Valid e-mail is required.')
self._assert_response(response, status=400, message=message)
def test_user_with_invalid_username(self):
"""
Try (and fail) user creation with invalid username
"""
response = self._do_post_request(self.user_url, 'user name', 'Test.Me64!', email='test@edx.org',
first_name='John', last_name='Doe', secure=True)
message = _('Username should only consist of A-Z and 0-9, with no spaces.')
self._assert_response(response, status=400, message=message)
def _do_post_request(self, url, username, password, **kwargs):
"""
Post the login info
"""
post_params, extra = {'username': username, 'password': password}, {}
if kwargs.get('email'):
post_params['email'] = kwargs.get('email')
if kwargs.get('first_name'):
post_params['first_name'] = kwargs.get('first_name')
if kwargs.get('last_name'):
post_params['last_name'] = kwargs.get('last_name')
headers = {'X-Edx-Api-Key': TEST_API_KEY, 'Content-Type': 'application/json'}
if secure:
if kwargs.get('secure', False):
extra['wsgi.url_scheme'] = 'https'
return self.client.post(self.url, post_params, headers=headers, **extra)
return self.client.post(url, post_params, headers=headers, **extra)
def _assert_response(self, response, status=200, success=None, message=None):
"""
......
......@@ -5,6 +5,9 @@ import logging
from django.contrib.auth.models import User, Group
from django.core.exceptions import ObjectDoesNotExist
from django.db import IntegrityError
from django.core.validators import validate_email, validate_slug, ValidationError
from django.conf import settings
from django.utils.translation import ugettext_lazy as _
from rest_framework import status
from rest_framework.decorators import api_view, permission_classes
......@@ -14,8 +17,12 @@ from api_manager.permissions import ApiKeyHeaderPermission
from courseware import module_render
from courseware.model_data import FieldDataCache
from courseware.views import get_module_for_descriptor, save_child_position, get_current_child
from student.models import CourseEnrollment
from student.models import CourseEnrollment, PasswordHistory
from xmodule.modulestore.django import modulestore
from util.password_policy_validators import (
validate_password_length, validate_password_complexity,
validate_password_dictionary
)
log = logging.getLogger(__name__)
......@@ -95,6 +102,32 @@ def user_list(request):
password = request.DATA['password']
first_name = request.DATA.get('first_name', '')
last_name = request.DATA.get('last_name', '')
# enforce password complexity as an optional feature
if settings.FEATURES.get('ENFORCE_PASSWORD_POLICY', False):
try:
validate_password_length(password)
validate_password_complexity(password)
validate_password_dictionary(password)
except ValidationError, err:
status_code = status.HTTP_400_BAD_REQUEST
response_data['message'] = _('Password: ') + '; '.join(err.messages)
return Response(response_data, status=status_code)
try:
validate_email(email)
except ValidationError:
status_code = status.HTTP_400_BAD_REQUEST
response_data['message'] = _('Valid e-mail is required.')
return Response(response_data, status=status_code)
try:
validate_slug(username)
except ValidationError:
status_code = status.HTTP_400_BAD_REQUEST
response_data['message'] = _('Username should only consist of A-Z and 0-9, with no spaces.')
return Response(response_data, status=status_code)
try:
user = User.objects.create(email=email, username=username)
except IntegrityError:
......@@ -105,6 +138,11 @@ def user_list(request):
user.last_name = last_name
user.save()
# add this account creation to password history
# NOTE, this will be a NOP unless the feature has been turned on in configuration
password_history_entry = PasswordHistory()
password_history_entry.create(user)
# CDODGE: @TODO: We will have to extend this to look in the CourseEnrollmentAllowed table and
# auto-enroll students when they create a new account. Also be sure to remove from
# the CourseEnrollmentAllow table after the auto-registration has taken place
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment