ModelAdmin thread-safety/caching issues - python

Ultimately, my goal is to extend Django's ModelAdmin to provide field-level permissions—that is, given properties of the request object and values of the fields of the object being edited, I would like to control whether or not the fields/inlines are visible to the user. I ultimately accomplished this by adding a can_view_field() method to the ModelAdmin and modifying the built-in get_form() and get_fieldset() methods to remove/exclude fields+inlines that the user does not have permissions (as determined by can_view_field()) to see. If you'd like to see the code, I placed it in a pastebin, since it's long and only somewhat relevant.
It works great...almost. I appear to have run into some sort of thread-safety or caching issue, where the state of the ModelAdmin object is being leaked from one request to another in a reproducible manner.
I'll illustrate the problem with a simple example. Suppose that I have a model whose ModelAdmin I have extended with the field-level permissions code. This model has two fields:
- public_field, which can be seen/edited by any staff member
- secret_field, which can only be seen/edited by superusers
In this case, the can_view_field() method would look like this:
def can_view_field(self, request, obj, field_name):
"""
Returns boolean indicating whether the user has necessary permissions to
view the passed field.
"""
if obj is None:
return request.user.has_perm('%s.%s_%s' % (
self.opts.app_label,
action,
obj.__class__.__name__.lower()
))
else:
if field_name == "public_field":
return True
if field_name == "secret_field" and request.is_superuser:
return True
return False
Test case 1: with a fresh server restart, if you first view the changelist form as a superuser, you see the form as should happen, with both public_field and secret_field visible. If you log out and view it as a staff member (but not superuser), you only see public_field.
Test case 2: with a fresh server restart, if you log in as a staff member first, you still only see public_field. However, if you then log out and view as a superuser, you do not see secret_field. This is 100% reproducible.
I've done some basic thread-safety diagnostics:
At the end of get_form(), I've printed out the memory address of the ModelForm object. As it should be, it is unique with each request. Therefore, the ModelForm object is not the problem.
Immediately before the admin registration, I tried printing the memory address of the ModelAdmin object. In test case 1, it is unique with both requests. However with test case 2, it does not print at all on the second request.
At this point, I'm clueless. My next point of research will be the admin registration system (which I admittedly know nothing about). The state resets with a server restart, so it seems that the ModelAdmin must be cached? Or is it a thread-safety issue? If I turn it into a factory and return a deepcopy() of the ModelAdmin, would it serve a fresh ModelAdmin with each request? I'm clueless and would appreciate any thoughts. Thanks!

I'm confused about why you think ModelAdmin should be a new instance on each request. The admin objects are instantiated by the admin.site.register(Model) calls in each admin.py, which in turn is called from admin.autodiscover() in urls.py. In other words, this happens on process startup. Given the dynamic multi-process nature of most web serving environments, you may or may not get a new process with any particular request - certainly you won't get one every single time.
Because of this, it's not wise to store or alter state on a global object like ModelAdmin. I haven't looked through your linked code properly, but there was at least one case where you were altering an attribute on self as a result of a method call. Don't do that - you'll need to find some other way of passing dynamic values between methods.

Related

What does this mean in the django documentation about class-based views?

A rookie here. As I am reading the django documentation, I came up with a note that I cannot fully understand.
It says:
Note
While your class is instantiated for each request dispatched to it, class attributes set through the as_view() entry point are configured only once at the time your URLs are imported.
Here is the link:
https://docs.djangoproject.com/en/2.2/topics/class-based-views/intro/
So which one is better? What advantage does each have? I've tried both and cannot experience any difference(Pretty sure that's because I've not considered enough)
If you are passing any values into the as_view() method that are likely to change after the server starts, for example some function call or database query whose return value could change after some users use the website, it will be evaluated only once, while the urls are loaded.
Let's say you are passing in the current time like:
path('about/', GreetingView.as_view(greeting=timezone.now())),
That note simply says that the value of the attribute 'greeting' for GreetingView will stay the same for all requests even if the server runs for a month, since timezone.now() is called only once.
Such arguments are good for reusing a View class with minimal changes. It depends completely on your use case.
For example:
path('add-car/', AddView.as_view(form=AddCarForm)),
path('add-bus/', AddView.as_view(form=AddBusForm)),

