My flask app centers around modifying models based on SQLAlchemy. Hence, I find flask-admin a great plugin because it maps my SQLA models to forms with views already defined with a customizable interface that is tried and tested.
I understand that Flask-admin is intended to be a plugin for administrators managing their site's data. However, I don't see why I can't use FA as a framework for my users to CRUD their data as well.
To do this, I have written the following:
class AuthorizationRequiredView(BaseView):
def get_roles(self):
raise NotImplemented("Override AuthorizationRequiredView.get_roles not set.")
def is_accessible(self):
if not is_authenticated():
return False
if not current_user.has_role(*self.get_roles()):
return False
return True
def inaccessible_callback(self, name, **kwargs):
if not is_authenticated():
return current_app.user_manager.unauthenticated_view_function()
if not current_user.has_role(*self.get_roles()):
return current_app.user_manager.unauthorized_view_function()
class InstructionModelView(DefaultModelView, AuthorizationRequiredView):
def get_roles(self):
return ["admin", "member"]
def get_query(self):
"""Jails the user to only see their instructions.
"""
base = super(InstructionModelView, self).get_query()
if current_user.has_role('admin'):
return base
else:
return base.filter(Instruction.user_id == current_user.id)
#expose('/edit/', methods=('GET', 'POST'))
def edit_view(self):
if not current_user.has_role('admin'):
instruction_id = request.args.get('id', None)
if instruction_id:
m = self.get_one(instruction_id)
if m.user_id != current_user.id:
return current_app.user_manager.unauthorized_view_function()
return super(InstructionModelView, self).edit_view()
#expose('/delete/', methods=('POST',))
def delete_view(self):
return_url = get_redirect_target() or self.get_url('.index_view')
if not self.can_delete:
return redirect(return_url)
form = self.delete_form()
if self.validate_form(form):
# id is InputRequired()
id = form.id.data
model = self.get_one(id)
if model is None:
flash(gettext('Record does not exist.'), 'error')
return redirect(return_url)
# message is flashed from within delete_model if it fails
if self.delete_model(model):
if not current_user.has_role('admin') \
and model.user_id != current_user.id:
# Denial: NOT admin AND NOT user_id match
return current_app.user_manager.unauthorized_view_function()
flash(gettext('Record was successfully deleted.'), 'success')
return redirect(return_url)
else:
flash_errors(form, message='Failed to delete record. %(error)s')
return redirect(return_url)
Note: I am using Flask-User which is built on top of Flask-Login.
The code above works. However, it is difficult to abstract as a base class for other models which I would like to implement access control for CRUD operations and Index/Edit/Detail/Delete views.
Mainly, the problems are:
the API method, is_accessible, does not provide the primary key of the model. This key is needed because in almost all cases relationships between users and entities are almost always stored via relationships or in the model table directly (i.e. having user_id in your model table).
some views, such as delete_view, do not provide the instance id that can be retrieve easily. In delete_view, I had to copy the entire function just to add one extra line to check if it belongs to the right user.
Surely someone has thought about these problems.
How should I go about rewriting this to something that is more DRY and maintainable?
I'm working in a CRUD and one of the features of the list is bulk delete. The user chooses (checkbox) the lines he wants to delete and press the "Delete selected" button.
I'm using generic views in my CRUD. My single delete, for example is like this:
class ContentDeleteView(NextRedirectMixin, DeleteView):
model = Content
template_name = 'content_delete.html'
def get_success_url(self):
messages.add_message(self.request, messages.SUCCESS, "Content deleted successfully.")
return reverse('content:content-detail')
My problem is creating the bulk delete view. Is there something from Django core features that I could use to bulk delete? I would like to avoid installing an app for that.
Thanks for any help
You should resort to rolling out your own kind of delete view, here is a basic example:
class BulkDeleteView(View):
model = None
def post(self, request, *args, **kwargs):
delete_ids = request.POST['delete_ids'].split(',') # should validate
self.model.objects.filter(pk__in=delete_ids).delete()
return render / redirect
So the basic idea is to subclass View and roll out your own implementation of a base BulkDeleteView
Is it possible to extend a generic view to allow user authentication? I want my view to limit the number of returned results from the model if a user is not logged in.
class CustomGalleryDetailView(DetailView):
def get_queryset(self):
if request.user.is_authenticated():
return Gallery.objects.on_site().is_public()
else:
return Gallery.objects.on_site().is_public()[:5]
This returns NameError global name 'request' is not defined.
The reason I want to extend the generic view is that here I am simply overriding a single of many views used by a 3rd party app in my program, and I want to maintain some consistency with the rest of the views which mainly rely on generic views.
just change it to self.request.user.is_authenticated(), so your class will become:
class CustomGalleryDetailView(DetailView):
def get_queryset(self):
if self.request.user.is_authenticated():
return Gallery.objects.on_site().is_public()
else:
return Gallery.objects.on_site().is_public()[:5]
I'm new to the web development world, to Django, and to applications that require securing the URL from users that change the foo/bar/pk to access other user data.
Is there a way to prevent this? Or is there a built-in way to prevent this from happening in Django?
E.g.:
foo/bar/22 can be changed to foo/bar/14 and exposes past users data.
I have read the answers to several questions about this topic and I have had little luck in an answer that can clearly and coherently explain this and the approach to prevent this. I don't know a ton about this so I don't know how to word this question to investigate it properly. Please explain this to me like I'm 5.
There are a few ways you can achieve this:
If you have the concept of login, just restrict the URL to:
/foo/bar/
and in the code, user=request.user and display data only for the logged in user.
Another way would be:
/foo/bar/{{request.user.id}}/
and in the view:
def myview(request, id):
if id != request.user.id:
HttpResponseForbidden('You cannot view what is not yours') #Or however you want to handle this
You could even write a middleware that would redirect the user to their page /foo/bar/userid - or to the login page if not logged in.
I'd recommend using django-guardian if you'd like to control per-object access. Here's how it would look after configuring the settings and installing it (this is from django-guardian's docs):
>>> from django.contrib.auth.models import User
>>> boss = User.objects.create(username='Big Boss')
>>> joe = User.objects.create(username='joe')
>>> task = Task.objects.create(summary='Some job', content='', reported_by=boss)
>>> joe.has_perm('view_task', task)
False
If you'd prefer not to use an external library, there's also ways to do it in Django's views.
Here's how that might look:
from django.http import HttpResponseForbidden
from .models import Bar
def view_bar(request, pk):
bar = Bar.objects.get(pk=pk)
if not bar.user == request.user:
return HttpResponseForbidden("You can't view this Bar.")
# The rest of the view goes here...
Just check that the object retrieved by the primary key belongs to the requesting user. In the view this would be
if some_object.user == request.user:
...
This requires that the model representing the object has a reference to the User model.
In my project, for several models/tables, a user should only be able to see data that he/she entered, and not data that other users entered. For these models/tables, there is a user column.
In the list view, that is easy enough to implement, just filter the query set passed to the list view for model.user = loggged_id.user.
But for the detail/update/delete views, seeing the PK up there in the URL, it is conceivable that user could edit the PK in the URL and access another user's row/data.
I'm using Django's built in class based views.
The views with PK in the URL already have the LoginRequiredMixin, but that does not stop a user from changing the PK in the URL.
My solution: "Does Logged In User Own This Row Mixin"
(DoesLoggedInUserOwnThisRowMixin) -- override the get_object method and test there.
from django.core.exceptions import PermissionDenied
class DoesLoggedInUserOwnThisRowMixin(object):
def get_object(self):
'''only allow owner (or superuser) to access the table row'''
obj = super(DoesLoggedInUserOwnThisRowMixin, self).get_object()
if self.request.user.is_superuser:
pass
elif obj.iUser != self.request.user:
raise PermissionDenied(
"Permission Denied -- that's not your record!")
return obj
Voila!
Just put the mixin on the view class definition line after LoginRequiredMixin, and with a 403.html template that outputs the message, you are good to go.
In django, the currently logged in user is available in your views as the property user of the request object.
The idea is to filter your models by the logged in user first, and then if there are any results only show those results.
If the user is trying to access an object that doesn't belong to them, don't show the object.
One way to take care of all of that is to use the get_object_or_404 shortcut function, which will raise a 404 error if an object that matches the given parameters is not found.
Using this, we can just pass the primary key and the current logged in user to this method, if it returns an object, that means the primary key belongs to this user, otherwise it will return a 404 as if the page doesn't exist.
Its quite simple to plug it into your view:
from django.shortcuts import get_object_or_404, render
from .models import YourModel
def some_view(request, pk=None):
obj = get_object_or_404(YourModel, pk=pk, user=request.user)
return render(request, 'details.html', {'object': obj})
Now, if the user tries to access a link with a pk that doesn't belong to them, a 404 is raised.
You're going to want to look into user authentication and authorization, which are both supplied by [Django's Auth package] (https://docs.djangoproject.com/en/4.0/topics/auth/) . There's a big difference between the two things, as well.
Authentication is making sure someone is who they say they are. Think, logging in. You get someone to entire their user name and password to prove they are the owner of the account.
Authorization is making sure that someone is able to access what they are trying to access. So, a normal user for instance, won't be able to just switch PK's.
Authorization is well documented in the link I provided above. I'd start there and run through some of the sample code. Hopefully that answers your question. If not, hopefully it provides you with enough information to come back and ask a more specific question.
This is a recurring question and also implies a serious security flaw. My contribution is this:
There are 2 basic aspects to take care of.
The first is the view:
a) Take care to add a decorator to the function-based view (such as #login_required) or a mixin to the class-based function (such as LoginRequiredMixin). I find the official Django documentation quite helpful on this (https://docs.djangoproject.com/en/4.0/topics/auth/default/).
b) When, in your view, you define the data to be retrieved or inserted (GET or POST methods), the data of the user must be filtered by the ID of that user. Something like this:
def get(self, request, *args, **kwargs):
self.object = self.get_object(queryset=User.objects.filter(pk=self.request.user.id))
return super().get(request, *args, **kwargs)
The second aspect is the URL:
In the URL you should also limit the URL to the pk that was defined in the view. Something like this:
path('int:pk/blog-add/', AddBlogView.as_view(), name='blog-add'),
In my experience, this prevents that an user sees the data of another user, simply by changing a number in the URL.
Hope it helps.
In django CBV (class based views) you can prevent this by comparing the
user entered pk and the current logged in user:
Note: I tested it in django 4 and python 3.9.
from django.http import HttpResponseForbidden
class UserDetailView(LoginRequiredMixin, DetailView):
model = your_model
def dispatch(self, request, *args, **kwargs):
if kwargs.get('pk') != self.request.user.pk:
return HttpResponseForbidden(_('You do not have permission to view this page'))
return super().dispatch(request, *args, **kwargs)
I wrote this, which seems to work just fine:
#app.route('/admin', methods=['GET','POST'])
#login_required
def admin():
if not current_user.role == ROLE_ADMIN:
flash('You do not have access to view this page.')
return redirect(url_for('index'))
...the rest of my code...
While trying to simplify things since I don't want to add these 3 lines to every area I want only visible to admins, I tried to put it in a function like so:
def admin_only():
if not current_user.role == ROLE_ADMIN:
flash('You do not have access to view this page.')
return redirect(url_for('index'))
and then put in my view function:
#app.route('/admin', methods=['GET','POST'])
#login_required
def admin():
admin_only()
...the rest of my code....
However this doesn't work as I expected. I get the flashed message, but it does not redirect like I thought it would.
So, two questions:
Why doesn't the returned redirect work?
Is there a better way to implement this functionality?
To actually answer your question. You should make the admin_only function a decorator and decorate the admin view method. The reason it does not redirect now is because you are not returning the redirect from the view.
def admin():
ret = admin_only()
if( not ret ):
return ret
....
That should fix your current issue, but it is not ideal and the functionality you wish should be moved to a decorator.
I also recommend the following:
Take a look at Flask-Principal it provides the ability to assign roles to users and then restrict access based on these roles to your views.
Along with Flask-Principal take a look at Flask-Security as it provides many useful security related Flask extensions and just makes it easier to use.
Example use:
#roles_required( "admin" )
def website_control_panel():
return "Only Admin's can see this."
Will ONLY allow users with the role admin attached to their account. Another use case is to allow a user to have one of many roles which can be specified with the roles_accepted and can be used as following:
#roles_accepted( "journalist", "editor" )
def edit_paper():
return render_template( "paper_editor.html", ... )
Will only allow users that have at least one of the journalist or editor roles tied to their account.