Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions cms/envs/aws_appsembler.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,3 +87,5 @@
# if the AppsemblerUsageRouter isn't enabled, then avoid mistakes by
# removing the database alias
del DATABASES['appsembler_usage']

CUSTOM_SSO_FIELDS_SYNC = ENV_TOKENS.get('CUSTOM_SSO_FIELDS_SYNC', {})
4 changes: 4 additions & 0 deletions cms/envs/devstack_appsembler.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,3 +76,7 @@
# if the AppsemblerUsageRouter isn't enabled, then avoid mistakes by
# removing the database alias
del DATABASES['appsembler_usage']

CUSTOM_SSO_FIELDS_SYNC = ENV_TOKENS.get('CUSTOM_SSO_FIELDS_SYNC', {})
# to allow to run python-saml with custom port
SP_SAML_RESTRICT_MODE = False
17 changes: 13 additions & 4 deletions common/djangoapps/student/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -1829,7 +1829,8 @@ def create_account_with_params(request, params):
not (
third_party_provider and third_party_provider.skip_email_verification and
user.email == running_pipeline['kwargs'].get('details', {}).get('email')
)
) and
params.get('send_activation_email', True) == True
)
if send_email:
dest_addr = user.email
Expand Down Expand Up @@ -2596,8 +2597,16 @@ class LogoutView(TemplateView):
oauth_client_ids = []
template_name = 'logout.html'

# Keep track of the page to which the user should ultimately be redirected.
target = reverse_lazy('cas-logout') if settings.FEATURES.get('AUTH_USE_CAS') else '/'
def get_target(self):
# Keep track of the page to which the user should ultimately be redirected.
if settings.FEATURES.get('AUTH_USE_CAS'):
target = reverse_lazy('cas-logout')
elif configuration_helpers.get_value('CUSTOM_LOGOUT_REDIRECT_URL', settings.CUSTOM_LOGOUT_REDIRECT_URL):
target = configuration_helpers.get_value('CUSTOM_LOGOUT_REDIRECT_URL', settings.CUSTOM_LOGOUT_REDIRECT_URL)
else:
target = '/'

return target

def dispatch(self, request, *args, **kwargs): # pylint: disable=missing-docstring
# We do not log here, because we have a handler registered to perform logging on successful logouts.
Expand All @@ -2612,7 +2621,7 @@ def dispatch(self, request, *args, **kwargs): # pylint: disable=missing-docstri
if LogoutViewConfiguration.current().enabled and self.oauth_client_ids:
response = super(LogoutView, self).dispatch(request, *args, **kwargs)
else:
response = redirect(self.target)
response = redirect(self.get_target())

# Clear the cookie used by the edx.org marketing site
delete_logged_in_cookies(response)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('third_party_auth', '0002_schema__provider_icon_image'),
]

operations = [
migrations.AddField(
model_name='samlproviderdata',
name='slo_url',
field=models.URLField(null=True, verbose_name=b'SLO URL'),
),
]
15 changes: 15 additions & 0 deletions common/djangoapps/third_party_auth/migrations/0006_merge.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('third_party_auth', '0003_samlproviderdata_slo_url'),
('third_party_auth', '0005_add_site_field'),
]

operations = [
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('third_party_auth', '0006_merge'),
]

