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?