handlers.py 5.93 KB
Newer Older
1 2
""" Handlers for OpenID Connect provider. """

3 4
from django.conf import settings

5 6
from courseware.access import has_access
from student.models import anonymous_id_for_user
7
from student.models import UserProfile
8 9
from user_api.models import UserPreference
from lang_pref import LANGUAGE_KEY
10 11
from xmodule.modulestore.django import modulestore
from xmodule.course_module import CourseDescriptor
12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39


class OpenIDHandler(object):
    """ Basic OpenID Connect scope handler. """

    def scope_openid(self, _data):
        """ Only override the sub (subject) claim. """
        return ['sub']

    def claim_sub(self, data):
        """
        Return the value of the sub (subject) claim. The value should be
        unique for each user.

        """

        # Use the anonymous ID without any course as unique identifier.
        # Note that this ID is derived using the value of the `SECRET_KEY`
        # setting, this means that users will have different sub
        # values for different deployments.
        value = anonymous_id_for_user(data['user'], None)
        return value


class ProfileHandler(object):
    """ Basic OpenID Connect `profile` scope handler with `locale` claim. """

    def scope_profile(self, _data):
40 41 42 43 44 45 46 47
        """ Add specialized claims. """
        return ['name', 'locale']

    def claim_name(self, data):
        """ User displayable full name. """
        user = data['user']
        profile = UserProfile.objects.get(user=user)
        return profile.name
48 49 50 51 52 53 54 55 56

    def claim_locale(self, data):
        """
        Return the locale for the users based on their preferences.
        Does not return a value if the users have not set their locale preferences.

        """

        language = UserPreference.get_preference(data['user'], LANGUAGE_KEY)
57 58 59 60 61

        # If the user has no language specified, return the default one.
        if not language:
            language = getattr(settings, 'LANGUAGE_CODE')

62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 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 153 154
        return language


class CourseAccessHandler(object):
    """
    Defines two new scopes: `course_instructor` and `course_staff`. Each one is
    valid only if the user is instructor or staff of at least one course.

    Each new scope has a corresponding claim: `instructor_courses` and
    `staff_courses` that lists the course_ids for which the user as instructor
    or staff privileges.

    The claims support claim request values. In other words, if no claim is
    requested it returns all the courses for the corresponding privileges. If a
    claim request is used, then it only returns the from the list of requested
    values that have the corresponding privileges.

    For example, if the user is staff of course_a and course_b but not
    course_c, the request:

        scope = openid course_staff

    will return:

        {staff_courses: [course_a, course_b] }

    If the request is:

        claims = {userinfo: {staff_courses=[course_b, course_d]}}

    the result will be:

        {staff_courses: [course_b] }.

    This is useful to quickly determine if a user has the right
    privileges for a given course.

    For a description of the function naming and arguments, see:

        `oauth2_provider/oidc/handlers.py`

    """

    def scope_course_instructor(self, data):
        """
        Scope `course_instructor` valid only if the user is an instructor
        of at least one course.

        """

        course_ids = self._courses_with_access_type(data, 'instructor')
        return ['instructor_courses'] if course_ids else None

    def scope_course_staff(self, data):
        """
        Scope `course_staff` valid only if the user is an instructor of at
        least one course.

        """

        course_ids = self._courses_with_access_type(data, 'staff')
        return ['staff_courses'] if course_ids else None

    def claim_instructor_courses(self, data):
        """
        Claim `instructor_courses` with list of course_ids for which the
        user has instructor privileges.

        """
        return self._courses_with_access_type(data, 'instructor')

    def claim_staff_courses(self, data):
        """
        Claim `staff_courses` with list of course_ids for which the user
        has staff privileges.

        """
        return self._courses_with_access_type(data, 'staff')

    def _courses_with_access_type(self, data, access_type):
        """
        Utility function to list all courses for a user according to the
        access type.

        The field `data` follows the handler specification in:

            `oauth2_provider/oidc/handlers.py`

        """

        user = data['user']
        values = set(data.get('values', []))

155
        courses = _get_all_courses()
156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189
        courses = (c for c in courses if has_access(user, access_type, c))
        course_ids = (unicode(c.id) for c in courses)

        # If values was provided, return only the requested authorized courses
        if values:
            return [c for c in course_ids if c in values]
        else:
            return [c for c in course_ids]


class IDTokenHandler(OpenIDHandler, ProfileHandler, CourseAccessHandler):
    """
    Configure the ID Token handler for the LMS.

    Note that the values of the claims `instructor_courses` and
    `staff_courses` are not included in the ID Token. The rationale is
    that for global staff, the list of courses returned could be very
    large. Instead they could check for specific courses using the
    UserInfo endpoint.

    """

    def claim_instructor_courses(self, data):
        # Don't return list of courses in ID Tokens
        return None

    def claim_staff_courses(self, data):
        # Don't return list of courses in ID Tokens
        return None


class UserInfoHandler(OpenIDHandler, ProfileHandler, CourseAccessHandler):
    """ Configure the UserInfo handler for the LMS. """
    pass
190 191 192 193 194 195 196 197 198 199


def _get_all_courses():
    """
    Utitilty function to list all available courses.

    """
    ms_courses = modulestore().get_courses()
    courses = [c for c in ms_courses if isinstance(c, CourseDescriptor)]
    return courses