Handling profile level permissions

Hey everyone. I have some difficulties in building custom profile level permissions

For context I have users and each user can have multiple profiles.

Each profile can belong to a School instance. A profile has a set of different roles such as manager, teacher, student.

When logging in in the frontend, a user selects which profile they want to access and will be presented with the matching interface.

What I am trying to do is to give the ability to a school owner to set permissions to other managers.

Now, managers can access all classes and have full control like owners.

So I need permissions that are predefined. And can be serialized and updated in the frontend.

Someone yesterday suggested I try django rules. It looks promising but lacks the option to predefine permissions for each profile so owners of schools can adjust them for each manager.

What I though of is writing a model that stores the permissions as boolean fields. And then check the permissions using djano rules predicates and then use the predicated in the views.

What do you think of this approach? Is it not a good solution? Are there any better solutions?

Thanks in advance for your help

class ManagerPermissions(models.Model):
    ALLOW_CLASSES_BY = (
        ("class", "class"),
        ("building", "building"),
    )
    manager = models.OneToOneField(Manager, on_delete=models.CASCADE, related_name="permissions")
    allowed_classes = models.ManyToManyField(Class, blank=True)
    allowed_classes_by = models.CharField(choices=ALLOW_CLASSES_BY, default="class")
    buildings = models.ManyToManyField(Masjid, blank=True)
    add_new_class = models.BooleanField(default=False)
    edit_class = models.BooleanField(default=False)
    add_students_class = models.BooleanField(default=False)
    remove_students_class = models.BooleanField(default=False)
    move_students_class = models.BooleanField(default=False)
    add_new_students = models.BooleanField(default=False)
    add_new_teachers = models.BooleanField(default=False)
    add_new_managers = models.BooleanField(default=False)
@rules.predicate
def is_manager_with_permission(profile, permission):
    return getattr(profile.permissions, permission, False)
class Profile(models.Model):
    USER_STATUS = (
        ("active", "active"),
        ("inactive", "inactive"),
        ("pending_invitation", "pending_invitation"),
    )

    created_at = models.DateTimeField(auto_now_add=True)
    user = models.ForeignKey(User, on_delete=models.CASCADE, related_name="profiles")
    role = models.CharField(choices=ROLES, max_length=10, blank=False)
    institution = models.ForeignKey(Institution, on_delete=models.CASCADE, related_name="profiles")
    code = models.CharField(null=False, max_length=8, default=0)
    status = models.CharField(choices=USER_STATUS, default="inactive")

I apologize for the long message. I appreciate any thoughts on this.

I would suggest that the better mechanism would be to use Django Groups and Permissions, rather than implementing your security framework from scratch.

You could create groups named Manager, Teacher, Student, etc.

Each one of those groups would have a set of permissions assigned to them.

You can then use the standard permission_required decorator / PermissionRequiredMixin along with a custom has_perm method.

Depending upon the specifics of the granularity needed for these tests, it’s possible that you might need to create your own subclass of that mixin, or possibly use the UserPassesTestMixin to check which School is being referenced, and test accordingly.

When that is true (where the School affects the permissions), this fits into the category of “row-level” or “object-level” security. There have been a handful of discussions here on that topic, in addition to some mention of Rules and django-guardian.

Thanks for the quick response.

It is essential for permissions to be tested against a selected user profile and not the user itself. A user can have multiple profiles. Moreover, each profile should have different permissions from one another. So the owner or admin can change permissions of each profile independently. This means a permission group per role wont work for my case.

I have been chatting with some folks on the discord server and I came to the following conclusion:

  1. Define custom permissions under the Meta attribute on the Class model and others if needed

  2. Create a custom permission group for each profile upon profile creation

  3. Now in this group I will have access to default CRUD permissions in addition to the custom ones I added.

  4. For the context, I think I will have to create a custom ManagerPermissions model and add the context as fields and foreign keys. Now this model will only contain context and not permissions of boolean fields.

    1. To enforce the permissions without django rules I will have to write custom permission classes and apply them in my views.

I think django rules would just make enforcing the permissions a bit easier.

What do you think of this solution? I appreciate your input.

Rereading through this, I’m not sure I’m following your requirements.