operations = [
migrations.AddField(
model_name='samlconfiguration',
name='slo_redirect_url',
field=models.CharField(default=b'/logout', help_text=b'The url to redirect the user after process the SLO response', max_length=255, verbose_name=b'SLO post redirect URL', blank=True),
),
]
24 changes: 23 additions & 1 deletion common/djangoapps/third_party_auth/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@
"""
from __future__ import absolute_import

import re
from random import randrange

from config_models.models import ConfigurationModel, cache
from django.conf import settings
from django.contrib.sites.models import Site
Expand Down Expand Up @@ -251,12 +254,18 @@ def get_register_form_data(cls, pipeline_kwargs):
else:
suggester_personal_name = details.get('fullname', '')

return {
registration_sso_overrides = {
'email': details.get('email', ''),
'name': suggester_personal_name,
'username': suggested_username,
}

if settings.CUSTOM_SSO_FIELDS_SYNC:
for field in settings.CUSTOM_SSO_FIELDS_SYNC:
registration_sso_overrides[field] = details.get(field)

return registration_sso_overrides

def get_authentication_backend(self):
"""Gets associated Django settings.AUTHENTICATION_BACKEND string."""
return '{}.{}'.format(self.backend_class.__module__, self.backend_class.__name__)
Expand Down Expand Up @@ -451,6 +460,7 @@ def get_config(self):
raise AuthNotConfigured(provider_name=self.name)
conf['x509cert'] = data.public_key
conf['url'] = data.sso_url
conf['slo_url'] = data.slo_url
return SAMLIdentityProvider(self.idp_slug, **conf)


Expand Down Expand Up @@ -502,6 +512,13 @@ class SAMLConfiguration(ConfigurationModel):
"Valid keys that can be set here include: SECURITY_CONFIG and SP_EXTRA"
),
)
slo_redirect_url = models.CharField(
max_length=255,
default='/logout',
verbose_name="SLO post redirect URL",
help_text="The url to redirect the user after process the SLO response",
blank=True
)

class Meta(object):
app_label = "third_party_auth"
Expand Down Expand Up @@ -545,6 +562,10 @@ def get_setting(self, name):
return self.private_key
# To allow instances to avoid storing keys in the DB, the private key can also be set via Django:
return getattr(settings, 'SOCIAL_AUTH_SAML_SP_PRIVATE_KEY', '')
if name == "LOGOUT_REDIRECT_URL":
return self.slo_redirect_url
if name == "SP_SAML_RESTRICT_MODE":
return getattr(settings, 'SP_SAML_RESTRICT_MODE', True)
other_config = {
# These defaults can be overriden by self.other_config_str
"EXTRA_DATA": ["attributes"], # Save all attribute values the IdP sends into the UserSocialAuth table
Expand All @@ -567,6 +588,7 @@ class SAMLProviderData(models.Model):

entity_id = models.CharField(max_length=255, db_index=True) # This is the key for lookups in this table
sso_url = models.URLField(verbose_name="SSO URL")
slo_url = models.URLField(verbose_name="SLO URL", null=True)
public_key = models.TextField()

class Meta(object):
Expand Down
28 changes: 28 additions & 0 deletions common/djangoapps/third_party_auth/pipeline.py
Original file line number Diff line number Diff line change
Expand Up @@ -369,6 +369,34 @@ def get_login_url(provider_id, auth_entry, redirect_url=None):
extra_params=enabled_provider.get_url_params(),
)

def get_logout_url(provider_id, auth_entry, redirect_url=None):
"""Gets URL for the endpoint that starts the SLO process.

Args:
provider_id: string identifier of the models.ProviderConfig child you want
to disconnect from.
auth_entry: string. Query argument specifying the desired entry point
for the auth pipeline. Used by the pipeline for later branching.
Must be one of _AUTH_ENTRY_CHOICES.

Keyword Args:
redirect_url (string): If provided, redirect to this URL at the end
of the authentication process.

Returns:
String. URL that starts the SLO process.

Raises:
ValueError: if no provider is enabled with the given ID.
"""
enabled_provider = _get_enabled_provider(provider_id)
return _get_url(
'social:end',
enabled_provider.backend_name,
auth_entry=auth_entry,
redirect_url=redirect_url,
extra_params=enabled_provider.get_url_params(),
)

def get_duplicate_provider(messages):
"""Gets provider from message about social account already in use.
Expand Down
19 changes: 14 additions & 5 deletions common/djangoapps/third_party_auth/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,8 +71,8 @@ def fetch_saml_metadata():

