@@ -1573,6 +1573,109 @@ def test_authz_scope_q_object_matches_exact_org_slug_pairs(self):
15731573 f"Result must contain exactly { expected_pairs } , got { result_pairs } " ,
15741574 )
15751575
1576+ def test_authz_scope_with_combined_authz_and_legacy_permissions (self ):
1577+ """
1578+ Test that the filter returns libraries when user has BOTH AuthZ AND legacy permissions.
1579+
1580+ The CAN_VIEW_THIS_CONTENT_LIBRARY permission uses OR logic:
1581+ is_user_active & (
1582+ is_global_staff |
1583+ (allow_public_read & is_course_creator) |
1584+ HasPermissionInContentLibraryScope(VIEW_LIBRARY) | # AuthZ
1585+ has_explicit_read_permission_for_library # Legacy
1586+ )
1587+
1588+ This means a user with BOTH types of permissions should get access through EITHER system.
1589+
1590+ Test scenario:
1591+ - lib1: User has AuthZ permission only
1592+ - lib2: User has legacy permission only
1593+ - lib3: User has BOTH AuthZ AND legacy permissions
1594+ - lib4: User has NO permissions
1595+
1596+ Expected behavior:
1597+ - Filter returns lib1, lib2, and lib3 (NOT lib4)
1598+ - Having both permission types doesn't break filtering
1599+ - Each permission system contributes its authorized libraries
1600+ """
1601+ user = UserFactory .create (username = "combined_perm_user" , is_staff = False )
1602+
1603+ Organization .objects .get_or_create (short_name = "comb-org" , defaults = {"name" : "Combined Org" })
1604+
1605+ with self .as_user (self .admin_user ):
1606+ lib1 = self ._create_library (slug = "comb-lib1" , org = "comb-org" , title = "AuthZ Only Library" )
1607+ lib2 = self ._create_library (slug = "comb-lib2" , org = "comb-org" , title = "Legacy Only Library" )
1608+ lib3 = self ._create_library (slug = "comb-lib3" , org = "comb-org" , title = "Both AuthZ and Legacy Library" )
1609+ lib4 = self ._create_library (slug = "comb-lib4" , org = "comb-org" , title = "No Permissions Library" )
1610+
1611+ # Retrieve library objects for permission assignment
1612+ lib1_obj = ContentLibrary .objects .get_by_key (LibraryLocatorV2 .from_string (lib1 ['id' ]))
1613+ lib2_obj = ContentLibrary .objects .get_by_key (LibraryLocatorV2 .from_string (lib2 ['id' ]))
1614+ lib3_obj = ContentLibrary .objects .get_by_key (LibraryLocatorV2 .from_string (lib3 ['id' ]))
1615+
1616+ # Set up legacy permissions: lib2 (legacy only), lib3 (both)
1617+ ContentLibraryPermission .objects .create (
1618+ library = lib2_obj ,
1619+ user = user ,
1620+ access_level = ContentLibraryPermission .READ_LEVEL ,
1621+ )
1622+ ContentLibraryPermission .objects .create (
1623+ library = lib3_obj ,
1624+ user = user ,
1625+ access_level = ContentLibraryPermission .READ_LEVEL ,
1626+ )
1627+
1628+ with patch (
1629+ 'openedx.core.djangoapps.content_libraries.permissions.get_scopes_for_user_and_permission'
1630+ ) as mock_get_scopes :
1631+ # Set up AuthZ permissions: lib1 (AuthZ only), lib3 (both)
1632+ lib1_key = LibraryLocatorV2 .from_string (lib1 ['id' ])
1633+ lib3_key = LibraryLocatorV2 .from_string (lib3 ['id' ])
1634+
1635+ mock_get_scopes .return_value = [
1636+ type ('Scope' , (), {'library_key' : lib1_key })(),
1637+ type ('Scope' , (), {'library_key' : lib3_key })(),
1638+ ]
1639+
1640+ all_libs = ContentLibrary .objects .filter (slug__in = ['comb-lib1' , 'comb-lib2' , 'comb-lib3' , 'comb-lib4' ])
1641+ filtered = perms [CAN_VIEW_THIS_CONTENT_LIBRARY ].filter (user , all_libs ).distinct ()
1642+
1643+ # TEST: Verify exactly 3 libraries returned (lib1, lib2, lib3 - NOT lib4)
1644+ self .assertEqual (
1645+ filtered .count (),
1646+ 3 ,
1647+ "Should return exactly 3 libraries: AuthZ-only, legacy-only, and both" ,
1648+ )
1649+
1650+ # TEST: Verify correct libraries are included
1651+ slugs = set (filtered .values_list ('slug' , flat = True ))
1652+ self .assertIn ('comb-lib1' , slugs , "lib1 should be accessible via AuthZ permission" )
1653+ self .assertIn ('comb-lib2' , slugs , "lib2 should be accessible via legacy permission" )
1654+ self .assertIn ('comb-lib3' , slugs , "lib3 should be accessible via BOTH AuthZ and legacy permissions" )
1655+ self .assertNotIn ('comb-lib4' , slugs , "lib4 should NOT be accessible (no permissions)" )
1656+
1657+ # TEST: Verify lib3 doesn't get duplicated despite having both permission types
1658+ lib3_results = filtered .filter (slug = 'comb-lib3' )
1659+ self .assertEqual (
1660+ lib3_results .count (),
1661+ 1 ,
1662+ "lib3 should appear exactly once despite having both AuthZ and legacy permissions" ,
1663+ )
1664+
1665+ # TEST: Verify the permission sources work independently
1666+ # This demonstrates the OR logic: user gets access if EITHER permission type grants it
1667+ result_pairs = set (filtered .values_list ('org__short_name' , 'slug' ))
1668+ expected_pairs = {
1669+ ('comb-org' , 'comb-lib1' ), # AuthZ only
1670+ ('comb-org' , 'comb-lib2' ), # Legacy only
1671+ ('comb-org' , 'comb-lib3' ), # Both
1672+ }
1673+ self .assertEqual (
1674+ result_pairs ,
1675+ expected_pairs ,
1676+ f"Should get exactly the 3 authorized libraries via OR logic, got { result_pairs } " ,
1677+ )
1678+
15761679
15771680@ddt .ddt
15781681class ContentLibraryXBlockValidationTest (APITestCase ):
0 commit comments