ViewFlow and django-guardian - django

I want to make use of django-guardian's object permissions and grant specific rights for specific users to one or more Django users.
I have tried to add some permissions to my Process class like this:
class TestProcess(Process):
title = models.CharField(max_length=64)
something = models.ForeignKey(ForInheritage, null=True, on_delete=models.CASCADE)
no_approval = models.BooleanField(default=False)
approved = models.BooleanField(default=False)
def something_is_approved(self):
try:
return self.something.approved
except:
return None
class Meta:
permissions = (
('view_process', 'View Process'),
)
Unfortunately this causes viewflow to immediately throw an error after starting runserver:
File "/home/me/.virtualenvs/viewflow3/lib/python3.4/site-packages/viewflow/mixins.py", line 253, in ready
self.flow_class.process_class._meta.permissions.append(
AttributeError: 'tuple' object has no attribute 'append'
My initial plan was to subclass Start and View flow classes to change how the Permission function, that is inherited from the PermissionMixin, works. But this seems to be more work than just this, too.
django-guardian is already mentioned in one of the cookbook sections here but currently leads to a 404 page.
What would be the recommended/cleanest way to use guardian permissions on Processes and Tasks?

Your specific problem happens b/c you specify permissions like a tuple, try list instead
class Meta:
permissions = [
('view_process', 'View Process'),
]
Viewflow already adds the 'view' and 'manage' permissions so you can reuse them.
But further restriction per-process view permissions on the object level with django-guardian is not very practical. On each new process creation, in a start view, you will have to grant view permission to all process participant. That leads to hudge permission table grows and slow down the permissions lookup.
The reasonable use case for the object-level permission could be something like to restrict user access to a task based on a user department, for example.
deliver = flow.View(
views.deliver
).Permission(
'parcel.land_on_planet',
obj=lambda process: process.department
).Next(this.report)

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.

Django - Log User Activity (Query - GET/FILTER/UPDATE/DELETE) To Database

I'm building a HIPAA compliant application and I would like to log every user action to the database.
That includes when a user calls a get, filter, or delete object method on a model. I would like to add this functionality to a pre-existing application.
_ = ModelA.objects.get(id=1)
_ = ModelA.objects.filter(age__gt=10)
_ = ModelA.objects.filter(age__lt=5).delete()
I want to log every database operation.
Django Activity Stream does not work for get method.
How do I implement this functionality?
If you look at django-hipaa, it can give you a framework from which to work. I didn't copy it directly, but the idea is to create an abstract logging model--very simplistically:
#in models.py
class Log(models.Model):
user = models.ForeignKey(User, on_delete=PROTECT) #because you want to preserve the log
created_on = models.DateTimeField(auto_now_add=True)
action_taken = models.CharField(max_length=255)
class Meta:
abstract = True
Then for each database that you want to log, create a log model:
#still in models.py
class ModelALog(Log):
row = models.ForeignKey(ModelA, on_delete=Protect)
#whatever other information you want to log
then when in your views.py, you save a log anytime you create a view that involves a database query that you want to log:
#in views.py
def some_view_with_ModelA(request):
something = ModelA.objects.get(pk=1)
log = ModelALog.objects.create(user=request.user,
action_take='your description', row=something)
return render(yourview.html)
This may potentially mean that you have to create additional views to perform the same functions as might otherwise be done from the admin portal, but it ensures all events you want logged are logged.
You might also look into the package django-safedelete as you contemplate deleting, and this package provides a soft-delete so the deleted information is still preserved to protect the integrity of your logs.

Django: Creating permissions that relate two `User` objects together

Is there any way to set up a permissions system where permissions relate User objects together, i.e. where one User may have the permission to read another User's info (but not necessarily the other way around)? I've read the documentation but it's sparse and from what I can tell permissions are not capable of handling dynamic use-cases.
Basically, I want a permission system that determines what two+ model objects can "do" to each other.
Bonus points if you have any input on how to integrate this into Django Rest Framework.
One approach is to override the BasePermission class to create your custom permission. You can specify the access permission using an extra field in the User model.
class UserPermission(BasePermission):
def has_permission(self, request, view):
is_allowed_user = False
try:
permission = Permission.objects.get(user=request.user.id, user1=request.GET.get('user'))
if permission.is_read_enabled:
is_allowed_user = True
else
is_allowed_user = False
except Permission.DoesNotExist as e:
is_allowed_user = False
return is_allowed_user
Create a model to specify the Permissions
class Permission(models.Model):
user = models.ForeignKey(User,...)
user1 = models.ForeignKey(User,...)
is_read_enabled = models.BooleanField(defualt=False)
Add this permission to view as needed.

Create user roles and permissions extending the existing User model

I want to create user roles (member, partner, admin, etc) extending the existing User model.
class Member(models.Model):
user = models.OneToOneField(User)
#custom fields
What is the best way to create and assign the permissions in this case?
I see many ways to do it:
Programmatically creating permissions:
permission = Permission.objects.create(codename='can_publish',
name='Can Publish Posts',
content_type=content_type)
Included in a model object:
class Task(models.Model):
...
class Meta:
permissions = (
("view_task", "Can see available tasks"),
("change_task_status", "Can change the status of tasks"),
("close_task", "Can remove a task by setting its status as closed"),
)
is it harcoded? But I want to include the permissions in the Member class, I don't know if it's a good way to do it.
How can I add the Member class to member group? Or do I have to add permissions to member group?:
view_task = Permission.objects.get(name='view_task')
membergroup.permissions.add(view_task)
and after that add user by user from Member class to member group?:
user = User.objects.get(username='test')
membergroup = Group.objects.get(name='member')
user.groups.add(membergroup)
So, I want to put the permissions in Member class and put the class in a member group. I don't know if it's ok.
I want to know if a user is in a member group for example. I see a way but I'm not sure if it is good. Because I have to retrive all user from the group each time:
users_in_group = Group.objects.get(name="member").user_set.all()
if user in users_in_group:
I'm not an expert with permissions in Django, but as far as I know, the main tasks to do (not especially in this order, you do what you need when you need it) are the following:
You create permissions
You create groups
You link users to your groups
You link permissions to your groups
Using this "pattern" allows you to be flexible giving users the ability to be in 0->N groups and the groups being linked to 0->N permissions.
This part of the doc explains how to use permissions with examples :
https://docs.djangoproject.com/en/dev/topics/auth/default/#topic-authorization
That being said, be careful to not mix model extension with permissions:
You extends User model when you want to add special attributes on it. For example, ClientUser and BusinessSeller could be User model extensions because you do want to have different attributes on them. This is not related to permissions.
This means you can use permissions on users without extending the User model.
Not sure this answers totally your question, but I hope it makes things clearer to you.

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.