diff --git a/src/openedx_core/__init__.py b/src/openedx_core/__init__.py index 05861982c..b5fa534ec 100644 --- a/src/openedx_core/__init__.py +++ b/src/openedx_core/__init__.py @@ -6,4 +6,4 @@ """ # The version for the entire repository -__version__ = "0.39.2" +__version__ = "0.40.0" diff --git a/src/openedx_tagging/rest_api/v1/serializers.py b/src/openedx_tagging/rest_api/v1/serializers.py index 9c5b62a06..b3e49f1ee 100644 --- a/src/openedx_tagging/rest_api/v1/serializers.py +++ b/src/openedx_tagging/rest_api/v1/serializers.py @@ -172,6 +172,14 @@ class ObjectTagsByTaxonomySerializer(UserPermissionsSerializerMixin, serializers class Meta: model = ObjectTag + def get_can_tag_object(self, obj_tag) -> bool | None: + """ + Returns True if the current request user may tag objects with this taxonomy. + Override to customize permission logic. + """ + perm_name = f"{self.app_label}.can_tag_object" + return self._can(perm_name, obj_tag) + def to_representation(self, instance: list[ObjectTag]) -> dict: """ Convert this list of ObjectTags to the serialized dictionary, grouped by Taxonomy @@ -179,7 +187,6 @@ def to_representation(self, instance: list[ObjectTag]) -> dict: # Allows consumers like edx-platform to override this ObjectTagViewMinimalSerializer = self.context["view"].minimal_serializer_class - can_tag_object_perm = f"{self.app_label}.can_tag_object" by_object: dict[str, dict[str, Any]] = {} for obj_tag in instance: if obj_tag.object_id not in by_object: @@ -192,7 +199,7 @@ def to_representation(self, instance: list[ObjectTag]) -> dict: tax_entry = { "name": obj_tag.taxonomy.name if obj_tag.taxonomy else None, "taxonomy_id": obj_tag.taxonomy_id, - "can_tag_object": self._can(can_tag_object_perm, obj_tag), + "can_tag_object": self.get_can_tag_object(obj_tag), "tags": [], "export_id": obj_tag.export_id, } diff --git a/src/openedx_tagging/rest_api/v1/views.py b/src/openedx_tagging/rest_api/v1/views.py index 348b5b143..5a22d5641 100644 --- a/src/openedx_tagging/rest_api/v1/views.py +++ b/src/openedx_tagging/rest_api/v1/views.py @@ -450,10 +450,43 @@ class ObjectTagView( serializer_class = ObjectTagSerializer # Serializer used in the result in `to_representation` in `ObjectTagsByTaxonomySerializer` minimal_serializer_class = ObjectTagMinimalSerializer + # Serializer used in `retrieve` to group object tags by taxonomy + taxonomy_serializer_class = ObjectTagsByTaxonomySerializer permission_classes = [ObjectTagObjectPermissions] lookup_field = "object_id" lookup_value_regex = r'[\w\.\+\-@:]+' + def check_view_object_tags_permission(self, object_id: str, taxonomy=None) -> None: + """ + Check if the current user can view object tags for the given object. + Raises PermissionDenied if not. Override to customize permission logic. + """ + perm_obj = ObjectTagPermissionItem(taxonomy=taxonomy, object_id=object_id) + if not self.request.user.has_perm( + "oel_tagging.view_objecttag", + # The obj arg expects a model, but we are passing an object + perm_obj, # type: ignore[arg-type] + ): + raise PermissionDenied( + "You do not have permission to view object tags for this taxonomy or object_id." + ) + + def check_can_tag_object_permission(self, object_id: str, taxonomy) -> None: + """ + Check if the current user can tag the given object with the given taxonomy. + Raises PermissionDenied if not. Override to customize permission logic. + """ + perm_obj = ObjectTagPermissionItem(taxonomy=taxonomy, object_id=object_id) + if not self.request.user.has_perm( + "oel_tagging.can_tag_object", + # The obj arg expects a model, but we are passing an object + perm_obj, # type: ignore[arg-type] + ): + raise PermissionDenied(f""" + You do not have permission to change object tags + for Taxonomy: {str(taxonomy)} or Object: {object_id}. + """) + def get_queryset(self) -> models.QuerySet: """ Return a queryset of object tags for a given object. @@ -477,14 +510,7 @@ def get_queryset(self) -> models.QuerySet: # objects, e.g. if object_id.endswith("*") then it results in a object_id__startswith query. However, for # now we have no use case for that so we retrieve tags for one object at a time. else: - if not self.request.user.has_perm( - "oel_tagging.view_objecttag", - # The obj arg expects a model, but we are passing an object - ObjectTagPermissionItem(taxonomy=taxonomy, object_id=object_id), # type: ignore[arg-type] - ): - raise PermissionDenied( - "You do not have permission to view object tags for this taxonomy or object_id." - ) + self.check_view_object_tags_permission(object_id, taxonomy) return get_object_tags(object_id, taxonomy_id) @@ -500,7 +526,7 @@ def retrieve(self, request, *args, **kwargs) -> Response: behavior we want. """ object_tags = self.filter_queryset(self.get_queryset()) - serializer = ObjectTagsByTaxonomySerializer(list(object_tags), context=self.get_serializer_context()) + serializer = self.taxonomy_serializer_class(list(object_tags), context=self.get_serializer_context()) response_data = serializer.data if self.kwargs["object_id"] not in response_data: # For consistency, the key with the object_id should always be present in the response, even if there @@ -556,7 +582,6 @@ def update(self, request, *args, **kwargs) -> Response: raise MethodNotAllowed("PATCH", detail="PATCH not allowed") object_id = kwargs.pop('object_id') - perm = "oel_tagging.can_tag_object" body = ObjectTagUpdateBodySerializer(data=request.data) body.is_valid(raise_exception=True) @@ -568,20 +593,7 @@ def update(self, request, *args, **kwargs) -> Response: # Check permissions for tagsData in data: taxonomy = tagsData.get("taxonomy") - - perm_obj = ObjectTagPermissionItem( - taxonomy=taxonomy, - object_id=object_id, - ) - if not request.user.has_perm( - perm, - # The obj arg expects a model, but we are passing an object - perm_obj, # type: ignore[arg-type] - ): - raise PermissionDenied(f""" - You do not have permission to change object tags - for Taxonomy: {str(taxonomy)} or Object: {object_id}. - """) + self.check_can_tag_object_permission(object_id, taxonomy) # Tag object_id per taxonomy for tagsData in data: