Managing user rights to access different content

Hey,

Developping a first full-scale django app. Going pretty well so far, but there’s one design decision I’m not so sure about - user rights. My situation is this:

  • This is pretty much an agile development project, as the customer wasn’t necessarily 100% on the idea of a webapp, didn’t want to invest too much. So having a prototype that is relatively quick to implement is a factor.
  • I have ~6 different “user profiles”, e.g. user who would ideally not have access to the full app, but some sections only, varying from one profile to the other.
  • The project will have 5 or 6 apps (for now)
  • I don’t need object-level access management and I don’t foresee this being necessary anytime soon. The access issues are really more at the model/conceptual level, not on specific rows of data which are all considered “equal” in the business logic.

For starter, I’m going to use a few booleans in class User(AbstractUser). Some user may have more than one role, so that gives me that option, bools are easy to check too. That seems like a sensible approach.

However, I’m less sure WHERE I should ultimately check & decide what to return the user. The mainlines of my thinking right now:

Put it in the template

The good thing about this is that I could get rather specific with what I want to check. Potentially some front-end guy could also tweak things without having to touch the django proper part of the app.

However that will litter my html will clunky {% if …%}. I’m not sure if it is “django-ic” either.

As an example - I have a sidebar, that display most of the places that a user can visit. The sidebar is divided into subsections (accordion-style using bootstrap). Some subsection wouldn’t be rendered at all, if the user has no business there. Other example, user X may have access to page A, and page A may contain links to page B. But that user may not be allowed to visit page B. In that case I simply wouldn’t display (or just display as text) the link to page B.

To be clear - in this approach, I still have a single template for a given page for all users. ANother approach would be to make entirely different templates for different users (and choose which template to render in the views). If I’m told that’s much smarter I may do that, but this is not my choosen approach at the moment.

Put it in the views

To some extent, part of the logic will be in the views (e.g. some users may have access to a DetailVIew, but not an UpdateView for instance, then the view is an easy place to check for that). I also know that technically, if I don’t check for rights in the view, a user could manually enter a URL, so simply “not showing” a link isn’t enough in some cases.

My questions:

  • I am currently not necessarily considering using django-authority or django groupes & permissions at this point. Basically the users will be managed through the admin page by a superuser and he/she woud be setting the attributes on each user to determine what they can access. Is that a case of misguided lazyness on my part? For my current requirements, seems this is overkill, but is it a case where down the road I may realize I would have been way, way smarter to just bit the bullet now? As oppose to refactor my entier user-rights management?
  • Am I doing anything stupid?
  • When both approaches work, is there a rule of thumb as to whether it’s prefereable to check user rights in the templates, or in the views? Or is it really just a matter of style?
  • Would it be preferrable to make entierly different templates for different users? As opposed to just tweak bits of templates for different users?

<opinion> Yes, you’re potentially creating a problem for yourself. Yes, go with group membership for roles instead of User-model booleans.
Check the user rights in the view - Assign the users to groups (roles). Assign permissions to those roles (e.g. “view_report”). Check in the view, user.has_perm - see the docs at Using the Django authentication system | Django documentation | Django
I would never directly check permissions in a template - that’s logic that, IMO, belongs solely in the view. You can add or not add data to the context based upon permissions, and then test for existence of those data elements in the template. (That actually provides more flexibility with combinations of different roles and data elements.)
</opinion>

1 Like

Interesting. You’re probably right, I should use the built-in feature right away (groups), it’s likely to be more flexible in the future. So that’s probably lazyness on my part. Will use those first.

Okay but then about the templates- say I have a sidebar component that counts 8 sections in total, and most users will use some subset (3-4) of those 8. If I have 5 different user profiles with different subsets of sections, I do need 5 different sidebar templates (with the view processing the logic as to which one to show which user). It seems less DRY to me, for the templates at least. But I do take your point about putting the logic where it belongs (not in temapltes).

