Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
05ea800
chore: removed unneeded code
pomegranited Jan 4, 2024
231036f
feat: adds user_permissions to the tagging REST API results
pomegranited Jan 4, 2024
9df5304
fix: removes ObjectTagsByTaxonomySerializer.editable
pomegranited Jan 7, 2024
0c54887
Merge branch 'main' into jill/serialize-permissions
pomegranited Jan 7, 2024
8d619c5
fix: determine "add" permissions without the object
pomegranited Jan 7, 2024
23789cc
feat: adds model-level user permissions to the Taxonomy and Tag REST …
pomegranited Jan 7, 2024
634168a
fix: return only the permissions we need
pomegranited Jan 10, 2024
c803f66
refactor: user permissions serializer and paginator share helper class
pomegranited Jan 11, 2024
2709d44
feat: adds can_tag_object field to ObjectTagsByTaxonomySerializer
pomegranited Jan 11, 2024
7470904
feat: check permissions only if ?include_perms
pomegranited Jan 11, 2024
1e2d9b0
feat: adds can_tag_object permission to Taxonomy List response
pomegranited Jan 11, 2024
bb1a0a2
fix: feat: check permissions only if ?include_perms
pomegranited Jan 11, 2024
a652313
chore: bumps version
pomegranited Jan 11, 2024
f90b305
fix: renames the `can_<action>` fields to `can_<action>_<model>`
pomegranited Jan 12, 2024
3e24b66
fix: check add + delete permissions when updating ObjectTags
pomegranited Jan 12, 2024
3dfea40
feat: adds "can_tag_object" rule
pomegranited Jan 18, 2024
786ef8b
revert: removes the include_perms querystring param from the tagging …
pomegranited Jan 23, 2024
ee41331
Merge branch 'main' into jill/serialize-permissions
pomegranited Jan 23, 2024
eaf9f67
fix: addresses PR review
pomegranited Jan 23, 2024
6398165
Merge branch 'main' into jill/serialize-permissions
pomegranited Jan 23, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
105 changes: 88 additions & 17 deletions openedx_tagging/core/tagging/rest_api/v1/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,71 @@ class TaxonomyExportQueryParamsSerializer(serializers.Serializer): # pylint: di
output_format = serializers.RegexField(r"(?i)^(json|csv)$", allow_blank=False)


class UserPermissionsSerializer(serializers.Serializer): # pylint: disable=abstract-method
"""
Serializer for the permissions on tagging instances granted to the current user.

Requires the current request to be passed into the serializer context, or all permissions will return False.
"""
can_add = serializers.SerializerMethodField()
can_view = serializers.SerializerMethodField()
can_change = serializers.SerializerMethodField()
can_delete = serializers.SerializerMethodField()

def _check_permission(self, action, instance) -> bool:
"""
Returns True if the current `request.user` may perform the given `action` on the `instance` object.

Uses the current request as passed into the serializer context.
"""
assert action in ("add", "change", "view", "delete")
Comment thread
pomegranited marked this conversation as resolved.
Outdated

if isinstance(instance, Taxonomy):
model = 'taxonomy'
elif isinstance(instance, Tag):
model = 'tag'
elif isinstance(instance, ObjectTag):
model = 'objecttag'
else:
return False

request = self.context.get('request')
assert request and request.user

permission = f"oel_tagging.{action}_{model}"
return request.user.has_perm(permission, instance)

def get_can_add(self, instance) -> bool:
"""
Returns True if the current user is allowed to create new instances.
"""
return self._check_permission('add', instance)

def get_can_change(self, instance) -> bool:
"""
Returns True if the current user is allowed to change this instance.
"""
return self._check_permission('change', instance)

def get_can_view(self, instance) -> bool:
"""
Returns True if the current user is allowed to view this instance.
"""
return self._check_permission('view', instance)

def get_can_delete(self, instance) -> bool:
"""
Returns True if the current user is allowed to delete this instance.
"""
return self._check_permission('delete', instance)


class TaxonomySerializer(serializers.ModelSerializer):
"""
Serializer for the Taxonomy model.
"""
tags_count = serializers.SerializerMethodField()
user_permissions = serializers.SerializerMethodField()

class Meta:
model = Taxonomy
Expand All @@ -48,6 +108,7 @@ class Meta:
"system_defined",
"visible_to_authors",
"tags_count",
"user_permissions",
]

def to_representation(self, instance):
Expand All @@ -60,6 +121,13 @@ def to_representation(self, instance):
def get_tags_count(self, instance):
return instance.tag_set.count()

