Skip to content

Commit 11c0255

Browse files
wgu-taylor-payneKiro
andcommitted
feat: extract overridable permission methods from ObjectTagView and serializers
Extract permission checks into overridable methods so downstream platforms can substitute their own authorization logic without duplicating parent view code. views.py: - check_view_object_tags_permission — gates read access (get_queryset) - check_can_tag_object_permission — gates write access (update) serializers.py: - ObjectTagsByTaxonomySerializer.get_can_tag_object — computes can_tag_object bool for response Co-authored-by: Kiro <kiro@amazon.com>
1 parent 5391abc commit 11c0255

3 files changed

Lines changed: 46 additions & 27 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.39.2"
9+
__version__ = "0.40.0"

src/openedx_tagging/rest_api/v1/serializers.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -172,14 +172,21 @@ class ObjectTagsByTaxonomySerializer(UserPermissionsSerializerMixin, serializers
172172
class Meta:
173173
model = ObjectTag
174174

175+
def get_can_tag_object(self, obj_tag) -> bool | None:
176+
"""
177+
Returns True if the current request user may tag objects with this taxonomy.
178+
Override to customize permission logic.
179+
"""
180+
perm_name = f"{self.app_label}.can_tag_object"
181+
return self._can(perm_name, obj_tag)
182+
175183
def to_representation(self, instance: list[ObjectTag]) -> dict:
176184
"""
177185
Convert this list of ObjectTags to the serialized dictionary, grouped by Taxonomy
178186
"""
179187
# Allows consumers like edx-platform to override this
180188
ObjectTagViewMinimalSerializer = self.context["view"].minimal_serializer_class
181189

182-
can_tag_object_perm = f"{self.app_label}.can_tag_object"
183190
by_object: dict[str, dict[str, Any]] = {}
184191
for obj_tag in instance:
185192
if obj_tag.object_id not in by_object:
@@ -192,7 +199,7 @@ def to_representation(self, instance: list[ObjectTag]) -> dict:
192199
tax_entry = {
193200
"name": obj_tag.taxonomy.name if obj_tag.taxonomy else None,
194201
"taxonomy_id": obj_tag.taxonomy_id,
195-
"can_tag_object": self._can(can_tag_object_perm, obj_tag),
202+
"can_tag_object": self.get_can_tag_object(obj_tag),
196203
"tags": [],
197204
"export_id": obj_tag.export_id,
198205
}

src/openedx_tagging/rest_api/v1/views.py

Lines changed: 36 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -450,10 +450,43 @@ class ObjectTagView(
450450
serializer_class = ObjectTagSerializer
451451
# Serializer used in the result in `to_representation` in `ObjectTagsByTaxonomySerializer`
452452
minimal_serializer_class = ObjectTagMinimalSerializer
453+
# Serializer used in `retrieve` to group object tags by taxonomy
454+
taxonomy_serializer_class = ObjectTagsByTaxonomySerializer
453455
permission_classes = [ObjectTagObjectPermissions]
454456
lookup_field = "object_id"
455457
lookup_value_regex = r'[\w\.\+\-@:]+'
456458

459+
def check_view_object_tags_permission(self, object_id: str, taxonomy=None) -> None:
460+
"""
461+
Check if the current user can view object tags for the given object.
462+
Raises PermissionDenied if not. Override to customize permission logic.
463+
"""
464+
perm_obj = ObjectTagPermissionItem(taxonomy=taxonomy, object_id=object_id)
465+
if not self.request.user.has_perm(
466+
"oel_tagging.view_objecttag",
467+
# The obj arg expects a model, but we are passing an object
468+
perm_obj, # type: ignore[arg-type]
469+
):
470+
raise PermissionDenied(
471+
"You do not have permission to view object tags for this taxonomy or object_id."
472+
)
473+
474+
def check_can_tag_object_permission(self, object_id: str, taxonomy) -> None:
475+
"""
476+
Check if the current user can tag the given object with the given taxonomy.
477+
Raises PermissionDenied if not. Override to customize permission logic.
478+
"""
479+
perm_obj = ObjectTagPermissionItem(taxonomy=taxonomy, object_id=object_id)
480+
if not self.request.user.has_perm(
481+
"oel_tagging.can_tag_object",
482+
# The obj arg expects a model, but we are passing an object
483+
perm_obj, # type: ignore[arg-type]
484+
):
485+
raise PermissionDenied(f"""
486+
You do not have permission to change object tags
487+
for Taxonomy: {str(taxonomy)} or Object: {object_id}.
488+
""")
489+
457490
def get_queryset(self) -> models.QuerySet:
458491
"""
459492
Return a queryset of object tags for a given object.
@@ -477,14 +510,7 @@ def get_queryset(self) -> models.QuerySet:
477510
# objects, e.g. if object_id.endswith("*") then it results in a object_id__startswith query. However, for
478511
# now we have no use case for that so we retrieve tags for one object at a time.
479512
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-
)
513+
self.check_view_object_tags_permission(object_id, taxonomy)
488514

489515
return get_object_tags(object_id, taxonomy_id)
490516

@@ -500,7 +526,7 @@ def retrieve(self, request, *args, **kwargs) -> Response:
500526
behavior we want.
501527
"""
502528
object_tags = self.filter_queryset(self.get_queryset())
503-
serializer = ObjectTagsByTaxonomySerializer(list(object_tags), context=self.get_serializer_context())
529+
serializer = self.taxonomy_serializer_class(list(object_tags), context=self.get_serializer_context())
504530
response_data = serializer.data
505531
if self.kwargs["object_id"] not in response_data:
506532
# 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:
556582
raise MethodNotAllowed("PATCH", detail="PATCH not allowed")
557583

558584
object_id = kwargs.pop('object_id')
559-
perm = "oel_tagging.can_tag_object"
560585
body = ObjectTagUpdateBodySerializer(data=request.data)
561586
body.is_valid(raise_exception=True)
562587

@@ -568,20 +593,7 @@ def update(self, request, *args, **kwargs) -> Response:
568593
# Check permissions
569594
for tagsData in data:
570595
taxonomy = tagsData.get("taxonomy")
571-
572-
perm_obj = ObjectTagPermissionItem(
573-
taxonomy=taxonomy,
574-
object_id=object_id,
575-
)
576-
if not request.user.has_perm(
577-
perm,
578-
# The obj arg expects a model, but we are passing an object
579-
perm_obj, # type: ignore[arg-type]
580-
):
581-
raise PermissionDenied(f"""
582-
You do not have permission to change object tags
583-
for Taxonomy: {str(taxonomy)} or Object: {object_id}.
584-
""")
596+
self.check_can_tag_object_permission(object_id, taxonomy)
585597

586598
# Tag object_id per taxonomy
587599
for tagsData in data:

0 commit comments

Comments
 (0)