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
- Migrate a model and, in the shell, list its four auto-created permissions. Check
has_permon a fresh user (False). - Grant
add_patientdirectly to a user; confirmhas_permflips to True. - Create a "Receptionists" group with
view_patientandadd_patient; add a user and confirm they have those permissions via the group (not a direct grant). - Protect
patient_createwith@permission_required(..., raise_exception=True); confirm a user without the permission gets 403 and one with it gets through. - 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). - Try to test a permission as a superuser and observe every check passing; redo as a normal user.
Official documentation
- Django — Permissions and authorization — Default permissions,
has_perm, groups. - Django —
permission_requiredandPermissionRequiredMixin— Enforcing in views. - Django — Permissions in templates (
perms) — Thepermstemplate object.
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