Wagtail per page user permission - django

My task is to enable users to edit their own page.
I see how to give permissions to a particular page to a group, but how do I do this for the user?
I'd tried use django guardian:
UserObjectPermission.objects.assign_perm('change_custompage', user, obj=custompage)
but this wasn't given me results
I also found the wagtailcore_grouppagepermission table and realized that the permissions principle is not the same as used in django auth_permission
I want to give permissions to edit, and access wagtail admin, I do not need anything else
In this particular task, wagtail I need because of the struct_blocks, I think this is a terrific concept.
Now I see two options: either an override template and the ability to use all the power of the struct_block outside the wagtail, or something to solve with permissions.

Two possibilities:
Create a group for each user - might not seem like a very sophisticated approach, but easily achievable with a bit of scripting...
Put your users in a group that has add permission, but not edit permission, on the parent page that contains all of your users' pages. In Wagtail's permission model http://docs.wagtail.io/en/v1.10.1/topics/permissions.html, 'add' permission also includes the ability to edit pages that you have created yourself. (If you'd rather have the pages created in advance, rather than having your users create them, then you need to set the owner field of the page record to the relevant user.)

Thanks to the help and advice of #gasman, I did this as follows:
Created two models:
first:
class PersonsIndexPage(RoutablePageMixin, Page):
...
#route(r'^$')
def index_view(self, request):
pages = PersonPage.objects.live().all()
return render(request, 'myapp/person_index_page.html', {
'page': self,
'pages': pages,
})
...
subpage_types = ['PersonPage']
second:
class PersonPage(Page):
...
parent_page_types = ['PersonsIndexPage']
subpage_types = []
For the group add access to wagtail admin, as well as gave permissions to create PersonsIndexPage
Since I create personal pages from Django admin, I created a simple action that creates a page for selected users:
I do not know if bulk_create is possible
def create_person_page(modeladmin, request, queryset):
root_page = PersonIndexPage.objects.first()
for user in queryset:
page = PersonPage(title=username), owner=user)
root_page.add_child(instance=page)
create_person_page.short_description = 'Create person page'

Related

Django Wagtail dynamic Menu's based on User and User Role

Using Django-Wagtail and Wagtail Menu's. I want to build a system with the following types of characteristics.
I have a class of user (Vendor, Supplier)
Each created user would need to be one of these two classes.
Each class of user has different roles and the roles differ from class to class. e.g.
Vendor: finance, stock, admin
Supplier: finance, stock, admin, driver.
What I have done is create a class called UserType and both "Vendor" & "Supplier" inherit from this class. Then added a field to the User class that selects which type of user they are.
class UserType(model.Model):
common fields.......
class Vendor(UserType):
child_vendor_only_fields.....
class Supplier(UserType):
child_supplier_only_fields.....
class User(AbstractUser):
userVar = models.ForeignKey(UserType, on_delete=models.SET_NULL, null=True, blank=True)
I have also used the wagtail admin to create custom roles for each of these classes as described above.
Clearly what "Vendors" can create, read, update and delete varies from that of a "Supplier". And still within that what the finance role can do within the "Vendor" differs from what "Stock" can do.
How would I create a dynamic menu that displays different menu's for these permutations?
My initial thoughts were inspired by this. Simply adding a choicefield
USER_TYPE_CHOICES = (
('Vendor', 'Vendor'),
('Supplier', 'Suplier'))
class GenericPage(MenuPage):
"""
This model will gain the fields, methods and `setting_panels` attribute
from `MenuPage`, but `settings_panels` is being overridden to include
other fields in the `Settings` tab.
"""
availableTo = CharField(choices=USER_TYPE_CHOICES,)
# 'menupage_panel' is a collapsible `MultiFieldPanel` with the important
# fields already grouped together, making it easy to include in custom
# panel definitions, like so:
settings_panels = [
FieldPanel('custom_settings_field_one'),
menupage_panel
]
I have two questions:
Am I on the correct path for generating custom menu's for type of users? If so how would I then do the same for roles under type of users?
In terms of CRUD should I simply implement a check each time a user does a crud action whether or whether not they can commit that action pragmatically?
For CRUD, I'm not a fan of giving someone the option to do something then tell them that option isn't allowed after the fact. They might have spent half an hour creating/editing that page then find out they've wasted that time. Better to tell them on page load.
For user type, I'd just create those as roles that you add the users to. Then you just have a simple test for the user.groups to see what role they have.
You could easily develop a custom restricted panel for each of the panel types you need to restrict, easier if they're all FieldPanels. In the BoundPanel class you have access to the request object, so you can pull the user object from that at runtime and and decide what to do then.
The Panel.BoundPanel has a method is_shown(), so you can create a custom panel that inherits FieldPanel, override that method to test if the request user in an authorised list parameter and set true/false accordingly.
# restricted_field_panel.py
from wagtail.admin.panels import FieldPanel
class RestrictedFieldPanel(FieldPanel):
def __init__(self, field_name, authorised_groups, **kwargs):
self.field_name = field_name
self.authorised_groups = authorised_groups if isinstance(authorised_groups, list) else [authorised_groups]
super().__init__(self.field_name, **kwargs)
def clone_kwargs(self):
kwargs = super().clone_kwargs()
kwargs.update(
authorised_groups=self.authorised_groups
)
return kwargs
class BoundPanel(FieldPanel.BoundPanel):
def is_shown(self):
show_field = super().is_shown()
is_authorised = self.request.user.groups.get_queryset().filter(name__in=self.panel.authorised_groups).exists()
return (show_field and is_authorised)
In the page model:
content_panels = Page.content_panels + [
RestrictedFieldPanel('some_vendor_field', 'Vendors'),
RestrictedFieldPanel('some_supplier_field', 'Suppliers'),
....
]
For the authorised groups, you might want to append a site admin role by default so that site admins will always see any restricted panel regardless.
Other panel types you'd need to inherit and do test wherever it's doing the rendering.

