Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Tagging: serialize object permissions to REST API [FC-0036] #138

Merged
merged 20 commits into from
Jan 24, 2024
Merged
Show file tree
Hide file tree
Changes from 18 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
2 changes: 1 addition & 1 deletion openedx_learning/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
"""
Open edX Learning ("Learning Core").
"""
__version__ = "0.4.3"
__version__ = "0.4.4"
63 changes: 61 additions & 2 deletions openedx_tagging/core/tagging/rest_api/paginators.py
Original file line number Diff line number Diff line change
@@ -1,23 +1,75 @@
"""
Paginators uses by the REST API
"""
from typing import Type

from edx_rest_framework_extensions.paginators import DefaultPagination # type: ignore[import]
from rest_framework.request import Request
from rest_framework.response import Response

from openedx_tagging.core.tagging.models import Tag, Taxonomy

from .utils import UserPermissionsHelper

# From this point, the tags begin to be paginated
MAX_FULL_DEPTH_THRESHOLD = 10_000


class TagsPagination(DefaultPagination):
class CanAddPermissionMixin(UserPermissionsHelper): # pylint: disable=abstract-method
"""
This mixin inserts a boolean "can_add_<model>" field at the top level of the paginated response.

The value of the field indicates whether request user may create new instances of the current model.
"""
@property
def _request(self) -> Request:
"""
Returns the current request.
"""
return self.request # type: ignore[attr-defined]

def get_paginated_response(self, data) -> Response:
"""
Injects the user's model-level permissions into the paginated response.
"""
response_data = super().get_paginated_response(data).data # type: ignore[misc]
field_name = f"can_add_{self.model_name}"
response_data[field_name] = self.get_can_add()
return Response(response_data)


class TaxonomyPagination(CanAddPermissionMixin, DefaultPagination):
"""
Inserts permissions data for Taxonomies into the top level of the paginated response.
"""
page_size = 500
max_page_size = 500

@property
def _model(self) -> Type:
"""
Returns the model that is being paginated.
"""
return Taxonomy


class TagsPagination(CanAddPermissionMixin, DefaultPagination):
"""
Custom pagination configuration for taxonomies
with a large number of tags. Used on the get tags API view.
"""
page_size = 10
max_page_size = 300

@property
def _model(self) -> Type:
"""
Returns the model that is being paginated.
"""
return Tag


class DisabledTagsPagination(DefaultPagination):
class DisabledTagsPagination(CanAddPermissionMixin, DefaultPagination):
"""
Custom pagination configuration for taxonomies
with a small number of tags. Used on the get tags API view
Expand All @@ -28,3 +80,10 @@ class DisabledTagsPagination(DefaultPagination):
"""
page_size = MAX_FULL_DEPTH_THRESHOLD
max_page_size = MAX_FULL_DEPTH_THRESHOLD + 1

@property
def _model(self) -> Type:
"""
Returns the model that is being paginated.
"""
return Tag
109 changes: 109 additions & 0 deletions openedx_tagging/core/tagging/rest_api/utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
"""
Utilities for the API
"""
from typing import Optional, Type

from edx_rest_framework_extensions.auth.jwt.authentication import JwtAuthentication # type: ignore[import]
from edx_rest_framework_extensions.auth.session.authentication import ( # type: ignore[import]
SessionAuthenticationAllowInactiveUser,
)
from rest_framework.request import Request


def view_auth_classes(func_or_class):
"""
Function and class decorator that abstracts the authentication classes for api views.
"""
def _decorator(func_or_class):
"""
Requires either OAuth2 or Session-based authentication;
are the same authentication classes used on edx-platform
"""
func_or_class.authentication_classes = (
JwtAuthentication,
SessionAuthenticationAllowInactiveUser,
)
return func_or_class
return _decorator(func_or_class)


class UserPermissionsHelper:
"""
Provides helper methods for serializing user permissions.
"""
@property
def _request(self) -> Request:
"""
Returns the current request.
"""
raise NotImplementedError # pragma: no cover

@property
def _model(self) -> Type:
"""
Returns the model used when checking permissions.
"""
raise NotImplementedError # pragma: no cover

@property
def app_label(self) -> Type:
"""
Returns the app_label for the model used when checking permissions.
"""
return self._model._meta.app_label

@property
def model_name(self) -> Type:
"""
Returns the name of the model used when checking permissions.
"""
return self._model._meta.model_name

def _get_permission_name(self, action: str) -> str:
"""
Returns the fully-qualified permission name corresponding to the current model and `action`.
"""
assert action in ("add", "view", "change", "delete")
return f'{self.app_label}.{action}_{self.model_name}'

def _can(self, perm_name: str, instance=None) -> Optional[bool]:
"""
Does the current `request.user` have the given `perm` on the `instance` object?

Returns None if no permissions were requested.
Returns True if they may.
Returns False if they may not.
"""
request = self._request
assert request and request.user
return request.user.has_perm(perm_name, instance)

def get_can_add(self, _instance=None) -> Optional[bool]:
"""
Returns True if the current user is allowed to add new instances.

Note: we omit the actual instance from the permissions check; most tagging models prefer this.
"""
perm_name = self._get_permission_name('add')
return self._can(perm_name)

def get_can_view(self, instance) -> Optional[bool]:
"""
Returns True if the current user is allowed to view/see this instance.
"""
perm_name = self._get_permission_name('view')
return self._can(perm_name, instance)

def get_can_change(self, instance) -> Optional[bool]:
"""
Returns True if the current user is allowed to edit/change this instance.
"""
perm_name = self._get_permission_name('change')
return self._can(perm_name, instance)

def get_can_delete(self, instance) -> Optional[bool]:
"""
Returns True if the current user is allowed to delete this instance.
"""
perm_name = self._get_permission_name('change')
return self._can(perm_name, instance)
Loading
Loading