Tastypie Authorization not exactly authorizating? - python

Lets say that we want to be able to update only those objects of which the stage integer field is lower than 3. (or a similar example, that we want to limit the permission to object update only to the Owner users=fields.foreignkey). So the first example authorization will look like this:
class RecordAuthorization(Authorization):
def update_detail(self, object_list, bundle):
if bundle.obj.stage < 3:
return True
raise Unauthorized("You can't update this")
or the second example:
class RecordAuthorization(Authorization):
def update_detail(self, object_list, bundle):
if bundle.obj.user == bundle.request.user:
return True
raise Unauthorized("You can't update this")
Actually neither of them will work (I tested the first one and it does not work).
When you look closely into the tastypie documentation they say:
object_list is the collection of objects being processed as part of
the request.
So this means that in object_list, there are the json objects only rewrote to python list of dicts? So there are NOT the real object from database, therefore this filtering:
def update_list(self, object_list, bundle):
return object_list.filter(stage__lt=3)
won't work as expected (allowing updates only to objects having stage lower than 3). And will do something like-> if the json (sent through API, not the object in database) stage is lower than 3 allow updated. So actually you can update an object with stage=5 (in database) to stage=1!
I get the same strange results for the update_detail function, too. So I suspect that the bundle.obj is also the object but already with "json-updated" properties.
So to make things work I need to do this:?!
class RecordAuthorization(Authorization):
def update_detail(self, object_list, bundle):
if User.objects.get(pk=bundle.obj.user.pk) == bundle.request.user:
return True
raise Unauthorized("You can't update this")

For update_list, the object_list argument will be a queryset (or other iterable for non-Django ORM data sources) that should be filtered.
For update_detail, you want to check the properties of bundle.obj, which is an instance of your Resource.Meta.object_class, such as a Django model. If you set Resource.Meta.queryset, Resource.Meta.object_class is set for you.
There are indentation errors in your code, you should be raising Unauthorized at the method level, not the class level. If fixing that doesn't solve the issue, please post your resources.

Related

How can I deal with a massive delete from Django Admin?

