user lookup util

This commit is contained in:
Zach Hancock
2019-04-16 11:26:57 -04:00
committed by Alex Dusenbery
parent 03db25bd50
commit afe3cdb3ec
5 changed files with 257 additions and 1 deletions

View File

@@ -0,0 +1,32 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.20 on 2019-04-18 20:33
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('organizations', '0006_auto_20171207_0259'),
('third_party_auth', '0022_auto_20181012_0307'),
]
operations = [
migrations.AddField(
model_name='ltiproviderconfig',
name='organization',
field=models.OneToOneField(blank=True, help_text="optional. If this provider is an Organization, this attribute can be used reference users in that Organization", null=True, on_delete=django.db.models.deletion.CASCADE, to='organizations.Organization'),
),
migrations.AddField(
model_name='oauth2providerconfig',
name='organization',
field=models.OneToOneField(blank=True, help_text="optional. If this provider is an Organization, this attribute can be used reference users in that Organization", null=True, on_delete=django.db.models.deletion.CASCADE, to='organizations.Organization'),
),
migrations.AddField(
model_name='samlproviderconfig',
name='organization',
field=models.OneToOneField(blank=True, help_text="optional. If this provider is an Organization, this attribute can be used reference users in that Organization", null=True, on_delete=django.db.models.deletion.CASCADE, to='organizations.Organization'),
),
]

View File