for entity_id in entity_ids:
log.info(u"Processing IdP with entityID %s", entity_id)
public_key, sso_url, expires_at = _parse_metadata_xml(xml, entity_id)
changed = _update_data(entity_id, public_key, sso_url, expires_at)
public_key, sso_url, slo_url, expires_at = _parse_metadata_xml(xml, entity_id)
changed = _update_data(entity_id, public_key, sso_url, slo_url, expires_at)
if changed:
log.info(u"→ Created new record for SAMLProviderData")
num_changed += 1
Expand Down Expand Up @@ -153,16 +153,24 @@ def _parse_metadata_xml(xml, entity_id):
raise MetadataParseError("Public Key missing. Expected an <X509Certificate>")
public_key = public_key.replace(" ", "")
binding_elements = sso_desc.iterfind("./{}".format(etree.QName(SAML_XML_NS, "SingleSignOnService")))
binding_elements_slo = sso_desc.iterfind("./{}".format(etree.QName(SAML_XML_NS, "SingleLogoutService")))
sso_bindings = {element.get('Binding'): element.get('Location') for element in binding_elements}
slo_bindings = {element.get('Binding'): element.get('Location') for element in binding_elements_slo}
try:
# The only binding supported by python-saml and python-social-auth is HTTP-Redirect:
sso_url = sso_bindings['urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect']
except KeyError:
raise MetadataParseError("Unable to find SSO URL with HTTP-Redirect binding.")
return public_key, sso_url, expires_at
try:
# The only binding supported by python-saml and python-social-auth is HTTP-Redirect:
slo_url = slo_bindings['urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect']
except KeyError:
slo_url = ""
log.info("Unable to find SLO URL with HTTP-Redirect binding.")
return public_key, sso_url, slo_url, expires_at


def _update_data(entity_id, public_key, sso_url, expires_at):
def _update_data(entity_id, public_key, sso_url, slo_url, expires_at):
"""
Update/Create the SAMLProviderData for the given entity ID.
Return value:
Expand All @@ -171,7 +179,7 @@ def _update_data(entity_id, public_key, sso_url, expires_at):
"""
data_obj = SAMLProviderData.current(entity_id)
fetched_at = datetime.datetime.now()
if data_obj and (data_obj.public_key == public_key and data_obj.sso_url == sso_url):
if data_obj and (data_obj.public_key == public_key and data_obj.sso_url == sso_url and data_obj.slo_url == slo_url):
data_obj.expires_at = expires_at
data_obj.fetched_at = fetched_at
data_obj.save()
Expand All @@ -182,6 +190,7 @@ def _update_data(entity_id, public_key, sso_url, expires_at):
fetched_at=fetched_at,
expires_at=expires_at,
sso_url=sso_url,
slo_url=slo_url,
public_key=public_key,
)
return True
10 changes: 10 additions & 0 deletions lms/envs/aws_appsembler.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,3 +104,13 @@
# if the AppsemblerUsageRouter isn't enabled, then avoid mistakes by
# removing the database alias
del DATABASES['appsembler_usage']

if FEATURES.get('ENABLE_CORS_HEADERS', False):
# This middleware class and setting allows to run cross requests when we are
# under https, CORS headers requests external referers are blocked under
# https, there is a new setting in Django 1.9, but until we upgrade to that
# version we need to use this.
# Docs: https://github.com/ottoyiu/django-cors-headers#cors_replace_https_referer
CORS_REPLACE_HTTPS_REFERER = True

CUSTOM_SSO_FIELDS_SYNC = ENV_TOKENS.get('CUSTOM_SSO_FIELDS_SYNC', {})
4 changes: 4 additions & 0 deletions lms/envs/devstack_appsembler.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,3 +101,7 @@
# if the AppsemblerUsageRouter isn't enabled, then avoid mistakes by
# removing the database alias
del DATABASES['appsembler_usage']