I'm working with Django 2.2.10.
I have a model called Site, and a model called Record.
Each record is associated with a single site (Foreign Key).
After my app runs for a few days/weeks/months, each site can have thousands of records associated with it. I use the database efficiently, so this isn't normally a problem.
In Django Admin, when I try to delete a site however, Django Admin tries to figure out every single associated object that will also be deleted, and because my ForeignKey uses on_delete=models.CASCADE, which is what I want, it tries to generate a page that lists thousands, possibly millions of records that will be deleted. Sometimes this succeeds, but takes a few seconds. Sometimes the browser just gives up waiting.
How can I have Django Admin not list every single record it intends to delete? Maybe just say something like "x number of records will be deleted" instead.
Update: Should I be overriding Django admin's delete_confirmation.html? It looks like the culprit might be this line:
<ul>{{ deleted_objects|unordered_list }}</ul>
Or is there an option somewhere that can be enabled to automatically not list every single object to be deleted, perhaps if the object count is over X number of objects?
Update 2: Removing the above line from delete_confirmation.html didn't help. I think it's the view that generates the deleted_objects variable that is taking too long. Not quite sure how to override a Django Admin view
Add this to your admin class, and than you can delete with this action without warning
actions = ["silent_delete"]
def silent_delete(self, request, queryset):
queryset.delete()
If you want to hide default delete action, add this to your admin class
def get_actions(self, request):
actions = super().get_actions(request)
if 'delete_selected' in actions:
del actions['delete_selected']
return actions
Since django 2.1 you can override get_deleted_objects to limit the amount of deleted objects listed (it's either a list or a nested list). The timeout is probably due to the django app server timing out on the view's response.
You could limit the size of the returned list:
class YourModelAdmin(django.admin.ModelAdmin):
def get_deleted_objects(self, objs, request):
deleted = super().get_deleted_objects(objs, request)
deleted_objs = deleted[0]
return (self.__limit_nested(deleted_objs),) + deleted[1:]
def __limit_nested(self, objs):
limit = 10
if isinstance(objs, list):
return list(map(self.__limit_nested, objs))
if len(objs) > limit:
return objs[:limit] + ['...']
return objs
But chances are the call to super takes too long as well, so you probably want to return [], {}, set(), [] instead of calling super; though it doesn't tell you about missing permissions or protected relations then (but I saw no alternative other than copy pasting code from django github). You will want to override the delete_confirmation.html and the delete_selected_confirmation.html template as well. You'll also want to make sure the admin has permission to delete any related objects that might get deleted by the cascading deletes.
In fact, the deletion itself may take too long. It's probably best defer the deletion (and permission checks if those are slow too) to a celery task.

Django Tastypie User Objects Only Authorization

I want to use Tastypie authorization to give users access to only their objects. However, I am having problems understanding if I am doing it correctly. I followed the example here:
http://django-tastypie.readthedocs.org/en/latest/authorization.html#implementing-your-own-authorization
When I try to create a new object, I get a 404 error because there are problems evaluating
def create_detail(self, object_list, bundle):
return bundle.obj.user == bundle.request.user
Everything works if I comment that out. I thought commenting those two lines out would allow the user to create objects for other users, but when I tried it, I correctly get a 401 (UNAUTHORIZED) response.
Does that mean those two lines are unnecessary? How is Tastypie able to correctly determine if I am authorized to create objects?
When I was running this, I sent a POST request with 'user' equal to the appropriate URI (something like '/api/v1/user/1/'). I'm not sure if Tastypie is having problems determining
bundle.obj.user
when I do it that way.
Is it safe to just leave those two lines commented out? Is Tastypie authorizing the user with one of the other methods?
try:
def create_detail(self, object_list, bundle):
return bundle.obj == bundle.request.user
It looks like bundle.obj isn't populated during the create_detail authorization.
Also, create_detail for a user really doesn't make much sense, because there's no object for the user to own until its created anyways. You could just check if bundle.request.user is a valid user with permissions on the model.
In my case, I needed to check if the created object referenced an object owned by the user, so here's what I came up with:
def create_detail(self, object_list, bundle):
resource=BookResource()
book=resource.get_via_uri(bundle.data["book"], bundle.request)
return book.user == bundle.request.user
Anyways, bottom line: tastypie's docs are a little off.
And, I hope this helps.

Can django-tastypie display a different set of fields in the list and detail views of a single resource?

I would like for a particular django-tastypie model resource to have only a subset of fields when listing objects, and all fields when showing a detail. Is this possible?
You can also now use the use_in attribute on a field to specify the relevant resource to show the field in. This can either be list or detail, or a callback.
You would have to specify all fields in the actual ModelResource then override the get_list method to filter out only the fields you want to show. See the internal implementation of get_list on Resource to see how to override it.
However, note this will only apply on GET requests, you should still be able to POST/PUT/PATCH on the resource with all fields if you authorization limits allow you to do so.
In a nut shell, you want to hot patch the internal field list before full_dehydrate is called on all ORM objects returned by obj_get_list.
Alternatively, you can let the full dehydrate mechanism take place and just at the end of it remove the fields you don't want to show if you don't care about squeezing out as much as speed as possible. Of course you would need to do this only if the URL is invoked as a consequence of get_list call. There is a convenience method for this alter_list_data_to_serialize(request, to_be_serialized).
Just do:
class SomeResource(Resource):
class Meta(...):
...
field_list_to_remove = [ 'field1', 'field2' ]
...
def alter_list_data_to_serialize(request, to_be_serialized):
for obj in to_be_serialized['objects']:
for field_name in self._meta.field_list_to_remove:
del obj.data[field_name]
return to_be_serialized
There is an open issue for this on GitHub, with a number of workarounds suggested there.
Can also use the dehydrate(self, bundle) method.
def dehydrate(self, bundle):
del bundle.data['attr-to-del]
return bundle

App Engine's UnindexedProperty contains strange code

Please help me understand this:
On v1.6.6 it's in line 2744 of google/appengine/ext/db/__init__.py:
class UnindexedProperty(Property):
"""A property that isn't indexed by either built-in or composite indices.
TextProperty and BlobProperty derive from this class.
"""
def __init__(self, *args, **kwds):
"""Construct property. See the Property class for details.
Raises:
ConfigurationError if indexed=True.
"""
self._require_parameter(kwds, 'indexed', False)
kwds['indexed'] = True
super(UnindexedProperty, self).__init__(*args, **kwds)
.
.
.
After they constrained the indexed parameter to be False - They set it to True!
Before 1.2.2, you could do filter queries for any property type, even Text and Blob. They did only return empty lists, but it worked. Version 1.2.2 introduced the indexed attribute for properties which allows you to disable indexing of selected properties[1]. Since then, the property you want to query on must be indexed or it will throw an exception.
We know that Text and Blob properties can not be indexed. Not changing anything else, queries on those properties would be raising exceptions from 1.2.2 on (which they didn't before). In order to not introduce a regression and break existing apps, the line kwds['indexed'] = True was added to the UnindexedProperty class.
If we would have control over all the depending code, it would have been a cleaner solution to start raising an exception. But in the light of not breaking existing apps, it was decided to patch it.
You can try it yourself by changing kwds['indexed'] = True to kwds['indexed'] = False and run this snippet:
from google.appengine.ext import db
class TestModel(db.Model):
text = db.TextProperty()
TestModel(text='foo').put()
print TestModel.all().filter('text =', 'foo').fetch(10)
[1] http://code.google.com/p/googleappengine/source/browse/trunk/python/RELEASE_NOTES#1165

Does a django query save its result after it's been called?

I'm trying to determine whether or not a simple caching trick will actually be useful. I know Django querysets are lazy to improve efficiency, but I'm wondering if they save the result of their query after the data has been called.
For instance, if I have two models:
class Klass1(models.Model):
k2 = models.ForeignKey('Klass2')
class Klass2(models.Model):
# Model Code ...
#property
def klasses(self):
self.klasses = Klass1.objects.filter(k2=self)
return self.klasses
And I call klass_2_instance.klasses[:] somewhere, then the database is accessed and returns a query. I'm wondering if I call klass_2_instance.klasses again, will the database be accessed a second time, or will the django query save the result from the first call?
Django will not cache it for you.
Instead of Klass1.objects.filter(k2=self), you could just do self.klass1_set.all().
Because Django always create a set in the many side of 1-n relations.
I guess this kind of cache is complicated because it should remember all filters, excludes and order_by used. Although it could be done using any well designed hash, you should at least have a parameter to disable cache.
If you would like any cache, you could do:
class Klass2(models.Model):
def __init__(self, *args, **kwargs):
self._klass1_cache = None
super(Klass2, self).__init__(*args, **kwargs)
def klasses(self):
if self._klass1_cache is None:
# Here you can't remove list(..) because it is forcing query execution exactly once.
self._klass1_cache = list(self.klass1_set.all())
return self._klass1_cache
This is very useful when you loop many times in all related objects. For me it often happens in template, when I need to loop more than one time.
This query isn't cached by Django.
The forwards FK relationship - ie given a Klass object klass, doing klass.k2 - is cached after the first lookup. But the reverse, which you're doing here - and which is actually usually spelled klass2.klass_set.all() - is not cached.
You can easily memoize it:
#property
def klasses(self):
if not hasattr(self, '_klasses'):
self._klasses = self.klass_set.all()
return self._klasses
(Note that your existing code won't work, as you're overriding the method klasses with an attribute.)
Try using johnny-cache if you want transparent caching of querysets.

Categories