From cf86da266a86f22657fae5d62f57a7567c92abbd Mon Sep 17 00:00:00 2001 From: Krzysztof Klimonda Date: Sat, 2 Mar 2013 17:00:58 -0800 Subject: [PATCH] Add a very simplistic Pin access control for the API As pointed in issue #75 we should get away with just checking if the pin submitter is the currently logged in user. Assuming that we can implement authorization for updating and deleting pins rather easily by subclassing DjangoAuthorization so it passes the object to the Authorization backend. --- pinry/core/api.py | 34 +++++++++++++++++++++++++++++- pinry/core/auth/backends.py | 12 ++++++++++- pinry/core/tests.py | 41 +++++++++++++++++++++++++------------ 3 files changed, 72 insertions(+), 15 deletions(-) diff --git a/pinry/core/api.py b/pinry/core/api.py index 1105745..259951b 100644 --- a/pinry/core/api.py +++ b/pinry/core/api.py @@ -1,5 +1,6 @@ from tastypie import fields from tastypie.authorization import DjangoAuthorization +from tastypie.exceptions import Unauthorized from tastypie.resources import ModelResource from django_images.models import Thumbnail @@ -7,6 +8,37 @@ from pinry.core.models import User from pinry.pins.models import Image, Pin +class PinryAuthorization(DjangoAuthorization): + """ + Pinry-specific Authorization backend with object-level permission checking. + """ + def update_detail(self, object_list, bundle): + klass = self.base_checks(bundle.request, bundle.obj.__class__) + + if klass is False: + raise Unauthorized("You are not allowed to access that resource.") + + permission = '%s.change_%s' % (klass._meta.app_label, klass._meta.module_name) + + if not bundle.request.user.has_perm(permission, bundle.obj): + raise Unauthorized("You are not allowed to access that resource.") + + return True + + def delete_detail(self, object_list, bundle): + klass = self.base_checks(bundle.request, bundle.obj.__class__) + + if klass is False: + raise Unauthorized("You are not allowed to access that resource.") + + permission = '%s.delete_%s' % (klass._meta.app_label, klass._meta.module_name) + + if not bundle.request.user.has_perm(permission, bundle.obj): + raise Unauthorized("You are not allowed to access that resource.") + + return True + + class UserResource(ModelResource): gravatar = fields.CharField(readonly=True) @@ -87,4 +119,4 @@ class PinResource(ModelResource): resource_name = 'pin' include_resource_uri = False always_return_data = True - authorization = DjangoAuthorization() + authorization = PinryAuthorization() diff --git a/pinry/core/auth/backends.py b/pinry/core/auth/backends.py index a1b0c02..149defc 100644 --- a/pinry/core/auth/backends.py +++ b/pinry/core/auth/backends.py @@ -1,6 +1,8 @@ from django.core.validators import email_re from pinry.core.models import User +from pinry.pins.models import Pin + class CombinedAuthBackend(object): def authenticate(self, username=None, password=None): @@ -22,4 +24,12 @@ class CombinedAuthBackend(object): try: return User.objects.get(pk=user_id) except User.DoesNotExist: - return None \ No newline at end of file + return None + + def has_perm(self, user, perm, obj=None): + """ + A very simplistic authorization mechanism for now. Basically a pin owner can do anything with the pin. + """ + if obj and isinstance(obj, Pin): + return obj.submitter == user + return False diff --git a/pinry/core/tests.py b/pinry/core/tests.py index 9a57cb2..e13b819 100644 --- a/pinry/core/tests.py +++ b/pinry/core/tests.py @@ -37,7 +37,6 @@ class ImageResourceTest(ResourceTestCase): self.client = Client() def test_list_detail(self): - self.maxDiff = None image = Image.objects.get(pk=1) thumbnail = filter_generator_for('thumbnail')(image) standard = filter_generator_for('standard')(image) @@ -107,28 +106,44 @@ class PinResourceTest(ResourceTestCase): self.assertEqual(Pin.objects.count(), 3) self.assertEquals(Tag.objects.count(), 4) - def test_put_details_unauthenticated(self): + def test_put_detail_unauthenticated(self): + self.api_client.client.logout() uri = '/api/v1/pin/{}/'.format(self.pin_1.pk) response = self.api_client.put(uri, format='json', data={}) self.assertHttpUnauthorized(response) - def test_put_details_unauthorized(self): + def test_put_detail_unauthorized(self): uri = '/api/v1/pin/{}/'.format(self.pin_1.pk) User.objects.create_user('test', 'test@example.com', 'test') self.api_client.client.login(username='test', password='test') response = self.api_client.put(uri, format='json', data={}) self.assertHttpUnauthorized(response) - # def test_put_details(self): - # uri = '/api/v1/pin/{}/'.format(self.pin_1.pk) - # original = self.deserialize(self.api_client.get(uri, format='json')) - # new = original.copy() - # new['description'] = 'Updated description' - # - # self.assertEqual(Pin.objects.count(), 2) - # response = self.api_client.put(uri, format='json', data=new) - # self.assertHttpAccepted(response) - # self.assertEqual(Pin.objects.count(), 2) + def test_put_detail(self): + uri = '/api/v1/pin/{}/'.format(self.pin_1.pk) + original = self.deserialize(self.api_client.get(uri, format='json')) + new = {'description': 'Updated description'} + + response = self.api_client.put(uri, format='json', data=new) + self.assertHttpAccepted(response) + self.assertEqual(Pin.objects.count(), 2) + self.assertEqual(Pin.objects.get(pk=self.pin_1.pk).description, new['description']) + + def test_delete_detail_unauthenticated(self): + uri = '/api/v1/pin/{}/'.format(self.pin_1.pk) + self.api_client.client.logout() + self.assertHttpUnauthorized(self.api_client.delete(uri)) + + def test_delete_detail_unauthorized(self): + uri = '/api/v1/pin/{}/'.format(self.pin_1.pk) + User.objects.create_user('test', 'test@example.com', 'test') + self.api_client.client.login(username='test', password='test') + self.assertHttpUnauthorized(self.api_client.delete(uri)) + + def test_delete_detail(self): + uri = '/api/v1/pin/{}/'.format(self.pin_1.pk) + self.assertHttpAccepted(self.api_client.delete(uri)) + self.assertEqual(Pin.objects.count(), 1) def test_get_list_json_ordered(self): pin = Pin.objects.latest('id')