[Fixed]-Adding per-object permissions to django admin

23👍

I would simply add a method to each model is_owned_by(user), and it is upto the model to decide if it is owned by that user or not. In most case is_owned_by can be a generic function in a base model class and you can tweak it in special cases. e.g.

class RentalPhoto(BaseModel):
    def is_owned_by(self, user):
        return self.rental.is_owned_by(user)

This is generic enough and being explicit you will have full control how things behave.

To add new permission you can add that to your models e.g.

class Rental(models.Model):
    # ...
    class Meta:
        permissions = (
            ("can_edit_any", "Can edit any rentals"),
        )

I think instead of adding two permission for any and own, you should add only own permission , so each object already has can_edit which you can treat as user can edit only his object, and if user has permission can_edit_any than only he is allowed to edit all

Using this we can extend auth by adding a custom backend e.g.

class PerObjectBackend(ModelBackend):

    def has_perm(self, user_obj, perm, obj=None):
        allowed = ModelBackend.has_perm(self, user_obj, perm)
        if perm.find('any') >=0 :
            return allowed

        if perm.find('edit') >=0 or perm.find('delete') >=0:
            if obj is None:
                raise Exception("Perm '%s' needs an object"%perm)
            if not obj.is_owned_by(user_obj):
                return False

        return allowed

This is a very quick implemenation, in reality you can extend permission objects to check if it needs and object or not e.g. permission.is_per_object instead of doing crude string search but that should also work if you have standard names

2👍

If you don’t want to implement your own Permission Backend, I recommend you to use https://github.com/chrisglass/django-rulez You will do what you want in a much easier way.

1👍

It’s in Django docs.

Basically you create custom admin class for your model, and define the method get_queryset. In your case it could be something like below. Super user would see all rentals, while owner only his.

class RentalAdmin(admin.ModelAdmin):
    def get_queryset(self, request):
        qs = super(RentalAdmin, self).get_queryset(request)
        if request.user.is_superuser:
            return qs
        return qs.filter(owner=request.user)

Here’s another possible lead: https://code.djangoproject.com/wiki/RowLevelPermissions

👤Ska

0👍

Can this be solved by some trickery in
the framework, following ForeignKeys
until an object is found with a
ForeignKey to user?

I don’t see where there is trickery necessairy:
RentalPhoto -> Rental -> User
So to get the User for a particular RentalPhoto you would call something like this in the instance:

photo.rental.user

Following multiple relations in one step can be considered as non-trickery.

0👍

class Rental(models.Model):
    owner: User = models.ForeignKey(
        User, verbose_name='owner', related_name='rentals',
        on_delete=models.CASCADE, blank=True, null=False
    )
    # owner_id automatically gets created by Django. Optionally annotate to help your IDE
    owner_id: int

    def is_owned_by(self, user: User):
        # You can use self.owner == user, or self.owner.id == user.id. 
        # But this way owner data won't be fetched from the database
        if self.owner_id == user.id:
            return True
        return False


class RentalPhoto(models.Model):

    rental: Rental = models.ForeignKey(
        Rental, on_delete=models.CASCADE, related_name='rental_photos',
        blank=False, null=False,
    )

    def is_owned_by(self, user: User):
        return self.rental.is_owned_by(user)


class RentalPhotoAdminInline(admin.StackedInline):
    model = RentalPhoto
    extra = 1


@admin.register(Rental)
class RentalAdmin(admin.ModelAdmin):

    inlines = (RentalPhotoAdminInline,)

    # staff members can only view/operate their rentals
    def get_queryset(self, request):
        queryset = super().get_queryset(request)
        if not request.user.is_superuser:
            queryset = queryset.filter(owner_id=request.user.id)

        return queryset

    def save_model(self, request, obj: Rental, form, change):
        # set rental owner on admin save
        if obj.owner_id is None:
            obj.owner = request.user
        obj.save()

    def has_view_or_change_permission(self, request, obj=None):
        allowed = super().has_view_or_change_permission(request, obj)
        if obj is None:
            return allowed
        return request.user.is_superuser or obj.is_owned_by(request.user)

    def has_view_permission(self, request, obj=None):
        allowed = super().has_view_permission(request, obj)
        if obj is None:
            return allowed
        return request.user.is_superuser or obj.is_owned_by(request.user)

    def has_change_permission(self, request, obj=None):
        allowed = super().has_change_permission(request, obj)
        if obj is None:
            return allowed
        return request.user.is_superuser or obj.is_owned_by(request.user)

    def has_delete_permission(self, request, obj=None):
        allowed = super().has_delete_permission(request, obj)
        if obj is None:
            return allowed
        return request.user.is_superuser or obj.is_owned_by(request.user)

Leave a comment