Questions:

  • What’s the relationship between these “profiles” and the “roles” you named in your original post?

  • In standard “Django speak”, the term “Profile” is typically used to refer to a model containing ancillary data relative to the User model. It now seems apparent to me that you are using this to describe a set of permissions. Is that correct?

  • It now sounds to me that these profiles are defined by school, such that profile “x” for school “A” has no functional relationship to profile “x” for school “B”. Is that correct?

  • It is also sounding like you’re saying that profile “x” for school “A” can imply a different set of permissions for person “L” than for person “M”. Is that correct?

    • If that’s true, then what’s the logical significance of identifying something as profile “x”?

You might want to take a look at the thread at How to create Workspaces and manage them for all the users in Django? for yet another perspective on this.

class Profile(models.Model):
    USER_STATUS = (
        ("active", "active"),
        ("inactive", "inactive"),
        ("pending_invitation", "pending_invitation"),
    )

    created_at = models.DateTimeField(auto_now_add=True)
    user = models.ForeignKey(User, on_delete=models.CASCADE, related_name="profiles")
    role = models.CharField(choices=ROLES, max_length=10, blank=False)
    institution = models.ForeignKey(Institution, on_delete=models.CASCADE, related_name="profiles")
    code = models.CharField(null=False, max_length=8, default=0)
    status = models.CharField(choices=USER_STATUS, default="inactive")

Sorry for not explaining. Basically a user can have multiple profiles to select from. The advantage to this is make it possible for a single user to have multiple user profiles , a manger at school A, a teacher at school B, or a student at school C simply by just siwtching the selected user profile in the frontend.

In the request header, I add a profile_id header that is the selected profile.

In django I override my auth class to include the profile retrieved by the id in the header and store it in the request.user object.

I’m with you on the usage, but I’m still not clear on the definitions of what these are, or mean.

Regarding question 1, you’ve now dropped the reference to the term “role”, but it’s still present in the Profile model. What is its significance in all this?

To rephrase question #3:

Does a “manager at school A” have the same set of permissions at school A, as a “manager at school B” would have at school B?

And for question #4:

Do all people who are a “manager at school A” have exactly the same permissions?

New questions:

  • Is it a given that a person will only have one role at a given school? (In other words, is it possible for a person to be both a “manager at school A” and a “teacher at school A” at the same time? If so, are the permissions strictly additive?)

  • If someone changes the permissions associated with a “teacher at school B”, does that immediately affect all “teachers at school B”?

(I’m trying to word these as Yes/No questions to ensure I understand exactly what your requirements are.)

I didnt drop the role. Profiles have a role and an institution (or school) fields. Under a school user are profiles with different roles. When selecting a profile in the frontend, the user will see an interface and data specific to that profile. In the backend I, I created a django app for each role type and each have their own views.

No. All managers have a certain set of accessibility. Right now I have no permissions set so all of them can view, add, remove classes, students, and every other functionality added to the frontend. What should happen is for each manager to have a customized set of permissions that are set and adjusted by an owner or admin. So maybe the admin adds a manager that only has access to classes in building B. Or add one with no ability to add new classes, etc.

Imagine in the frontend a set of toggles, each being a permission, in manager A details page.

Not necessarily, a user can have multiple profiles with different roles in the same school or institution. But the role and institution fields are unique together. So a user cannot have more than one profile with the same role in the same school or institution.

They are not. Because when the user selects a profile, they are presented with a different interface in the frontend. In addition when I handle their request, I read the request.user.profile, and use that as the main source of querying and handling other functionality.

No, each teacher or manager must have their own set of customizable permissions. For now I am just working on the managers permissions.

1 Like

Ok, I think I’ve got a clearer picture now.

I can see now why the standard Group mechanism is not a good fit.

I might even go a step further and suggest you may want to consider creating a ManyToMany relationship between Profile and Permission, completely bypassing the Group model completely.

1 Like

Interesting.

So creating a group for each profile, or creating a many to many relationship between profile and permissions will lead to the same result?

I have some concerns I would like to qsk you about. One is querying the permissions. I need to select a set of permissions including the disabled ones and serialize them. Meaning the admin would see a list of permissions that I define and they see wether the permissions is on or off. How would I do that?