Dynamically created proxy models do not have listed permissions on admin site

Here is my models.py:
class Item(models.Model):
# ... some irrelevent fields ...
tags = models.ManyToManyField('Tag')
class Tag(models.Model):
name = models.CharField(max_lenght=30)
category_id = models.IntegerField()
Tag is actually a general-purpose name. Each item has many different type of tags - currently there are four types: team tags, subject tags, admin tags and special tags. eventually there will probably be a few more.
The idea is, they all have basically the same fields, so instead of having like 4 tables with manytomany relationship, and instead of adding a new column for Item whenever adding a new type, everything is called a 'tag' and it's very easy to add new types without any change to the schema.
Now to handle this in the admin.py I'm using dyamically created proxy models (based on this), as such:
def create_modeladmin(modeladmin, model, name = None):
class Meta:
proxy = True
app_label = model._meta.app_label
attrs = {'__module__': '', 'Meta': Meta}
newmodel = type(name, (model,), attrs)
admin.site.register(newmodel, modeladmin)
return modeladmin
class TagAdmin(models.Model):
def queryset(self):
return self.model.objects.filter(category_id = self.cid)
class TeamAdmin(TagAdmin):
cid = 1
class SubjectAdmin(TagAdmin):
cid = 2
# ... and so on ...
create_modeladmin(TeamAdmin, name='Teams', model=Tag)
create_modeladmin(SubjectAdmin, name='Subject', model=Tag)
#... and so on ...
This works great for me. However, different staff members need different editing permissions - one guy shouldn't access admin tags, while the other should only have access to edit subject-tags and team-tags. But as far as the admin site is concerned - the dynamic models do not exist in the permission list, and I can't give anyone permissions regarding them.
i.e. a user given all permissions on the list will still not have access to edit any of the dynamic models, and the only way to let anyone access them at all is to give him a superuser which obviously defies the point
I searched SO and the web and I can't anyone with a similar problem, and the docs don't say anything about this not in the dynamic models section or the proxy models section. so I'm guessing this is a different kind of problem. Any help would be greatly appreciated
UPDATE
So after some research into it, the answer was simple enough. Since permissions in django are objects that are saved to the database, what I needed to do was simple - add the relevent permissions (and create new ContentType objects as well) to the db, and then I could give people specific pemissions.
However, this raised a new question - is it a good convention to put the function that creates the permissions inside create_modeladmin as a find_or_create sort of function (that basically runs every time) or should it be used as an external script that I should run once every time I add a new dynamic model (sort of like how syncdb does it)?
And is there a way to also create the permissions dynamically (which seems to me like the ideal and most fitting solution)?
of course you can create permissions, django have django.contrib.auth.management.create_permissions to do this

