Django Tastypie Record-Level Authorization
Asked Answered
T

2

2

I'm having trouble implementing record-level authorization with tastypie 0.9.12+.

My setup looks something like this:

Model

class UserProfile(models.Model):
    def __unicode__(self):
        return self.user.get_full_name()

    user = models.OneToOneField(User)

class Sample(models.Model):
    def __unicode__(self):
        return '%s' % self.id

    OPEN = 0
    CLAIMED = 1
    CLOSED = 2
    MANUAL = 3
    MODIFIED = 4
    DELETED = 5
    ERROR = 6
    RESERVED = 7
    STATUS_CHOICES = (
        (OPEN, 'Open'),
        (CLAIMED, 'Claimed'),
        (CLOSED, 'Closed'),
        (MANUAL, 'Manual'),
        (MODIFIED, 'Modified'),
        (DELETED, 'Deleted'),
        (ERROR, 'Error'),
        (RESERVED, 'Reserved'),
    )

    status = models.SmallIntegerField(max_length = 1, default = OPEN, choices = STATUS_CHOICES)
    user_profile = models.ForeignKey(UserProfile, blank = True, null = True)

Resource

class BaseResource(ModelResource):
    # Base class with rather strict default settings
    # All other Resources extend this and override any defaults to higher permissions
    class Meta:
        authentication = DjangoAuthentication()
        authorization = ReadOnlyAuthorization()
        allowed_methods = []

class SampleResource(BaseResource): # BaseResource defines a default Meta, setting allowed_methods and authentication for all other resources in the API
    UserProfile = fields.ForeignKey(UserProfileResource, 'user_profile', null = True, full = True)
    class Meta(BaseResource.Meta):
        queryset = Sample.objects.all()
        resource_name = 'sample'
        allowed_methods = ['get', 'post', 'put', 'patch']
        authorization = SampleAuthorization()
        always_return_data = True

    def dehydrate_status(self, bundle):
        return Sample.STATUS_CHOICES[bundle.data['status']][1]

    def hydrate_status(self, bundle):
        bundle.data['status'] = Sample.__dict__[bundle.data['status'].upper()]
        return bundle

Authorization

class SampleAuthorization(Authorization):
    # Checks that the records' owner is either None or the logged in user
    def authorize_user(self, bundle):
        return bundle.obj.user_profile in (None, self.user_profile(bundle))

    def user_profile(self, bundle):
        return user_profile.objects.get(user = bundle.request.user)



    def read_list(self, object_list, bundle):
        print 'Read List'
        return object_list.filter(Q(user_profile = self.user_profile(bundle)) | Q(user_profile = None))

    def read_detail(self, object_list, bundle):
        print 'Read Detail'
        return self.authorize_user(bundle)

    def create_list(self, object_list, bundle):
        return object_list

    def create_detail(self, object_list, bundle):
        return self.authorize_user(bundle)

    def update_list(self, object_list, bundle):
        print 'Update List'
        allowed = []
        for obj in object_list:
            if obj.user_profile in (None, self.user_profile(bundle)):
                allowed.append(obj)

        return allowed

    def update_detail(self, object_list, bundle):
        print 'Update Detail'
        print bundle.obj.status, bundle.data['status']
        # Compare status stored on the server against the user-set status
        # If server status is >= user status
        # Raise Unauthorized
        if bundle.obj.status >= bundle.data['status']:
                raise Unauthorized('New status must be higher than current status')
        return self.authorize_user(bundle)

    def delete_list(self, object_list, bundle):
        raise Unauthorized('Deletion not allowed through API')

    def delete_detail(self, object_list, bundle):
        raise Unauthorized('Deletion not allowed through API')

My problem is that it appears that update_detail is called twice, with different inputs. The update requested is attempting to change the status of the record stored on the server. The new status must be higher than the stored status, or the change is Unauthorized.

When running the above code, my output looks like this:

Read Detail
Update Detail
0 Claimed
Update Detail
1 1
[27/Mar/2013 09:35:23] "PATCH /api/1.0/sample/1/ HTTP/1.1" 401 0

On the first pass, the bundle.obj.status has the correct value, but the bundle.data['status'] HAS NOT been hydrated. On the second pass, the bundle.obj.status has been changed to the new status, and the new status HAS been hydrated.

Because the status hasn't been hydrated on the first pass, I can't compare them reliably, and don't want to manually call hydrate_status as it messes up the entire hydrate process done in the background. Because the values on the second pass are the same, no matter what status I set it to, it always raises the Unauthorized exception.

How can I implement the record-level authorization if the method is called twice by Tastypie with different inputs for both the stored and new status values?

Tutelage answered 27/3, 2013 at 13:42 Comment(0)
T
1

Turns out, the multiple calls to update_detail was a bug in the tastypie framework.

Issue was submitted on github and resolved in a bug fix.

Tutelage answered 28/4, 2013 at 15:30 Comment(0)
M
0

Have a look at using django-guardian instead of hardcoding the relationship on your model. Something like the following authorization class would be a good start:

https://gist.github.com/airtonix/5476453

Morrissette answered 28/4, 2013 at 9:59 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.