users.py 5.13 KB
Newer Older
1 2 3 4 5 6 7 8 9
"""
LTI user management functionality. This module reconciles the two identities
that an individual has in the campus LMS platform and on edX.
"""

import string
import random
import uuid

10 11
from django.conf import settings
from django.contrib.auth import authenticate, login
12
from django.contrib.auth.models import User
13
from django.core.exceptions import PermissionDenied
14
from django.db import IntegrityError, transaction
15
from lti_provider.models import LtiUser
16
from student.models import UserProfile
17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40


def authenticate_lti_user(request, lti_user_id, lti_consumer):
    """
    Determine whether the user specified by the LTI launch has an existing
    account. If not, create a new Django User model and associate it with an
    LtiUser object.

    If the currently logged-in user does not match the user specified by the LTI
    launch, log out the old user and log in the LTI identity.
    """
    try:
        lti_user = LtiUser.objects.get(
            lti_user_id=lti_user_id,
            lti_consumer=lti_consumer
        )
    except LtiUser.DoesNotExist:
        # This is the first time that the user has been here. Create an account.
        lti_user = create_lti_user(lti_user_id, lti_consumer)

    if not (request.user.is_authenticated() and
            request.user == lti_user.edx_user):
        # The user is not authenticated, or is logged in as somebody else.
        # Switch them to the LTI user
41
        switch_user(request, lti_user, lti_consumer)
42 43 44 45 46 47 48 49 50 51 52 53 54


def create_lti_user(lti_user_id, lti_consumer):
    """
    Generate a new user on the edX platform with a random username and password,
    and associates that account with the LTI identity.
    """
    edx_password = str(uuid.uuid4())

    created = False
    while not created:
        try:
            edx_user_id = generate_random_edx_username()
55
            edx_email = "{}@{}".format(edx_user_id, settings.LTI_USER_EMAIL_DOMAIN)
56 57 58 59 60 61 62 63 64 65 66
            with transaction.atomic():
                edx_user = User.objects.create_user(
                    username=edx_user_id,
                    password=edx_password,
                    email=edx_email,
                )
                # A profile is required if PREVENT_CONCURRENT_LOGINS flag is set.
                # TODO: We could populate user information from the LTI launch here,
                # but it's not necessary for our current uses.
                edx_user_profile = UserProfile(user=edx_user)
                edx_user_profile.save()
67 68 69 70 71 72 73 74 75 76 77 78 79 80 81
            created = True
        except IntegrityError:
            # The random edx_user_id wasn't unique. Since 'created' is still
            # False, we will retry with a different random ID.
            pass

    lti_user = LtiUser(
        lti_consumer=lti_consumer,
        lti_user_id=lti_user_id,
        edx_user=edx_user
    )
    lti_user.save()
    return lti_user


82
def switch_user(request, lti_user, lti_consumer):
83 84 85 86
    """
    Log out the current user, and log in using the edX identity associated with
    the LTI ID.
    """
87 88 89 90 91 92 93 94 95 96
    edx_user = authenticate(
        username=lti_user.edx_user.username,
        lti_user_id=lti_user.lti_user_id,
        lti_consumer=lti_consumer
    )
    if not edx_user:
        # This shouldn't happen, since we've created edX accounts for any LTI
        # users by this point, but just in case we can return a 403.
        raise PermissionDenied()
    login(request, edx_user)
97 98 99 100 101 102 103 104 105 106 107 108 109


def generate_random_edx_username():
    """
    Create a valid random edX user ID. An ID is at most 30 characters long, and
    can contain upper and lowercase letters and numbers.
    :return:
    """
    allowable_chars = string.ascii_letters + string.digits
    username = ''
    for _index in range(30):
        username = username + random.SystemRandom().choice(allowable_chars)
    return username
110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152


class LtiBackend(object):
    """
    A Django authentication backend that authenticates users via LTI. This
    backend will only return a User object if it is associated with an LTI
    identity (i.e. the user was created by the create_lti_user method above).
    """

    def authenticate(self, username=None, lti_user_id=None, lti_consumer=None):
        """
        Try to authenticate a user. This method will return a Django user object
        if a user with the corresponding username exists in the database, and
        if a record that links that user with an LTI user_id field exists in
        the LtiUser collection.

        If such a user is not found, the method returns None (in line with the
        authentication backend specification).
        """
        try:
            edx_user = User.objects.get(username=username)
        except User.DoesNotExist:
            return None

        try:
            LtiUser.objects.get(
                edx_user_id=edx_user.id,
                lti_user_id=lti_user_id,
                lti_consumer=lti_consumer
            )
        except LtiUser.DoesNotExist:
            return None
        return edx_user

    def get_user(self, user_id):
        """
        Return the User object for a user that has already been authenticated by
        this backend.
        """
        try:
            return User.objects.get(id=user_id)
        except User.DoesNotExist:
            return None