From f41bf2f409c51a763f1bee4b07dd7149fc8062d2 Mon Sep 17 00:00:00 2001 From: Brittney Exline Date: Wed, 4 Apr 2018 09:41:55 -0400 Subject: [PATCH] ENT-944 Create SSOVerifications for users in tpa pipeline based on provider's settings --- .../migrations/0021_sso_id_verification.py | 30 ++++++ common/djangoapps/third_party_auth/models.py | 9 ++ .../djangoapps/third_party_auth/pipeline.py | 28 +++++ .../djangoapps/third_party_auth/settings.py | 1 + .../tests/test_pipeline_integration.py | 101 ++++++++++++++++++ lms/djangoapps/verify_student/admin.py | 13 ++- 6 files changed, 181 insertions(+), 1 deletion(-) create mode 100644 common/djangoapps/third_party_auth/migrations/0021_sso_id_verification.py diff --git a/common/djangoapps/third_party_auth/migrations/0021_sso_id_verification.py b/common/djangoapps/third_party_auth/migrations/0021_sso_id_verification.py new file mode 100644 index 0000000000..d8a26a12f2 --- /dev/null +++ b/common/djangoapps/third_party_auth/migrations/0021_sso_id_verification.py @@ -0,0 +1,30 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.12 on 2018-04-11 15:33 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('third_party_auth', '0020_cleanup_slug_fields'), + ] + + operations = [ + migrations.AddField( + model_name='ltiproviderconfig', + name='enable_sso_id_verification', + field=models.BooleanField(default=False, help_text=b'Use the presence of a profile from a trusted third party as proof of identity verification.'), + ), + migrations.AddField( + model_name='oauth2providerconfig', + name='enable_sso_id_verification', + field=models.BooleanField(default=False, help_text=b'Use the presence of a profile from a trusted third party as proof of identity verification.'), + ), + migrations.AddField( + model_name='samlproviderconfig', + name='enable_sso_id_verification', + field=models.BooleanField(default=False, help_text=b'Use the presence of a profile from a trusted third party as proof of identity verification.'), + ), + ] diff --git a/common/djangoapps/third_party_auth/models.py b/common/djangoapps/third_party_auth/models.py index 01867e5e11..c25d6fbf19 100644 --- a/common/djangoapps/third_party_auth/models.py +++ b/common/djangoapps/third_party_auth/models.py @@ -196,6 +196,10 @@ class ProviderConfig(ConfigurationModel): "with their account is changed as a part of this synchronization." ) ) + enable_sso_id_verification = models.BooleanField( + default=False, + help_text="Use the presence of a profile from a trusted third party as proof of identity verification.", + ) prefix = None # used for provider_id. Set to a string value in subclass backend_name = None # Set to a field or fixed value in subclass accepts_logins = True # Whether to display a sign-in button when the provider is enabled @@ -223,6 +227,11 @@ class ProviderConfig(ConfigurationModel): """ Get the python-social-auth backend class used for this provider """ return _PSA_BACKENDS[self.backend_name] + @property + def full_class_name(self): + """ Get the fully qualified class name of this provider. """ + return '{}.{}'.format(self.__module__, self.__class__.__name__) + def get_url_params(self): """ Get a dict of GET parameters to append to login links for this provider """ return {} diff --git a/common/djangoapps/third_party_auth/pipeline.py b/common/djangoapps/third_party_auth/pipeline.py index db510c15e8..5223f2f786 100644 --- a/common/djangoapps/third_party_auth/pipeline.py +++ b/common/djangoapps/third_party_auth/pipeline.py @@ -83,6 +83,8 @@ from edxmako.shortcuts import render_to_string from eventtracking import tracker from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers from third_party_auth.utils import user_exists +from lms.djangoapps.verify_student.models import SSOVerification +from lms.djangoapps.verify_student.utils import earliest_allowed_verification_date from . import provider @@ -774,3 +776,29 @@ def user_details_force_sync(auth_entry, strategy, details, user=None, *args, **k except SMTPException: logger.exception('Error sending IdP learner data sync-initiated email change ' 'notification email for user [%s].', user.username) + + +def set_id_verification_status(auth_entry, strategy, details, user=None, *args, **kwargs): + """ + Use the user's authentication with the provider, if configured, as evidence of their identity being verified. + """ + current_provider = provider.Registry.get_from_pipeline({'backend': strategy.request.backend.name, 'kwargs': kwargs}) + if user and current_provider.enable_sso_id_verification: + # Get previous valid, non expired verification attempts for this SSO Provider and user + verifications = SSOVerification.objects.filter( + user=user, + status="approved", + created_at__gte=earliest_allowed_verification_date(), + identity_provider_type=current_provider.full_class_name, + identity_provider_slug=current_provider.slug, + ) + + # If there is none, create a new approved verification for the user. + if not verifications: + SSOVerification.objects.create( + user=user, + status="approved", + name=user.profile.name, + identity_provider_type=current_provider.full_class_name, + identity_provider_slug=current_provider.slug, + ) diff --git a/common/djangoapps/third_party_auth/settings.py b/common/djangoapps/third_party_auth/settings.py index a97f0f0e0d..632780842f 100644 --- a/common/djangoapps/third_party_auth/settings.py +++ b/common/djangoapps/third_party_auth/settings.py @@ -58,6 +58,7 @@ def apply_settings(django_settings): 'social_core.pipeline.social_auth.load_extra_data', 'social_core.pipeline.user.user_details', 'third_party_auth.pipeline.user_details_force_sync', + 'third_party_auth.pipeline.set_id_verification_status', 'third_party_auth.pipeline.set_logged_in_cookies', 'third_party_auth.pipeline.login_analytics', ] diff --git a/common/djangoapps/third_party_auth/tests/test_pipeline_integration.py b/common/djangoapps/third_party_auth/tests/test_pipeline_integration.py index 5d9ca5b6e3..510d0c8f90 100644 --- a/common/djangoapps/third_party_auth/tests/test_pipeline_integration.py +++ b/common/djangoapps/third_party_auth/tests/test_pipeline_integration.py @@ -2,7 +2,9 @@ import unittest +import datetime import mock +import pytz import ddt from django import test from django.contrib.auth import models @@ -12,6 +14,7 @@ from social_django import models as social_models from student.tests.factories import UserFactory from third_party_auth import pipeline, provider from third_party_auth.tests import testutil +from lms.djangoapps.verify_student.models import SSOVerification # Get Django User model by reference from python-social-auth. Not a type # constant, pylint. @@ -440,3 +443,101 @@ class UserDetailsForceSyncTestCase(testutil.TestCase, test.TestCase): # An email should still be sent because the email changed. assert len(mail.outbox) == 1 + + +@unittest.skipUnless(testutil.AUTH_FEATURE_ENABLED, testutil.AUTH_FEATURES_KEY + ' not enabled') +class SetIDVerificationStatusTestCase(testutil.TestCase, test.TestCase): + """Tests to ensure SSO ID Verification for the user is set if the provider requires it.""" + + def setUp(self): + super(SetIDVerificationStatusTestCase, self).setUp() + self.user = UserFactory.create() + self.provider_class_name = 'third_party_auth.models.SAMLProviderConfig' + self.provider_slug = 'default' + self.details = {} + + # Mocks + self.strategy = mock.MagicMock() + self.strategy.storage.user.changed.side_effect = lambda user: user.save() + + get_from_pipeline = mock.patch('third_party_auth.pipeline.provider.Registry.get_from_pipeline') + self.get_from_pipeline = get_from_pipeline.start() + self.get_from_pipeline.return_value = mock.MagicMock( + enable_sso_id_verification=True, + full_class_name=self.provider_class_name, + slug=self.provider_slug, + ) + self.addCleanup(get_from_pipeline.stop) + + def test_set_id_verification_status_new_user(self): + """ + The user details are synced properly and an email is sent when the email is changed. + """ + # Begin the pipeline. + pipeline.set_id_verification_status( + auth_entry=pipeline.AUTH_ENTRY_LOGIN, + strategy=self.strategy, + details=self.details, + user=self.user, + ) + + verification = SSOVerification.objects.get(user=self.user) + + assert verification.identity_provider_type == self.provider_class_name + assert verification.identity_provider_slug == self.provider_slug + assert verification.status == 'approved' + assert verification.name == self.user.profile.name + + def test_set_id_verification_status_returning_user(self): + """ + The user details are synced properly and an email is sent when the email is changed. + """ + + SSOVerification.objects.create( + user=self.user, + status="approved", + name=self.user.profile.name, + identity_provider_type=self.provider_class_name, + identity_provider_slug=self.provider_slug, + ) + + # Begin the pipeline. + pipeline.set_id_verification_status( + auth_entry=pipeline.AUTH_ENTRY_LOGIN, + strategy=self.strategy, + details=self.details, + user=self.user, + ) + + assert SSOVerification.objects.filter(user=self.user).count() == 1 + + def test_set_id_verification_status_expired(self): + """ + The user details are synced properly and an email is sent when the email is changed. + """ + + SSOVerification.objects.create( + user=self.user, + status="approved", + name=self.user.profile.name, + identity_provider_type=self.provider_class_name, + identity_provider_slug=self.provider_slug, + ) + + with mock.patch('third_party_auth.pipeline.earliest_allowed_verification_date') as earliest_date: + earliest_date.return_value = datetime.datetime.now(pytz.UTC) + datetime.timedelta(days=1) + # Begin the pipeline. + pipeline.set_id_verification_status( + auth_entry=pipeline.AUTH_ENTRY_LOGIN, + strategy=self.strategy, + details=self.details, + user=self.user, + ) + + assert SSOVerification.objects.filter( + user=self.user, + status="approved", + name=self.user.profile.name, + identity_provider_type=self.provider_class_name, + identity_provider_slug=self.provider_slug, + ).count() == 2 diff --git a/lms/djangoapps/verify_student/admin.py b/lms/djangoapps/verify_student/admin.py index c73f562d38..8a2e8315dd 100644 --- a/lms/djangoapps/verify_student/admin.py +++ b/lms/djangoapps/verify_student/admin.py @@ -5,7 +5,7 @@ Admin site configurations for verify_student. from django.contrib import admin -from lms.djangoapps.verify_student.models import SoftwareSecurePhotoVerification +from lms.djangoapps.verify_student.models import SoftwareSecurePhotoVerification, SSOVerification @admin.register(SoftwareSecurePhotoVerification) @@ -16,3 +16,14 @@ class SoftwareSecurePhotoVerificationAdmin(admin.ModelAdmin): list_display = ('id', 'user', 'status', 'receipt_id', 'submitted_at', 'updated_at',) raw_id_fields = ('user', 'reviewing_user', 'copy_id_photo_from',) search_fields = ('receipt_id', 'user__username',) + + +@admin.register(SSOVerification) +class SSOVerificationAdmin(admin.ModelAdmin): + """ + Admin for the SSOVerification table. + """ + list_display = ('id', 'user', 'status', 'identity_provider_slug', 'created_at', 'updated_at',) + readonly_fields = ('user', 'identity_provider_slug', 'identity_provider_type',) + raw_id_fields = ('user',) + search_fields = ('user__username', 'identity_provider_slug',)