Commit 82668c34 by Matt Drayer Committed by Jonathan Piacenti

mattdrayer/api-userdetailpostfix: Addressed several issues related to updating…

mattdrayer/api-userdetailpostfix: Addressed several issues related to updating user/profile information

FInal fix
parent c907daa4
......@@ -122,4 +122,7 @@ class CourseContentGroupRelationship(TimeStampedModel):
record_active = models.BooleanField(default=True)
class Meta:
"""
Mapping model to enable grouping of course content such as chapters
"""
unique_together = ("course_id", "content_id", "group")
......@@ -123,6 +123,37 @@ class UserPasswordResetTest(TestCase):
)
self._assert_response(response, status=200)
@override_settings(ADVANCED_SECURITY_CONFIG={'MIN_DIFFERENT_STAFF_PASSWORDS_BEFORE_REUSE': 20,
'MIN_TIME_IN_DAYS_BETWEEN_ALLOWED_RESETS': 0})
def test_password_reset_not_allowable_reuse_staff_user(self):
"""
Try resetting staff user password with an already-used password
Hits a very specific LOC in the view code
"""
response = self._do_post_request(
self.user_url, 'test2', 'Test.Me64!', email='test@edx.org',
first_name='John', last_name='Doe', secure=True, is_staff=True
)
self._assert_response(response, status=201)
user_id = response.data['id']
pass_reset_url = "%s/%s" % (self.user_url, str(user_id))
response = self._do_post_pass_reset_request(
pass_reset_url, password='Test.Me64#', secure=True
)
self._assert_response(response, status=200)
response = self._do_post_pass_reset_request(
pass_reset_url, password='Test.Me64#', secure=True
)
message = _(
"You are re-using a password that you have used recently. You must "
"have 20 distinct password(s) before reusing a previous password."
)
self._assert_response(response, status=403, message=message)
@override_settings(ADVANCED_SECURITY_CONFIG={'MIN_TIME_IN_DAYS_BETWEEN_ALLOWED_RESETS': 1})
def test_is_password_reset_too_frequent(self):
"""
......@@ -202,6 +233,8 @@ class UserPasswordResetTest(TestCase):
post_params['first_name'] = kwargs.get('first_name')
if kwargs.get('last_name'):
post_params['last_name'] = kwargs.get('last_name')
if kwargs.get('is_staff'):
post_params['is_staff'] = kwargs.get('is_staff')
headers = {'X-Edx-Api-Key': TEST_API_KEY, 'Content-Type': 'application/json'}
if kwargs.get('secure', False):
......
......@@ -34,7 +34,8 @@ class SecureClient(Client):
@override_settings(MODULESTORE=TEST_DATA_MIXED_MODULESTORE)
@override_settings(EDX_API_KEY=TEST_API_KEY)
@patch.dict("django.conf.settings.FEATURES", {'ENFORCE_PASSWORD_POLICY': False})
@override_settings(PASSWORD_MIN_LENGTH=4)
@patch.dict("django.conf.settings.FEATURES", {'ENFORCE_PASSWORD_POLICY': True})
class UsersApiTests(TestCase):
""" Test suite for Users API views """
......@@ -144,19 +145,30 @@ class UsersApiTests(TestCase):
def test_user_detail_post(self):
test_uri = '/api/users'
local_username = self.test_username + str(randint(11, 99))
data = {'email': self.test_email, 'username': local_username, 'password':
self.test_password, 'first_name': self.test_first_name, 'last_name': self.test_last_name}
data = {'email': self.test_email,
'username': local_username, 'password':self.test_password,
'first_name': self.test_first_name, 'last_name': self.test_last_name}
response = self.do_post(test_uri, data)
self.assertEqual(response.status_code, 201)
test_uri = test_uri + '/' + str(response.data['id'])
data = {'is_active': False}
auth_data = {'username': local_username, 'password': self.test_password}
self.do_post('/api/sessions', auth_data)
self.assertEqual(response.status_code, 201)
data = {'is_active': False, 'is_staff': True}
response = self.do_post(test_uri, data)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.data['is_active'], False)
self.assertEqual(response.data['is_staff'], True)
response = self.do_get(test_uri)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.data['email'], self.test_email)
self.assertEqual(response.data['username'], local_username)
self.assertEqual(response.data['first_name'], self.test_first_name)
self.assertEqual(response.data['last_name'], self.test_last_name)
self.assertEqual(response.data['full_name'], '{} {}'.format(self.test_first_name, self.test_last_name))
self.assertEqual(response.data['is_active'], False)
def test_user_detail_post_username(self):
def test_user_detail_post_duplicate_username(self):
"""
Create two users, then pass the same first username in request in order to update username of second user.
Must return bad request against username, Already exist!
......@@ -188,6 +200,19 @@ class UsersApiTests(TestCase):
self.assertEqual(response.status_code, 400)
self.assertEqual(response.data['message'], message)
def test_user_detail_post_invalid_password(self):
test_uri = '/api/users'
local_username = self.test_username + str(randint(11, 99))
data = {'email': self.test_email,
'username': local_username, 'password': self.test_password,
'first_name': self.test_first_name, 'last_name': self.test_last_name}
response = self.do_post(test_uri, data)
self.assertEqual(response.status_code, 201)
test_uri = test_uri + '/' + str(response.data['id'])
data = {'password': 'x'}
response = self.do_post(test_uri, data)
self.assertEqual(response.status_code, 400)
def test_user_detail_post_user_profile_added_updated(self):
"""
Create a user, then add the user profile
......
......@@ -122,12 +122,12 @@ class UsersList(APIView):
first_name = request.DATA.get('first_name', '')
last_name = request.DATA.get('last_name', '')
is_active = request.DATA.get('is_active', None)
is_staff = request.DATA.get('is_staff', False)
city = request.DATA.get('city', '')
country = request.DATA.get('country', '')
level_of_education = request.DATA.get('level_of_education', '')
year_of_birth = request.DATA.get('year_of_birth', '')
gender = request.DATA.get('gender', '')
# enforce password complexity as an optional feature
if settings.FEATURES.get('ENFORCE_PASSWORD_POLICY', False):
try:
......@@ -154,7 +154,7 @@ class UsersList(APIView):
# Create the User, UserProfile, and UserPreference records
try:
user = User.objects.create(email=email, username=username)
user = User.objects.create(email=email, username=username, is_staff=is_staff)
except IntegrityError:
user = None
else:
......@@ -163,6 +163,8 @@ class UsersList(APIView):
user.last_name = last_name
if is_active is not None:
user.is_active = is_active
if is_staff is not None:
user.is_staff = is_staff
user.save()
profile = UserProfile(user=user)
......@@ -238,17 +240,10 @@ class UsersDetail(APIView):
POST provides the ability to update information about an existing user
"""
response_data = {}
first_name = request.DATA.get('first_name', '')
last_name = request.DATA.get('last_name', '')
city = request.DATA.get('city', '')
country = request.DATA.get('country', '')
level_of_education = request.DATA.get('level_of_education', '')
year_of_birth = request.DATA.get('year_of_birth', '')
gender = request.DATA.get('gender', '')
base_uri = _generate_base_uri(request)
response_data['uri'] = _generate_base_uri(request)
first_name = request.DATA.get('first_name') # Used in multiple spots below
last_name = request.DATA.get('last_name') # Used in multiple spots below
# Add some rate limiting here by re-using the RateLimitMixin as a helper class
limiter = BadRequestRateLimiter()
if limiter.is_rate_limit_exceeded(request):
......@@ -260,112 +255,123 @@ class UsersDetail(APIView):
except ObjectDoesNotExist:
limiter.tick_bad_request_counter(request)
existing_user = None
if existing_user:
username = request.DATA.get('username', None)
if username:
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)
existing_username = User.objects.filter(username=username).filter(~Q(id=user_id))
if existing_username:
status_code = status.HTTP_409_CONFLICT
response_data['message'] = "User '%s' already exists" % (username)
response_data['field_conflict'] = "username"
return Response(response_data, status=status_code)
if existing_user is None:
return Response({}, status=status.HTTP_404_NOT_FOUND)
existing_user.username = username
response_data['username'] = existing_user.username
existing_user.save()
# Ok, valid User, now update the provided fields
if first_name:
existing_user.first_name = first_name
if last_name:
existing_user.last_name = last_name
is_active = request.DATA.get('is_active')
if is_active is not None:
existing_user.is_active = is_active
response_data['is_active'] = existing_user.is_active
is_staff = request.DATA.get('is_staff')
if is_staff is not None:
existing_user.is_staff = is_staff
response_data['is_staff'] = existing_user.is_staff
existing_user.save()
username = request.DATA.get('username', None)
if username:
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)
is_active = request.DATA.get('is_active', None)
if is_active is not None:
existing_user.is_active = is_active
response_data['is_active'] = existing_user.is_active
existing_user.save()
password = request.DATA.get('password')
if password:
old_password_hash = existing_user.password
_serialize_user(response_data, existing_user)
password = request.DATA['password']
if settings.FEATURES.get('ENFORCE_PASSWORD_POLICY', False):
try:
validate_password_length(password)
validate_password_complexity(password)
validate_password_dictionary(password)
except ValidationError, err:
# bad user? tick the rate limiter counter
AUDIT_LOG.warning("API::Bad password in password_reset.")
status_code = status.HTTP_400_BAD_REQUEST
response_data['message'] = _('Password: ') + '; '.join(err.messages)
return Response(response_data, status=status_code)
# also, check the password reuse policy
err_msg = None
if not PasswordHistory.is_allowable_password_reuse(existing_user, password):
if existing_user.is_staff:
num_distinct = settings.ADVANCED_SECURITY_CONFIG['MIN_DIFFERENT_STAFF_PASSWORDS_BEFORE_REUSE']
else:
num_distinct = settings.ADVANCED_SECURITY_CONFIG['MIN_DIFFERENT_STUDENT_PASSWORDS_BEFORE_REUSE']
err_msg = _(
"You are re-using a password that you have used recently. You must "
"have {0} distinct password(s) before reusing a previous password."
).format(num_distinct)
# also, check to see if passwords are getting reset too frequent
if PasswordHistory.is_password_reset_too_soon(existing_user):
num_days = settings.ADVANCED_SECURITY_CONFIG['MIN_TIME_IN_DAYS_BETWEEN_ALLOWED_RESETS']
err_msg = _(
"You are resetting passwords too frequently. Due to security policies, "
"{0} day(s) must elapse between password resets"
).format(num_days)
if err_msg:
# We have an password reset attempt which violates some security policy,
status_code = status.HTTP_403_FORBIDDEN
response_data['message'] = err_msg
return Response(response_data, status=status_code)
existing_username = User.objects.filter(username=username).filter(~Q(id=user_id))
if existing_username:
status_code = status.HTTP_409_CONFLICT
response_data['message'] = "User '%s' already exists" % (username)
response_data['field_conflict'] = "username"
return Response(response_data, status=status_code)
existing_user.is_active = True
existing_user.set_password(password)
existing_user.save()
update_user_password_hash = existing_user.password
existing_user.username = username
response_data['username'] = existing_user.username
existing_user.save()
if update_user_password_hash != old_password_hash:
# 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(existing_user)
password = request.DATA.get('password')
if password:
old_password_hash = existing_user.password
_serialize_user(response_data, existing_user)
if settings.FEATURES.get('ENFORCE_PASSWORD_POLICY', False):
try:
validate_password_length(password)
validate_password_complexity(password)
validate_password_dictionary(password)
except ValidationError, err:
# bad user? tick the rate limiter counter
AUDIT_LOG.warning("API::Bad password in password_reset.")
status_code = status.HTTP_400_BAD_REQUEST
response_data['message'] = _('Password: ') + '; '.join(err.messages)
return Response(response_data, status=status_code)
# also, check the password reuse policy
err_msg = None
if not PasswordHistory.is_allowable_password_reuse(existing_user, password):
if existing_user.is_staff:
num_distinct = settings.ADVANCED_SECURITY_CONFIG['MIN_DIFFERENT_STAFF_PASSWORDS_BEFORE_REUSE']
else:
num_distinct = settings.ADVANCED_SECURITY_CONFIG['MIN_DIFFERENT_STUDENT_PASSWORDS_BEFORE_REUSE']
err_msg = _(
"You are re-using a password that you have used recently. You must "
"have {0} distinct password(s) before reusing a previous password."
).format(num_distinct)
# also, check to see if passwords are getting reset too frequent
if PasswordHistory.is_password_reset_too_soon(existing_user):
num_days = settings.ADVANCED_SECURITY_CONFIG['MIN_TIME_IN_DAYS_BETWEEN_ALLOWED_RESETS']
err_msg = _(
"You are resetting passwords too frequently. Due to security policies, "
"{0} day(s) must elapse between password resets"
).format(num_days)
if err_msg:
# We have an password reset attempt which violates some security policy,
status_code = status.HTTP_403_FORBIDDEN
response_data['message'] = err_msg
return Response(response_data, status=status_code)
existing_user_profile = UserProfile.objects.get(user_id=user_id)
if existing_user_profile:
existing_user.is_active = True
existing_user.set_password(password)
existing_user.save()
update_user_password_hash = existing_user.password
if update_user_password_hash != old_password_hash:
# 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(existing_user)
# Also update the UserProfile record for this User
existing_user_profile = UserProfile.objects.get(user_id=user_id)
if existing_user_profile:
if first_name and last_name:
existing_user_profile.name = '{} {}'.format(first_name, last_name)
city = request.DATA.get('city')
if city:
existing_user_profile.city = city
country = request.DATA.get('country')
if country:
existing_user_profile.country = country
level_of_education = request.DATA.get('level_of_education')
if level_of_education:
existing_user_profile.level_of_education = level_of_education
year_of_birth = request.DATA.get('year_of_birth')
try:
year_of_birth = int(year_of_birth)
except (ValueError, TypeError):
# If they give us garbage, just ignore it instead
# of asking them to put an integer.
year_of_birth = None
if year_of_birth:
existing_user_profile.year_of_birth = year_of_birth
gender = request.DATA.get('gender')
if gender:
existing_user_profile.gender = gender
try:
existing_user_profile.year_of_birth = int(year_of_birth)
except ValueError:
# If they give us garbage, just ignore it instead
# of asking them to put an integer.
existing_user_profile.year_of_birth = None
existing_user_profile.save()
status_code = status.HTTP_200_OK
else:
status_code = status.HTTP_404_NOT_FOUND
response_data['message'] = 'User not exist'
return Response(response_data, status=status_code)
existing_user_profile.save()
return Response(response_data, status=status.HTTP_200_OK)
class UsersGroupsList(APIView):
......
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