Merge pull request #67 from edx/features/jbau/stanford-shib
Shibboleth Auth
This commit is contained in:
@@ -14,6 +14,7 @@ from xmodule.modulestore import Location
|
||||
from xmodule.x_module import XModule, XModuleDescriptor
|
||||
|
||||
from student.models import CourseEnrollmentAllowed
|
||||
from external_auth.models import ExternalAuthMap
|
||||
from courseware.masquerade import is_masquerading_as_student
|
||||
from django.utils.timezone import UTC
|
||||
|
||||
@@ -129,15 +130,33 @@ def _has_access_course_desc(user, course, action):
|
||||
|
||||
def can_enroll():
|
||||
"""
|
||||
If the course has an enrollment period, check whether we are in it.
|
||||
First check if restriction of enrollment by login method is enabled, both
|
||||
globally and by the course.
|
||||
If it is, then the user must pass the criterion set by the course, e.g. that ExternalAuthMap
|
||||
was set by 'shib:https://idp.stanford.edu/", in addition to requirements below.
|
||||
Rest of requirements:
|
||||
Enrollment can only happen in the course enrollment period, if one exists.
|
||||
or
|
||||
|
||||
(CourseEnrollmentAllowed always overrides)
|
||||
(staff can always enroll)
|
||||
"""
|
||||
# if using registration method to restrict (say shibboleth)
|
||||
if settings.MITX_FEATURES.get('RESTRICT_ENROLL_BY_REG_METHOD') and course.enrollment_domain:
|
||||
if user is not None and user.is_authenticated() and \
|
||||
ExternalAuthMap.objects.filter(user=user, external_domain=course.enrollment_domain):
|
||||
debug("Allow: external_auth of " + course.enrollment_domain)
|
||||
reg_method_ok = True
|
||||
else:
|
||||
reg_method_ok = False
|
||||
else:
|
||||
reg_method_ok = True #if not using this access check, it's always OK.
|
||||
|
||||
now = datetime.now(UTC())
|
||||
start = course.enrollment_start
|
||||
end = course.enrollment_end
|
||||
|
||||
if (start is None or now > start) and (end is None or now < end):
|
||||
if reg_method_ok and (start is None or now > start) and (end is None or now < end):
|
||||
# in enrollment period, so any user is allowed to enroll.
|
||||
debug("Allow: in enrollment period")
|
||||
return True
|
||||
|
||||
@@ -81,7 +81,7 @@ class AccessTestCase(TestCase):
|
||||
u = Mock()
|
||||
yesterday = datetime.datetime.now(UTC()) - datetime.timedelta(days=1)
|
||||
tomorrow = datetime.datetime.now(UTC()) + datetime.timedelta(days=1)
|
||||
c = Mock(enrollment_start=yesterday, enrollment_end=tomorrow)
|
||||
c = Mock(enrollment_start=yesterday, enrollment_end=tomorrow, enrollment_domain='')
|
||||
|
||||
# User can enroll if it is between the start and end dates
|
||||
self.assertTrue(access._has_access_course_desc(u, c, 'enroll'))
|
||||
@@ -91,7 +91,7 @@ class AccessTestCase(TestCase):
|
||||
u = Mock(email='test@edx.org', is_staff=False)
|
||||
u.is_authenticated.return_value = True
|
||||
|
||||
c = Mock(enrollment_start=tomorrow, enrollment_end=tomorrow, id='edX/test/2012_Fall')
|
||||
c = Mock(enrollment_start=tomorrow, enrollment_end=tomorrow, id='edX/test/2012_Fall', enrollment_domain='')
|
||||
|
||||
allowed = CourseEnrollmentAllowedFactory(email=u.email, course_id=c.id)
|
||||
|
||||
@@ -101,7 +101,7 @@ class AccessTestCase(TestCase):
|
||||
u = Mock(email='test@edx.org', is_staff=True)
|
||||
u.is_authenticated.return_value = True
|
||||
|
||||
c = Mock(enrollment_start=tomorrow, enrollment_end=tomorrow, id='edX/test/Whenever')
|
||||
c = Mock(enrollment_start=tomorrow, enrollment_end=tomorrow, id='edX/test/Whenever', enrollment_domain='')
|
||||
self.assertTrue(access._has_access_course_desc(u, c, 'enroll'))
|
||||
|
||||
# TODO:
|
||||
|
||||
@@ -138,6 +138,10 @@ MKTG_URL_LINK_MAP.update(ENV_TOKENS.get('MKTG_URL_LINK_MAP', {}))
|
||||
#Timezone overrides
|
||||
TIME_ZONE = ENV_TOKENS.get('TIME_ZONE', TIME_ZONE)
|
||||
|
||||
#Additional installed apps
|
||||
for app in ENV_TOKENS.get('ADDL_INSTALLED_APPS', []):
|
||||
INSTALLED_APPS += (app,)
|
||||
|
||||
for feature, value in ENV_TOKENS.get('MITX_FEATURES', {}).items():
|
||||
MITX_FEATURES[feature] = value
|
||||
|
||||
|
||||
@@ -91,6 +91,14 @@ MITX_FEATURES = {
|
||||
'AUTH_USE_OPENID': False,
|
||||
'AUTH_USE_MIT_CERTIFICATES': False,
|
||||
'AUTH_USE_OPENID_PROVIDER': False,
|
||||
'AUTH_USE_SHIB': False,
|
||||
|
||||
# This flag disables the requirement of having to agree to the TOS for users registering
|
||||
# with Shib. Feature was requested by Stanford's office of general counsel
|
||||
'SHIB_DISABLE_TOS': False,
|
||||
|
||||
# Enables ability to restrict enrollment in specific courses by the user account login method
|
||||
'RESTRICT_ENROLL_BY_REG_METHOD': False,
|
||||
|
||||
# analytics experiments
|
||||
'ENABLE_INSTRUCTOR_ANALYTICS': False,
|
||||
@@ -699,6 +707,10 @@ INSTALLED_APPS = (
|
||||
'licenses',
|
||||
'course_groups',
|
||||
|
||||
# External auth (OpenID, shib)
|
||||
'external_auth',
|
||||
'django_openid_auth',
|
||||
|
||||
#For the wiki
|
||||
'wiki', # The new django-wiki from benjaoming
|
||||
'django_notify',
|
||||
|
||||
@@ -232,6 +232,9 @@ FILE_UPLOAD_HANDLERS = (
|
||||
'django.core.files.uploadhandler.TemporaryFileUploadHandler',
|
||||
)
|
||||
|
||||
MITX_FEATURES['AUTH_USE_SHIB'] = True
|
||||
MITX_FEATURES['RESTRICT_ENROLL_BY_REG_METHOD'] = True
|
||||
|
||||
########################### PIPELINE #################################
|
||||
|
||||
PIPELINE_SASS_ARGUMENTS = '--debug-info --require {proj_dir}/static/sass/bourbon/lib/bourbon.rb'.format(proj_dir=PROJECT_ROOT)
|
||||
|
||||
@@ -137,14 +137,16 @@ SECRET_KEY = '85920908f28904ed733fe576320db18cabd7b6cd'
|
||||
MITX_FEATURES['AUTH_USE_OPENID'] = True
|
||||
MITX_FEATURES['AUTH_USE_OPENID_PROVIDER'] = True
|
||||
|
||||
################################## SHIB #######################################
|
||||
MITX_FEATURES['AUTH_USE_SHIB'] = True
|
||||
MITX_FEATURES['SHIB_DISABLE_TOS'] = True
|
||||
MITX_FEATURES['RESTRICT_ENROLL_BY_REG_METHOD'] = True
|
||||
|
||||
OPENID_CREATE_USERS = False
|
||||
OPENID_UPDATE_DETAILS_FROM_SREG = True
|
||||
OPENID_USE_AS_ADMIN_LOGIN = False
|
||||
OPENID_PROVIDER_TRUSTED_ROOTS = ['*']
|
||||
|
||||
INSTALLED_APPS += ('external_auth',)
|
||||
INSTALLED_APPS += ('django_openid_auth',)
|
||||
|
||||
################################# CELERY ######################################
|
||||
|
||||
CELERY_ALWAYS_EAGER = True
|
||||
|
||||
@@ -24,6 +24,26 @@
|
||||
event.preventDefault();
|
||||
});
|
||||
|
||||
## making the conditional around this entire JS block for sanity
|
||||
%if settings.MITX_FEATURES.get('RESTRICT_ENROLL_BY_REG_METHOD') and course.enrollment_domain:
|
||||
$('#class_enroll_form').on('ajax:complete', function(event, xhr) {
|
||||
if(xhr.status == 200) {
|
||||
location.href = "${reverse('dashboard')}";
|
||||
} else if (xhr.status == 403) {
|
||||
location.href = "${reverse('course-specific-register', args=[course.id])}?course_id=${course.id}&enrollment_action=enroll";
|
||||
} else if (xhr.status == 400) { //This means the user did not have permission
|
||||
$('#register_error').html('This course has restricted enrollment. Sorry, you do not have permission to enroll.<br />' +
|
||||
'You may need to log out and re-login with a university account, such as WebAuth'
|
||||
).css("display", "block");
|
||||
} else {
|
||||
$('#register_error').html(
|
||||
(xhr.responseText ? xhr.responseText : 'An error occurred. Please try again later.')
|
||||
).css("display", "block");
|
||||
}
|
||||
});
|
||||
|
||||
%else:
|
||||
|
||||
$('#class_enroll_form').on('ajax:complete', function(event, xhr) {
|
||||
if(xhr.status == 200) {
|
||||
location.href = "${reverse('dashboard')}";
|
||||
@@ -35,13 +55,16 @@
|
||||
).css("display", "block");
|
||||
}
|
||||
});
|
||||
|
||||
%endif
|
||||
|
||||
|
||||
})(this)
|
||||
</script>
|
||||
|
||||
<script src="${static.url('js/course_info.js')}"></script>
|
||||
</%block>
|
||||
|
||||
|
||||
<%block name="title"><title>About ${course.number}</title></%block>
|
||||
|
||||
<section class="course-info">
|
||||
@@ -92,7 +115,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
|
||||
<section class="container">
|
||||
<section class="details">
|
||||
<nav>
|
||||
|
||||
@@ -138,8 +138,14 @@
|
||||
<span class="title"><div class="icon name-icon"></div>Full Name (<a href="#apply_name_change" rel="leanModal" class="edit-name">edit</a>)</span> <span class="data">${ user.profile.name | h }</span>
|
||||
</li>
|
||||
<li>
|
||||
<span class="title"><div class="icon email-icon"></div>Email (<a href="#change_email" rel="leanModal" class="edit-email">edit</a>)</span> <span class="data">${ user.email | h }</span>
|
||||
<span class="title"><div class="icon email-icon"></div>Email
|
||||
% if external_auth_map is None or 'shib' not in external_auth_map.external_domain:
|
||||
(<a href="#change_email" rel="leanModal" class="edit-email">edit</a>)
|
||||
% endif
|
||||
</span> <span class="data">${ user.email | h }</span>
|
||||
</li>
|
||||
|
||||
% if external_auth_map is None or 'shib' not in external_auth_map.external_domain:
|
||||
<li>
|
||||
<span class="title"><a href="#password_reset_complete" rel="leanModal" id="pwd_reset_button">Reset Password</a></span>
|
||||
<form id="password_reset_form" method="post" data-remote="true" action="${reverse('password_reset')}">
|
||||
@@ -147,6 +153,8 @@
|
||||
<!-- <input type="submit" id="pwd_reset_button" value="Reset Password" /> -->
|
||||
</form>
|
||||
</li>
|
||||
% endif
|
||||
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
"http://www.w3.org/TR/html4/strict.dtd">
|
||||
<html>
|
||||
<head>
|
||||
<title>OpenID failed</title>
|
||||
<title>External Authentication failed</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>OpenID failed</h1>
|
||||
<h1>External Authentication failed</h1>
|
||||
<p>${message}</p>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -95,16 +95,26 @@ site_status_msg = get_site_status_msg(course_id)
|
||||
% endif
|
||||
</%block>
|
||||
% if not settings.MITX_FEATURES['DISABLE_LOGIN_BUTTON']:
|
||||
<li class="nav-global-04">
|
||||
<a class="cta cta-register" href="/register">Register Now</a>
|
||||
</li>
|
||||
% if course and settings.MITX_FEATURES.get('RESTRICT_ENROLL_BY_REG_METHOD') and course.enrollment_domain:
|
||||
<li class="nav-global-04">
|
||||
<a class="cta cta-register" href="${reverse('course-specific-register', args=[course.id])}">Register Now</a>
|
||||
</li>
|
||||
% else:
|
||||
<li class="nav-global-04">
|
||||
<a class="cta cta-register" href="/register">Register Now</a>
|
||||
</li>
|
||||
% endif
|
||||
% endif
|
||||
</ol>
|
||||
|
||||
<ol class="right nav-courseware">
|
||||
<li class="nav-courseware-01">
|
||||
% if not settings.MITX_FEATURES['DISABLE_LOGIN_BUTTON']:
|
||||
<a class="cta cta-login" href="/login${login_query()}">Log in</a>
|
||||
% if course and settings.MITX_FEATURES.get('RESTRICT_ENROLL_BY_REG_METHOD') and course.enrollment_domain:
|
||||
<a class="cta cta-login" href="${reverse('course-specific-login', args=[course.id])}${login_query()}">Log in</a>
|
||||
% else:
|
||||
<a class="cta cta-login" href="/login${login_query()}">Log in</a>
|
||||
% endif
|
||||
% endif
|
||||
</li>
|
||||
</ol>
|
||||
|
||||
@@ -136,16 +136,37 @@
|
||||
% else:
|
||||
|
||||
<div class="message">
|
||||
<h3 class="message-title">Welcome ${extauth_email}</h3>
|
||||
<h3 class="message-title">Welcome ${extauth_id}</h3>
|
||||
<p class="message-copy">Enter a public username:</p>
|
||||
</div>
|
||||
|
||||
<ol class="list-input">
|
||||
|
||||
% if ask_for_email:
|
||||
|
||||
<li class="field required text" id="field-email">
|
||||
<label for="email">E-mail</label>
|
||||
<input class="" id="email" type="email" name="email" value="" placeholder="example: username@domain.com" />
|
||||
</li>
|
||||
|
||||
% endif
|
||||
|
||||
<li class="field required text" id="field-username">
|
||||
<label for="username">Public Username</label>
|
||||
<input id="username" type="text" name="username" value="${extauth_username}" placeholder="example: JaneDoe" required aria-required="true" />
|
||||
<span class="tip tip-input">Will be shown in any discussions or forums you participate in</span>
|
||||
</li>
|
||||
|
||||
% if ask_for_fullname:
|
||||
|
||||
<li class="field required text" id="field-name">
|
||||
<label for="name">Full Name</label>
|
||||
<input id="name" type="text" name="name" value="" placeholder="example: Jane Doe" />
|
||||
<span class="tip tip-input">Needed for any certificates you may earn <strong>(cannot be changed later)</strong></span>
|
||||
</li>
|
||||
|
||||
% endif
|
||||
|
||||
</ol>
|
||||
|
||||
% endif
|
||||
@@ -210,11 +231,16 @@
|
||||
|
||||
<ol class="list-input">
|
||||
<li class="field-group">
|
||||
|
||||
% if has_extauth_info is UNDEFINED or ask_for_tos :
|
||||
|
||||
<div class="field required checkbox" id="field-tos">
|
||||
<input id="tos-yes" type="checkbox" name="terms_of_service" value="true" required aria-required="true" />
|
||||
<label for="tos-yes">I agree to the <a href="${marketing_link('TOS')}" class="new-vp">Terms of Service</a></label>
|
||||
</div>
|
||||
|
||||
% endif
|
||||
|
||||
<div class="field required checkbox" id="field-honorcode">
|
||||
<input id="honorcode-yes" type="checkbox" name="honor_code" value="true" />
|
||||
<%
|
||||
@@ -246,6 +272,8 @@
|
||||
<h3 class="sr">Registration Help</h3>
|
||||
</header>
|
||||
|
||||
% if has_extauth_info is UNDEFINED:
|
||||
|
||||
<div class="cta">
|
||||
<h3>Already registered?</h3>
|
||||
<p class="instructions">
|
||||
@@ -254,6 +282,8 @@
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
% endif
|
||||
|
||||
## TODO: Use a %block tag or something to allow themes to
|
||||
## override in a more generalizable fashion.
|
||||
|
||||
@@ -32,11 +32,23 @@
|
||||
<label data-field="name" for="signup_fullname">Full Name *</label>
|
||||
<input id="signup_fullname" type="text" name="name" placeholder="e.g. Your Name (for certificates)" required />
|
||||
% else:
|
||||
<p><i>Welcome</i> ${extauth_email}</p><br/>
|
||||
<p><i>Welcome</i> ${extauth_id}</p><br/>
|
||||
<p><i>Enter a public username:</i></p>
|
||||
|
||||
|
||||
<label data-field="username" for="signup_username">Public Username *</label>
|
||||
<input id="signup_username" type="text" name="username" value="${extauth_username}" placeholder="e.g. yourname (shown on forums)" required />
|
||||
<input id="signup_username" type="text" name="username" value="${extauth_username}" placeholder="e.g. yourname (shown on forums)" required />
|
||||
|
||||
% if ask_for_email:
|
||||
<label data-field="email" for="signup_email">E-mail *</label>
|
||||
<input id="signup_email" type="email" name="email" placeholder="e.g. yourname@domain.com" required />
|
||||
% endif
|
||||
|
||||
|
||||
% if ask_for_fullname:
|
||||
<label data-field="name" for="signup_fullname">Full Name *</label>
|
||||
<input id="signup_fullname" type="text" name="name" placeholder="e.g. Your Name (for certificates)" required />
|
||||
% endif
|
||||
|
||||
% endif
|
||||
</div>
|
||||
|
||||
|
||||
15
lms/urls.py
15
lms/urls.py
@@ -364,6 +364,21 @@ if settings.MITX_FEATURES.get('AUTH_USE_OPENID'):
|
||||
url(r'^openid/logo.gif$', 'django_openid_auth.views.logo', name='openid-logo'),
|
||||
)
|
||||
|
||||
if settings.MITX_FEATURES.get('AUTH_USE_SHIB'):
|
||||
urlpatterns += (
|
||||
url(r'^shib-login/$', 'external_auth.views.shib_login', name='shib-login'),
|
||||
)
|
||||
|
||||
if settings.MITX_FEATURES.get('RESTRICT_ENROLL_BY_REG_METHOD'):
|
||||
urlpatterns += (
|
||||
url(r'^course_specific_login/(?P<course_id>[^/]+/[^/]+/[^/]+)/$',
|
||||
'external_auth.views.course_specific_login', name='course-specific-login'),
|
||||
url(r'^course_specific_register/(?P<course_id>[^/]+/[^/]+/[^/]+)/$',
|
||||
'external_auth.views.course_specific_register', name='course-specific-register'),
|
||||
|
||||
)
|
||||
|
||||
|
||||
if settings.MITX_FEATURES.get('AUTH_USE_OPENID_PROVIDER'):
|
||||
urlpatterns += (
|
||||
url(r'^openid/provider/login/$', 'external_auth.views.provider_login', name='openid-provider-login'),
|
||||
|
||||
15
lms/wsgi_apache_lms.py
Normal file
15
lms/wsgi_apache_lms.py
Normal file
@@ -0,0 +1,15 @@
|
||||
import os
|
||||
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "lms.envs.aws")
|
||||
os.environ.setdefault("SERVICE_VARIANT", "lms")
|
||||
|
||||
# This application object is used by the development server
|
||||
# as well as any WSGI server configured to use this file.
|
||||
from django.core.wsgi import get_wsgi_application
|
||||
application = get_wsgi_application()
|
||||
|
||||
from django.conf import settings
|
||||
from xmodule.modulestore.django import modulestore
|
||||
|
||||
for store_name in settings.MODULESTORE:
|
||||
modulestore(store_name)
|
||||
Reference in New Issue
Block a user