@@ -65,7 +65,12 @@ def get_scope_value(self, request) -> str | None:
6565 Returns:
6666 str | None: The scope value if found (e.g., 'lib:DemoX:CSPROB'), or None if not present.
6767 """
68- return request .data .get ("scope" ) or request .query_params .get ("scope" )
68+ scope = request .data .get ("scope" ) or request .query_params .get ("scope" )
69+ if not scope :
70+ scopes = request .data .get ("scopes" )
71+ if scopes and isinstance (scopes , list ):
72+ scope = scopes [0 ]
73+ return scope
6974
7075 def get_scope_namespace (self , request ) -> str :
7176 """Derive the namespace from the request scope value.
@@ -175,13 +180,26 @@ def has_permission(self, request, view) -> bool:
175180 users, the permission check is delegated to the permission class registered
176181 for the request's scope namespace.
177182
183+ For bulk PUT requests that carry a ``scopes`` list, every scope in the list
184+ must pass at least one of the required permissions (OR logic per permission,
185+ AND logic across scopes).
186+
178187 Examples:
179188 >>> # Regular user gets scope-specific check
180189 >>> request.data = {"scope": "lib:DemoX:CSPROB"}
181190 >>> permission.has_permission(request, view) # Delegates to ContentLibraryPermission
182191 """
183192 if request .user .is_superuser or request .user .is_staff :
184193 return True
194+ scopes_list = request .data .get ("scopes" )
195+ if scopes_list and isinstance (scopes_list , list ):
196+ perm_instance = self ._get_permission_instance (request ) # namespace resolved from scopes[0]
197+ if not isinstance (perm_instance , MethodPermissionMixin ):
198+ return False
199+ required = perm_instance .get_required_permissions (request , view )
200+ if not required :
201+ return False
202+ return all (perm_instance .validate_permissions (request , required , sv ) for sv in scopes_list )
185203 return self ._get_permission_instance (request ).has_permission (request , view )
186204
187205 def has_object_permission (self , request , view , obj ) -> bool :
@@ -240,23 +258,19 @@ def get_required_permissions(self, request, view) -> list[str]:
240258 return []
241259
242260 def validate_permissions (self , request , permissions : list [str ], scope_value : str ) -> bool :
243- """Validate that the user has all required permissions for the scope.
261+ """Validate that the user has at least one of the required permissions for the scope.
244262
245263 Args:
246264 request: The Django REST framework request object.
247- permissions: List of permission identifiers to check.
265+ permissions: List of permission identifiers to check (OR logic — any one suffices) .
248266 scope_value: The scope to check permissions against.
249267
250268 Returns:
251- bool: True if user has all required permissions , False otherwise.
269+ bool: True if user has at least one required permission , False otherwise.
252270 """
253271 if not permissions :
254272 return False
255-
256- for permission in permissions :
257- if not api .is_user_allowed (request .user .username , permission , scope_value ):
258- return False
259- return True
273+ return any (api .is_user_allowed (request .user .username , permission , scope_value ) for permission in permissions )
260274
261275
262276class AnyScopePermission (MethodPermissionMixin , BasePermission ):
@@ -282,6 +296,21 @@ def has_permission(self, request, view) -> bool:
282296 return any (api .get_scopes_for_user_and_permission (request .user .username , permission ) for permission in required )
283297
284298
299+ class CoursePermission (MethodPermissionMixin , BaseScopePermission ):
300+ """Permission handler for course scopes (namespace ``course-v1``)."""
301+
302+ NAMESPACE : ClassVar [str ] = "course-v1"
303+
304+ def has_permission (self , request , view ) -> bool :
305+ scope_value = self .get_scope_value (request )
306+ if not scope_value :
307+ return False
308+ permissions = self .get_required_permissions (request , view )
309+ if permissions :
310+ return self .validate_permissions (request , permissions , scope_value )
311+ return True
312+
313+
285314class ContentLibraryPermission (MethodPermissionMixin , BaseScopePermission ):
286315 """Permission handler for content library scopes.
287316
0 commit comments