Skip to content

Commit def42f0

Browse files
wgu-taylor-payneKiro
andcommitted
feat: extract overridable permission methods from ObjectTagView
Extract 4 overridable methods from ObjectTagView so downstream platforms can substitute their own permission logic without duplicating parent view code. Serializers (ObjectTagsByTaxonomySerializer, ObjectTagMinimalSerializer) now delegate can_tag_object and can_delete_objecttag computation to the view when available, falling back to the existing _can() method. No behavior change for existing consumers. Co-authored-by: Kiro <kiro@amazon.com>
1 parent 5391abc commit def42f0

3 files changed

Lines changed: 67 additions & 26 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: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,10 @@ def get_can_delete_objecttag(self, instance) -> bool | None:
147147
"""
148148
Returns True if the current request user may delete object tags on this taxonomy
149149
"""
150+
view = self.context.get("view")
151+
request = self.context.get("request")
152+
if view and request and hasattr(view, "get_can_delete_objecttag_value"):
153+
return view.get_can_delete_objecttag_value(request.user, instance.object_id)
150154
perm_name = f'{self.app_label}.remove_objecttag_objectid'
151155
return self._can(perm_name, instance.object_id)
152156

@@ -179,6 +183,8 @@ def to_representation(self, instance: list[ObjectTag]) -> dict:
179183
# Allows consumers like edx-platform to override this
180184
ObjectTagViewMinimalSerializer = self.context["view"].minimal_serializer_class
181185

186+
view = self.context.get("view")
187+
request = self.context.get("request")
182188
can_tag_object_perm = f"{self.app_label}.can_tag_object"
183189
by_object: dict[str, dict[str, Any]] = {}
184190
for obj_tag in instance:
@@ -189,10 +195,14 @@ def to_representation(self, instance: list[ObjectTag]) -> dict:
189195
taxonomies = by_object[obj_tag.object_id]["taxonomies"]
190196
tax_entry = next((t for t in taxonomies if t["taxonomy_id"] == obj_tag.taxonomy_id), None)
191197
if tax_entry is None:
198+
if view and request and hasattr(view, "get_can_tag_object_value"):
199+
can_tag_object = view.get_can_tag_object_value(request.user, obj_tag)
200+
else:
201+
can_tag_object = self._can(can_tag_object_perm, obj_tag)
192202
tax_entry = {
193203
"name": obj_tag.taxonomy.name if obj_tag.taxonomy else None,
194204
"taxonomy_id": obj_tag.taxonomy_id,
195-
"can_tag_object": self._can(can_tag_object_perm, obj_tag),
205+
"can_tag_object": can_tag_object,
196206
"tags": [],
197207
"export_id": obj_tag.export_id,
198208
}

src/openedx_tagging/rest_api/v1/views.py

Lines changed: 55 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@
3131
from ...data import TagDataQuerySet
3232
from ...import_export.api import export_tags, import_tags
3333
from ...import_export.parsers import ParserFormat
34-
from ...models import Tag, Taxonomy
34+
from ...models import ObjectTag, Tag, Taxonomy
3535
from ...rules import ObjectTagPermissionItem
3636
from ..paginators import MAX_FULL_DEPTH_THRESHOLD, DisabledTagsPagination, TagsPagination, TaxonomyPagination
3737
from ..utils import view_auth_classes
@@ -454,6 +454,58 @@ class ObjectTagView(
454454
lookup_field = "object_id"
455455
lookup_value_regex = r'[\w\.\+\-@:]+'
456456

457+
def check_view_object_tags_permission(self, object_id: str, taxonomy=None) -> None:
458+
"""
459+
Check if the current user can view object tags for the given object.
460+
Raises PermissionDenied if not. Override to customize permission logic.
461+
"""
462+
perm_obj = ObjectTagPermissionItem(taxonomy=taxonomy, object_id=object_id)
463+
if not self.request.user.has_perm(
464+
"oel_tagging.view_objecttag",
465+
perm_obj, # type: ignore[arg-type]
466+
):
467+
raise PermissionDenied(
468+
"You do not have permission to view object tags for this taxonomy or object_id."
469+
)
470+
471+
def check_can_tag_object_permission(self, object_id: str, taxonomy) -> None:
472+
"""
473+
Check if the current user can tag the given object with the given taxonomy.
474+
Raises PermissionDenied if not. Override to customize permission logic.
475+
"""
476+
perm_obj = ObjectTagPermissionItem(taxonomy=taxonomy, object_id=object_id)
477+
if not self.request.user.has_perm(
478+
"oel_tagging.can_tag_object",
479+
perm_obj, # type: ignore[arg-type]
480+
):
481+
raise PermissionDenied(f"""
482+
You do not have permission to change object tags
483+
for Taxonomy: {str(taxonomy)} or Object: {object_id}.
484+
""")
485+
486+
def get_can_tag_object_value(self, user, obj_tag) -> bool | None:
487+
"""
488+
Return whether the user can tag the object associated with obj_tag.
489+
Used by serializers to populate the can_tag_object field.
490+
Override to customize permission logic.
491+
"""
492+
perm_name = f"{self.app_label}.can_tag_object"
493+
return user.has_perm(perm_name, obj_tag)
494+
495+
def get_can_delete_objecttag_value(self, user, object_id: str) -> bool | None:
496+
"""
497+
Return whether the user can delete object tags for the given object_id.
498+
Used by serializers to populate the can_delete_objecttag field.
499+
Override to customize permission logic.
500+
"""
501+
perm_name = f"{self.app_label}.remove_objecttag_objectid"
502+
return user.has_perm(perm_name, object_id)
503+
504+
@property
505+
def app_label(self) -> str:
506+
"""Returns the app_label for permission name construction."""
507+
return ObjectTag._meta.app_label # pylint: disable=protected-access
508+
457509
def get_queryset(self) -> models.QuerySet:
458510
"""
459511
Return a queryset of object tags for a given object.
@@ -477,14 +529,7 @@ def get_queryset(self) -> models.QuerySet:
477529
# objects, e.g. if object_id.endswith("*") then it results in a object_id__startswith query. However, for
478530
# now we have no use case for that so we retrieve tags for one object at a time.
479531
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-
)
532+
self.check_view_object_tags_permission(object_id, taxonomy)
488533

489534
return get_object_tags(object_id, taxonomy_id)
490535

@@ -556,7 +601,6 @@ def update(self, request, *args, **kwargs) -> Response:
556601
raise MethodNotAllowed("PATCH", detail="PATCH not allowed")
557602

558603
object_id = kwargs.pop('object_id')
559-
perm = "oel_tagging.can_tag_object"
560604
body = ObjectTagUpdateBodySerializer(data=request.data)
561605
body.is_valid(raise_exception=True)
562606

@@ -568,20 +612,7 @@ def update(self, request, *args, **kwargs) -> Response:
568612
# Check permissions
569613
for tagsData in data:
570614
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-
""")
615+
self.check_can_tag_object_permission(object_id, taxonomy)
585616

586617
# Tag object_id per taxonomy
587618
for tagsData in data:

0 commit comments

Comments
 (0)