Merge pull request #17561 from edx/jmbowman/PLAT-1976
PLAT-1976 Add waffle switch to block auth_user write attempts
This commit is contained in:
@@ -987,7 +987,7 @@ INSTALLED_APPS = [
|
||||
'openedx.core.djangoapps.contentserver',
|
||||
'course_creators',
|
||||
'openedx.core.djangoapps.external_auth',
|
||||
'student', # misleading name due to sharing with lms
|
||||
'student.apps.StudentConfig', # misleading name due to sharing with lms
|
||||
'openedx.core.djangoapps.course_groups', # not used in cms (yet), but tests run
|
||||
'xblock_config.apps.XBlockConfig',
|
||||
|
||||
|
||||
20
common/djangoapps/student/apps.py
Normal file
20
common/djangoapps/student/apps.py
Normal file
@@ -0,0 +1,20 @@
|
||||
"""
|
||||
Configuration for the ``student`` Django application.
|
||||
"""
|
||||
from __future__ import absolute_import
|
||||
|
||||
from django.apps import AppConfig
|
||||
from django.contrib.auth.signals import user_logged_in
|
||||
|
||||
|
||||
class StudentConfig(AppConfig):
|
||||
"""
|
||||
Default configuration for the ``student`` application.
|
||||
"""
|
||||
name = 'student'
|
||||
|
||||
def ready(self):
|
||||
from django.contrib.auth.models import update_last_login as django_update_last_login
|
||||
user_logged_in.disconnect(django_update_last_login)
|
||||
from .signals.receivers import update_last_login
|
||||
user_logged_in.connect(update_last_login)
|
||||
19
common/djangoapps/student/signals/receivers.py
Normal file
19
common/djangoapps/student/signals/receivers.py
Normal file
@@ -0,0 +1,19 @@
|
||||
"""
|
||||
Signal receivers for the "student" application.
|
||||
"""
|
||||
from __future__ import absolute_import
|
||||
|
||||
from django.utils import timezone
|
||||
|
||||
from openedx.core.djangoapps.user_api.config.waffle import PREVENT_AUTH_USER_WRITES, waffle
|
||||
|
||||
|
||||
def update_last_login(sender, user, **kwargs): # pylint: disable=unused-argument
|
||||
"""
|
||||
Replacement for Django's ``user_logged_in`` signal handler that knows not
|
||||
to attempt updating the ``last_login`` field when we're trying to avoid
|
||||
writes to the ``auth_user`` table while running a migration.
|
||||
"""
|
||||
if not waffle().is_enabled(PREVENT_AUTH_USER_WRITES):
|
||||
user.last_login = timezone.now()
|
||||
user.save(update_fields=['last_login'])
|
||||
@@ -9,6 +9,7 @@ from mock import patch
|
||||
|
||||
from edxmako.shortcuts import render_to_string
|
||||
from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers
|
||||
from openedx.core.djangoapps.user_api.config.waffle import PREVENT_AUTH_USER_WRITES, SYSTEM_MAINTENANCE_MSG, waffle
|
||||
from student.models import Registration
|
||||
from student.tests.factories import UserFactory
|
||||
|
||||
@@ -199,3 +200,14 @@ class TestActivateAccount(TestCase):
|
||||
response = self.client.get(reverse('activate', args=[uuid4().hex]), follow=True)
|
||||
self.assertRedirects(response, login_page_url)
|
||||
self.assertContains(response, 'Your account could not be activated')
|
||||
|
||||
def test_account_activation_prevent_auth_user_writes(self):
|
||||
login_page_url = "{login_url}?next={redirect_url}".format(
|
||||
login_url=reverse('signin_user'),
|
||||
redirect_url=reverse('dashboard'),
|
||||
)
|
||||
with waffle().override(PREVENT_AUTH_USER_WRITES, True):
|
||||
response = self.client.get(reverse('activate', args=[self.registration.activation_key]), follow=True)
|
||||
self.assertRedirects(response, login_page_url)
|
||||
self.assertContains(response, SYSTEM_MAINTENANCE_MSG)
|
||||
assert not self.user.is_active
|
||||
|
||||
@@ -14,9 +14,7 @@ from django.core.urlresolvers import reverse
|
||||
from django.test import TestCase, TransactionTestCase
|
||||
from django.test.client import RequestFactory
|
||||
from django.test.utils import override_settings
|
||||
from mock import patch
|
||||
|
||||
import student
|
||||
from django_comment_common.models import ForumsConfig
|
||||
from notification_prefs import NOTIFICATION_PREF_KEY
|
||||
from openedx.core.djangoapps.external_auth.models import ExternalAuthMap
|
||||
@@ -25,10 +23,11 @@ from openedx.core.djangoapps.site_configuration.tests.mixins import SiteMixin
|
||||
from openedx.core.djangoapps.user_api.accounts import (
|
||||
USERNAME_BAD_LENGTH_MSG, USERNAME_INVALID_CHARS_ASCII, USERNAME_INVALID_CHARS_UNICODE
|
||||
)
|
||||
from openedx.core.djangoapps.user_api.config.waffle import PREVENT_AUTH_USER_WRITES, waffle
|
||||
from openedx.core.djangoapps.user_api.preferences.api import get_user_preference
|
||||
from student.models import UserAttribute
|
||||
from student.views import REGISTRATION_AFFILIATE_ID, REGISTRATION_UTM_CREATED_AT, REGISTRATION_UTM_PARAMETERS, \
|
||||
skip_activation_email
|
||||
create_account, skip_activation_email
|
||||
from student.tests.factories import UserFactory
|
||||
from third_party_auth.tests import factories as third_party_auth_factory
|
||||
|
||||
@@ -279,7 +278,7 @@ class TestCreateAccount(SiteMixin, TestCase):
|
||||
|
||||
with mock.patch('edxmako.request_context.get_current_request', return_value=request):
|
||||
with mock.patch('django.core.mail.send_mail') as mock_send_mail:
|
||||
student.views.create_account(request)
|
||||
create_account(request)
|
||||
|
||||
# check that send_mail is called
|
||||
if bypass_activation_email:
|
||||
@@ -288,7 +287,8 @@ class TestCreateAccount(SiteMixin, TestCase):
|
||||
self.assertTrue(mock_send_mail.called)
|
||||
|
||||
@unittest.skipUnless(settings.FEATURES.get('AUTH_USE_SHIB'), "AUTH_USE_SHIB not set")
|
||||
@mock.patch.dict(settings.FEATURES, {'BYPASS_ACTIVATION_EMAIL_FOR_EXTAUTH': True, 'AUTOMATIC_AUTH_FOR_TESTING': False})
|
||||
@mock.patch.dict(settings.FEATURES,
|
||||
{'BYPASS_ACTIVATION_EMAIL_FOR_EXTAUTH': True, 'AUTOMATIC_AUTH_FOR_TESTING': False})
|
||||
def test_extauth_bypass_sending_activation_email_with_bypass(self):
|
||||
"""
|
||||
Tests user creation without sending activation email when
|
||||
@@ -297,7 +297,8 @@ class TestCreateAccount(SiteMixin, TestCase):
|
||||
self.base_extauth_bypass_sending_activation_email(True)
|
||||
|
||||
@unittest.skipUnless(settings.FEATURES.get('AUTH_USE_SHIB'), "AUTH_USE_SHIB not set")
|
||||
@mock.patch.dict(settings.FEATURES, {'BYPASS_ACTIVATION_EMAIL_FOR_EXTAUTH': False, 'AUTOMATIC_AUTH_FOR_TESTING': False})
|
||||
@mock.patch.dict(settings.FEATURES,
|
||||
{'BYPASS_ACTIVATION_EMAIL_FOR_EXTAUTH': False, 'AUTOMATIC_AUTH_FOR_TESTING': False})
|
||||
def test_extauth_bypass_sending_activation_email_without_bypass_1(self):
|
||||
"""
|
||||
Tests user creation without sending activation email when
|
||||
@@ -306,7 +307,8 @@ class TestCreateAccount(SiteMixin, TestCase):
|
||||
self.base_extauth_bypass_sending_activation_email(False)
|
||||
|
||||
@unittest.skipUnless(settings.FEATURES.get('AUTH_USE_SHIB'), "AUTH_USE_SHIB not set")
|
||||
@mock.patch.dict(settings.FEATURES, {'BYPASS_ACTIVATION_EMAIL_FOR_EXTAUTH': False, 'AUTOMATIC_AUTH_FOR_TESTING': False, 'SKIP_EMAIL_VALIDATION': True})
|
||||
@mock.patch.dict(settings.FEATURES, {'BYPASS_ACTIVATION_EMAIL_FOR_EXTAUTH': False,
|
||||
'AUTOMATIC_AUTH_FOR_TESTING': False, 'SKIP_EMAIL_VALIDATION': True})
|
||||
def test_extauth_bypass_sending_activation_email_without_bypass_2(self):
|
||||
"""
|
||||
Tests user creation without sending activation email when
|
||||
@@ -395,8 +397,8 @@ class TestCreateAccount(SiteMixin, TestCase):
|
||||
instance = config.return_value
|
||||
instance.utm_cookie_name = utm_cookie_name
|
||||
|
||||
self.assertIsNone(self.client.cookies.get(settings.AFFILIATE_COOKIE_NAME)) # pylint: disable=no-member
|
||||
self.assertIsNone(self.client.cookies.get(utm_cookie_name)) # pylint: disable=no-member
|
||||
self.assertIsNone(self.client.cookies.get(settings.AFFILIATE_COOKIE_NAME))
|
||||
self.assertIsNone(self.client.cookies.get(utm_cookie_name))
|
||||
user = self.create_account_and_fetch_profile().user
|
||||
self.assertIsNone(UserAttribute.get_user_attribute(user, REGISTRATION_AFFILIATE_ID))
|
||||
self.assertIsNone(UserAttribute.get_user_attribute(user, REGISTRATION_UTM_PARAMETERS.get('utm_source')))
|
||||
@@ -449,7 +451,7 @@ class TestCreateAccount(SiteMixin, TestCase):
|
||||
UserAttribute.get_user_attribute(user, REGISTRATION_UTM_CREATED_AT)
|
||||
)
|
||||
|
||||
@patch("openedx.core.djangoapps.site_configuration.helpers.get_value", mock.Mock(return_value=False))
|
||||
@mock.patch("openedx.core.djangoapps.site_configuration.helpers.get_value", mock.Mock(return_value=False))
|
||||
def test_create_account_not_allowed(self):
|
||||
"""
|
||||
Test case to check user creation is forbidden when ALLOW_PUBLIC_ACCOUNT_CREATION feature flag is turned off
|
||||
@@ -457,6 +459,11 @@ class TestCreateAccount(SiteMixin, TestCase):
|
||||
response = self.client.get(self.url)
|
||||
self.assertEqual(response.status_code, 403)
|
||||
|
||||
def test_create_account_prevent_auth_user_writes(self):
|
||||
with waffle().override(PREVENT_AUTH_USER_WRITES, True):
|
||||
response = self.client.get(self.url)
|
||||
assert response.status_code == 403
|
||||
|
||||
def test_created_on_site_user_attribute_set(self):
|
||||
profile = self.create_account_and_fetch_profile(host=self.site.domain)
|
||||
self.assertEqual(UserAttribute.get_user_attribute(profile.user, 'created_on_site'), self.site.domain)
|
||||
@@ -883,7 +890,7 @@ class TestUnicodeUsername(TestCase):
|
||||
'honor_code': 'true',
|
||||
}
|
||||
|
||||
@patch.dict(settings.FEATURES, {'ENABLE_UNICODE_USERNAME': False})
|
||||
@mock.patch.dict(settings.FEATURES, {'ENABLE_UNICODE_USERNAME': False})
|
||||
def test_with_feature_disabled(self):
|
||||
"""
|
||||
Ensures backward-compatible defaults.
|
||||
@@ -897,14 +904,14 @@ class TestUnicodeUsername(TestCase):
|
||||
with self.assertRaises(User.DoesNotExist):
|
||||
User.objects.get(email=self.url_params['email'])
|
||||
|
||||
@patch.dict(settings.FEATURES, {'ENABLE_UNICODE_USERNAME': True})
|
||||
@mock.patch.dict(settings.FEATURES, {'ENABLE_UNICODE_USERNAME': True})
|
||||
def test_with_feature_enabled(self):
|
||||
response = self.client.post(self.url, self.url_params)
|
||||
self.assertEquals(response.status_code, 200)
|
||||
|
||||
self.assertTrue(User.objects.get(email=self.url_params['email']))
|
||||
|
||||
@patch.dict(settings.FEATURES, {'ENABLE_UNICODE_USERNAME': True})
|
||||
@mock.patch.dict(settings.FEATURES, {'ENABLE_UNICODE_USERNAME': True})
|
||||
def test_special_chars_with_feature_enabled(self):
|
||||
"""
|
||||
Ensures that special chars are still prevented.
|
||||
|
||||
@@ -2,14 +2,13 @@
|
||||
import json
|
||||
import unittest
|
||||
|
||||
import mock
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import User
|
||||
from django.core import mail
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.db import transaction
|
||||
from django.http import HttpResponse
|
||||
from django.test import override_settings, TestCase, TransactionTestCase
|
||||
from django.test import override_settings, TransactionTestCase
|
||||
from django.test.client import RequestFactory
|
||||
from mock import Mock, patch
|
||||
from six import text_type
|
||||
@@ -17,6 +16,7 @@ from six import text_type
|
||||
from edxmako.shortcuts import render_to_string
|
||||
from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers
|
||||
from openedx.core.djangoapps.theming.tests.test_util import with_comprehensive_theme
|
||||
from openedx.core.djangoapps.user_api.config.waffle import PREVENT_AUTH_USER_WRITES, SYSTEM_MAINTENANCE_MSG, waffle
|
||||
from openedx.core.djangolib.testing.utils import CacheIsolationTestCase, CacheIsolationMixin
|
||||
from student.models import PendingEmailChange, Registration, UserProfile
|
||||
from student.tests.factories import PendingEmailChangeFactory, RegistrationFactory, UserFactory
|
||||
@@ -145,7 +145,7 @@ class ActivationEmailTests(CacheIsolationTestCase):
|
||||
for fragment in body_fragments:
|
||||
self.assertIn(fragment, msg.body)
|
||||
|
||||
@mock.patch('student.tasks.log')
|
||||
@patch('student.tasks.log')
|
||||
def test_send_email_to_inactive_user(self, mock_log):
|
||||
"""
|
||||
Tests that when an inactive user logs-in using the social auth, system
|
||||
@@ -217,7 +217,7 @@ class ReactivationEmailTests(EmailTestMixin, CacheIsolationTestCase):
|
||||
self.assertReactivateEmailSent(email_user)
|
||||
self.assertFalse(response_data['success'])
|
||||
|
||||
def test_reactivation_for_unregistered_user(self, email_user):
|
||||
def test_reactivation_for_unregistered_user(self, email_user): # pylint: disable=unused-argument
|
||||
"""
|
||||
Test that trying to send a reactivation email to an unregistered
|
||||
user fails without throwing a 500 error.
|
||||
@@ -226,7 +226,7 @@ class ReactivationEmailTests(EmailTestMixin, CacheIsolationTestCase):
|
||||
|
||||
self.assertFalse(response_data['success'])
|
||||
|
||||
def test_reactivation_for_no_user_profile(self, email_user):
|
||||
def test_reactivation_for_no_user_profile(self, email_user): # pylint: disable=unused-argument
|
||||
"""
|
||||
Test that trying to send a reactivation email to a user without
|
||||
user profile fails without throwing 500 error.
|
||||
@@ -496,6 +496,14 @@ class EmailChangeConfirmationTests(EmailTestMixin, CacheIsolationMixin, Transact
|
||||
)
|
||||
self.assertEquals(0, PendingEmailChange.objects.count())
|
||||
|
||||
@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', "Test only valid in LMS")
|
||||
def test_prevent_auth_user_writes(self, email_user): # pylint: disable=unused-argument
|
||||
with waffle().override(PREVENT_AUTH_USER_WRITES, True):
|
||||
self.check_confirm_email_change('email_change_failed.html', {
|
||||
'err_msg': SYSTEM_MAINTENANCE_MSG
|
||||
})
|
||||
self.assertRolledBack()
|
||||
|
||||
@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', "Test only valid in LMS")
|
||||
@override_settings(MKTG_URLS={'ROOT': 'https://dummy-root', 'CONTACT': '/help/contact-us'})
|
||||
def test_marketing_contact_link(self, _email_user):
|
||||
|
||||
@@ -18,6 +18,7 @@ from six import text_type
|
||||
from social_django.models import UserSocialAuth
|
||||
|
||||
from openedx.core.djangoapps.external_auth.models import ExternalAuthMap
|
||||
from openedx.core.djangoapps.user_api.config.waffle import PREVENT_AUTH_USER_WRITES, waffle
|
||||
from openedx.core.djangolib.testing.utils import CacheIsolationTestCase
|
||||
from openedx.tests.util import expected_redirect_url
|
||||
from student.tests.factories import RegistrationFactory, UserFactory, UserProfileFactory
|
||||
@@ -62,13 +63,15 @@ class LoginTest(CacheIsolationTestCase):
|
||||
self.url = reverse('login')
|
||||
|
||||
def test_login_success(self):
|
||||
response, mock_audit_log = self._login_response('test@edx.org', 'test_password', patched_audit_log='student.models.AUDIT_LOG')
|
||||
response, mock_audit_log = self._login_response('test@edx.org', 'test_password',
|
||||
patched_audit_log='student.models.AUDIT_LOG')
|
||||
self._assert_response(response, success=True)
|
||||
self._assert_audit_log(mock_audit_log, 'info', [u'Login success', u'test@edx.org'])
|
||||
|
||||
@patch.dict("django.conf.settings.FEATURES", {'SQUELCH_PII_IN_LOGS': True})
|
||||
def test_login_success_no_pii(self):
|
||||
response, mock_audit_log = self._login_response('test@edx.org', 'test_password', patched_audit_log='student.models.AUDIT_LOG')
|
||||
response, mock_audit_log = self._login_response('test@edx.org', 'test_password',
|
||||
patched_audit_log='student.models.AUDIT_LOG')
|
||||
self._assert_response(response, success=True)
|
||||
self._assert_audit_log(mock_audit_log, 'info', [u'Login success'])
|
||||
self._assert_not_in_audit_log(mock_audit_log, 'info', [u'test@edx.org'])
|
||||
@@ -78,10 +81,24 @@ class LoginTest(CacheIsolationTestCase):
|
||||
self.user.email = unicode_email
|
||||
self.user.save()
|
||||
|
||||
response, mock_audit_log = self._login_response(unicode_email, 'test_password', patched_audit_log='student.models.AUDIT_LOG')
|
||||
response, mock_audit_log = self._login_response(unicode_email, 'test_password',
|
||||
patched_audit_log='student.models.AUDIT_LOG')
|
||||
self._assert_response(response, success=True)
|
||||
self._assert_audit_log(mock_audit_log, 'info', [u'Login success', unicode_email])
|
||||
|
||||
def test_last_login_updated(self):
|
||||
old_last_login = self.user.last_login
|
||||
self.test_login_success()
|
||||
self.user.refresh_from_db()
|
||||
assert self.user.last_login > old_last_login
|
||||
|
||||
def test_login_success_prevent_auth_user_writes(self):
|
||||
with waffle().override(PREVENT_AUTH_USER_WRITES, True):
|
||||
old_last_login = self.user.last_login
|
||||
self.test_login_success()
|
||||
self.user.refresh_from_db()
|
||||
assert old_last_login == self.user.last_login
|
||||
|
||||
def test_login_fail_no_user_exists(self):
|
||||
nonexistent_email = u'not_a_user@edx.org'
|
||||
response, mock_audit_log = self._login_response(
|
||||
@@ -126,7 +143,8 @@ class LoginTest(CacheIsolationTestCase):
|
||||
)
|
||||
self._assert_response(response, success=False,
|
||||
value='Email or password is incorrect')
|
||||
self._assert_audit_log(mock_audit_log, 'warning', [u'Login failed', u'password for', u'test@edx.org', u'invalid'])
|
||||
self._assert_audit_log(mock_audit_log, 'warning',
|
||||
[u'Login failed', u'password for', u'test@edx.org', u'invalid'])
|
||||
|
||||
@patch.dict("django.conf.settings.FEATURES", {'SQUELCH_PII_IN_LOGS': True})
|
||||
def test_login_fail_wrong_password_no_pii(self):
|
||||
@@ -190,7 +208,8 @@ class LoginTest(CacheIsolationTestCase):
|
||||
'student.views.login.AUDIT_LOG'
|
||||
)
|
||||
self._assert_response(response, success=False)
|
||||
self._assert_audit_log(mock_audit_log, 'warning', [u'Login failed', u'password for', u'test@edx.org', u'invalid'])
|
||||
self._assert_audit_log(mock_audit_log, 'warning',
|
||||
[u'Login failed', u'password for', u'test@edx.org', u'invalid'])
|
||||
|
||||
def test_logout_logging(self):
|
||||
response, _ = self._login_response('test@edx.org', 'test_password')
|
||||
@@ -585,7 +604,7 @@ class LoginOAuthTokenMixin(ThirdPartyOAuthTestMixin):
|
||||
self._setup_provider_response(success=True)
|
||||
response = self.client.post(self.url, {"access_token": "dummy"})
|
||||
self.assertEqual(response.status_code, 204)
|
||||
self.assertEqual(int(self.client.session['_auth_user_id']), self.user.id) # pylint: disable=no-member
|
||||
self.assertEqual(int(self.client.session['_auth_user_id']), self.user.id)
|
||||
|
||||
def test_invalid_token(self):
|
||||
self._setup_provider_response(success=False)
|
||||
|
||||
@@ -22,6 +22,7 @@ from provider.oauth2 import models as dop_models
|
||||
|
||||
from openedx.core.djangoapps.oauth_dispatch.tests import factories as dot_factories
|
||||
from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers
|
||||
from openedx.core.djangoapps.user_api.config.waffle import PREVENT_AUTH_USER_WRITES, SYSTEM_MAINTENANCE_MSG, waffle
|
||||
from openedx.core.djangolib.testing.utils import CacheIsolationTestCase
|
||||
from student.tests.factories import UserFactory
|
||||
from student.tests.test_email import mock_render_to_string
|
||||
@@ -281,6 +282,18 @@ class ResetPasswordTests(EventTestMixin, CacheIsolationTestCase):
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
self.assertFalse(User.objects.get(pk=self.user.pk).is_active)
|
||||
|
||||
def test_password_reset_prevent_auth_user_writes(self):
|
||||
with waffle().override(PREVENT_AUTH_USER_WRITES, True):
|
||||
url = reverse(
|
||||
"password_reset_confirm",
|
||||
kwargs={"uidb36": self.uidb36, "token": self.token}
|
||||
)
|
||||
for request in [self.request_factory.get(url), self.request_factory.post(url)]:
|
||||
response = password_reset_confirm_wrapper(request, self.uidb36, self.token)
|
||||
assert response.context_data['err_msg'] == SYSTEM_MAINTENANCE_MSG
|
||||
self.user.refresh_from_db()
|
||||
assert not self.user.is_active
|
||||
|
||||
@override_settings(PASSWORD_MIN_LENGTH=2)
|
||||
@override_settings(PASSWORD_MAX_LENGTH=10)
|
||||
@ddt.data(
|
||||
|
||||
@@ -66,7 +66,8 @@ from openedx.core.djangoapps.site_configuration import helpers as configuration_
|
||||
from openedx.core.djangoapps.theming import helpers as theming_helpers
|
||||
from openedx.core.djangoapps.user_api import accounts as accounts_settings
|
||||
from openedx.core.djangoapps.user_api.preferences import api as preferences_api
|
||||
from openedx.core.djangolib.markup import HTML
|
||||
from openedx.core.djangoapps.user_api.config.waffle import PREVENT_AUTH_USER_WRITES, SYSTEM_MAINTENANCE_MSG, waffle
|
||||
from openedx.core.djangolib.markup import HTML, Text
|
||||
from student.cookies import set_logged_in_cookies
|
||||
from student.forms import AccountCreationForm, PasswordResetFormNoActive, get_registration_extension_form
|
||||
from student.helpers import (
|
||||
@@ -112,7 +113,7 @@ AUDIT_LOG = logging.getLogger("audit")
|
||||
ReverifyInfo = namedtuple(
|
||||
'ReverifyInfo',
|
||||
'course_id course_name course_number date status display'
|
||||
) # pylint: disable=invalid-name
|
||||
)
|
||||
SETTING_CHANGE_INITIATED = 'edx.user.settings.change_initiated'
|
||||
# Used as the name of the user attribute for tracking affiliate registrations
|
||||
REGISTRATION_AFFILIATE_ID = 'registration_affiliate_id'
|
||||
@@ -731,7 +732,7 @@ def create_account_with_params(request, params):
|
||||
if hasattr(settings, 'LMS_SEGMENT_KEY') and settings.LMS_SEGMENT_KEY:
|
||||
tracking_context = tracker.get_tracker().resolve_context()
|
||||
identity_args = [
|
||||
user.id, # pylint: disable=no-member
|
||||
user.id,
|
||||
{
|
||||
'email': user.email,
|
||||
'username': user.username,
|
||||
@@ -957,6 +958,9 @@ def create_account(request, post_override=None):
|
||||
):
|
||||
return HttpResponseForbidden(_("Account creation not allowed."))
|
||||
|
||||
if waffle().is_enabled(PREVENT_AUTH_USER_WRITES):
|
||||
return HttpResponseForbidden(SYSTEM_MAINTENANCE_MSG)
|
||||
|
||||
warnings.warn("Please use RegistrationView instead.", DeprecationWarning)
|
||||
|
||||
try:
|
||||
@@ -1014,7 +1018,26 @@ def activate_account(request, key):
|
||||
extra_tags='account-activation aa-icon'
|
||||
)
|
||||
else:
|
||||
if not registration.user.is_active:
|
||||
if registration.user.is_active:
|
||||
messages.info(
|
||||
request,
|
||||
HTML(_('{html_start}This account has already been activated.{html_end}')).format(
|
||||
html_start=HTML('<p class="message-title">'),
|
||||
html_end=HTML('</p>'),
|
||||
),
|
||||
extra_tags='account-activation aa-icon',
|
||||
)
|
||||
elif waffle().is_enabled(PREVENT_AUTH_USER_WRITES):
|
||||
messages.error(
|
||||
request,
|
||||
HTML(u'{html_start}{message}{html_end}').format(
|
||||
message=Text(SYSTEM_MAINTENANCE_MSG),
|
||||
html_start=HTML('<p class="message-title">'),
|
||||
html_end=HTML('</p>'),
|
||||
),
|
||||
extra_tags='account-activation aa-icon',
|
||||
)
|
||||
else:
|
||||
registration.activate()
|
||||
# Success message for logged in users.
|
||||
message = _('{html_start}Success{html_end} You have activated your account.')
|
||||
@@ -1036,15 +1059,6 @@ def activate_account(request, key):
|
||||
),
|
||||
extra_tags='account-activation aa-icon',
|
||||
)
|
||||
else:
|
||||
messages.info(
|
||||
request,
|
||||
HTML(_('{html_start}This account has already been activated.{html_end}')).format(
|
||||
html_start=HTML('<p class="message-title">'),
|
||||
html_end=HTML('</p>'),
|
||||
),
|
||||
extra_tags='account-activation aa-icon',
|
||||
)
|
||||
|
||||
# Enroll student in any pending courses he/she may have if auto_enroll flag is set
|
||||
_enroll_user_in_pending_courses(registration.user)
|
||||
@@ -1068,6 +1082,9 @@ def activate_account_studio(request, key):
|
||||
user_logged_in = request.user.is_authenticated()
|
||||
already_active = True
|
||||
if not registration.user.is_active:
|
||||
if waffle().is_enabled(PREVENT_AUTH_USER_WRITES):
|
||||
return render_to_response('registration/activation_invalid.html',
|
||||
{'csrf': csrf(request)['csrf_token']})
|
||||
registration.activate()
|
||||
already_active = False
|
||||
|
||||
@@ -1184,8 +1201,6 @@ def validate_password_security_policy(user, password):
|
||||
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']
|
||||
# Because of how ngettext is, splitting the following into shorter lines would be ugly.
|
||||
# pylint: disable=line-too-long
|
||||
err_msg = ungettext(
|
||||
"You are re-using a password that you have used recently. "
|
||||
"You must have {num} distinct password before reusing a previous password.",
|
||||
@@ -1197,8 +1212,6 @@ def validate_password_security_policy(user, password):
|
||||
# also, check to see if passwords are getting reset too frequent
|
||||
if PasswordHistory.is_password_reset_too_soon(user):
|
||||
num_days = settings.ADVANCED_SECURITY_CONFIG['MIN_TIME_IN_DAYS_BETWEEN_ALLOWED_RESETS']
|
||||
# Because of how ngettext is, splitting the following into shorter lines would be ugly.
|
||||
# pylint: disable=line-too-long
|
||||
err_msg = ungettext(
|
||||
"You are resetting passwords too frequently. Due to security policies, "
|
||||
"{num} day must elapse between password resets.",
|
||||
@@ -1231,6 +1244,18 @@ def password_reset_confirm_wrapper(request, uidb36=None, token=None):
|
||||
request, uidb64=uidb64, token=token, extra_context=platform_name
|
||||
)
|
||||
|
||||
if waffle().is_enabled(PREVENT_AUTH_USER_WRITES):
|
||||
context = {
|
||||
'validlink': False,
|
||||
'form': None,
|
||||
'title': _('Password reset unsuccessful'),
|
||||
'err_msg': SYSTEM_MAINTENANCE_MSG,
|
||||
}
|
||||
context.update(platform_name)
|
||||
return TemplateResponse(
|
||||
request, 'registration/password_reset_confirm.html', context
|
||||
)
|
||||
|
||||
if request.method == 'POST':
|
||||
password = request.POST['new_password1']
|
||||
valid_link = False
|
||||
@@ -1354,7 +1379,7 @@ def do_email_change_request(user, new_email, activation_key=None):
|
||||
)
|
||||
try:
|
||||
mail.send_mail(subject, message, from_address, [pec.new_email])
|
||||
except Exception: # pylint: disable=broad-except
|
||||
except Exception:
|
||||
log.error(u'Unable to send email activation link to user from "%s"', from_address, exc_info=True)
|
||||
raise ValueError(_('Unable to send email activation link. Please try again later.'))
|
||||
|
||||
@@ -1378,6 +1403,9 @@ def confirm_email_change(request, key): # pylint: disable=unused-argument
|
||||
User requested a new e-mail. This is called when the activation
|
||||
link is clicked. We confirm with the old e-mail, and update
|
||||
"""
|
||||
if waffle().is_enabled(PREVENT_AUTH_USER_WRITES):
|
||||
return render_to_response('email_change_failed.html', {'err_msg': SYSTEM_MAINTENANCE_MSG})
|
||||
|
||||
with transaction.atomic():
|
||||
try:
|
||||
pec = PendingEmailChange.objects.get(activation_key=key)
|
||||
|
||||
@@ -2116,7 +2116,7 @@ INSTALLED_APPS = [
|
||||
|
||||
# Our courseware
|
||||
'courseware',
|
||||
'student',
|
||||
'student.apps.StudentConfig',
|
||||
|
||||
'static_template_view',
|
||||
'staticbook',
|
||||
|
||||
@@ -8,7 +8,11 @@
|
||||
<h1 class="invalid">${_("E-mail change failed")}</h1>
|
||||
<hr class="horizontal-divider">
|
||||
|
||||
% if err_msg is not UNDEFINED:
|
||||
<p>${err_msg}</p>
|
||||
% else:
|
||||
<p>${_("We were unable to send a confirmation email to {email}").format(email=email)}</p>
|
||||
% endif
|
||||
|
||||
<p>${_('Go back to the {link_start}home page{link_end}.').format(link_start='<a href="/">', link_end='</a>')}</p>
|
||||
</section>
|
||||
|
||||
@@ -11,8 +11,6 @@ from django.core.exceptions import ObjectDoesNotExist
|
||||
from django.conf import settings
|
||||
from django.core.validators import validate_email, ValidationError
|
||||
from django.http import HttpResponseForbidden
|
||||
from openedx.core.djangoapps.user_api.preferences.api import update_user_preferences
|
||||
from openedx.core.djangoapps.user_api.errors import PreferenceValidationError, AccountValidationError
|
||||
from six import text_type
|
||||
|
||||
from student.models import User, UserProfile, Registration
|
||||
@@ -20,15 +18,21 @@ from student import forms as student_forms
|
||||
from student import views as student_views
|
||||
from util.model_utils import emit_setting_changed_event
|
||||
|
||||
from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers
|
||||
from openedx.core.djangoapps.user_api import errors, accounts, forms, helpers
|
||||
from openedx.core.djangoapps.user_api.config.waffle import PREVENT_AUTH_USER_WRITES, SYSTEM_MAINTENANCE_MSG, waffle
|
||||
from openedx.core.djangoapps.user_api.errors import (
|
||||
AccountUpdateError,
|
||||
AccountValidationError,
|
||||
PreferenceValidationError,
|
||||
)
|
||||
from openedx.core.djangoapps.user_api.preferences.api import update_user_preferences
|
||||
from openedx.core.lib.api.view_utils import add_serializer_errors
|
||||
|
||||
from .serializers import (
|
||||
AccountLegacyProfileSerializer, AccountUserSerializer,
|
||||
UserReadOnlySerializer, _visible_fields # pylint: disable=invalid-name
|
||||
)
|
||||
from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers
|
||||
from openedx.core.djangoapps.user_api import errors, accounts, forms, helpers
|
||||
|
||||
|
||||
# Public access point for this function.
|
||||
visible_fields = _visible_fields
|
||||
@@ -242,21 +246,21 @@ def update_account_settings(requesting_user, update, username=None):
|
||||
|
||||
except PreferenceValidationError as err:
|
||||
raise AccountValidationError(err.preference_errors)
|
||||
except AccountValidationError as err:
|
||||
except (AccountUpdateError, AccountValidationError) as err:
|
||||
raise err
|
||||
except Exception as err:
|
||||
raise errors.AccountUpdateError(
|
||||
raise AccountUpdateError(
|
||||
u"Error thrown when saving account updates: '{}'".format(text_type(err))
|
||||
)
|
||||
|
||||
# And try to send the email change request if necessary.
|
||||
if changing_email:
|
||||
if not settings.FEATURES['ALLOW_EMAIL_ADDRESS_CHANGE']:
|
||||
raise errors.AccountUpdateError(u"Email address changes have been disabled by the site operators.")
|
||||
raise AccountUpdateError(u"Email address changes have been disabled by the site operators.")
|
||||
try:
|
||||
student_views.do_email_change_request(existing_user, new_email)
|
||||
except ValueError as err:
|
||||
raise errors.AccountUpdateError(
|
||||
raise AccountUpdateError(
|
||||
u"Error thrown from do_email_change_request: '{}'".format(text_type(err)),
|
||||
user_message=text_type(err)
|
||||
)
|
||||
@@ -310,6 +314,9 @@ def create_account(username, password, email):
|
||||
):
|
||||
return HttpResponseForbidden(_("Account creation not allowed."))
|
||||
|
||||
if waffle().is_enabled(PREVENT_AUTH_USER_WRITES):
|
||||
raise errors.UserAPIInternalError(SYSTEM_MAINTENANCE_MSG)
|
||||
|
||||
# Validate the username, password, and email
|
||||
# This will raise an exception if any of these are not in a valid format.
|
||||
_validate_username(username)
|
||||
@@ -383,6 +390,8 @@ def activate_account(activation_key):
|
||||
errors.UserAPIInternalError: the operation failed due to an unexpected error.
|
||||
|
||||
"""
|
||||
if waffle().is_enabled(PREVENT_AUTH_USER_WRITES):
|
||||
raise errors.UserAPIInternalError(SYSTEM_MAINTENANCE_MSG)
|
||||
try:
|
||||
registration = Registration.objects.get(activation_key=activation_key)
|
||||
except Registration.DoesNotExist:
|
||||
|
||||
@@ -33,6 +33,7 @@ from openedx.core.djangoapps.user_api.accounts.tests.testutils import (
|
||||
INVALID_USERNAMES,
|
||||
VALID_USERNAMES_UNICODE
|
||||
)
|
||||
from openedx.core.djangoapps.user_api.config.waffle import PREVENT_AUTH_USER_WRITES, SYSTEM_MAINTENANCE_MSG, waffle
|
||||
from openedx.core.djangoapps.user_api.errors import (
|
||||
AccountEmailInvalid,
|
||||
AccountPasswordInvalid,
|
||||
@@ -41,6 +42,7 @@ from openedx.core.djangoapps.user_api.errors import (
|
||||
AccountUserAlreadyExists,
|
||||
AccountUsernameInvalid,
|
||||
AccountValidationError,
|
||||
UserAPIInternalError,
|
||||
UserNotAuthorized,
|
||||
UserNotFound
|
||||
)
|
||||
@@ -408,10 +410,21 @@ class AccountCreationActivationAndPasswordChangeTest(TestCase):
|
||||
def test_create_account_invalid_username(self, invalid_username):
|
||||
create_account(invalid_username, self.PASSWORD, self.EMAIL)
|
||||
|
||||
def test_create_account_prevent_auth_user_writes(self):
|
||||
with pytest.raises(UserAPIInternalError, message=SYSTEM_MAINTENANCE_MSG):
|
||||
with waffle().override(PREVENT_AUTH_USER_WRITES, True):
|
||||
create_account(self.USERNAME, self.PASSWORD, self.EMAIL)
|
||||
|
||||
@raises(UserNotAuthorized)
|
||||
def test_activate_account_invalid_key(self):
|
||||
activate_account(u'invalid')
|
||||
|
||||
def test_activate_account_prevent_auth_user_writes(self):
|
||||
activation_key = create_account(self.USERNAME, self.PASSWORD, self.EMAIL)
|
||||
with pytest.raises(UserAPIInternalError, message=SYSTEM_MAINTENANCE_MSG):
|
||||
with waffle().override(PREVENT_AUTH_USER_WRITES, True):
|
||||
activate_account(activation_key)
|
||||
|
||||
@skip_unless_lms
|
||||
def test_request_password_change(self):
|
||||
# Create and activate an account
|
||||
|
||||
0
openedx/core/djangoapps/user_api/config/__init__.py
Normal file
0
openedx/core/djangoapps/user_api/config/__init__.py
Normal file
21
openedx/core/djangoapps/user_api/config/waffle.py
Normal file
21
openedx/core/djangoapps/user_api/config/waffle.py
Normal file
@@ -0,0 +1,21 @@
|
||||
"""
|
||||
Waffle flags and switches to change user API functionality.
|
||||
"""
|
||||
from __future__ import absolute_import
|
||||
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from openedx.core.djangoapps.waffle_utils import WaffleSwitchNamespace
|
||||
|
||||
SYSTEM_MAINTENANCE_MSG = _(u'System maintenance in progress. Please try again later.')
|
||||
WAFFLE_NAMESPACE = u'user_api'
|
||||
|
||||
# Switches
|
||||
PREVENT_AUTH_USER_WRITES = u'prevent_auth_user_writes'
|
||||
|
||||
|
||||
def waffle():
|
||||
"""
|
||||
Returns the namespaced, cached, audited Waffle class for user_api.
|
||||
"""
|
||||
return WaffleSwitchNamespace(name=WAFFLE_NAMESPACE, log_prefix=u'UserAPI: ')
|
||||
Reference in New Issue
Block a user