200 lines
5.9 KiB
Python
200 lines
5.9 KiB
Python
""" Handlers for OpenID Connect provider. """
|
|
|
|
from django.conf import settings
|
|
|
|
from courseware.access import has_access
|
|
from student.models import anonymous_id_for_user
|
|
from student.models import UserProfile
|
|
from user_api.models import UserPreference
|
|
from lang_pref import LANGUAGE_KEY
|
|
from xmodule.modulestore.django import modulestore
|
|
from xmodule.course_module import CourseDescriptor
|
|
|
|
|
|
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):
|
|
""" 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
|
|
|
|
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)
|
|
|
|
# If the user has no language specified, return the default one.
|
|
if not language:
|
|
language = getattr(settings, 'LANGUAGE_CODE')
|
|
|
|
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', []))
|
|
|
|
courses = _get_all_courses()
|
|
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
|
|
|
|
|
|
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
|