CUSTOM_SSO_FIELDS_SYNC = ENV_TOKENS.get('CUSTOM_SSO_FIELDS_SYNC', {})
# to allow to run python-saml with custom port
SP_SAML_RESTRICT_MODE = False
6 changes: 4 additions & 2 deletions lms/templates/emails/activation_email.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
<%! from django.utils.translation import ugettext as _ %>
<%! from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers %>

${_("Thank you for creating an account with {platform_name}!").format(
platform_name=configuration_helpers.get_value('PLATFORM_NAME', settings.PLATFORM_NAME)
)}
Expand All @@ -11,10 +12,11 @@ ${_("There's just one more step before you can enroll in a course: "
platform_name=configuration_helpers.get_value('PLATFORM_NAME', settings.PLATFORM_NAME)
)}

<% base_url=configuration_helpers.get_value('SITE_NAME', settings.SITE_NAME) %>
% if is_secure:
https://${ site }/activate/${ key }
https://${ base_url }/activate/${ key }
% else:
http://${ site }/activate/${ key }
http://${ base_url }/activate/${ key }
% endif
${_("If you didn't create an account, you don't need to do anything; you "
"won't receive any more email from us. If you need assistance, please "
Expand Down
2 changes: 1 addition & 1 deletion lms/templates/student_account/form_field.underscore
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
} %>
<% if ( required ) { %> aria-required="true" required<% } %>>
<% _.each(options, function(el) { %>
<option value="<%= el.value%>"<% if ( el.default ) { %> data-isdefault="true"<% } %>><%= el.name %></option>
<option value="<%= el.value%>"<% if ( el.default ) { %> data-isdefault="true"<% } %><% if (el.value === defaultValue ) { %> selected="selected" <% } %>><%= el.name %></option>
<% }); %>
</select>
<% if ( instructions ) { %> <span class="tip tip-input" id="<%= form %>-<%= name %>-desc"><%= instructions %></span><% } %>
Expand Down
15 changes: 15 additions & 0 deletions openedx/core/djangoapps/appsembler/external_courses/admin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
from django.contrib import admin

from openedx.core.djangoapps.appsembler.external_courses.models import ExternalCourseTile

@admin.register(ExternalCourseTile)
class ExternalCourseTileAdmin(admin.ModelAdmin):

fields = ('course_duration', 'course_key', 'title', 'org', 'course_link', 'image_url', 'starts', 'ends', 'pacing_type', 'is_credit_eligible', 'is_verified_eligible')
readonly_fields = ('course_key', 'title', 'org', 'course_link', 'image_url', 'starts', 'ends', 'pacing_type', 'is_credit_eligible', 'is_verified_eligible')


class Meta:
verbose_name = "External Course"
verbose_name_plural = "External Courses"

Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('external_courses', '0001_initial'),
]

operations = [
migrations.AddField(
model_name='externalcoursetile',
name='course_duration',
field=models.CharField(max_length=255, null=True, blank=True),
),
]
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ class ExternalCourseTile(models.Model):
pacing_type = models.CharField(max_length=255, null=False, blank=False)
is_credit_eligible = models.BooleanField(default=False)
is_verified_eligible = models.BooleanField(default=False)
course_duration = models.CharField(max_length=255, null=True, blank=True)

def __unicode__(self):
return "%s (%s)" % (self.title, self.org)
Expand Down
8 changes: 8 additions & 0 deletions openedx/core/djangoapps/user_api/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -880,6 +880,14 @@ def _apply_third_party_auth_overrides(self, request, form_desc):
field_name, default=field_overrides[field_name]
)

if settings.CUSTOM_SSO_FIELDS_SYNC:
for field_name in settings.CUSTOM_SSO_FIELDS_SYNC:
if field_name in field_overrides:
form_desc.override_field_properties(
field_name,
default=field_overrides[field_name]
)

# Hide the password field
form_desc.override_field_properties(
"password",
Expand Down