@@ -16,6 +16,7 @@ from django.core.exceptions import ValidationError
from django.db import models
from django.utils import timezone
from django.utils.translation import ugettext_lazy as _
from organizations.models import Organization
from provider.oauth2.models import Client
from provider.utils import long_token
from social_core.backends.base import BaseAuth
@@ -125,6 +126,16 @@ class ProviderConfig(ConfigurationModel):
'in a separate list of "Institution" login providers.'
),
)
organization = models.OneToOneField(
Organization,
blank=True,
null=True,
on_delete=models.CASCADE,
help_text=_(
'optional. If this provider is an Organization, this attribute '
'can be used reference users in that Organization'
)
)
site = models.ForeignKey(
Site,
default=settings.SITE_ID,

View File

@@ -64,9 +64,10 @@ class Oauth2ProviderConfigAdminTest(testutil.TestCase):
# Remove the icon_image from the POST data, to simulate unchanged icon_image
post_data = models.model_to_dict(provider1)
del post_data['icon_image']
# Remove max_session_length; it has a default null value which must be POSTed
# Remove max_session_length and organization. A default null value must be POSTed
# back as an absent value, rather than as a "null-like" included value.
del post_data['max_session_length']
del post_data['organization']
# Change the name, to verify POST
post_data['name'] = 'Another name'

View File

@@ -0,0 +1,123 @@
"""
Unit tests for program_enrollments utils.
"""
from uuid import uuid4
import pytest
from django.core.cache import cache
from openedx.core.djangoapps.catalog.cache import PROGRAM_CACHE_KEY_TPL
from openedx.core.djangoapps.catalog.tests.factories import (
OrganizationFactory as CatalogOrganizationFactory, ProgramFactory
)
from openedx.core.djangolib.testing.utils import CacheIsolationTestCase
from organizations.tests.factories import OrganizationFactory
from program_enrollments.utils import (
get_user_by_program_id, ProgramDoesNotExistException, OrganizationDoesNotExistException,
ProviderDoesNotExistException
)
from social_django.models import UserSocialAuth
from student.tests.factories import UserFactory
from third_party_auth.tests.factories import SAMLProviderConfigFactory
class GetPlatformUserTests(CacheIsolationTestCase):
"""
Tests for the get_platform_user function
"""
ENABLED_CACHES = ['default']
def setUp(self):
super(GetPlatformUserTests, self).setUp()
self.program_uuid = uuid4()
self.organization_key = 'ufo'
self.external_user_id = '1234'
self.user = UserFactory.create()
self.setup_catalog_cache(self.program_uuid, self.organization_key)
def setup_catalog_cache(self, program_uuid, organization_key):
"""
helper function to initialize a cached program with an single authoring_organization
"""
catalog_org = CatalogOrganizationFactory.create(key=organization_key)
program = ProgramFactory.create(
uuid=program_uuid,
authoring_organizations=[catalog_org]
)
cache.set(PROGRAM_CACHE_KEY_TPL.format(uuid=program_uuid), program, None)
def create_social_auth_entry(self, user, provider, external_id):
"""
helper functio to create a user social auth entry
"""
UserSocialAuth.objects.create(
user=user,
uid='{0}:{1}'.format(provider.slug, external_id)
)
def test_get_user_success(self):
"""
Test lms user is successfully found
"""
organization = OrganizationFactory.create(short_name=self.organization_key)
provider = SAMLProviderConfigFactory.create(organization=organization)
self.create_social_auth_entry(self.user, provider, self.external_user_id)
user = get_user_by_program_id(self.external_user_id, self.program_uuid)
self.assertEquals(user, self.user)
def test_social_auth_user_not_created(self):
"""
None should be returned if no lms user exists for an external id
"""
organization = OrganizationFactory.create(short_name=self.organization_key)
SAMLProviderConfigFactory.create(organization=organization)
user = get_user_by_program_id(self.external_user_id, self.program_uuid)
self.assertIsNone(user)
def test_catalog_program_does_not_exist(self):
"""
Test ProgramDoesNotExistException is thrown if the program cache does
not include the requested program uuid.
"""
with pytest.raises(ProgramDoesNotExistException):
get_user_by_program_id('school-id-1234', uuid4())
def test_catalog_program_missing_org(self):
"""
Test OrganizationDoesNotExistException is thrown if the cached program does not
have an authoring organization.
"""
program = ProgramFactory.create(
uuid=self.program_uuid,
authoring_organizations=[]
)
cache.set(PROGRAM_CACHE_KEY_TPL.format(uuid=self.program_uuid), program, None)
organization = OrganizationFactory.create(short_name=self.organization_key)
provider = SAMLProviderConfigFactory.create(organization=organization)
self.create_social_auth_entry(self.user, provider, self.external_user_id)
with pytest.raises(OrganizationDoesNotExistException):
get_user_by_program_id(self.external_user_id, self.program_uuid)
def test_lms_organization_not_found(self):
"""
Test an OrganizationDoesNotExistException is thrown if the LMS has no organization
matching the catalog program's authoring_organization
"""
organization = OrganizationFactory.create(short_name='some_other_org')
provider = SAMLProviderConfigFactory.create(organization=organization)
self.create_social_auth_entry(self.user, provider, self.external_user_id)
with pytest.raises(OrganizationDoesNotExistException):
get_user_by_program_id(self.external_user_id, self.program_uuid)
def test_saml_provider_not_found(self):
"""
Test an sdf is thrown if no SAML provider exists for this program's organization
"""
OrganizationFactory.create(short_name=self.organization_key)
with pytest.raises(ProviderDoesNotExistException):
get_user_by_program_id(self.external_user_id, self.program_uuid)

View File

@@ -0,0 +1,89 @@
"""
utility functions for program enrollments
"""
import logging
from openedx.core.djangoapps.catalog.utils import get_programs
from organizations.models import Organization
from social_django.models import UserSocialAuth
log = logging.getLogger(__name__)
class UserLookupException(Exception):
pass
class ProgramDoesNotExistException(UserLookupException):
pass
class OrganizationDoesNotExistException(UserLookupException):
pass
class ProviderDoesNotExistException(UserLookupException):
pass
def get_user_by_program_id(external_user_id, program_uuid):
"""
Returns a User model for an external_user_id with a social auth entry.
Args:
external_user_id: external user id used for social auth
program_uuid: a program this user is/will be enrolled in
Returns:
A User object or None, if no user with the given external id for the given organization exists.
Raises:
ProgramDoesNotExistException if no such program exists.
OrganizationDoesNotExistException if no organization exists.
ProviderDoesNotExistException if there is no SAML provider configured for the related organization.
"""
program = get_programs(uuid=program_uuid)
if program is None:
log.error(u'Unable to find catalog program matching uuid [%s]', program_uuid)
raise ProgramDoesNotExistException
try:
org_key = program['authoring_organizations'][0]['key']
organization = Organization.objects.get(short_name=org_key)
except (KeyError, IndexError):
log.error(u'Cannot determine authoring organization key for catalog program [%s]', program_uuid)
raise OrganizationDoesNotExistException
except Organization.DoesNotExist:
log.error(u'Unable to find organization for short_name [%s]', org_key)
raise OrganizationDoesNotExistException
return get_user_by_organization(external_user_id, organization)
def get_user_by_organization(external_user_id, organization):
"""
Returns a User model for an external_user_id with a social auth entry.
This function finds a matching SAML Provider for the given organization, and looks
for a social auth entry with the provided exernal id.
Args:
external_user_id: external user id used for social auth
organization: organization providing saml authentication for this user
Returns:
A User object or None, if no user with the given external id for the given organization exists.
Raises:
ProviderDoesNotExistException if there is no SAML provider configured for the related organization.
"""
try:
provider_slug = organization.samlproviderconfig.provider_id.strip('saml-')
except Organization.samlproviderconfig.RelatedObjectDoesNotExist:
log.error(u'No SAML provider found for organization id [%s]', organization.id)
raise ProviderDoesNotExistException
try:
social_auth_uid = '{0}:{1}'.format(provider_slug, external_user_id)
return UserSocialAuth.objects.get(uid=social_auth_uid).user
except UserSocialAuth.DoesNotExist:
return None