diff --git a/tastypie/authorization.py b/tastypie/authorization.py index e8c82e699..88af33c2d 100644 --- a/tastypie/authorization.py +++ b/tastypie/authorization.py @@ -1,3 +1,6 @@ +from tastypie.exceptions import TastypieError, Unauthorized + + class Authorization(object): """ A base class that provides no permissions checking. @@ -13,29 +16,81 @@ def __get__(self, instance, owner): def apply_limits(self, request, object_list): """ - A means of narrowing a list of objects on a per-user/request basis. + Deprecated. + """ + raise TastypieError("Authorization classes no longer support `apply_limits`. Please update to using `read_list`.") - Default simply returns the unaltered list. + def read_list(self, object_list, bundle): + """ + Returns a list of all the objects a user is allowed to read. + + Should return an empty list if none are allowed. + + Returns the entire list by default. """ return object_list - def to_read(self, bundle): + def read_single(self, object_list, bundle): """ - Checks if the user is authorized to perform the request. If ``object`` - is provided, it can do additional row-level checks. + Returns either ``True`` if the user is allowed to read the object in + question or throw ``Unauthorized`` if they are not. - Should return either ``True`` if allowed, ``False`` if not or an - ``HttpResponse`` if you need something custom. + Returns ``True`` by default. """ return True - def to_add(self, bundle): + def create_list(self, object_list, bundle): + """ + Unimplemented, as Tastypie never creates entire new lists, but + present for consistency & possible extension. + """ + raise NotImplementedError("Tastypie has not way to determine if all objects should be allowed to be created.") + + def create_single(self, object_list, bundle): + """ + Returns either ``True`` if the user is allowed to create the object in + question or throw ``Unauthorized`` if they are not. + + Returns ``True`` by default. + """ return True - def to_change(self, bundle): + def update_list(self, object_list, bundle): + """ + Returns a list of all the objects a user is allowed to update. + + Should return an empty list if none are allowed. + + Returns the entire list by default. + """ + return object_list + + def update_single(self, object_list, bundle): + """ + Returns either ``True`` if the user is allowed to update the object in + question or throw ``Unauthorized`` if they are not. + + Returns ``True`` by default. + """ return True - def to_delete(self, bundle): + def delete_list(self, object_list, bundle): + """ + Returns a list of all the objects a user is allowed to delete. + + Should return an empty list if none are allowed. + + Returns the entire list by default. + """ + return object_list + + def delete_single(self, object_list, bundle): + """ + Returns either ``True`` if the user is allowed to delete the object in + question or throw ``Unauthorized`` if they are not. + + Returns ``True`` by default. + """ return True @@ -45,16 +100,28 @@ class ReadOnlyAuthorization(Authorization): Only allows ``GET`` requests. """ - def to_read(self, bundle): + def read_list(self, object_list, bundle): + return object_list + + def read_single(self, object_list, bundle): return True - def to_add(self, bundle): + def create_list(self, object_list, bundle): + return [] + + def create_single(self, object_list, bundle): return False - def to_change(self, bundle): + def update_list(self, object_list, bundle): + return [] + + def update_single(self, object_list, bundle): return False - def to_delete(self, bundle): + def delete_list(self, object_list, bundle): + return [] + + def delete_single(self, object_list, bundle): return False @@ -63,22 +130,32 @@ class DjangoAuthorization(Authorization): Uses permission checking from ``django.contrib.auth`` to map ``POST / PUT / DELETE / PATCH`` to their equivalent Django auth permissions. - """ - def base_checks(self, bundle): - klass = self.resource_meta.object_class + Both the list & detail variants simply check the model they're based + on, as that's all the more granular Django's permission setup gets. + """ + def base_checks(self, model_klass): # If it doesn't look like a model, we can't check permissions. - if not klass or not getattr(klass, '_meta', None): + if not model_klass or not getattr(model_klass, '_meta', None): return False # User must be logged in to check permissions. if not hasattr(bundle.request, 'user'): return False - return klass + return model_klass + + def read_list(self, object_list, bundle): + klass = self.base_checks(object_list.model) + + if klass is False: + return False + + # GET-style methods are always allowed. + return True - def to_read(self, bundle): - klass = self.base_checks(bundle) + def read_single(self, object_list, bundle): + klass = self.base_checks(bundle.obj.__class__) if klass is False: return False @@ -86,8 +163,17 @@ def to_read(self, bundle): # GET-style methods are always allowed. return True - def to_add(self, bundle): - klass = self.base_checks(bundle) + def create_list(self, object_list, bundle): + klass = self.base_checks(object_list.model) + + if klass is False: + return False + + permission = '%s.add_%s' % (klass._meta.app_label, klass._meta.module_name) + return bundle.request.user.has_perm(permission) + + def create_single(self, object_list, bundle): + klass = self.base_checks(bundle.obj.__class__) if klass is False: return False @@ -95,8 +181,17 @@ def to_add(self, bundle): permission = '%s.add_%s' % (klass._meta.app_label, klass._meta.module_name) return bundle.request.user.has_perm(permission) - def to_change(self, bundle): - klass = self.base_checks(bundle) + def update_list(self, object_list, bundle): + klass = self.base_checks(object_list.model) + + if klass is False: + return False + + permission = '%s.change_%s' % (klass._meta.app_label, klass._meta.module_name) + return bundle.request.user.has_perm(permission) + + def update_single(self, object_list, bundle): + klass = self.base_checks(bundle.obj.__class__) if klass is False: return False @@ -104,8 +199,17 @@ def to_change(self, bundle): permission = '%s.change_%s' % (klass._meta.app_label, klass._meta.module_name) return bundle.request.user.has_perm(permission) - def to_delete(self, bundle): - klass = self.base_checks(bundle) + def delete_list(self, object_list, bundle): + klass = self.base_checks(object_list.model) + + if klass is False: + return False + + permission = '%s.delete_%s' % (klass._meta.app_label, klass._meta.module_name) + return bundle.request.user.has_perm(permission) + + def delete_single(self, object_list, bundle): + klass = self.base_checks(bundle.obj.__class__) if klass is False: return False diff --git a/tastypie/exceptions.py b/tastypie/exceptions.py index 162197187..836705c23 100644 --- a/tastypie/exceptions.py +++ b/tastypie/exceptions.py @@ -25,6 +25,16 @@ class NotFound(TastypieError): pass +class Unauthorized(TastypieError): + """ + Raised when the request object is not accessible to the user. + + This is different than the ``tastypie.http.HttpUnauthorized`` & is handled + differently internally. + """ + pass + + class ApiFieldError(TastypieError): """ Raised when there is a configuration error with a ``ApiField``. @@ -42,7 +52,7 @@ class UnsupportedFormat(TastypieError): class BadRequest(TastypieError): """ A generalized exception for indicating incorrect request parameters. - + Handled specially in that the message tossed by this exception will be presented to the end user. """ @@ -73,14 +83,14 @@ class ImmediateHttpResponse(TastypieError): """ This exception is used to interrupt the flow of processing to immediately return a custom HttpResponse. - + Common uses include:: - + * for authentication (like digest/OAuth) * for throttling - + """ response = HttpResponse("Nothing provided.") - + def __init__(self, response): self.response = response diff --git a/tastypie/resources.py b/tastypie/resources.py index 760bdf27d..b4fa013af 100644 --- a/tastypie/resources.py +++ b/tastypie/resources.py @@ -14,7 +14,7 @@ from tastypie.bundle import Bundle from tastypie.cache import NoCache from tastypie.constants import ALL, ALL_WITH_RELATIONS -from tastypie.exceptions import NotFound, BadRequest, InvalidFilterError, HydrationError, InvalidSortError, ImmediateHttpResponse +from tastypie.exceptions import NotFound, BadRequest, InvalidFilterError, HydrationError, InvalidSortError, ImmediateHttpResponse, Unauthorized from tastypie import fields from tastypie import http from tastypie.paginator import Paginator @@ -536,49 +536,104 @@ def log_throttled_access(self, request): request_method = request.method.lower() self._meta.throttle.accessed(self._meta.authentication.get_identifier(request), url=request.get_full_path(), request_method=request_method) - def handle_authorized_result(self, result, fail_silently=False): - if isinstance(result, HttpResponse): - raise ImmediateHttpResponse(response=result) + def unauthorized_result(self, exception): + raise ImmediateHttpResponse(response=http.HttpUnauthorized()) - if not result is True: - if fail_silently: - return False - - raise ImmediateHttpResponse(response=http.HttpUnauthorized()) + def authorized_read_list(self, bundle): + """ + Handles checking of permissions to see if the user has authorization + to GET this resource. + """ + try: + auth_result = self._meta.authorization.read_list(bundle) + except Unauthorized, e: + self.unauthorized_result(e) - return result + return auth_result - def authorized_to_read(self, bundle, fail_silently=False): + def authorized_read_detail(self, bundle): """ Handles checking of permissions to see if the user has authorization to GET this resource. """ - auth_result = self._meta.authorization.to_read(bundle) - return self.handle_authorized_result(auth_result, fail_silently) + try: + auth_result = self._meta.authorization.read_detail(bundle) + except Unauthorized, e: + self.unauthorized_result(e) - def authorized_to_add(self, bundle, fail_silently=False): + return auth_result + + def authorized_create_list(self, bundle): """ Handles checking of permissions to see if the user has authorization to POST this resource. """ - auth_result = self._meta.authorization.to_add(bundle) - return self.handle_authorized_result(auth_result, fail_silently) + try: + auth_result = self._meta.authorization.create_list(bundle) + except Unauthorized, e: + self.unauthorized_result(e) - def authorized_to_change(self, bundle, fail_silently=False): + return auth_result + + def authorized_create_detail(self, bundle): + """ + Handles checking of permissions to see if the user has authorization + to POST this resource. + """ + try: + auth_result = self._meta.authorization.create_detail(bundle) + except Unauthorized, e: + self.unauthorized_result(e) + + return auth_result + + def authorized_update_list(self, bundle): """ Handles checking of permissions to see if the user has authorization to PUT this resource. """ - auth_result = self._meta.authorization.to_change(bundle) - return self.handle_authorized_result(auth_result, fail_silently) + try: + auth_result = self._meta.authorization.update_list(bundle) + except Unauthorized, e: + self.unauthorized_result(e) + + return auth_result - def authorized_to_delete(self, bundle, fail_silently=False): + def authorized_update_detail(self, bundle): + """ + Handles checking of permissions to see if the user has authorization + to PUT this resource. + """ + try: + auth_result = self._meta.authorization.update_detail(bundle) + except Unauthorized, e: + self.unauthorized_result(e) + + return auth_result + + def authorized_delete_list(self, bundle): + """ + Handles checking of permissions to see if the user has authorization + to DELETE this resource. + """ + try: + auth_result = self._meta.authorization.delete_list(bundle) + except Unauthorized, e: + self.unauthorized_result(e) + + return auth_result + + def authorized_delete_detail(self, bundle): """ Handles checking of permissions to see if the user has authorization to DELETE this resource. """ - auth_result = self._meta.authorization.to_delete(bundle) - return self.handle_authorized_result(auth_result, fail_silently) + try: + auth_result = self._meta.authorization.delete_detail(bundle) + except Unauthorized, e: + self.unauthorized_result(e) + + return auth_result def build_bundle(self, obj=None, data=None, request=None): """ @@ -1063,6 +1118,7 @@ def get_list(self, request, **kwargs): # TODO: Uncached for now. Invalidation that works for everyone may be # impossible. objects = self.obj_get_list(request=request, **self.remove_api_resource_names(kwargs)) + objects = self.authorized_read_list(objects, self.build_bundle(request=request)) sorted_objects = self.apply_sorting(objects, options=request.GET) paginator = self._meta.paginator_class(request.GET, sorted_objects, resource_uri=self.get_resource_list_uri(), limit=self._meta.limit, max_limit=self._meta.max_limit) @@ -1073,9 +1129,7 @@ def get_list(self, request, **kwargs): for obj in to_be_serialized['objects']: bundle = self.build_bundle(obj=obj, request=request) - - if self.authorized_to_read(bundle, fail_silently=True): - bundles.append(self.full_dehydrate(bundle)) + bundles.append(self.full_dehydrate(bundle)) to_be_serialized['objects'] = bundles to_be_serialized = self.alter_list_data_to_serialize(request, to_be_serialized) @@ -1098,9 +1152,9 @@ def get_detail(self, request, **kwargs): return http.HttpMultipleChoices("More than one resource is found at this URI.") bundle = self.build_bundle(obj=obj, request=request) + self.authorized_read_detail(self.get_object_list(request), bundle) bundle = self.full_dehydrate(bundle) bundle = self.alter_detail_data_to_serialize(request, bundle) - self.authorized_to_read(bundle) return self.create_response(request, bundle) def post_list(self, request, **kwargs): @@ -1118,7 +1172,7 @@ def post_list(self, request, **kwargs): deserialized = self.alter_deserialized_detail_data(request, deserialized) bundle = self.build_bundle(data=dict_strip_unicode_keys(deserialized), request=request) self.is_valid(bundle, request) - self.authorized_to_add(bundle) + self.authorized_create_detail(self.get_object_list(request), bundle) updated_bundle = self.obj_create(bundle, request=request, **self.remove_api_resource_names(kwargs)) location = self.get_resource_uri(updated_bundle) @@ -1138,8 +1192,6 @@ def post_detail(self, request, **kwargs): If a new resource is created, return ``HttpCreated`` (201 Created). """ - bundle = self.build_bundle(request=request) - self.authorized_to_add(bundle) return http.HttpNotImplemented() def put_list(self, request, **kwargs): @@ -1171,7 +1223,7 @@ def put_list(self, request, **kwargs): # objects if validation fails. try: self.is_valid(bundle, request) - self.authorized_to_change(bundle) + self.authorized_update_detail(self.get_object_list(request), bundle) except ImmediateHttpResponse: self.rollback(bundles_seen) raise @@ -1210,7 +1262,7 @@ def put_detail(self, request, **kwargs): deserialized = self.alter_deserialized_detail_data(request, deserialized) bundle = self.build_bundle(data=dict_strip_unicode_keys(deserialized), request=request) self.is_valid(bundle, request) - self.authorized_to_change(bundle) + self.authorized_update_detail(self.get_object_list(request), bundle) try: updated_bundle = self.obj_update(bundle, request=request, **self.remove_api_resource_names(kwargs)) @@ -1241,7 +1293,9 @@ def delete_list(self, request, **kwargs): If the resources are deleted, return ``HttpNoContent`` (204 No Content). """ bundle = self.build_bundle(request=request) - self.authorized_to_delete(bundle) + # FIXME: This still kinda bites because it may not be the correct list + # of objects. + self.authorized_delete_list(self.get_object_list(request), bundle) self.obj_delete_list(request=request, **self.remove_api_resource_names(kwargs)) return http.HttpNoContent() @@ -1255,7 +1309,7 @@ def delete_detail(self, request, **kwargs): If the resource did not exist, return ``Http404`` (404 Not Found). """ bundle = self.build_bundle(request=request) - self.authorized_to_delete(bundle) + self.authorized_delete_detail(self.get_object_list(request), bundle) try: self.obj_delete(request=request, **self.remove_api_resource_names(kwargs)) @@ -1335,7 +1389,7 @@ def patch_list(self, request, **kwargs): bundle = self.build_bundle(data=dict_strip_unicode_keys(data)) bundle.obj.pk = obj.pk self.is_valid(bundle, request) - self.authorized_to_add(bundle) + self.authorized_create_detail(self.get_object_list(request), bundle) self.obj_create(bundle, request=request) else: # There's no resource URI, so this is a create call just @@ -1343,7 +1397,7 @@ def patch_list(self, request, **kwargs): data = self.alter_deserialized_detail_data(request, data) bundle = self.build_bundle(data=dict_strip_unicode_keys(data)) self.is_valid(bundle, request) - self.authorized_to_add(bundle) + self.authorized_create_detail(self.get_object_list(request), bundle) self.obj_create(bundle, request=request) if len(deserialized.get('deleted_objects', [])) and 'delete' not in self._meta.detail_allowed_methods: @@ -1352,7 +1406,7 @@ def patch_list(self, request, **kwargs): for uri in deserialized.get('deleted_objects', []): obj = self.get_via_uri(uri, request=request) bundle = self.build_bundle(obj=obj, request=request) - self.authorized_to_delete(bundle) + self.authorized_delete_detail(self.get_object_list(request), bundle) self.obj_delete(request=request, _obj=obj) return http.HttpAccepted() @@ -1401,7 +1455,7 @@ def update_in_place(self, request, original_bundle, new_data): # function is cribbed from put_detail. self.alter_deserialized_detail_data(request, original_bundle.data) self.is_valid(original_bundle, request) - self.authorized_to_change(original_bundle) + self.authorized_update_detail(self.get_object_list(request), original_bundle) return self.obj_update(original_bundle, request=request, pk=original_bundle.obj.pk) def get_schema(self, request, **kwargs): @@ -1446,9 +1500,9 @@ def get_multiple(self, request, **kwargs): bundle = self.build_bundle(obj=obj, request=request) bundle = self.full_dehydrate(bundle) - if self.authorized_to_read(bundle, fail_silently=True): + if self.authorized_read_detail(self.get_object_list(request), bundle): objects.append(bundle) - except ObjectDoesNotExist: + except (ObjectDoesNotExist, Unauthorized): not_found.append(pk) object_list = {