I created in wagtail a model called Regbox in model.py and also RegboxModelAdmin in wagtail_hooks.py. Wagtail admin includes item Regbox in wagtail side bar menu. Then I programmatically created a new collection, a new group with permissions add, edit, delete Regbox and this group is assigned to new user after registration. New user can add (edit and delete) new Regbox (model Regbox has forein key User) and this new user can see in wagtail admin only his own regboxes (I used queryset filter so that superuser could see all regboxes in wagtail admin and the current user only his own regboxes). But if this new user plays with urls in his browser he can see also other regboxes (not only his own regboxes). Simply he can change url from for example /regbox/edit/5/ to /regbox/edit/8/ and he can see this page although this page/regbox belongs to another user (it would need here Permission denied). Could someone please advice me how can I do it in wagtail admin so that any user can see only his own regbox pages ? Thanks
Permissions in Django (and Wagtail by extension) are handled on a per model basis, not per instance. Therefore, giving edit permissions on the Regbox to a user will allow him/her to edit every instances of that model. There are a few exceptions in Wagtail (like the Page model).
Anyway, you should be able to achieve what you want and add custom permission check by attaching a custom permission helper class to you ModelAdmin definition.
from wagtail.contrib.modeladmin.helpers import PermissionHelper
from wagtail.contrib.modeladmin.options import ModelAdmin, modeladmin_register
from .models import Regbox
class RegboxPermissionHelper(PermissionHelper):
def user_is_owner(self, user, obj):
if user.pk == obj.owner:
return True
else:
return False
def user_can_inspect_obj(self, user, obj):
"""
Return a boolean to indicate whether `user` is permitted to 'inspect'
a specific `self.model` instance.
"""
return self.user_is_owner(user, obj) && super().user_can_inspect_obj(user, obj)
def user_can_edit_obj(self, user, obj):
"""
Return a boolean to indicate whether `user` is permitted to 'change'
a specific `self.model` instance.
"""
return self.user_is_owner(user, obj) && super().user_can_edit_obj(user, obj)
def user_can_delete_obj(self, user, obj):
"""
Return a boolean to indicate whether `user` is permitted to 'delete'
a specific `self.model` instance.
"""
return self.user_is_owner(user, obj) && super().user_can_delete_obj(user, obj)
class RegboxModelAdmin(ModelAdmin):
model = Regbox
permission_helper_class = RegboxPermissionHelper
modeladmin_register(RegboxModelAdmin)
As you can see, we create a new RegboxPermissionHelper helper class and define methods for the inspect, edit and delete permissions which first check that the user is the owner (this has been extracter to its own method) an then call super to let the original permission check happen. The call to super is important so it return false if the user is not the owner, but it will also return false the user is the owner but doesn't have a particular permission (e.g. you can create and edit but not delete).
FWIW, I think you are using the correct mechanism to filter the list view by filtering the queryset so don't change anything there.
Related
On a Wagtail site I've built, I have a model type that is editable by authenticated users who do not have full admin privileges. They can only save as a draft or submit the changes for moderation. The problem I'm having is that Wagtail is inconsistent about where it redirects after executing these two actions. Saving the Draft takes the user back to the edit screen they were just on, with a note saying the draft was saved (good). Submitting for Moderation returns the user to the admin browse view of the parent page, which shows all of the sibling nodes in a list. They can't edit the vast majority of items on that list, so I think this is confusing for a non-admin user. I would like to have the "Submit for Moderation" action detect whether the user belongs to a group other than admin (or, failing that, whether the page has unpublished changes, as in my code example below) and, if so, redirect them back to the edit screen just like "Save as Draft" does.
I tried this in my model definition and it didn't work:
def save(self, *args, **kwargs):
#do some field value manipulations here before saving
super().save(*args, **kwargs)
if self.id:
if self.has_unpublished_changes:
return HttpResponseRedirect('/admin/pages/' + str(self.id) + '/edit/')
There's probably some sort of Wagtail admin action that I need to hook into and override, rather than trying to accomplish this in models.py, but I don't have much experience with this, so I need a better understanding of what to change and where.
Set up an after_create_page and after_edit_page hook: https://docs.wagtail.io/en/stable/reference/hooks.html#after-create-page
To do this, add a wagtail_hooks.py file in one of the apps in your project. This is where you can define functions to be called after creating or editing a page through the admin, such as:
from wagtail.core import hooks
#hooks.register('after_create_page')
def redirect_after_page_create(request, page):
if not request.user.is_superuser:
return HttpResponseRedirect('/admin/pages/' + str(page.id) + '/edit/')
#hooks.register('after_edit_page')
def redirect_after_page_edit(request, page):
if not request.user.is_superuser:
return HttpResponseRedirect('/admin/pages/' + str(page.id) + '/edit/')
For example: if a user creates n number of users . I want to display the n number of users created by that particular user alone in django .
I can't come up with a solution.
Do i have to create a model like profile with OnetoOnemodel of user?
It's a fair question, but no, you can't guarantee knowledge of who created the user. Users just exist in a database and thus can be created completed outside of Django. What you could do is create a UserAdmin and overwrite save_model to set some field, i.e, creator, as the current user:
# set creator as logged-in user
def save_model(self, request, obj, form, change):
obj.creator = request.user
super(MyUserAdmin, self).save_model(request, obj, form, change)
But this still does not guarantee an audit of any user; only those created through the admin. See the docs for more on admin methods.
Which is the proper way to test object based permissions?
Sample:
from rest_framework import permissions
class IsOfficeAdmin(permissions.BasePermission):
def has_object_permission(self, request, view, obj):
office = obj
return office.admin == request.user
Which are the "asserts" that I shouldn't miss?
Do I need to create a view?
To your questions:
it's up to you to write the logic which will allow a user to access the object. As a result, you have to return a Boolean.
yes. You will specify to the view which permission classes you want to apply. In case of object permissions they will be queried on detail routes (get, update, delete)
I had to override the delete method for admin like so :
def fully_delete_selected_photos(self, request, queryset):
# Code to do my specific delete method.
fully_delete_selected_photos.short_description = "Delete Selected Photos"
class VehiclePhotoAdmin(admin.ModelAdmin):
search_fields = ('listing_id',)
list_display = ('listing_id', 'disp_VehiclePhoto')
actions = [fully_delete_selected_photos]
def get_actions(self, request):
actions = super(VehiclePhotoAdmin, self).get_actions(request)
del actions['delete_selected']
return actions
This works fine but I lost the confirmation of deletion. So when the user selects my "Delete Selected Photos" option it immediate just goes through with the delete. How can I get some kind of confirmation for the overridden deletion? I am confused about the redirections of views in admin.
Thank you for your time!
You can return HttpResponseRedirect in fully_delete_selected_photos method, and make custom view , thats inherit template from admin, and takes queryset parameter, and lists all selected photos.
But if you want to simplify it, add some JavaScript to admin view.
I would like to implement a 2nd admin site which provides a subset of feature of the primary admin site. That's possible and described in the Django docs
However, I would like to limit access on the primary admin site. Some users can access the 2ndary site but not the primary site.
In order to implement that feature, I would like these users not to be in the staff (is_staff=False) and rewrite the AdminSite.has_permission
class SecondaryAdminSite(AdminSite):
def has_permission(self, request):
if request.user.is_anonymous:
try:
username = request.POST['username']
password = request.POST['password']
except KeyError:
return False
try:
user = User.objects.get(username = username)
if user.check_password(password):
return user.has_perm('app.change_onlythistable')
else:
return False
except User.DoesNotExist:
return False
else:
return request.user.has_perm('app.change_onlythistable')
Unfortunately, this approach doesn't work. The user can login but can't see anything in the secondary admin site.
What's wrong with this approach?
Any idea how to implement this feature?
Thanks in advance
Here's what worked for me with Django >= 3.2.
Create a subclass of AdminSite
Override the has_permission() method to remove the is_staff check.
Override the login_form to use AuthenticationForm.
AdminSite uses AdminAuthenticationForm, which extends AuthenticationForm and adds a check for is_staff.
Code
# PROJECT/APP/admin.py
from django.contrib.admin import AdminSite
from django.contrib.admin.forms import AuthenticationForm
class MyAdminSite(AdminSite):
"""
App-specific admin site implementation
"""
login_form = AuthenticationForm
site_header = 'Todomon'
def has_permission(self, request):
"""
Checks if the current user has access.
"""
return request.user.is_active
site = MyAdminSite(name='myadmin')
I think that your approach should now be possible: http://code.djangoproject.com/ticket/14434 (closed 5 weeks ago)
However, the explicit "is_staff" check is still done in two places (apart from the staff_member_required decorator):
django.contrib.admin.forms.AdminAuthenticationForm.clean()
On top of "has_permission()" you'd need to provide your non-staff AdminSite with a "login_form" that doesn't do the is_staff check, so could just subclass and adjust clean() accordingly.
templates/admin/base.html
would need to be slightly customized.
The div with id "user-tools" is only shown for active staff members. I'm assuming that's done because the login form also uses this template, and someone could be logged in as an active non-staff member but still should'nt see those links.
What's wrong with this approach? Any idea how to implement this feature?
What's wrong with this approach is that permissions and groups can already provide you with what you need. There is no need to subclass AdminSite if all you need is to divide users.
This is probably why this feature is so poorly documented, IMHO