Eve framework: user restricted resource access

I'm using Eve framework and I'm trying to use User-Restricted resource access as described in:
http://python-eve.org/authentication.html#user-restricted-resource-access
I'm doing something like:
class CustomAuth(TokenAuth):
def check_auth(self, token, allowed_roles, resource, method):
# Get user as an instance of UserResource.
if user and hasattr(user, 'id'):
self.set_request_auth_value(user['id'])
request.authenticated_user = user
...
So, there are a few question from my side:
Is it enough for using User-Restricted Resource Access?
How this field adds into user created objects?
Is this additional field called id in my user created objects? Is it possible to rename it?
As I understand it should be named same as it's called in User resource. Is it true?
Does this field (property) applies for newly created objects only? Is it possible to fetch previously created objects by current user following this way?
Well, I want to know an answers for my questions + clarify how it may be used.
Is it an expected way to extract it somehow in my hooks?
user_id = current_app.auth.get_request_auth_value()
current_app.data.driver.session.query(resource).find({'id': user_id})
Is this block of code from hook expected?
How it behaves if my requested resource has its own id field?
P.S. I was reading a post:
https://stackoverflow.com/a/35654252/7335432
The user-restricted access feature prevents users from accessing records they didn't create. The set_request_auth_value() method does:
1) Upon making a POST request to create a record, it automatically adds a field specified as AUTH_FIELD (or auth_field if you only want to do it to a specific resource). So for example, if you declare in settings.py
AUTH_FIELD = "my_auth_field"
and then add
set_request_auth_value(user['id'])
to your authentication method, that means that your app creates a field "my_auth_field" that has its value set to whatever user["id"] is. So if you were to go into Mongo Compass or some other DBMS and manually inspect your records, you'd see a "my_auth_field" field in there.
2) On GET requests when you access those records, Eve checks the "my_auth_field" value against whatever user["id"] is, and only displays the records where "my_auth_field" is equal to user["id"]. Since this field is added automatically when you create a record using Eve, it effectively filters out everything that specific user didn't create.
So yes, it only applies to newly created objects. I'm not sure exactly what you mean by "is it enough", but it doesn't look like 'user' is declared anywhere in your authentication class. You might wanna check out this tutorial they do incorporating user restricted access into token authentication.

Django ModelForm, having a foreign key as a hidden field

I'm basically building a very trivial form. Let's stick to the books/publisher examples given in the django tutorials and build upon that.
I have a user login to the web app, at which point the first thing they can do is click on a publisher. This publisher then gets saved for their session. Upon that I take them to a create book form. In there I embed the the publisher's id from the database into a hidden field.
Upon the user submitting an HTTP POST, I do something like:
mybookform = BookForm(request.POST)
if mybookform.is_valid():
abook = mybookform.save(commit=False)
abook.publisher_id = request.POST['publisher_id']
mybookform.save()
Yes there's a few naive things done here, such as blindly grabbing the publisher_id and verifying if it's indeed a real publisher id, amongst other security issues. Let's just not pay attention to that for the moment.
My question is, is there a better way of handling this? Although hypothetically this example doesn't make logistical sense, in my particular app the example actually makes sense. The problem is I get a ValueError exception saying publisher_id needs to be a Publisher instance.
Now I can easily retrieve a publisher instance with Publisher.objects.filter(id=..) and use that instead. The question is, is it really necessary? Can I avoid the additional query to the database and somehow update this form instance in a more 'elegant' fashion?
Also, is it possible to somehow embed the publisher in a hidden field so that I do not need to do mybookform.save(commit=False) and just do mybookform = BookForm(request.POST) followed by mybookform.save() immediately?
Retrieving the instance of the publisher does protect against client-side changes that might reference a completely invalid publisher.
To your second question, yes you can include that field as a hidden field by overriding the field in the ModelForm with the approriate form field setting the widget to HiddenInput.
There is no better way to do this.
I would use the get_object_or_404 function for this.
And yes, you can prevent this to be modified by the user by setting the model field to editable=False,

Django - alternative to subclassing User?