Better to use 3rd Party Row-Level Permission App or to Filter Query Results in a Django View?

I am writing a Django app that involves the creation of documents. I have a few requirements:
Check to see if the user is able to view any documents at all.
If the user is allowed to view documents, only allow them to view documents they have permission for.
I have come up with two solutions and am wondering if one is philosophically/practically better than the other.
The two solutions I've come up with are:
Solution One (using third-party Django-Guardian)
Models.py
class Document(models.Model)
owner = models.ForeignKey(User)
document_name = models.CharField(max_length=60)
document_content = models.TextField()
class Meta:
permissions = (
('view_document', 'View Document'),
)
views.py
#permission_required('document.view_document', (Document, 'pk', 'document_id'))
def view_document(request, document_id):
document = Document.objects.get(pk=document_id)
return render_to_response("native/view_events.html",
{
'document' : document,
}, context_instance=RequestContext(request))
The downside I see to solution number one is that I have to explicitly set permissions every time I create an object, plus I have to hit the database twice: once to check permissions and again to retrieve the document object.
Solution Two (using built-in Django permissions)
Models.py
class Document(models.Model)
owner = models.ForeignKey(User)
document_name = models.CharField(max_length=60)
document_content = models.TextField()
viewers = models.ManyToManyField(User)
class Meta:
permissions = (
('view_document', 'View Document'),
)
views.py
#permission_required('document.view_document')
def view_document(request, document_id):
document = Document.objects.filter(pk=document_id, viewers__pk=request.user.pk)[0]
return render_to_response("native/view_events.html",
{
'document' : document,
}, context_instance=RequestContext(request))
The downside I see to solution number one is that I have to do two checks, one to see if they are able to view documents at all, and one to see if they can view the particular document. A plus side to this is that if I have an admin that I want to be able to view all documents, I don't need to explicitly grant permission to each one; I can just give him the 'view document' permission.
It seems both solutions have their pros and cons. Is there one that is better in theory/practice?
I find the second approach better. Since, you can check the model level permission on the object. Though in the first I guess, you should be able to accomplish similar things. I am not sure but if django-guardian provides a way to check the permission inside the view code instead of the decorator. You could manually check for Model Level Permission.
For example,
def view_doc(request, doc_id):
if user can not view doc: #Model Level Permission
return HttpResponse("Sorry can not view")
if check django-guardian permission #Object Level Permission
return HttpResponse("Can not view doc")
#further code
But I would suggest the second approach since, you can create an api just to check permission and customize it which is more scalable.

How to restrict editing of records to the logged-in user?