Another is enforcing the permissions. Do you suggest I go with django rules? Or permissions classes? What are your thoughts on this?

Effectively yes. Instead of having a OneToOne between Profile and Group, and the ManyToMany between Group and Permission, you’re eliminating that extra reference.

The Permission model is a model like any other. They’re queried and referenced and used in exactly the same way.

Also, there is no such thing as a “disabled” Permission. A Permission is either assigned or not assigned.

Side question: Why serialize them?

Effectively, the same way you would handle any other ManyToMany relationship. For one idea, see how the Django Admin handles group and permission membership for users in the admin. (e.g. Either a ModelMultipleChoiceField or something like the admin filter_horizontal widget.)

We looked at both Rules and Django-guardian back in 2017, and neither one of them were adequate for our requirements. As a result, we decided to roll-our-own solution.
Given that you are tied to these “individual-based” profiles, I seriously doubt that an existing package is going to satisfy your requirements either. I think you’re going to need to write your own equivalents to the has_perm, and permission_required methods to work best with what you’re building.
(Don’t underestimate the level-of-effort required for customizing a package that doesn’t fit your specific requirements. That can become a very deep rabbit hole that you find yourself in.)

1 Like

I get it. Basically the toggle should be on if a permission is in the permissions list and off if its not.

I use DRF with a custom separate react frontend.

Thank you for all the help so far, I sincerely appreciate it!

I was trying to create a mixing and couldnt find much resources. But I tried this:

class ProfilePermissionsRequiredMixin:
    required_permissions = []

    def get_required_permissions(self):
        return self.required_permissions

    def dispatch(self, request, *args, **kwargs):
        # Check if the user is authenticated
        if not request.user.is_authenticated:
            raise PermissionDenied("You must be logged in to access this resource.")

        # Get the user's profile
        profile = request.user.profile

        # Check if the profile has all the required permissions
        if all(profile.has_perm(perm) for perm in self.get_required_permissions()):
            return super().dispatch(request, *args, **kwargs)

        # Return 403 Forbidden if any permission check fails
        raise PermissionDenied("You do not have permission to access this resource.")

The mixin is returning user not authenticated even though the user is authenticated and can be accessed from the view.

How to fix this?

Also the raised error is 500 and not 403 response.

If you’re getting a 500 error, then there should be a traceback somewhere (either on the server console or in a log file) that provides the details needed to find / fix the error.

Side note: Profile has a many_to_one relationship with User. Unless your User model also as a “profile” field or property, the expression request.user.profile is an ambiguous reference.

It turns out I cant raise drf exceptions in the dispatch. Returinig Httresponse fixed the crash.

The user is a foreign key in the profile model. But thats not how I get the profile anyway, I get it from the request headers in an authentication class:

class AuthenticationWithProfile(JWTAuthentication):
    def authenticate(self, request):
        header = self.get_header(request)
        if header is None:
            return None

        raw_token = self.get_raw_token(header)
        if raw_token is None:
            return None

        try:
            validated_token = self.get_validated_token(raw_token)
        except:
            raise AuthenticationFailed("invalid access token")

        profile_id = request.headers.get("Profile-Id")
        profile_role = request.headers.get("Profile-Role")

        if profile_id is None or profile_id == "":
            raise AuthenticationFailed("profile id missing")

        if profile_role is None or profile_role == "":
            raise AuthenticationFailed("profile role missing")

        request_user = self.get_user(validated_token)

        try:
            profile = Profile.objects.get(id=profile_id)
            if profile.user != request_user:
                raise AuthenticationFailed("profile id not valid")

            if profile.role != profile_role:
                raise AuthenticationFailed("profile role not valid")

        except Profile.DoesNotExist:
            raise AuthenticationFailed("profile id does not exist")

        except Exception as e:
            raise AuthenticationFailed("profile id not valid")

        request_user.profile = profile

        return request_user, validated_token

For some reason this class isnt applied in the dispatch. Do DRF auth classes not work with dispatch?

I’m guessing it’s more an issue of the middleware and how the user object gets assigned to the request. (I don’t know/use DRF, so I can’t address how that works with this.)