I am using the standard User model (django.contrib.auth) which comes with Django. I have made some of my own models in a Django application and created a relationship between like this:
from django.db import models
from django.contrib.auth.models import User
class GroupMembership(models.Model):
user = models.ForeignKey(User, null = True, blank = True, related_name='memberships')
#other irrelevant fields removed from example
So I can now do this to get all of a user's current memberships:
user.memberships.all()
However, I want to be able to do a more complex query, like this:
user.memberships.all().select_related('group__name')
This works fine but I want to fetch this data in a template. It seems silly to try to put this sort of logic inside a template (and I can't seem to make it work anyway), so I want to create a better way of doing it. I could sub-class User, but that doesn't seem like a great solution - I may in future want to move my application into other Django sites, and presumably if there was any another application that sub-classed User I wouldn't be able to get it to work.
Is the best to create a method inside GroupMembership called something like get_by_user(user)? Would I be able to call this from a template?
I would appreciate any advice anybody can give on structuring this - sorry if this is a bit long/vague.
First, calling select_related and passing arguments, doesn't do anything. It's a hint that cache should be populated.
You would never call select_related in a template, only a view function. And only when you knew you needed all those related objects for other processing.
"Is the best to create a method inside GroupMembership called something like get_by_user(user)?"
You have this. I'm not sure what's wrong with it.
GroupMembership.objects.filter( user="someUser" )
"Would I be able to call this from a template?"
No. That's what view functions are for.
groups = GroupMembership.objects.filter( user="someUser" )
Then you provide the groups object to the template for rendering.
Edit
This is one line of code; it doesn't seem that onerous a burden to include this in all your view functions.
If you want this to appear on every page, you have lots of choices that do not involve repeating this line of code..
A view function can call another function.
You might want to try callable objects instead of simple functions; these can subclass a common callable object that fills in this information.
You can add a template context processor to put this into the context of all templates that are rendered.
You could write your own decorator to assure that this is done in every view function that has the decorator.

Nullable ForeignKeys and deleting a referenced model instance

I have a ForeignKey which can be null in my model to model a loose coupling between the models. It looks somewhat like that:
class Message(models.Model):
sender = models.ForeignKey(User, null=True, blank=True)
sender_name = models.CharField(max_length=255)
On save the senders name is written to the sender_name attribute. Now, I want to be able to delete the User instance referenced by the sender and leave the message in place.
Out of the box, this code always results in deleted messages as soon as I delete the User instance. So I thought a signal handler would be a good idea.
def my_signal_handler(sender, instance, **kwargs):
instance.message_set.clear()
pre_delete.connect(my_signal_handler, sender=User)
Sadly, it is by no means a solution. Somehow Django first collects what it wants to delete and then fires the pre_delete handler.
Any ideas? Where is the knot in my brain?
Django does indeed emulate SQL's ON DELETE CASCADE behaviour, and there's no out-of-the box documented way to change this. The docs where they mention this are near the end of this section: Deleting objects.
You are right that Django's collects all related model instances, then calls the pre-delete handler for each. The sender of the signal will be the model class about to be deleted, in this case Message, rather than User, which makes it hard to detect the difference between a cascade delete triggered by User and a normal delete... especially since the signal for deleting the User class comes last, since that's the last deletion :-)
You can, however, get the list of objects that Django is proposing to delete in advance of calling the User.delete() function. Each model instance has a semi-private method called _collect_sub_objects() that compiles the list of instances with foreign keys pointing to it (it compiles this list without deleting the instances). You can see how this method is called by looking at delete() in django.db.base.
If this was one of your own objects, I'd recommend overriding the delete() method on your instance to run _collect_sub_objects(), and then break the ForeignKeys before calling the super class delete. Since you're using a built-in Django object that you may find too difficult to subclass (though it is possible to substitute your own User object for django's), you may have to rely on view logic to run _collect_sub_objects and break the FKs before deletion.
Here's a quick-and-dirty example:
from django.db.models.query import CollectedObjects
u = User.objects.get(id=1)
instances_to_be_deleted = CollectedObjects()
u._collect_sub_objects(instances_to_be_deleted)
for k in instances_to_be_deleted.ordered_keys():
inst_dict = instances_to_be_deleted.data[k]
for i in inst_dict.values():
i.sender = None # You will need a more generic way for this
i.save()
u.delete()
Having just discovered the ON DELETE CASCADE behaviour myself, I see that in Django 1.3 they have made the foreign key behaviour configurable.

Categories

Resources