def get_user_permissions(self, instance):
"""
Returns the serialized user permissions on the given instance.
"""
permissions = UserPermissionsSerializer(instance, context=self.context)
return permissions.to_representation(instance)


class ObjectTagListQueryParamsSerializer(serializers.Serializer): # pylint: disable=abstract-method
"""
Expand All @@ -78,9 +146,17 @@ class ObjectTagMinimalSerializer(serializers.ModelSerializer):

class Meta:
model = ObjectTag
fields = ["value", "lineage"]
fields = ["value", "lineage", "user_permissions"]

lineage = serializers.ListField(child=serializers.CharField(), source="get_lineage", read_only=True)
user_permissions = serializers.SerializerMethodField()

def get_user_permissions(self, instance):
"""
Returns the serialized user permissions on the given instance.
"""
permissions = UserPermissionsSerializer(instance, context=self.context)
return permissions.to_representation(instance)


class ObjectTagSerializer(ObjectTagMinimalSerializer):
Expand Down Expand Up @@ -118,11 +194,10 @@ def to_representation(self, instance: list[ObjectTag]) -> dict:
tax_entry = {
"name": obj_tag.name,
"taxonomy_id": obj_tag.taxonomy_id,
"editable": (not obj_tag.taxonomy.cast().system_defined) if obj_tag.taxonomy else False,
"tags": []
}
taxonomies.append(tax_entry)
tax_entry["tags"].append(ObjectTagMinimalSerializer(obj_tag).data)
tax_entry["tags"].append(ObjectTagMinimalSerializer(obj_tag, context=self.context).data)
return by_object


Expand All @@ -144,7 +219,7 @@ class ObjectTagUpdateQueryParamsSerializer(serializers.Serializer): # pylint: d
)


class TagDataSerializer(serializers.Serializer):
class TagDataSerializer(serializers.Serializer): # pylint: disable=abstract-method
"""
Serializer for TagData dicts. Also can serialize Tag instances.

Expand All @@ -161,6 +236,7 @@ class TagDataSerializer(serializers.Serializer):
_id = serializers.IntegerField(allow_null=True)

sub_tags_url = serializers.SerializerMethodField()
user_permissions = serializers.SerializerMethodField()

def get_sub_tags_url(self, obj: TagData | Tag):
"""
Expand All @@ -184,6 +260,13 @@ def get_sub_tags_url(self, obj: TagData | Tag):
return request.build_absolute_uri(url)
return None

def get_user_permissions(self, instance):
"""
Returns the serialized user permissions on the given instance.
"""
permissions = UserPermissionsSerializer(instance, context=self.context)
return permissions.to_representation(instance)

def to_representation(self, instance: TagData | Tag) -> dict:
"""
Convert this TagData (or Tag model instance) to the serialized dictionary
Expand All @@ -194,12 +277,6 @@ def to_representation(self, instance: TagData | Tag) -> dict:
data["parent_value"] = instance.parent.value if instance.parent else None
return data

def update(self, instance, validated_data):
raise RuntimeError('`update()` is not supported by the TagData serializer.')

def create(self, validated_data):
raise RuntimeError('`create()` is not supported by the TagData serializer.')


class TaxonomyTagCreateBodySerializer(serializers.Serializer): # pylint: disable=abstract-method
"""
Expand Down Expand Up @@ -273,7 +350,7 @@ class Meta:
]


class TaxonomyImportPlanResponseSerializer(serializers.Serializer):
class TaxonomyImportPlanResponseSerializer(serializers.Serializer): # pylint: disable=abstract-method
"""
Serializer for the response of the Taxonomy Import Plan request
"""
Expand All @@ -290,9 +367,3 @@ def get_plan(self, obj):
return plan.plan()

return None

def update(self, instance, validated_data):
raise RuntimeError('`update()` is not supported by the TagImportTask serializer.')

def create(self, validated_data):
raise RuntimeError('`create()` is not supported by the TagImportTask serializer.')
2 changes: 1 addition & 1 deletion openedx_tagging/core/tagging/rest_api/v1/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -444,7 +444,7 @@ def retrieve(self, request, *args, **kwargs) -> Response:
behavior we want.
"""
object_tags = self.filter_queryset(self.get_queryset())
serializer = ObjectTagsByTaxonomySerializer(list(object_tags))
serializer = ObjectTagsByTaxonomySerializer(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
Expand Down
Loading