Let’s say that instead, I build myself a simple_tag. I could pass it the context, along with the current section in the sidebar. That tag is responsible to return a boolean telling me whether or not, for the current user, I need to render that section.

Would you say that your warming about putting the logic in the template is heeded in that case, or would you still say this is logic that belongs elsewhere? The actual logic would be the tag(s). The templates would only get a render/don’t render.

Thanks a bunch - those types of questions are always the tricky thing IMO when picking up a new framework/lib/paradigm. It tends to be easy to find A solution to a problem, but then it’s kinda like stepping into a dark room without really knowing all that’s in there…

What I’m suggesting is that each of those sections could be a separate element in your context. Then, the sections being rendered depend upon those corresponding elements being in the context. If they’re not in the context, they don’t get rendered. Your view builds the context, adding only those elements that the requesting user has permissions to see.

Our internal practices don’t allow for any business logic to leak into the templates. A template must render whatever is presented to it in the context. Any logic to identify what is going to be in the context is made in the view. It greatly simplifies the debugging and analysis process.

1 Like

Okay gotcha. I see the nuance you’re bringing there. So I would break up my sidebar sub-sections into further partials.html. The view’s job would be to pick up which ones to include for that user. Template’s job is just to render whatever is there. Makes sense. Thanks!

Not going to lie, I think I arrived at a pretty decent solution there, thanks to Ken’s wise advices, so I’m just going to put the key points in here for others who may come across similar reflections in their journeys.

So first, I split up my sidebar into partials component. Each is just a section of tags wrapped within a fancy bootstrap accordion menu. So a simple {include} works here. The relevent bit in my sidebar.html:

<ul class="navbar-nav ...">
    <!-- Nav Items  -->
    {% for section in sections_to_render  %}
        {% include section %}
    {% endfor %}

Then I created a SideBarMixin - since I’m going to have to include it in any view that needs to render that sidebar customized for different user groups. Something like this:

class SideBarMixin:
    # maps out the available sidebar sections, along with all the groups that should see that section rendered in the template
    side_sections_and_groups = {"side_pos.html":"see_pos", "side_acomba.html":"see_acomba",
                                "side_contracts.html":"see_contracts",
                                "side_pickings.html":"see_pickings", "side_others.html":"see_others",  }

    # since permission in django are application-based, and we use that dummy application to assign non-app specific permission
    application_prefix = "projectlibs"


    def get_context_data(self, **kwargs):
        # since depending on the MRO, we may not have a super().get_context_data method.
        try:
            ctx = super().get_context_data(**kwargs)
        except:
            ctx = {}
        ctx["sections_to_render"] = []
        for section, permission in self.side_sections_and_groups.items():
            if self.request.user.has_perm(f"{self.application_prefix}.{permission}"):
                 ctx["sections_to_render"].append(section)
        return ctx

(The above would kinda swallow the Exceptions raised, so you’d want a more specific exception clause, and then a generic one to bubble up the others… this is just prototype code)

Then in the relevent View class, all you need is to add the SideBarMixin:
class ProductListView(LoginRequiredMixin, PermissionRequiredMixin, AccessMixin, SideBarMixin, ListView):

It seemed reasonable to me to put it towards the end. The actual View class must be last, but then I’d check if the user is logged in before checking.

Then all that’s left is to create users, create groups and permissions. My SideBarMixin checks for specific permissions (see_contracts, see_pos, etc.). Some user groups may have to see multiple sections - that’s fine, I just need to add more than one permission to that group.

So basically, the SideBarMixin does all the work. It adds to the context the name of the template(s) that can be rendered. The template then just renders everything (or nothing, if nothing was added in context[“sections_to_render”].

The above does suppose you’re using something like a DetailView, ListView etc… basically any view that calls get_context_data(). If you’re using a plain View, then you’d have to adjust so that this appends in your get() methodd or something.

“projectlibs” is basically a meta-application in my project. Basically its job is to manage common stuff to many applications, such as common tags for instance. I like to put that in an application, because then I can easily migrate that back to my generic template (since a Django app is easy to move to another Django project).