Commit c867be79 by Diana Huang

Limit the rate of logins.

parent a628b62d
...@@ -5,6 +5,8 @@ These are notable changes in edx-platform. This is a rolling list of changes, ...@@ -5,6 +5,8 @@ These are notable changes in edx-platform. This is a rolling list of changes,
in roughly chronological order, most recent first. Add your entries at or near in roughly chronological order, most recent first. Add your entries at or near
the top. Include a label indicating the component affected. the top. Include a label indicating the component affected.
Common: Added ratelimiting to our authentication backend.
Common: Add additional logging to cover login attempts and logouts. Common: Add additional logging to cover login attempts and logouts.
Studio: Send e-mails to new Studio users (on edge only) when their course creator Studio: Send e-mails to new Studio users (on edge only) when their course creator
......
from django.test.client import Client from django.test.client import Client
from django.core.cache import cache
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from .utils import parse_json, user, registration from .utils import parse_json, user, registration
...@@ -79,6 +80,8 @@ class AuthTestCase(ContentStoreTestCase): ...@@ -79,6 +80,8 @@ class AuthTestCase(ContentStoreTestCase):
self.pw = 'xyz' self.pw = 'xyz'
self.username = 'testuser' self.username = 'testuser'
self.client = Client() self.client = Client()
# clear the cache so ratelimiting won't affect these tests
cache.clear()
def check_page_get(self, url, expected): def check_page_get(self, url, expected):
resp = self.client.get(url) resp = self.client.get(url)
...@@ -119,6 +122,18 @@ class AuthTestCase(ContentStoreTestCase): ...@@ -119,6 +122,18 @@ class AuthTestCase(ContentStoreTestCase):
# Now login should work # Now login should work
self.login(self.email, self.pw) self.login(self.email, self.pw)
def test_login_ratelimited(self):
# try logging in 30 times, the default limit in the number of failed
# login attempts in one 5 minute period before the rate gets limited
for i in xrange(30):
resp = self._login(self.email, 'wrong_password{0}'.format(i))
self.assertEqual(resp.status_code, 200)
resp = self._login(self.email, 'wrong_password')
self.assertEqual(resp.status_code, 200)
data = parse_json(resp)
self.assertFalse(data['success'])
self.assertIn('Too many failed login attempts.', data['value'])
def test_login_link_on_activation_age(self): def test_login_link_on_activation_age(self):
self.create_account(self.username, self.email, self.pw) self.create_account(self.username, self.email, self.pw)
# we want to test the rendering of the activation page when the user isn't logged in # we want to test the rendering of the activation page when the user isn't logged in
......
...@@ -5,7 +5,7 @@ django admin page for the course creators table ...@@ -5,7 +5,7 @@ django admin page for the course creators table
from course_creators.models import CourseCreator, update_creator_state from course_creators.models import CourseCreator, update_creator_state
from course_creators.views import update_course_creator_group from course_creators.views import update_course_creator_group
from django.contrib import admin from ratelimitbackend import admin
from django.conf import settings from django.conf import settings
from django.dispatch import receiver from django.dispatch import receiver
from mitxmako.shortcuts import render_to_string from mitxmako.shortcuts import render_to_string
......
...@@ -43,14 +43,14 @@ class CourseCreatorAdminTest(TestCase): ...@@ -43,14 +43,14 @@ class CourseCreatorAdminTest(TestCase):
""" """
Tests that updates to state impact the creator group maintained in authz.py and that e-mails are sent. Tests that updates to state impact the creator group maintained in authz.py and that e-mails are sent.
""" """
STUDIO_REQUEST_EMAIL = 'mark@marky.mark' STUDIO_REQUEST_EMAIL = 'mark@marky.mark'
def change_state(state, is_creator): def change_state(state, is_creator):
""" Helper method for changing state """ """ Helper method for changing state """
self.table_entry.state = state self.table_entry.state = state
self.creator_admin.save_model(self.request, self.table_entry, None, True) self.creator_admin.save_model(self.request, self.table_entry, None, True)
self.assertEqual(is_creator, is_user_in_creator_group(self.user)) self.assertEqual(is_creator, is_user_in_creator_group(self.user))
context = {'studio_request_email': STUDIO_REQUEST_EMAIL} context = {'studio_request_email': STUDIO_REQUEST_EMAIL}
if state == CourseCreator.GRANTED: if state == CourseCreator.GRANTED:
template = 'emails/course_creator_granted.txt' template = 'emails/course_creator_granted.txt'
...@@ -69,7 +69,8 @@ class CourseCreatorAdminTest(TestCase): ...@@ -69,7 +69,8 @@ class CourseCreatorAdminTest(TestCase):
{ {
"ENABLE_CREATOR_GROUP": True, "ENABLE_CREATOR_GROUP": True,
"STUDIO_REQUEST_EMAIL": STUDIO_REQUEST_EMAIL "STUDIO_REQUEST_EMAIL": STUDIO_REQUEST_EMAIL
}): }
):
# User is initially unrequested. # User is initially unrequested.
self.assertFalse(is_user_in_creator_group(self.user)) self.assertFalse(is_user_in_creator_group(self.user))
...@@ -106,3 +107,18 @@ class CourseCreatorAdminTest(TestCase): ...@@ -106,3 +107,18 @@ class CourseCreatorAdminTest(TestCase):
self.request.user = self.user self.request.user = self.user
self.assertFalse(self.creator_admin.has_change_permission(self.request)) self.assertFalse(self.creator_admin.has_change_permission(self.request))
def test_rate_limit_login(self):
with mock.patch.dict('django.conf.settings.MITX_FEATURES', {'ENABLE_CREATOR_GROUP': True}):
post_params = {'username': self.user.username, 'password': 'wrong_password'}
# try logging in 30 times, the default limit in the number of failed
# login attempts in one 5 minute period before the rate gets limited
for _ in xrange(30):
response = self.client.post('/admin/', post_params)
self.assertEquals(response.status_code, 200)
response = self.client.post('/admin/', post_params)
# Since we are using the default rate limit behavior, we are
# expecting this to return a 403 error to indicate that there have
# been too many attempts
self.assertEquals(response.status_code, 403)
...@@ -108,6 +108,11 @@ TEMPLATE_CONTEXT_PROCESSORS = ( ...@@ -108,6 +108,11 @@ TEMPLATE_CONTEXT_PROCESSORS = (
'django.core.context_processors.csrf' 'django.core.context_processors.csrf'
) )
# use the ratelimit backend to prevent brute force attacks
AUTHENTICATION_BACKENDS = (
'ratelimitbackend.backends.RateLimitModelBackend',
)
LMS_BASE = None LMS_BASE = None
#################### CAPA External Code Evaluation ############################# #################### CAPA External Code Evaluation #############################
...@@ -152,7 +157,10 @@ MIDDLEWARE_CLASSES = ( ...@@ -152,7 +157,10 @@ MIDDLEWARE_CLASSES = (
# Detects user-requested locale from 'accept-language' header in http request # Detects user-requested locale from 'accept-language' header in http request
'django.middleware.locale.LocaleMiddleware', 'django.middleware.locale.LocaleMiddleware',
'django.middleware.transaction.TransactionMiddleware' 'django.middleware.transaction.TransactionMiddleware',
# catches any uncaught RateLimitExceptions and returns a 403 instead of a 500
'ratelimitbackend.middleware.RateLimitMiddleware',
) )
############################ SIGNAL HANDLERS ################################ ############################ SIGNAL HANDLERS ################################
...@@ -188,8 +196,8 @@ STATICFILES_DIRS = [ ...@@ -188,8 +196,8 @@ STATICFILES_DIRS = [
COMMON_ROOT / "static", COMMON_ROOT / "static",
PROJECT_ROOT / "static", PROJECT_ROOT / "static",
# This is how you would use the textbook images locally # This is how you would use the textbook images locally
# ("book", ENV_ROOT / "book_images") # ("book", ENV_ROOT / "book_images")
] ]
# Locale/Internationalization # Locale/Internationalization
......
...@@ -15,6 +15,7 @@ sessions. Assumes structure: ...@@ -15,6 +15,7 @@ sessions. Assumes structure:
from .common import * from .common import *
import os import os
from path import path from path import path
from warnings import filterwarnings
# Nose Test Runner # Nose Test Runner
INSTALLED_APPS += ('django_nose',) INSTALLED_APPS += ('django_nose',)
...@@ -124,6 +125,9 @@ CACHES = { ...@@ -124,6 +125,9 @@ CACHES = {
} }
} }
# hide ratelimit warnings while running tests
filterwarnings('ignore', message='No request passed to the backend, unable to rate-limit')
################################# CELERY ###################################### ################################# CELERY ######################################
CELERY_ALWAYS_EAGER = True CELERY_ALWAYS_EAGER = True
......
...@@ -6,7 +6,7 @@ from django.conf.urls import patterns, include, url ...@@ -6,7 +6,7 @@ from django.conf.urls import patterns, include, url
from . import one_time_startup from . import one_time_startup
# There is a course creators admin table. # There is a course creators admin table.
from django.contrib import admin from ratelimitbackend import admin
admin.autodiscover() admin.autodiscover()
urlpatterns = ('', # nopep8 urlpatterns = ('', # nopep8
......
...@@ -3,7 +3,7 @@ django admin pages for courseware model ...@@ -3,7 +3,7 @@ django admin pages for courseware model
''' '''
from external_auth.models import * from external_auth.models import *
from django.contrib import admin from ratelimitbackend import admin
class ExternalAuthMapAdmin(admin.ModelAdmin): class ExternalAuthMapAdmin(admin.ModelAdmin):
......
...@@ -9,12 +9,15 @@ from urlparse import parse_qs ...@@ -9,12 +9,15 @@ from urlparse import parse_qs
from django.conf import settings from django.conf import settings
from django.test import TestCase, LiveServerTestCase from django.test import TestCase, LiveServerTestCase
from django.core.cache import cache
from django.test.utils import override_settings from django.test.utils import override_settings
# from django.contrib.auth.models import User
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.test.client import RequestFactory from django.test.client import RequestFactory
from unittest import skipUnless from unittest import skipUnless
from student.tests.factories import UserFactory
from external_auth.views import provider_login
class MyFetcher(HTTPFetcher): class MyFetcher(HTTPFetcher):
"""A fetcher that uses server-internal calls for performing HTTP """A fetcher that uses server-internal calls for performing HTTP
...@@ -199,6 +202,49 @@ class OpenIdProviderTest(TestCase): ...@@ -199,6 +202,49 @@ class OpenIdProviderTest(TestCase):
""" Test for 403 error code when the url""" """ Test for 403 error code when the url"""
self.attempt_login(403, return_to="http://apps.cs50.edx.or") self.attempt_login(403, return_to="http://apps.cs50.edx.or")
def _send_bad_redirection_login(self):
"""
Attempt to log in to the provider with setup parameters
Intentionally fail the login to force a redirect
"""
user = UserFactory()
factory = RequestFactory()
post_params = {'email': user.email, 'password': 'password'}
fake_url = 'fake url'
request = factory.post(reverse('openid-provider-login'), post_params)
openid_setup = {
'request': factory.request(),
'url': fake_url
}
request.session = {
'openid_setup': openid_setup
}
response = provider_login(request)
return response
@skipUnless(settings.MITX_FEATURES.get('AUTH_USE_OPENID') or
settings.MITX_FEATURES.get('AUTH_USE_OPENID_PROVIDER'), True)
def test_login_openid_handle_redirection(self):
""" Test to see that we can handle login redirection properly"""
response = self._send_bad_redirection_login()
self.assertEquals(response.status_code, 302)
@skipUnless(settings.MITX_FEATURES.get('AUTH_USE_OPENID') or
settings.MITX_FEATURES.get('AUTH_USE_OPENID_PROVIDER'), True)
def test_login_openid_handle_redirection_ratelimited(self):
# try logging in 30 times, the default limit in the number of failed
# log in attempts before the rate gets limited
for _ in xrange(30):
self._send_bad_redirection_login()
response = self._send_bad_redirection_login()
# verify that we are not returning the default 403
self.assertEquals(response.status_code, 302)
# clear the ratelimit cache so that we don't fail other logins
cache.clear()
class OpenIdProviderLiveServerTest(LiveServerTestCase): class OpenIdProviderLiveServerTest(LiveServerTestCase):
""" """
......
...@@ -39,6 +39,7 @@ from openid.consumer.consumer import SUCCESS ...@@ -39,6 +39,7 @@ from openid.consumer.consumer import SUCCESS
from openid.server.server import Server, ProtocolError, UntrustedReturnURL from openid.server.server import Server, ProtocolError, UntrustedReturnURL
from openid.server.trustroot import TrustRoot from openid.server.trustroot import TrustRoot
from openid.extensions import ax, sreg from openid.extensions import ax, sreg
from ratelimitbackend.exceptions import RateLimitException
import student.views as student_views import student.views as student_views
# Required for Pearson # Required for Pearson
...@@ -191,7 +192,7 @@ def _external_login_or_signup(request, ...@@ -191,7 +192,7 @@ def _external_login_or_signup(request,
user.backend = auth_backend user.backend = auth_backend
AUDIT_LOG.info('Linked user "%s" logged in via Shibboleth', user.email) AUDIT_LOG.info('Linked user "%s" logged in via Shibboleth', user.email)
else: else:
user = authenticate(username=uname, password=eamap.internal_password) user = authenticate(username=uname, password=eamap.internal_password, request=request)
if user is None: if user is None:
# we want to log the failure, but don't want to log the password attempted: # we want to log the failure, but don't want to log the password attempted:
AUDIT_LOG.warning('External Auth Login failed for "%s"', uname) AUDIT_LOG.warning('External Auth Login failed for "%s"', uname)
...@@ -718,7 +719,12 @@ def provider_login(request): ...@@ -718,7 +719,12 @@ def provider_login(request):
# Failure is again redirected to the login dialog. # Failure is again redirected to the login dialog.
username = user.username username = user.username
password = request.POST.get('password', None) password = request.POST.get('password', None)
user = authenticate(username=username, password=password) try:
user = authenticate(username=username, password=password, request=request)
except RateLimitException:
AUDIT_LOG.warning('OpenID - Too many failed login attempts.')
return HttpResponseRedirect(openid_request_url)
if user is None: if user is None:
request.session['openid_error'] = True request.session['openid_error'] = True
msg = "OpenID login failed - password for %s is invalid" msg = "OpenID login failed - password for %s is invalid"
......
...@@ -4,7 +4,7 @@ django admin pages for courseware model ...@@ -4,7 +4,7 @@ django admin pages for courseware model
from student.models import UserProfile, UserTestGroup, CourseEnrollmentAllowed from student.models import UserProfile, UserTestGroup, CourseEnrollmentAllowed
from student.models import CourseEnrollment, Registration, PendingNameChange from student.models import CourseEnrollment, Registration, PendingNameChange
from django.contrib import admin from ratelimitbackend import admin
admin.site.register(UserProfile) admin.site.register(UserProfile)
......
...@@ -6,6 +6,7 @@ from mock import patch ...@@ -6,6 +6,7 @@ from mock import patch
from django.test import TestCase from django.test import TestCase
from django.test.client import Client from django.test.client import Client
from django.core.cache import cache
from django.core.urlresolvers import reverse, NoReverseMatch from django.core.urlresolvers import reverse, NoReverseMatch
from student.tests.factories import UserFactory, RegistrationFactory, UserProfileFactory from student.tests.factories import UserFactory, RegistrationFactory, UserProfileFactory
...@@ -29,6 +30,7 @@ class LoginTest(TestCase): ...@@ -29,6 +30,7 @@ class LoginTest(TestCase):
# Create the test client # Create the test client
self.client = Client() self.client = Client()
cache.clear()
# Store the login url # Store the login url
try: try:
...@@ -95,6 +97,27 @@ class LoginTest(TestCase): ...@@ -95,6 +97,27 @@ class LoginTest(TestCase):
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self._assert_audit_log(mock_audit_log, 'info', [u'Logout', u'test']) self._assert_audit_log(mock_audit_log, 'info', [u'Logout', u'test'])
def test_login_ratelimited_success(self):
# Try (and fail) logging in with fewer attempts than the limit of 30
# and verify that you can still successfully log in afterwards.
for i in xrange(20):
password = u'test_password{0}'.format(i)
response, _audit_log = self._login_response('test@edx.org', password)
self._assert_response(response, success=False)
# now try logging in with a valid password
response, _audit_log = self._login_response('test@edx.org', 'test_password')
self._assert_response(response, success=True)
def test_login_ratelimited(self):
# try logging in 30 times, the default limit in the number of failed
# login attempts in one 5 minute period before the rate gets limited
for i in xrange(30):
password = u'test_password{0}'.format(i)
self._login_response('test@edx.org', password)
# check to see if this response indicates that this was ratelimited
response, _audit_log = self._login_response('test@edx.org', 'wrong_password')
self._assert_response(response, success=False, value='Too many failed login attempts')
def _login_response(self, email, password, patched_audit_log='student.views.AUDIT_LOG'): def _login_response(self, email, password, patched_audit_log='student.views.AUDIT_LOG'):
''' Post the login info ''' ''' Post the login info '''
post_params = {'email': email, 'password': password} post_params = {'email': email, 'password': password}
......
...@@ -28,6 +28,8 @@ from django.utils.http import cookie_date ...@@ -28,6 +28,8 @@ from django.utils.http import cookie_date
from django.utils.http import base36_to_int from django.utils.http import base36_to_int
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from ratelimitbackend.exceptions import RateLimitException
from mitxmako.shortcuts import render_to_response, render_to_string from mitxmako.shortcuts import render_to_response, render_to_string
from bs4 import BeautifulSoup from bs4 import BeautifulSoup
...@@ -421,13 +423,23 @@ def login_user(request, error=""): ...@@ -421,13 +423,23 @@ def login_user(request, error=""):
user = User.objects.get(email=email) user = User.objects.get(email=email)
except User.DoesNotExist: except User.DoesNotExist:
AUDIT_LOG.warning(u"Login failed - Unknown user email: {0}".format(email)) AUDIT_LOG.warning(u"Login failed - Unknown user email: {0}".format(email))
return HttpResponse(json.dumps({'success': False, user = None
'value': _('Email or password is incorrect.')})) # TODO: User error message
username = user.username # if the user doesn't exist, we want to set the username to an invalid
user = authenticate(username=username, password=password) # username so that authentication is guaranteed to fail and we can take
# advantage of the ratelimited backend
username = user.username if user else ""
try:
user = authenticate(username=username, password=password, request=request)
# this occurs when there are too many attempts from the same IP address
except RateLimitException:
return HttpResponse(json.dumps({'success': False,
'value': _('Too many failed login attempts. Try again later.')}))
if user is None: if user is None:
AUDIT_LOG.warning(u"Login failed - password for {0} is invalid".format(email)) # if we didn't find this username earlier, the account for this email
# doesn't exist, and doesn't have a corresponding password
if username != "":
AUDIT_LOG.warning(u"Login failed - password for {0} is invalid".format(email))
return HttpResponse(json.dumps({'success': False, return HttpResponse(json.dumps({'success': False,
'value': _('Email or password is incorrect.')})) 'value': _('Email or password is incorrect.')}))
...@@ -942,7 +954,7 @@ def auto_auth(request): ...@@ -942,7 +954,7 @@ def auto_auth(request):
# if they already are a user, log in # if they already are a user, log in
try: try:
user = User.objects.get(username=username) user = User.objects.get(username=username)
user = authenticate(username=username, password=password) user = authenticate(username=username, password=password, request=request)
login(request, user) login(request, user)
# else create and activate account info # else create and activate account info
......
...@@ -3,6 +3,6 @@ django admin pages for courseware model ...@@ -3,6 +3,6 @@ django admin pages for courseware model
''' '''
from track.models import TrackingLog from track.models import TrackingLog
from django.contrib import admin from ratelimitbackend import admin
admin.site.register(TrackingLog) admin.site.register(TrackingLog)
...@@ -3,7 +3,7 @@ django admin pages for courseware model ...@@ -3,7 +3,7 @@ django admin pages for courseware model
''' '''
from courseware.models import StudentModule, OfflineComputedGrade, OfflineComputedGradeLog from courseware.models import StudentModule, OfflineComputedGrade, OfflineComputedGradeLog
from django.contrib import admin from ratelimitbackend import admin
from django.contrib.auth.models import User from django.contrib.auth.models import User
admin.site.register(StudentModule) admin.site.register(StudentModule)
......
...@@ -236,6 +236,10 @@ TEMPLATE_CONTEXT_PROCESSORS = ( ...@@ -236,6 +236,10 @@ TEMPLATE_CONTEXT_PROCESSORS = (
'mitxmako.shortcuts.marketing_link_context_processor', 'mitxmako.shortcuts.marketing_link_context_processor',
) )
# use the ratelimit backend to prevent brute force attacks
AUTHENTICATION_BACKENDS = (
'ratelimitbackend.backends.RateLimitModelBackend',
)
STUDENT_FILEUPLOAD_MAX_SIZE = 4 * 1000 * 1000 # 4 MB STUDENT_FILEUPLOAD_MAX_SIZE = 4 * 1000 * 1000 # 4 MB
MAX_FILEUPLOADS_PER_INPUT = 20 MAX_FILEUPLOADS_PER_INPUT = 20
...@@ -434,10 +438,10 @@ OPEN_ENDED_GRADING_INTERFACE = { ...@@ -434,10 +438,10 @@ OPEN_ENDED_GRADING_INTERFACE = {
'url': 'http://sandbox-grader-001.m.edx.org/peer_grading', 'url': 'http://sandbox-grader-001.m.edx.org/peer_grading',
'username': 'incorrect_user', 'username': 'incorrect_user',
'password': 'incorrect_pass', 'password': 'incorrect_pass',
'staff_grading' : 'staff_grading', 'staff_grading': 'staff_grading',
'peer_grading' : 'peer_grading', 'peer_grading': 'peer_grading',
'grading_controller' : 'grading_controller' 'grading_controller': 'grading_controller'
} }
# Used for testing, debugging peer grading # Used for testing, debugging peer grading
MOCK_PEER_GRADING = False MOCK_PEER_GRADING = False
...@@ -492,6 +496,9 @@ MIDDLEWARE_CLASSES = ( ...@@ -492,6 +496,9 @@ MIDDLEWARE_CLASSES = (
'django_comment_client.utils.ViewNameMiddleware', 'django_comment_client.utils.ViewNameMiddleware',
'codejail.django_integration.ConfigureCodeJailMiddleware', 'codejail.django_integration.ConfigureCodeJailMiddleware',
# catches any uncaught RateLimitExceptions and returns a 403 instead of a 500
'ratelimitbackend.middleware.RateLimitMiddleware',
) )
############################### Pipeline ####################################### ############################### Pipeline #######################################
......
"""
This config file runs the simplest dev environment using sqlite, and db-based
sessions. Assumes structure:
/envroot/
/db # This is where it'll write the database file
/mitx # The location of this repo
/log # Where we're going to write log files
"""
# We intentionally define lots of variables that aren't used, and
# want to import all variables from base settings files
# pylint: disable=W0401, W0614
import socket
if 'eecs1' in socket.gethostname():
MITX_ROOT_URL = '/mitx2'
from .common import *
from .dev import *
if 'eecs1' in socket.gethostname():
MITX_ROOT_URL = '/mitx2'
#-----------------------------------------------------------------------------
# edx4edx content server
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
MITX_FEATURES['REROUTE_ACTIVATION_EMAIL'] = 'ichuang@mitx.mit.edu'
EDX4EDX_ROOT = ENV_ROOT / "data/edx4edx"
#EMAIL_BACKEND = 'django_ses.SESBackend'
#-----------------------------------------------------------------------------
# ichuang
DEBUG = True
ENABLE_MULTICOURSE = True # set to False to disable multicourse display (see lib.util.views.mitxhome)
MAKO_TEMPLATES['course'] = [DATA_DIR, EDX4EDX_ROOT]
#MITX_FEATURES['USE_DJANGO_PIPELINE'] = False
MITX_FEATURES['DISPLAY_HISTOGRAMS_TO_STAFF'] = False
MITX_FEATURES['DISPLAY_EDIT_LINK'] = True
COURSE_DEFAULT = "edx4edx"
COURSE_NAME = "edx4edx"
COURSE_NUMBER = "edX.01"
COURSE_TITLE = "edx4edx: edX Author Course"
SITE_NAME = "ichuang.mitx.mit.edu"
COURSE_SETTINGS = {'edx4edx': {'number' : 'edX.01',
'title': 'edx4edx: edX Author Course',
'xmlpath': '/edx4edx/',
'github_url': 'https://github.com/MITx/edx4edx',
'active': True,
'default_chapter': 'Introduction',
'default_section': 'edx4edx_Course',
},
}
#-----------------------------------------------------------------------------
MIDDLEWARE_CLASSES = MIDDLEWARE_CLASSES + (
'ssl_auth.ssl_auth.NginxProxyHeaderMiddleware', # ssl authentication behind nginx proxy
)
AUTHENTICATION_BACKENDS = (
'ssl_auth.ssl_auth.SSLLoginBackend',
'django.contrib.auth.backends.ModelBackend',
)
INSTALLED_APPS = INSTALLED_APPS + (
'ssl_auth',
)
LOGIN_REDIRECT_URL = MITX_ROOT_URL + '/'
LOGIN_URL = MITX_ROOT_URL + '/'
...@@ -15,6 +15,7 @@ sessions. Assumes structure: ...@@ -15,6 +15,7 @@ sessions. Assumes structure:
from .common import * from .common import *
import os import os
from path import path from path import path
from warnings import filterwarnings
# can't test start dates with this True, but on the other hand, # can't test start dates with this True, but on the other hand,
# can test everything else :) # can test everything else :)
...@@ -135,6 +136,9 @@ CACHES = { ...@@ -135,6 +136,9 @@ CACHES = {
# Dummy secret key for dev # Dummy secret key for dev
SECRET_KEY = '85920908f28904ed733fe576320db18cabd7b6cd' SECRET_KEY = '85920908f28904ed733fe576320db18cabd7b6cd'
# hide ratelimit warnings while running tests
filterwarnings('ignore', message='No request passed to the backend, unable to rate-limit')
################################## OPENID ##################################### ################################## OPENID #####################################
MITX_FEATURES['AUTH_USE_OPENID'] = True MITX_FEATURES['AUTH_USE_OPENID'] = True
MITX_FEATURES['AUTH_USE_OPENID_PROVIDER'] = True MITX_FEATURES['AUTH_USE_OPENID_PROVIDER'] = True
......
from django.conf import settings from django.conf import settings
from django.conf.urls import patterns, include, url from django.conf.urls import patterns, include, url
from django.contrib import admin from ratelimitbackend import admin
from django.conf.urls.static import static from django.conf.urls.static import static
# Not used, the work is done in the imported module. # Not used, the work is done in the imported module.
......
...@@ -52,6 +52,7 @@ sorl-thumbnail==11.12 ...@@ -52,6 +52,7 @@ sorl-thumbnail==11.12
South==0.7.6 South==0.7.6
sympy==0.7.1 sympy==0.7.1
xmltodict==0.4.1 xmltodict==0.4.1
django-ratelimit-backend==0.6
# Used for debugging # Used for debugging
ipython==0.13.1 ipython==0.13.1
......
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