Allow user to access some part of a project depending on the role they have - Django

634 Views Asked by At

I just finished coding a django app that helps music producers coordinate projects and I am trying to solve a small problem I am facing: For every music project, users are assigned specific roles (sound tech, drummer, production manager et cetera) and depending on their role, they should be able to see/do only some things within the project.

For example, the production manager should be able to see a project's budget and the drummer shouldn't; but they both should be able to see the location of the next recording session.

One thing to notice is that what some roles might be allowed to see might change from one project to the other. That is to say that it's possible that a Drummer should have visibility over the budget on one project and not on another (so I can't fix permissions based on the role and I need something more fluid where I can add and remove roles).

To make it simpler, we can consider that currently I only have the following models (on top of basic user model)

class JobProject(models.Model):
    id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
    client = models.ManyToManyField(Company, related_name='client_company', blank=True)
    title = models.CharField(max_length=40)
    number = models.CharField(max_length=40,  null=True)
class JobPosition(models.Model):
    id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
    title = models.ForeignKey(Title, on_delete=models.CASCADE)
    city = models.ForeignKey(City, on_delete=models.CASCADE)
    rate = models.CharField(max_length=20)
    jobproject = models.ForeignKey(JobProject, related_name='crew', on_delete=models.CASCADE, blank=True, null=True)
 
class Event(models.Model):
    id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
    date = models.DateField(blank=True, null=True)
    event_name = models.ForeignKey(EventName, blank=True, on_delete=models.CASCADE, related_name='events', null=True)
    project = models.ForeignKey(JobProject, blank=True, on_delete=models.CASCADE, related_name='jobproject_events', null=True)
    ...

class dept_budget(models.Model):
    id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
    name = models.CharField(choices=DEPARTMENTS, max_length=55)
    budget = models.CharField(max_length=250)
    jobproject = models.ForeignKey(JobProject, on_delete=models.CASCADE)

2

There are 2 best solutions below

5
AMG On

Anticipate you'll be changing permissions for those roles for each project? I'd create a through table for ProjectRole with a set of fields that includes the booleans for what you want to restrict by.

Fields like: project, role, view_budget, view_members, admin_project

You'd also have a table for UserRole which maps the users to project_roles. It becomes easy to query a project based permission for any given user. This all depends on if you are going to have fixed permissions for each project, or if they are going to vary. Like can a production manager see budget for every project, for every project they are a production manager, or just for this one project, but maybe another project doesn't let the production manager see the budget.

Decide where you want to assign the permissions (role, role & project) first. If it is just on the role, then you may want to use the built in Django permissions and a subsequent query to ensure they are also on the project.

Provide concrete details on your requirements, and then it will become easier to suggest a model structure to fit.

After update edit:

You need to decide if you think you are going to have a list of growing permissions, or if it is pretty much fixed. If fixed I would suggest adding a couple of booleans to the JobPosition class for the permissions. can_view_budget, can_add_members,etc. However, nothing is ever that simple as things grow. If it were me I'd add another table for JobPositionPermission like

class JobPositionPermission(models.Model):

    class CategoryType(models.TextChoices):
        ALLOW = 'allow'
        DENY = 'deny'

    description = models.CharField(max_length=64, help_text="Description of permission")
    category_type = models.CharField(max_length=10, choices=CategoryType, default=CategoryType.ALLOW)

class JobPosition(models.Model):
    ...
    permissions = models.ManyToManyField(JobPositionPermission)

Then you'd need to write something that checks if the person has a role that allows the permission. Alternatively, the deny option gives you the ability to prevent a certain role from doing something. I believe Django's permission system loads up all the permissions for a user once, and it refers to that values list. A similar approach might be warranted for your case for performance.

Something like in the auth model's source: (https://github.com/django/django/blob/05cde4764da022ae80e9d7d97ef67c30e896c607/django/contrib/auth/backends.py#L27)

def has_perm(self, user_obj, perm, obj=None):
    return perm in self.get_all_permissions(user_obj, obj=obj)

and (https://github.com/django/django/blob/05cde4764da022ae80e9d7d97ef67c30e896c607/django/contrib/auth/backends.py#L67)

def _get_permissions(self, user_obj, obj, from_name):
    """
    Return the permissions of `user_obj` from `from_name`. `from_name` can
    be either "group" or "user" to return permissions from
    `_get_group_permissions` or `_get_user_permissions` respectively.
    """
    if not user_obj.is_active or user_obj.is_anonymous or obj is not None:
        return set()

    perm_cache_name = '_%s_perm_cache' % from_name
    if not hasattr(user_obj, perm_cache_name):
        if user_obj.is_superuser:
            perms = Permission.objects.all()
        else:
            perms = getattr(self, '_get_%s_permissions' % from_name)(user_obj)
        perms = perms.values_list('content_type__app_label', 'codename').order_by()
        setattr(user_obj, perm_cache_name, {"%s.%s" % (ct, name) for ct, name in perms})
    return getattr(user_obj, perm_cache_name)
2
jahantaila On

First, you would have to assign different roles to the different users (you can do this manually, in the admin panel, or even computationally).

To create a group, you can try something like this, where drummer can be any role that you want:

from django.contrib.auth.models import Group doctor_group, created = Group.objects.get_or_create(name='drummer')

Next, you can add, set, remove, or clear a user's permissions. For example, if I wanted to assign user b the role drummer, I would use something like this: doctor_group.permissions.set([permission_list])

Here is a full list of permission functions:

doctor_group.permissions.set([permission_list])
doctor_group.permissions.add(permission, permission, ...)
doctor_group.permissions.remove(permission, permission, ...)
doctor_group.permissions.clear()

You can also add users to groups using this syntax, depending on how your models are set up. user.groups.add(doctor_group)

Finally, you can check if a specific user is in a group by using the .exists() method, and authenticating a user.

Something like this should work:

def is_drummer(user):
    return user.groups.filter(name='drummer').exists()

from django.contrib.auth.decorators import user_passes_test
@user_passes_test(is_drummer)
def my_view(request):
    pass

You can learn more about specific Django groups & roles here.