Sorry, I am still new to Django, hopefully the question isn't out of place.
When I have the following in my template:
<td>{{ item.last_name }}</td>
By clicking on last name the user will be redirected to the following link in order to edit it.
http://127.0.0.1:8000/contact/edit/?id=1
But then what prevents any logged in user to just inject a different id in there on the browser and edit a record that doesn't belong to him?
Update
I just had an idea when I read the comment and answer below. Rather than using a third party app, couldn't I just create a UserProfile for each user and attach a unique company wide uuid.uuid1(). Each time a loggedin user attempts to edit something, his unique company uuid will be also passed in the link as an additional parameter.
On the edit side, it would harvest this guid and compare it against the logged in user and see if they match. If they do match, he is authorized to proceed with the editing, otherwise he will be redirected.
What do you think? Any weaknesses?
If you use Django's new class based views, e.g. the generic UpdateView, you can extend the dispatch handler.
def dispatch(self, request, *args, **kwargs):
handler = super(MyEditView, self).dispatch(request, *args, **kwargs)
# Only allow editing if current user is owner
if self.object.author != request.user:
return HttpResponseForbidden(u"Can't touch this.")
return handler
In this case, the code verifies that the author field of the model object corresponds to the currently logged in user, before even handling the rest of the request.
You can see a reallife example of this in a project of mine.
When you're using Django auth, always rely on the session mechanism to identify an user instead of making some other id such as uuid1() (except, for example, when you need extra sessions in an e-commerce site under HTTPS).
For the permission part, you could check the ownership directly, mainly as described by Koliber Services. The relationships between Company, User and Contact are crucial for the logic of permission checking. There are many ways to model the relationships and thus the code would differ much. For example:
# a modeling way
User.company -> Company : an user belongs to a company
Contact.contributor -> User : a contact is contributed by an user, would be invalid is user is leaving the company
# could check accessibility by
can_view = contact.contributor.company_id == current_user.company_id
# another modeling way
User.company -> Company : an user belongs to a company
Contact.company -> Company : a contact info is owned by a company, does not share globally
# could check accessibility by
can_view = contact.company_id == current_user.company_id
When can_view is False, user should get a 403 for his unauthorized attempting and get logged.
Normally the above method is enough for content protection(not yet in Django Admin). However, when you have many different types of permission checking and even row-permission checkings, it's better to use some uniform permission API.
Take Django-guardian for example, you could simply map companies to groups and assign can_view permission for a contact for the group representing the user's company. Or, assign the can_view permission to all users in a company when a contact is created by using signal or celery task.
Furthermore, you could use /contact/1/edit/ instead of /contact/edit/?id=1. In this way the int(request.GET('id')) part is moved to urlconf like r'^contact/(?P<pk>\d+)/$', less code and much clearer.
There are some third-party apps that does what you want, its called "row-level permission" where you can give different users different access to specific objects, "Row level" comes from SQL where each object is a row in the database
I use django-guardian to do the job
In the function handling the saving of the data, check to see if the object being edited has the same ID as the presently logged in user.
For example, if the object in question is called EmailPrefs and it has a field called user_id:
Load the EmailPrefs object with the ID of the object being edited
If the user_id does not match the current user, stop further processing
Modify the EmailPrefs object
Save the EmailPrefs object to the database

Django admin -- restricting access by user

I was wondering if the django admin page can be used for external users.
Let's say that I have these models:
class Publisher(models.Model):
admin_user = models.ForeignKey(Admin.User)
..
class Publication(models.Model):
publisher = models.ForeignKey(Publisher)
..
I'm not exactly sure what admin_user would be -- perhaps it could be the email of an admin user?
Anyways. Is there a way allow an admin user to only add/edit/delete Publications whose publisher is associated with that admin user?
-Thanks!
-Chris
If you need finer-grained permissions in your own applications, it should be noted that Django's administrative application supports this, via the following methods which can be overridden on subclasses of ModelAdmin. Note that all of these methods receive the current HttpRequest object as an argument, allowing for customization based on the specific authenticated user:
queryset(self, request): Should return a QuerySet for use in the admin's list of objects for a model. Objects not present in this QuerySet will not be shown.
has_add_permission(self, request): Should return True if adding an object is permitted, False otherwise.
has_change_permission(self, request, obj=None): Should return True if editing obj is permitted, False otherwise. If obj is None, should return True or False to indicate whether editing of objects of this type is permitted in general (e.g., if False will be interpreted as meaning that the current user is not permitted to edit any object of this type).
has_delete_permission(self, request, obj=None): Should return True if deleting obj is permitted, False otherwise. If obj is None, should return True or False to indicate whether deleting objects of this type is permitted in general (e.g., if False will be interpreted as meaning that the current user is not permitted to delete any object of this type).
[django.com]
I see chris's answer was useful at the time question was asked.
But now it's almost 2016 and I guess it gets more easier to enable restricted access of Django Admin panel to end user.
Django authentication system provides:
Groups: A generic way of applying labels and permissions to more than one user.
Where one can add specific permissions and apply that group to user via admin panel or with writing codes.
After adding user to those specific groups, Admin need to enable is_staff flag for those users.
User will be able access restricted registered models in admin.
I hope this helps.
django admin can, to a certain extent, be restricted. For a given user, first, they must have admin rights in order to log into the admin site. Anyone with this flag set can view all admin pages. If you want to restrict viewing, you're out of luck, because that just isn't implemented. From there, each user has a host of permissions, for create, update and delete, for each model in the admin site. The most convenient way to handle this is to create groups, and then assign permissions to the groups.