2020from rest_framework .test import APIClient
2121
2222from openedx_authz import api
23+ from openedx_authz .api .data import (
24+ OrgContentLibraryGlobData ,
25+ OrgCourseOverviewGlobData ,
26+ )
2327from openedx_authz .api .users import assign_role_to_user_in_scope
2428from openedx_authz .constants import permissions , roles
2529from openedx_authz .models .scopes import get_content_library_model , get_course_overview_model
@@ -937,10 +941,20 @@ def setUp(self):
937941
938942 # Default combined result used by most tests.
939943 self .fake_scopes = [
940- {"scope_id" : self .COURSE_ORG1 , "display_name" : "Course Org1" , "org_name" : "Org1" , "scope_type" : "course" },
941- {"scope_id" : "LIB1" , "display_name" : "Library LIB1" , "org_name" : "Org1" , "scope_type" : "library" },
942- {"scope_id" : self .COURSE_ORG2 , "display_name" : "Course Org2" , "org_name" : "Org2" , "scope_type" : "course" },
943- {"scope_id" : "LIB2" , "display_name" : "Library LIB2" , "org_name" : "Org2" , "scope_type" : "library" },
944+ {
945+ "scope_id" : self .COURSE_ORG1 ,
946+ "display_name_col" : "Course Org1" ,
947+ "org_name" : "Org1" ,
948+ "scope_type" : "course" ,
949+ },
950+ {"scope_id" : "LIB1" , "display_name_col" : "Library LIB1" , "org_name" : "Org1" , "scope_type" : "library" },
951+ {
952+ "scope_id" : self .COURSE_ORG2 ,
953+ "display_name_col" : "Course Org2" ,
954+ "org_name" : "Org2" ,
955+ "scope_type" : "course" ,
956+ },
957+ {"scope_id" : "LIB2" , "display_name_col" : "Library LIB2" , "org_name" : "Org2" , "scope_type" : "library" },
944958 ]
945959
946960 # Patch _build_queryset so tests don't need real DB querysets.
@@ -955,21 +969,26 @@ def setUp(self):
955969 # The stub ContentLibrary uses 'title' directly instead of 'learning_package__title'.
956970 # Patch _get_libraries_queryset to use the stub-compatible field, aliased as 'display_name'
957971 # to match the column name the union and serializer expect.
958- def stub_get_libraries_queryset (_ , allowed_pairs = None , search = "" ): # pylint: disable=unused-argument
972+ def stub_get_libraries_queryset (_ , allowed_pairs = None , allowed_orgs = None , search = "" ): # pylint: disable=unused-argument
959973 qs = ContentLibrary .objects
960- if allowed_pairs is not None :
961- if not allowed_pairs :
974+ if allowed_pairs is not None or allowed_orgs is not None :
975+ org_filter = Q (org__short_name__in = allowed_orgs ) if allowed_orgs else Q ()
976+ pair_filter = (
977+ reduce (operator .or_ , (Q (org__short_name = org , slug = slug ) for org , slug in allowed_pairs ))
978+ if allowed_pairs
979+ else Q ()
980+ )
981+ combined = org_filter | pair_filter
982+ if not combined :
962983 qs = qs .none ()
963984 else :
964- qs = qs .filter (
965- reduce (operator .or_ , (Q (org__short_name = org , slug = slug ) for org , slug in allowed_pairs ))
966- )
985+ qs = qs .filter (combined )
967986 return qs .annotate (
968987 scope_id = Cast ("slug" , output_field = CharField ()),
969- display_name = Cast ("title" , output_field = CharField ()),
988+ display_name_col = Cast ("title" , output_field = CharField ()),
970989 org_name = Cast ("org__short_name" , output_field = CharField ()),
971990 scope_type = Value ("library" , output_field = CharField ()),
972- ).values ("scope_id" , "display_name " , "org_name" , "scope_type" )
991+ ).values ("scope_id" , "display_name_col " , "org_name" , "scope_type" )
973992
974993 self .libraries_qs_patcher = patch .object (
975994 views .AdminConsoleScopesAPIView ,
@@ -1103,9 +1122,9 @@ def test_search_filters_by_display_name(self):
11031122 def test_pagination (self , query_params : dict , expected_page_count : int , has_next : bool ):
11041123 """Results are paginated correctly."""
11051124 mixed = [
1106- {"scope_id" : self .COURSE_ORG1 , "display_name " : "Course 1" , "org_name" : "Org1" , "scope_type" : "course" },
1107- {"scope_id" : "LIB1" , "display_name " : "Library 1" , "org_name" : "Org1" , "scope_type" : "library" },
1108- {"scope_id" : self .COURSE_ORG2 , "display_name " : "Course 2" , "org_name" : "Org2" , "scope_type" : "course" },
1125+ {"scope_id" : self .COURSE_ORG1 , "display_name_col " : "Course 1" , "org_name" : "Org1" , "scope_type" : "course" },
1126+ {"scope_id" : "LIB1" , "display_name_col " : "Library 1" , "org_name" : "Org1" , "scope_type" : "library" },
1127+ {"scope_id" : self .COURSE_ORG2 , "display_name_col " : "Course 2" , "org_name" : "Org2" , "scope_type" : "course" },
11091128 ]
11101129 self .build_qs_patcher .stop ()
11111130 with patch .object (views .AdminConsoleScopesAPIView , "_build_queryset" , return_value = mixed ):
@@ -1266,6 +1285,48 @@ def test_empty_allowed_library_pairs_returns_no_libraries(self):
12661285 self .assertEqual (response .status_code , status .HTTP_200_OK )
12671286 self .assertEqual (response .data ["count" ], 0 )
12681287
1288+ def test_org_glob_scope_returns_all_org_libraries (self ):
1289+ """A user with an org-level glob permission (lib:ORG:*) sees all libraries in that org."""
1290+ user = User .objects .get (username = "regular_1" )
1291+ self .client .force_authenticate (user = user )
1292+ self .build_qs_patcher .stop ()
1293+
1294+ # Simulate get_scopes_for_user_and_permission returning an org-level glob.
1295+ glob_scope = OrgContentLibraryGlobData (external_key = "lib:Org1:*" )
1296+ with patch (
1297+ "openedx_authz.rest_api.v1.views.get_scopes_for_user_and_permission" ,
1298+ return_value = [glob_scope ],
1299+ ):
1300+ response = self .client .get (self .url , {"type" : "library" })
1301+
1302+ self .build_qs_patcher .start ()
1303+ self .assertEqual (response .status_code , status .HTTP_200_OK )
1304+ external_keys = [item ["external_key" ] for item in response .data ["results" ]]
1305+ self .assertIn (self .LIBRARY_ORG1 , external_keys )
1306+ self .assertNotIn (self .LIBRARY_ORG2 , external_keys )
1307+
1308+ def test_org_glob_scope_returns_all_org_courses (self ):
1309+ """A user with an org-level glob permission (course-v1:ORG+*) sees all courses in that org."""
1310+ user = User .objects .get (username = "regular_9" )
1311+ self .client .force_authenticate (user = user )
1312+ self .build_qs_patcher .stop ()
1313+ self .libraries_qs_patcher .stop ()
1314+
1315+ # Simulate get_scopes_for_user_and_permission returning an org-level glob.
1316+ glob_scope = OrgCourseOverviewGlobData (external_key = "course-v1:Org1+*" )
1317+ with patch (
1318+ "openedx_authz.rest_api.v1.views.get_scopes_for_user_and_permission" ,
1319+ return_value = [glob_scope ],
1320+ ):
1321+ response = self .client .get (self .url , {"type" : "course" })
1322+
1323+ self .libraries_qs_patcher .start ()
1324+ self .build_qs_patcher .start ()
1325+ self .assertEqual (response .status_code , status .HTTP_200_OK )
1326+ external_keys = [item ["external_key" ] for item in response .data ["results" ]]
1327+ self .assertIn (self .COURSE_ORG1 , external_keys )
1328+ self .assertNotIn (self .COURSE_ORG2 , external_keys )
1329+
12691330 def test_manage_permission_only_uses_manage_permission (self ):
12701331 """management_permission_only=true calls get_admin_manage_permission, not get_admin_view_permission."""
12711332 user = User .objects .get (username = "regular_1" )
0 commit comments