Skip to content

Commit 2d1faa8

Browse files
refactor: make object tag permission checks modular
1 parent c48445b commit 2d1faa8

3 files changed

Lines changed: 64 additions & 34 deletions

File tree

src/openedx_core/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,4 @@
66
"""
77

88
# The version for the entire repository
9-
__version__ = "0.44.0"
9+
__version__ = "0.45.0"

src/openedx_tagging/rest_api/v1/serializers.py

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,17 @@ class Meta:
145145

146146
def get_can_delete_objecttag(self, instance) -> bool | None:
147147
"""
148-
Returns True if the current request user may delete object tags on this taxonomy
148+
SerializerMethodField callback for `can_delete_objecttag`.
149+
150+
Delegates to `can_delete_object_tag` so subclasses can
151+
override a stable method name that isn't tied to DRF's
152+
SerializerMethodField naming convention.
153+
"""
154+
return self.can_delete_object_tag(instance)
155+
156+
def can_delete_object_tag(self, instance) -> bool | None:
157+
"""
158+
Check if the user is authorized to delete the provided tag.
149159
"""
150160
perm_name = f'{self.app_label}.remove_objecttag_objectid'
151161
return self._can(perm_name, instance.object_id)
@@ -179,7 +189,6 @@ def to_representation(self, instance: list[ObjectTag]) -> dict:
179189
# Allows consumers like edx-platform to override this
180190
ObjectTagViewMinimalSerializer = self.context["view"].minimal_serializer_class
181191

182-
can_tag_object_perm = f"{self.app_label}.can_tag_object"
183192
by_object: dict[str, dict[str, Any]] = {}
184193
for obj_tag in instance:
185194
if obj_tag.object_id not in by_object:
@@ -192,14 +201,21 @@ def to_representation(self, instance: list[ObjectTag]) -> dict:
192201
tax_entry = {
193202
"name": obj_tag.taxonomy.name if obj_tag.taxonomy else None,
194203
"taxonomy_id": obj_tag.taxonomy_id,
195-
"can_tag_object": self._can(can_tag_object_perm, obj_tag),
204+
"can_tag_object": self.can_tag_object(obj_tag),
196205
"tags": [],
197206
"export_id": obj_tag.export_id,
198207
}
199208
taxonomies.append(tax_entry)
200209
tax_entry["tags"].append(ObjectTagViewMinimalSerializer(obj_tag, context=self.context).data)
201210
return by_object
202211

212+
def can_tag_object(self, obj_tag) -> bool | None:
213+
"""
214+
Check if the user is authorized to tag the provided object.
215+
"""
216+
perm_name = f"{self.app_label}.can_tag_object"
217+
return self._can(perm_name, obj_tag)
218+
203219

204220
class ObjectTagUpdateByTaxonomySerializer(serializers.Serializer): # pylint: disable=abstract-method
205221
"""

src/openedx_tagging/rest_api/v1/views.py

