RizTech Academy logo
RizTech Academy
Authentication and PermissionsLesson 3 of 530 min

Permissions and access control

Authentication proves who a user is; authorisation decides what they may do. A clinic receptionist should add patients but not delete billing records; a lab technician should upload reports but not manage staff accounts. Django has a built-in permission system — per-model permissions, grouped into roles — that handles most of this without a line of custom code. This lesson is how to use it, and how to enforce it in views and templates.

The permissions Django creates for you

For every model, Django automatically creates four permissions when you migrate: add, change, delete, and view. For Patient they are named patients.add_patient, patients.change_patient, patients.delete_patient, patients.view_patient (the pattern is app_label.action_modelname). You did not write these — they exist the moment the model does. Checking one:

user.has_perm("patients.add_patient")

Verified: a fresh user returns False; after the permission is granted, True. has_perm is the single question you ask to authorise an action — "may this user do this thing?" — and it returns a boolean you branch on.

Granting permissions: directly, or (better) via groups

You can assign a permission straight to a user:

from django.contrib.auth.models import Permission
perm = Permission.objects.get(codename="add_patient")
user.user_permissions.add(perm)
# verified: has_perm("patients.add_patient") flips False -> True after this

But assigning permissions user-by-user does not scale — with twenty staff you would repeat the same grants twenty times, and a policy change means editing twenty users. The better approach is groups: a group is a named bundle of permissions (a role), and you put users in groups.

from django.contrib.auth.models import Group, Permission

reception = Group.objects.create(name="Receptionists")
reception.permissions.add(Permission.objects.get(codename="view_patient"))
reception.permissions.add(Permission.objects.get(codename="add_patient"))

user.groups.add(reception)      # user now has the group's permissions
# verified: user.has_perm("patients.view_patient") is now True via the group

has_perm checks both direct permissions and those inherited from groups — verified, a user gains view_patient purely by group membership. Define groups for your roles (Receptionists, Technicians, Managers), grant each the permissions its role needs, and add users to groups. Now "what can a receptionist do?" is answered in one place, and onboarding a new receptionist is one group assignment. Model roles as groups, not per-user grants — it is the difference between a maintainable permission system and a mess.

Superuser and the permission short-circuit

A superuser (is_superuser=True) passes every has_perm check automatically — has_perm returns True for a superuser regardless of what is actually assigned. This is why the superuser can do everything in the admin. Useful, but it means you cannot test permission logic as a superuser (every check passes); use a normal user with specific permissions to verify authorisation actually works. Note too the is_staff flag (distinct from permissions) simply controls whether a user may log into the admin at all.

Enforcing permissions in views

Checking a permission is one thing; enforcing it is another. For function views, the permission_required decorator:

from django.contrib.auth.decorators import permission_required

@permission_required("patients.add_patient", raise_exception=True)
def patient_create(request):
    ...

Without the permission, the user gets a 403 Forbidden (with raise_exception=True) or is redirected to login (without it). For class-based views, the PermissionRequiredMixin:

from django.contrib.auth.mixins import PermissionRequiredMixin

class PatientCreateView(PermissionRequiredMixin, CreateView):
    permission_required = "patients.add_patient"
    ...

You can also check inline when the logic is conditional:

if not request.user.has_perm("patients.delete_patient"):
    return HttpResponseForbidden("Not allowed")

The decorator/mixin is cleaner for a whole view; the inline has_perm is for branching within a view.

Hiding what a user cannot do, in templates

Enforcing on the server is mandatory (never trust the client), but you should also hide controls a user cannot use, so they are not shown buttons that will only 403. Permissions are available in templates via the perms object:

{% if perms.patients.add_patient %}
  <a href="{% url 'patient_create' %}">Add patient</a>
{% endif %}
{% if perms.patients.delete_patient %}
  <button>Delete</button>
{% endif %}

perms.patients.add_patient is the template form of has_perm("patients.add_patient"). This is presentation only — it hides the link, it does not secure the action. The security is the permission_required on the view; the template check is courtesy. Always enforce on the server; hide in the template as well. Hiding without enforcing is no security at all (a user can just visit the URL); enforcing without hiding shows dead buttons. Do both.

Check your work

The four automatic permissions. Every model gets add, change, delete, view, named app.action_model (e.g. patients.add_patient), created on migrate.

How to check. user.has_perm("patients.add_patient") returns a boolean — verified False for a fresh user, True after granting.

Direct grants versus groups. user.user_permissions.add(perm) grants directly; better, put permissions on a Group (a role) and add users to it — has_perm checks both (verified via group). Model roles as groups.

Superuser and is_staff. A superuser passes every has_perm (so test authorisation as a normal user); is_staff only controls admin login, separate from permissions.

Enforcing in views. @permission_required(..., raise_exception=True) / PermissionRequiredMixin for a whole view; inline has_perm for conditional branches.

Templates hide, views secure. {% if perms.app.perm %} hides controls (courtesy); the permission_required on the view is the real security. Do both — hiding alone is no protection.

Practice

  1. Migrate a model and, in the shell, list its four auto-created permissions. Check has_perm on a fresh user (False).
  2. Grant add_patient directly to a user; confirm has_perm flips to True.
  3. Create a "Receptionists" group with view_patient and add_patient; add a user and confirm they have those permissions via the group (not a direct grant).
  4. Protect patient_create with @permission_required(..., raise_exception=True); confirm a user without the permission gets 403 and one with it gets through.
  5. Add {% if perms.patients.add_patient %} around the "Add patient" link; confirm it hides for a user without the permission — then visit the URL directly and confirm the view still blocks them (proving the template check is not security).
  6. Try to test a permission as a superuser and observe every check passing; redo as a normal user.

Official documentation

Next: object-level access for sensitive records.

Stuck on this lesson?

Being stuck is part of it — but being stuck alone for three days is not. Our internship programme pairs this curriculum with code review and one-to-one help from working developers, and it is free.

About the internship