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

3
from django.conf import settings
4
from django.core.cache import cache
5

6
from courseware.access import has_access
7
from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
8
from openedx.core.djangoapps.lang_pref import LANGUAGE_KEY
9
from openedx.core.djangoapps.user_api.models import UserPreference
10 11
from student.models import UserProfile, anonymous_id_for_user
from student.roles import CourseInstructorRole, CourseStaffRole, GlobalStaff
12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35


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


36 37 38 39 40 41 42 43 44 45 46 47 48 49 50
class PermissionsHandler(object):
    """ Permissions scope handler """

    def scope_permissions(self, _data):
        return ['administrator']

    def claim_administrator(self, data):
        """
        Return boolean indicating user's administrator status.

        For our purposes an administrator is any user with is_staff set to True.
        """
        return data['user'].is_staff


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

    def scope_profile(self, _data):
55
        """ Add specialized claims. """
56
        return ['name', 'locale', 'user_tracking_id']
57 58 59 60 61 62

    def claim_name(self, data):
        """ User displayable full name. """
        user = data['user']
        profile = UserProfile.objects.get(user=user)
        return profile.name
63 64 65 66 67 68 69

    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.
        """

70 71
        # Calling UserPreference directly because it is not clear which user made the request.
        language = UserPreference.get_value(data['user'], LANGUAGE_KEY)
72 73 74

        # If the user has no language specified, return the default one.
        if not language:
75
            language = settings.LANGUAGE_CODE
76

77 78
        return language

79 80 81 82
    def claim_user_tracking_id(self, data):
        """ User tracking ID. """
        return data['user'].id

83 84 85 86 87 88 89

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
90
    `staff_courses` that lists the course_ids for which the user has instructor
91 92
    or staff privileges.

93 94 95 96 97
    The claims support claim request values: if there is no claim request, the
    value of the claim is the list all the courses for which the user has the
    corresponding privileges. If a claim request is used, then the value of the
    claim the list of courses from the requested values that have the
    corresponding privileges.
98 99

    For example, if the user is staff of course_a and course_b but not
100
    course_c, the claim corresponding to the scope request:
101 102 103

        scope = openid course_staff

104
    has the value:
105 106 107

        {staff_courses: [course_a, course_b] }

108
    For the claim request:
109

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

112
    the corresponding claim will have the value:
113 114 115

        {staff_courses: [course_b] }.

116 117
    This is useful to quickly determine if a user has the right privileges for a
    given course.
118 119 120

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

121
        `edx_oauth2_provider/oidc/handlers.py`
122 123 124

    """

125 126 127 128 129
    COURSE_CACHE_TIMEOUT = getattr(settings, 'OIDC_COURSE_HANDLER_CACHE_TIMEOUT', 60)  # In seconds.

    def __init__(self, *_args, **_kwargs):
        self._course_cache = {}

130 131 132 133 134 135 136
    def scope_course_instructor(self, data):
        """
        Scope `course_instructor` valid only if the user is an instructor
        of at least one course.

        """

137 138 139
        # TODO: unfortunately there is not a faster and still correct way to
        # check if a user is instructor of at least one course other than
        # checking the access type against all known courses.
140
        course_ids = self.find_courses(data['user'], CourseInstructorRole.ROLE)
141 142 143 144 145 146 147 148
        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.

        """
149
        # TODO: see :method:CourseAccessHandler.scope_course_instructor
150
        course_ids = self.find_courses(data['user'], CourseStaffRole.ROLE)
151 152 153 154 155 156 157 158 159

        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.

        """
160

161
        return self.find_courses(data['user'], CourseInstructorRole.ROLE, data.get('values'))
162 163 164 165 166 167 168 169

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

        """

170
        return self.find_courses(data['user'], CourseStaffRole.ROLE, data.get('values'))
171 172

    def find_courses(self, user, access_type, values=None):
173
        """
174 175
        Find all courses for which the user has the specified access type. If
        `values` is specified, check only the courses from `values`.
176

177
        """
178

179 180 181
        # Check the instance cache and update if not present.  The instance
        # cache is useful since there are multiple scope and claims calls in the
        # same request.
182

183 184 185 186 187 188
        key = (user.id, access_type)
        if key in self._course_cache:
            course_ids = self._course_cache[key]
        else:
            course_ids = self._get_courses_with_access_type(user, access_type)
            self._course_cache[key] = course_ids
189

190 191 192
        # If values was specified, filter out other courses.
        if values is not None:
            course_ids = list(set(course_ids) & set(values))
193

194
        return course_ids
195

196
    # pylint: disable=missing-docstring
197 198 199
    def _get_courses_with_access_type(self, user, access_type):
        # Check the application cache and update if not present. The application
        # cache is useful since there are calls to different endpoints in close
200
        # succession, for example the id_token and user_info endpoints.
201

202 203
        key = '-'.join([str(self.__class__), str(user.id), access_type])
        course_ids = cache.get(key)
204

205
        if not course_ids:
206
            course_keys = CourseOverview.get_all_course_keys()
207 208 209

            # Global staff have access to all courses. Filter courses for non-global staff.
            if not GlobalStaff().has_user(user):
210
                course_keys = [course_key for course_key in course_keys if has_access(user, access_type, course_key)]
211

212
            course_ids = [unicode(course_key) for course_key in course_keys]
213

214
            cache.set(key, course_ids, self.COURSE_CACHE_TIMEOUT)
215

216
        return course_ids
217

218

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

222
    def claim_instructor_courses(self, data):
223 224 225 226 227
        # Don't return list of courses unless they are requested as essential.
        if data.get('essential'):
            return super(IDTokenHandler, self).claim_instructor_courses(data)
        else:
            return None
228 229

    def claim_staff_courses(self, data):
230 231 232 233 234
        # Don't return list of courses unless they are requested as essential.
        if data.get('essential'):
            return super(IDTokenHandler, self).claim_staff_courses(data)
        else:
            return None
235 236


237
class UserInfoHandler(OpenIDHandler, ProfileHandler, CourseAccessHandler, PermissionsHandler):
238 239
    """ Configure the UserInfo handler for the LMS. """
    pass