1
1
""" Handlers for OpenID Connect provider. """
2
2
3
3
from django .conf import settings
4
+ from django .core .cache import cache
4
5
5
6
from courseware .access import has_access
6
7
from student .models import anonymous_id_for_user
@@ -68,48 +69,57 @@ class CourseAccessHandler(object):
68
69
valid only if the user is instructor or staff of at least one course.
69
70
70
71
Each new scope has a corresponding claim: `instructor_courses` and
71
- `staff_courses` that lists the course_ids for which the user as instructor
72
+ `staff_courses` that lists the course_ids for which the user has instructor
72
73
or staff privileges.
73
74
74
- The claims support claim request values. In other words, if no claim is
75
- requested it returns all the courses for the corresponding privileges. If a
76
- claim request is used, then it only returns the from the list of requested
77
- values that have the corresponding privileges.
75
+ The claims support claim request values: if there is no claim request, the
76
+ value of the claim is the list all the courses for which the user has the
77
+ corresponding privileges. If a claim request is used, then the value of the
78
+ claim the list of courses from the requested values that have the
79
+ corresponding privileges.
78
80
79
81
For example, if the user is staff of course_a and course_b but not
80
- course_c, the request:
82
+ course_c, the claim corresponding to the scope request:
81
83
82
84
scope = openid course_staff
83
85
84
- will return :
86
+ has the value :
85
87
86
88
{staff_courses: [course_a, course_b] }
87
89
88
- If the request is :
90
+ For the claim request :
89
91
90
- claims = {userinfo: {staff_courses=[course_b, course_d]}}
92
+ claims = {userinfo: {staff_courses: {values =[course_b, course_d]} }}
91
93
92
- the result will be :
94
+ the corresponding claim will have the value :
93
95
94
96
{staff_courses: [course_b] }.
95
97
96
- This is useful to quickly determine if a user has the right
97
- privileges for a given course.
98
+ This is useful to quickly determine if a user has the right privileges for a
99
+ given course.
98
100
99
101
For a description of the function naming and arguments, see:
100
102
101
103
`oauth2_provider/oidc/handlers.py`
102
104
103
105
"""
104
106
107
+ COURSE_CACHE_TIMEOUT = getattr (settings , 'OIDC_COURSE_HANDLER_CACHE_TIMEOUT' , 60 ) # In seconds.
108
+
109
+ def __init__ (self , * _args , ** _kwargs ):
110
+ self ._course_cache = {}
111
+
105
112
def scope_course_instructor (self , data ):
106
113
"""
107
114
Scope `course_instructor` valid only if the user is an instructor
108
115
of at least one course.
109
116
110
117
"""
111
118
112
- course_ids = self ._courses_with_access_type (data , 'instructor' )
119
+ # TODO: unfortunately there is not a faster and still correct way to
120
+ # check if a user is instructor of at least one course other than
121
+ # checking the access type against all known courses.
122
+ course_ids = self .find_courses (data ['user' ], 'instructor' )
113
123
return ['instructor_courses' ] if course_ids else None
114
124
115
125
def scope_course_staff (self , data ):
@@ -118,8 +128,9 @@ def scope_course_staff(self, data):
118
128
least one course.
119
129
120
130
"""
131
+ # TODO: see :method:CourseAccessHandler.scope_course_instructor
132
+ course_ids = self .find_courses (data ['user' ], 'staff' )
121
133
122
- course_ids = self ._courses_with_access_type (data , 'staff' )
123
134
return ['staff_courses' ] if course_ids else None
124
135
125
136
def claim_instructor_courses (self , data ):
@@ -128,60 +139,75 @@ def claim_instructor_courses(self, data):
128
139
user has instructor privileges.
129
140
130
141
"""
131
- return self ._courses_with_access_type (data , 'instructor' )
142
+
143
+ return self .find_courses (data ['user' ], 'instructor' , data .get ('values' ))
132
144
133
145
def claim_staff_courses (self , data ):
134
146
"""
135
147
Claim `staff_courses` with list of course_ids for which the user
136
148
has staff privileges.
137
149
138
150
"""
139
- return self ._courses_with_access_type (data , 'staff' )
140
151
141
- def _courses_with_access_type (self , data , access_type ):
152
+ return self .find_courses (data ['user' ], 'staff' , data .get ('values' ))
153
+
154
+ def find_courses (self , user , access_type , values = None ):
142
155
"""
143
- Utility function to list all courses for a user according to the
144
- access type .
156
+ Find all courses for which the user has the specified access type. If
157
+ `values` is specified, check only the courses from `values` .
145
158
146
- The field `data` follows the handler specification in:
159
+ """
147
160
148
- `oauth2_provider/oidc/handlers.py`
161
+ # Check the instance cache and update if not present. The instance
162
+ # cache is useful since there are multiple scope and claims calls in the
163
+ # same request.
149
164
150
- """
165
+ key = (user .id , access_type )
166
+ if key in self ._course_cache :
167
+ course_ids = self ._course_cache [key ]
168
+ else :
169
+ course_ids = self ._get_courses_with_access_type (user , access_type )
170
+ self ._course_cache [key ] = course_ids
151
171
152
- user = data ['user' ]
153
- values = set (data .get ('values' , []))
172
+ # If values was specified, filter out other courses.
173
+ if values is not None :
174
+ course_ids = list (set (course_ids ) & set (values ))
154
175
155
- courses = _get_all_courses ()
156
- courses = (c for c in courses if has_access (user , access_type , c ))
157
- course_ids = (unicode (c .id ) for c in courses )
176
+ return course_ids
158
177
159
- # If values was provided, return only the requested authorized courses
160
- if values :
161
- return [c for c in course_ids if c in values ]
162
- else :
163
- return [c for c in course_ids ]
178
+ # pylint: disable=missing-docstring
179
+ def _get_courses_with_access_type (self , user , access_type ):
164
180
181
+ # Check the application cache and update if not present. The application
182
+ # cache is useful since there are calls to different endpoints in close
183
+ # succession, for example the id_token and user_info endpoins.
165
184
166
- class IDTokenHandler (OpenIDHandler , ProfileHandler , CourseAccessHandler ):
167
- """
168
- Configure the ID Token handler for the LMS.
185
+ key = '-' .join ([str (self .__class__ ), str (user .id ), access_type ])
186
+ course_ids = cache .get (key )
169
187
170
- Note that the values of the claims `instructor_courses` and
171
- `staff_courses` are not included in the ID Token. The rationale is
172
- that for global staff, the list of courses returned could be very
173
- large. Instead they could check for specific courses using the
174
- UserInfo endpoint.
188
+ if course_ids is None :
189
+ course_ids = [unicode (course .id ) for course in _get_all_courses ()
190
+ if has_access (user , access_type , course )]
191
+ cache .set (key , course_ids , self .COURSE_CACHE_TIMEOUT )
175
192
176
- """
193
+ return course_ids
177
194
195
+
196
+ class IDTokenHandler (OpenIDHandler , ProfileHandler , CourseAccessHandler ):
197
+ """ Configure the ID Token handler for the LMS. """
178
198
def claim_instructor_courses (self , data ):
179
- # Don't return list of courses in ID Tokens
180
- return None
199
+ # Don't return list of courses unless they are requested as essential.
200
+ if data .get ('essential' ):
201
+ return super (IDTokenHandler , self ).claim_instructor_courses (data )
202
+ else :
203
+ return None
181
204
182
205
def claim_staff_courses (self , data ):
183
- # Don't return list of courses in ID Tokens
184
- return None
206
+ # Don't return list of courses unless they are requested as essential.
207
+ if data .get ('essential' ):
208
+ return super (IDTokenHandler , self ).claim_staff_courses (data )
209
+ else :
210
+ return None
185
211
186
212
187
213
class UserInfoHandler (OpenIDHandler , ProfileHandler , CourseAccessHandler ):
@@ -194,6 +220,8 @@ def _get_all_courses():
194
220
Utitilty function to list all available courses.
195
221
196
222
"""
223
+
197
224
ms_courses = modulestore ().get_courses ()
198
225
courses = [c for c in ms_courses if isinstance (c , CourseDescriptor )]
226
+
199
227
return courses
0 commit comments