def patch(self, request: HttpRequest, *args: Any, **kwargs: Any) -> HttpResponse: # permission check deferred to .perform_update() instance = self.model_object has_read_permissions = False if p.has_perms_shortcut(self.user_object, instance, "r"): has_read_permissions = True serializer = self.get_serializer( instance=instance, data=self.request_data, partial=True, ) serializer.is_valid(raise_exception=True) # User must have write permission on the field being modified. for field_name, field_val in serializer.validated_data.items(): # Note that in case field_name is a foreign key, there are two # cases: # 1. brand # 2. brand_id # If the field_name is brand then field_val is usually a brand # object. If the field name is brand_id, then field val is UUID. field = self.get_model_field(field_name) if not field: continue # For foreign keys, user must have 3-way permissions. See # .check_3way_permissions(). if isinstance(field, ForeignKey): field_name = field.name # this removes "_id" if isinstance(field_val, DCFModel): field_val_pk = field_val.pk else: field_val_pk = field_val new_related_obj: QuerySet = QuerySet(model=field.related_model).filter( pk=field_val_pk ) if not new_related_obj.exists(): raise e.NotFound(f"Related object {field_val_pk} does not exist.") self.check_3way_permissions( self.user_object, self.model_object, field_name, new_related_obj, "w", ) elif not p.has_perms_shortcut( self.user_object, self.model_object, "w", field_name ): raise APIPermissionDenied(self.model_object, "w", field=field_name) # when permited serializer.save() if has_read_permissions: p.add_perms_shortcut(self.user_object, instance, "r") if p.has_perms_shortcut(self.user_object, instance, "r"): return Response( self.get_serializer(instance=instance).data, status=200, ) else: raise UpdatedHiddenObject()
def get(self, request: HttpRequest, *args: Any, **kwargs: Any) -> HttpResponse: """Handles GET request.""" if not p.has_perms_shortcut(self.user_object, self.model_object, "r", self.field_name): raise APIPermissionDenied(self.model_object, "r", self.field_name) if self.is_related_object_api: if self.field_val: if not p.has_perms_shortcut(self.user_object, self.field_val, "r"): raise APIPermissionDenied(self.field_val, "r") serializer = self.get_serializer(instance=self.field_val) return Response(serializer.data) else: raise e.NotFound() else: queryset = self.filter_queryset(self.queryset) assert self.paginator page: Iterable[Serializable] = self.paginator.paginate_queryset( queryset, self.request, view=self) serializer = self.get_serializer() return self.paginator.get_paginated_response([ data.json( version=self.version, context=self.get_serializer_context(), serializer=serializer, ) for data in page ])
def post(self, request: HttpRequest, *args: Any, **kwargs: Any) -> HttpResponse: if not p.has_perms_shortcut(self.user_object, self.model.as_model_type(), "c"): raise e.PermissionDenied("You have no permission to perform POST.") serializer = self.get_serializer(data=self.request_data) serializer.is_valid(raise_exception=True) # Make sure user has related field's write permission for each related # object for field_name, field_value in serializer.validated_data.items(): field = self.get_model_field(field_name) if field and isinstance(field, ForeignKey) and field_value: # Note that in case field_name is a foreign key, there are two # cases: # 1. brand # 2. brand_id # If the field_name is brand then field_val is usually a brand # object. If the field name is brand_id, then field val is UUID. new_related_obj: None | DCFModel if isinstance(field_value, DCFModel): new_related_obj = field_value else: new_related_obj = ( QuerySet(model=field.related_model) .filter(pk=field_value) .first() ) if new_related_obj is None: raise e.NotFound(f"Related object {field_value} does not exist.") else: # Make sure the related object's related name can be # written. For example, for product/<id>/brand, this is # checking if brand.products can be written. if not p.has_perms_shortcut( self.user_object, new_related_obj, "w", field_name=field.remote_field.name, ): raise APIPermissionDenied( new_related_obj, "w", field=field.remote_field.name ) instance = serializer.save() if p.has_perms_shortcut(self.user_object, instance, "r"): return Response( self.get_serializer(instance).data, status=201, ) else: raise CreatedHiddenObject()
def delete(self, request: HttpRequest, *args: Any, **kwargs: Any) -> HttpResponse: if not p.has_perms_shortcut(self.user_object, self.model_object, "d"): raise APIPermissionDenied(self.model_object, "d") try: serializer = self.get_serializer(self.model_object, data=self.request_data) if hasattr(serializer, "delete_obj"): serializer.delete_obj() else: self.model_object.delete() except ProtectedError as excpt: raise e.ValidationError(str(excpt)) else: return Response(status=204)
def __return_get_result_if_permitted(self, request: HttpRequest, *args: Any, **kwargs: Any) -> HttpResponse: """Checks for the user's read permission of the current route's model object. If no read permission, Responses with error, otherwise handles as a GET request. """ if p.has_perms_shortcut(self.user_object, self.model_object, "r", field_name=self.field_name): return self.get(request, *args, **kwargs) else: raise ActionSuccessHiddenObject()
def check_3way_permissions( cls, user: DCFAbstractUser, model_object: Model, field_name: str, new_vals: QuerySet, perms: str, ) -> None: """Check user's permission on three things: 1. The object's field (field_name) 2. Each old related object (field on the reverse side) 3. Each new related object (field on the reverse side) """ field = cls._get_field(model_object._meta.model, field_name) assert isinstance( field, ( ManyToManyRel, ManyToManyField, ManyToOneRel, ForeignKey, ), ) if not p.has_perms_shortcut( user, model_object, perms, field_name=field_name, ): raise APIPermissionDenied(model_object, perms, field=field_name) old_vals = cls._get_field_as_queryset(model_object, field_name) diff_remove = old_vals.difference(new_vals) diff_add = new_vals.difference(old_vals) # Django doesn't support calling .filter() after .union() and # .difference() symmetric_diff_pks = diff_add.union(diff_remove).values_list("pk") diff_queryset: QuerySet = QuerySet(model=field.related_model).filter( pk__in=symmetric_diff_pks) cls.__check_perms_for_each( user, diff_queryset, field_name=field.remote_field.name, perms=perms, )
def get(self, request: HttpRequest, *args: Any, **kwargs: Any) -> HttpResponse: if not p.has_perms_shortcut(self.user_object, self.model_object, "r"): raise APIPermissionDenied(self.model_object, "r") serializer = self.get_serializer(instance=self.model_object) return Response(serializer.data)
def test_object_perm_does_not_imply_model_perm(self) -> None: add_perms_shortcut(self.user, self.product, "r") self.assertFalse(has_perms_shortcut(self.user, Product, "r"))
def test_field_perm_does_not_imply_object_perm(self) -> None: add_perms_shortcut(self.user, self.product, "r", field_name="barcode") self.assertFalse(has_perms_shortcut(self.user, self.product, "r"))
def test_model_perm_implies_object_perm_for_field(self) -> None: add_perms_shortcut(self.user, Product, "r", "brand") self.assertTrue(has_perms_shortcut(self.user, self.product, "r", "brand"))
def test_object_perm_implies_field_perm_for_field(self) -> None: product = Product.objects.create() add_perms_shortcut(self.user, product, "r", "brand") self.assertTrue(has_perms_shortcut(self.user, product, "r", "brand"))
def test_assign_field(self) -> None: self.assertFalse(has_perms_shortcut(self.user, self.product, "r", "brand")) add_perms_shortcut(self.user, self.product, "r") self.assertTrue(has_perms_shortcut(self.user, self.product, "r", "brand"))