Lines changed: 44 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -448,6 +448,8 @@ class ObjectTagView(
448448

449449
# Serializer used in `get_queryset` when getting tags per taxonomy
450450
serializer_class = ObjectTagSerializer
451+
# Serializer used in `retrieve` to represent all tags of a given object
452+
object_tags_serializer_class = ObjectTagsByTaxonomySerializer
451453
# Serializer used in the result in `to_representation` in `ObjectTagsByTaxonomySerializer`
452454
minimal_serializer_class = ObjectTagMinimalSerializer
453455
permission_classes = [ObjectTagObjectPermissions]
@@ -471,23 +473,29 @@ def get_queryset(self) -> models.QuerySet:
471473
taxonomy = taxonomy.cast()
472474
taxonomy_id = taxonomy.id
473475

474-
if object_id.endswith("*") or "," in object_id: # pylint: disable=no-else-raise
475-
raise ValidationError("Retrieving tags from multiple objects is not yet supported.")
476+
if object_id.endswith("*") or "," in object_id:
476477
# Note: This API is actually designed so that in the future it can be extended to return tags for multiple
477478
# objects, e.g. if object_id.endswith("*") then it results in a object_id__startswith query. However, for
478479
# now we have no use case for that so we retrieve tags for one object at a time.
479-
else:
480-
if not self.request.user.has_perm(
481-
"oel_tagging.view_objecttag",
482-
# The obj arg expects a model, but we are passing an object
483-
ObjectTagPermissionItem(taxonomy=taxonomy, object_id=object_id), # type: ignore[arg-type]
484-
):
485-
raise PermissionDenied(
486-
"You do not have permission to view object tags for this taxonomy or object_id."
487-
)
480+
raise ValidationError("Retrieving tags from multiple objects is not yet supported.")
481+
482+
self.ensure_has_view_object_tag_permission(self.request.user, taxonomy, object_id)
488483

489484
return get_object_tags(object_id, taxonomy_id)
490485

486+
def ensure_has_view_object_tag_permission(self, user, taxonomy, object_id):
487+
"""
488+
Check if user has permission to view object tags.
489+
"""
490+
if not user.has_perm(
491+
"oel_tagging.view_objecttag",
492+
# The obj arg expects a model, but we are passing an object
493+
ObjectTagPermissionItem(taxonomy=taxonomy, object_id=object_id), # type: ignore[arg-type]
494+
):
495+
raise PermissionDenied(
496+
"You do not have permission to view object tags for this taxonomy or object_id."
497+
)
498+
491499
def retrieve(self, request, *args, **kwargs) -> Response:
492500
"""
493501
Retrieve ObjectTags that belong to a given object_id
@@ -500,7 +508,7 @@ def retrieve(self, request, *args, **kwargs) -> Response:
500508
behavior we want.
501509
"""
502510
object_tags = self.filter_queryset(self.get_queryset())
503-
serializer = ObjectTagsByTaxonomySerializer(list(object_tags), context=self.get_serializer_context())
511+
serializer = self.object_tags_serializer_class(list(object_tags), context=self.get_serializer_context())
504512
response_data = serializer.data
505513
if self.kwargs["object_id"] not in response_data:
506514
# For consistency, the key with the object_id should always be present in the response, even if there
@@ -514,7 +522,7 @@ def update(self, request, *args, **kwargs) -> Response:
514522
515523
Pass a list of Tag ids or Tag values to be applied to an object id in the
516524
body `tag` parameter, by each taxonomy. Passing an empty list will remove all tags from
517-
the object id on an specific taxonomy.
525+
the object id on a specific taxonomy.
518526
519527
**Example Body Requests**
520528
@@ -550,13 +558,13 @@ def update(self, request, *args, **kwargs) -> Response:
550558
},
551559
]
552560
}
561+
```
553562
"""
554563
partial = kwargs.pop('partial', False)
555564
if partial:
556565
raise MethodNotAllowed("PATCH", detail="PATCH not allowed")
557566

558567
object_id = kwargs.pop('object_id')
559-
perm = "oel_tagging.can_tag_object"
560568
body = ObjectTagUpdateBodySerializer(data=request.data)
561569
body.is_valid(raise_exception=True)
562570

@@ -566,14 +574,33 @@ def update(self, request, *args, **kwargs) -> Response:
566574
return self.retrieve(request, object_id)
567575

568576
# Check permissions
569-
for tagsData in data:
570-
taxonomy = tagsData.get("taxonomy")
577+
self.ensure_user_has_can_tag_object_permissions(request.user, data, object_id)
578+
579+
# Tag object_id per taxonomy
580+
for tag_data in data:
581+
taxonomy = tag_data.get("taxonomy")
582+
tags = tag_data.get("tags", [])
583+
try:
584+
tag_object(object_id, taxonomy, tags)
585+
except TagDoesNotExist as e:
586+
raise ValidationError from e
587+
except ValueError as e:
588+
raise ValidationError from e
571589

590+
return self.retrieve(request, object_id)
591+
592+
def ensure_user_has_can_tag_object_permissions(self, user, tags_data, object_id):
593+
"""
594+
Check if user has permission to tag object for each taxonomy.
595+
"""
596+
perm = "oel_tagging.can_tag_object"
597+
for tag_data in tags_data:
598+
taxonomy = tag_data.get("taxonomy")
572599
perm_obj = ObjectTagPermissionItem(
573600
taxonomy=taxonomy,
574601
object_id=object_id,
575602
)
576-
if not request.user.has_perm(
603+
if not user.has_perm(
577604
perm,
578605
# The obj arg expects a model, but we are passing an object
579606
perm_obj, # type: ignore[arg-type]
@@ -583,19 +610,6 @@ def update(self, request, *args, **kwargs) -> Response:
583610
for Taxonomy: {str(taxonomy)} or Object: {object_id}.
584611
""")
585612

586-
# Tag object_id per taxonomy
587-
for tagsData in data:
588-
taxonomy = tagsData.get("taxonomy")
589-
tags = tagsData.get("tags", [])
590-
try:
591-
tag_object(object_id, taxonomy, tags)
592-
except TagDoesNotExist as e:
593-
raise ValidationError from e
594-
except ValueError as e:
595-
raise ValidationError from e
596-
597-
return self.retrieve(request, object_id)
598-
599613

600614
@view_auth_classes
601615
class ObjectTagCountsView(

0 commit comments

Comments
 (0)