Merge pull request #17908 from edx/bexline/sso_id_verification

ENT-944 Create SSOVerifications for users in tpa pipeline based on provider's settings
This commit is contained in:
Brittney Exline
2018-04-23 08:46:26 -06:00
committed by GitHub
6 changed files with 181 additions and 1 deletions

View File

@@ -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.'),
),
]

View File

@@ -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 {}

View File

@@ -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,
)

View File

@@ -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',
]

View File

@@ -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

View File

@@ -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',)