@@ -199,14 +199,17 @@ def setUp(self):
199199 ([{"action" : permissions .VIEW_LIBRARY .identifier , "scope" : "lib:Org2:LIB2" }], [False ]),
200200 # Single permission - denied (action not assigned to user)
201201 ([{"action" : "content_libraries.edit_library" , "scope" : "lib:Org1:LIB1" }], [False ]),
202+ # Course permission - denied (user holds no course role in the scope)
203+ ([{"action" : permissions .COURSES_VIEW_COURSE .identifier , "scope" : COURSE_SCOPE_ORG1 }], [False ]),
202204 # Multiple permissions - mixed results
203205 (
204206 [
205207 {"action" : permissions .VIEW_LIBRARY .identifier , "scope" : "lib:Org1:LIB1" },
206208 {"action" : permissions .VIEW_LIBRARY .identifier , "scope" : "lib:Org2:LIB2" },
207209 {"action" : "content_libraries.edit_library" , "scope" : "lib:Org1:LIB1" },
210+ {"action" : permissions .COURSES_VIEW_COURSE .identifier , "scope" : COURSE_SCOPE_ORG1 },
208211 ],
209- [True , False , False ],
212+ [True , False , False , False ],
210213 ),
211214 )
212215 @unpack
@@ -296,14 +299,18 @@ def test_permission_validation_invalid_data(self, invalid_data: list[dict]):
296299 ([{"action" : permissions .VIEW_LIBRARY .identifier }], [True ]),
297300 # Single action the user has in no scope - denied
298301 ([{"action" : permissions .MANAGE_LIBRARY_TEAM .identifier }], [False ]),
299- # Multiple actions - mixed results
302+ # Course actions the user holds in no scope (LIBRARY_USER only) - denied
303+ ([{"action" : permissions .COURSES_VIEW_COURSE .identifier }], [False ]),
304+ ([{"action" : permissions .COURSES_EDIT_COURSE_CONTENT .identifier }], [False ]),
305+ # Multiple actions across namespaces - mixed results
300306 (
301307 [
302308 {"action" : permissions .VIEW_LIBRARY .identifier },
303309 {"action" : permissions .MANAGE_LIBRARY_TEAM .identifier },
310+ {"action" : permissions .COURSES_VIEW_COURSE .identifier },
304311 {"action" : permissions .COURSES_MANAGE_COURSE_TEAM .identifier },
305312 ],
306- [True , False , False ],
313+ [True , False , False , False ],
307314 ),
308315 )
309316 @unpack
@@ -347,6 +354,79 @@ def test_permission_validation_any_scope_staff_always_allowed(self):
347354 self .assertEqual (response .status_code , status .HTTP_200_OK )
348355 self .assertEqual (response .data , expected_response )
349356
357+ def test_permission_validation_course_scoped (self ):
358+ """Test scoped permission validation for course permissions.
359+
360+ A non-staff user assigned course_staff on a course holds the staff course
361+ permissions in that scope, but not in an unrelated course scope.
362+
363+ Expected result:
364+ - Returns 200 OK status
365+ - The scope is echoed back and the result reflects the user's course role
366+ """
367+ assign_role_to_user_in_scope (
368+ user_external_key = self .regular_user .username ,
369+ role_external_key = roles .COURSE_STAFF .external_key ,
370+ scope_external_key = COURSE_SCOPE_ORG1 ,
371+ )
372+ self .client .force_authenticate (user = self .regular_user )
373+ request_data = [
374+ # Held in the assigned scope
375+ {"action" : permissions .COURSES_VIEW_COURSE .identifier , "scope" : COURSE_SCOPE_ORG1 },
376+ # course_staff does not manage the course team
377+ {"action" : permissions .COURSES_MANAGE_COURSE_TEAM .identifier , "scope" : COURSE_SCOPE_ORG1 },
378+ # Not held in an unrelated course scope
379+ {
380+ "action" : permissions .COURSES_VIEW_COURSE .identifier ,
381+ "scope" : "course-v1:Org2+COURSE2+2024" ,
382+ },
383+ ]
384+ expected_response = [
385+ {** request_data [0 ], "allowed" : True },
386+ {** request_data [1 ], "allowed" : False },
387+ {** request_data [2 ], "allowed" : False },
388+ ]
389+
390+ response = self .client .post (self .url , data = request_data , format = "json" )
391+
392+ self .assertEqual (response .status_code , status .HTTP_200_OK )
393+ self .assertEqual (response .data , expected_response )
394+
395+ def test_permission_validation_any_scope_course_permissions (self ):
396+ """Test any-scope permission validation for course permissions.
397+
398+ A non-staff user assigned course_staff in at least one course scope holds the
399+ staff course permissions across any scope, but not the permissions the role
400+ never grants.
401+
402+ Expected result:
403+ - Returns 200 OK status
404+ - Response omits the scope key and reports the any-scope result
405+ """
406+ assign_role_to_user_in_scope (
407+ user_external_key = self .regular_user .username ,
408+ role_external_key = roles .COURSE_STAFF .external_key ,
409+ scope_external_key = COURSE_SCOPE_ORG1 ,
410+ )
411+ self .client .force_authenticate (user = self .regular_user )
412+ request_data = [
413+ # Held in some scope via the course_staff role
414+ {"action" : permissions .COURSES_VIEW_COURSE .identifier },
415+ {"action" : permissions .COURSES_EDIT_COURSE_CONTENT .identifier },
416+ # Never granted by course_staff
417+ {"action" : permissions .COURSES_MANAGE_COURSE_TEAM .identifier },
418+ ]
419+ expected_response = [
420+ {"action" : permissions .COURSES_VIEW_COURSE .identifier , "allowed" : True },
421+ {"action" : permissions .COURSES_EDIT_COURSE_CONTENT .identifier , "allowed" : True },
422+ {"action" : permissions .COURSES_MANAGE_COURSE_TEAM .identifier , "allowed" : False },
423+ ]
424+
425+ response = self .client .post (self .url , data = request_data , format = "json" )
426+
427+ self .assertEqual (response .status_code , status .HTTP_200_OK )
428+ self .assertEqual (response .data , expected_response )
429+
350430 def test_permission_validation_unauthenticated (self ):
351431 """Test permission validation without authentication.
352432
0 commit comments