In Django, how does a user get permissions through a Group, and how do group grants combine with user.user_permissions?
answer
- two many-to-many sources
- a set union
- no deny rule
- user_set on the group side
basics
~20 sA Group holds a set of permissions, and every user in user.groups inherits them. ModelBackend takes the union of the group permissions and the direct user.user_permissions, so a permission from either source grants access and nothing can subtract one.
solid answer
~30 sA `Group` is a named bundle with a many-to-many `permissions` field; adding a user with `user.groups.add(group)` gives them all of its permissions. `ModelBackend` computes the effective set as the union of `get_user_permissions()` (direct grants in `user.user_permissions`) and `get_group_permissions()` (from every group), and `has_perm()` tests membership in that union. Permissions are purely additive: there is no deny, so removing someone from a group does not remove a direct grant of the same permission, and belonging to several groups gives the union of all of them. In a clinic, Nurses, Doctors and Clinic admins become groups, with direct grants kept for true exceptions.
code
python · 17 linesfrom django.contrib.auth import get_user_model
from django.contrib.auth.models import Group, Permission
User = get_user_model()
nurses = Group.objects.get(name="Nurses")
nurses.permissions.add(
Permission.objects.get(content_type__app_label="clinic", codename="add_vitalsign")
)
nurse = User.objects.get(username="amira")
nurse.groups.add(nurses)
nurse = User.objects.get(pk=nurse.pk) # new instance, empty permission cache
nurse.get_group_permissions() # {'clinic.add_vitalsign', ...}
nurse.get_user_permissions() # direct grants only
nurse.has_perm("clinic.add_vitalsign") # Truego deeper
Know that Group bundles permissions, that user.groups and user.user_permissions are the two sources, and that has_perm() sees both.
Explain the union computed by ModelBackend, the three get_*_permissions methods, and why there is no way to subtract a permission.
Keep direct grants rare, check permissions rather than group names, and know how to handle an exception without inventing deny semantics.
Decide when global groups stop being enough — per-site or per-patient scoping — and what that implies for the authorization model.
## The two ways a user holds a permission In `django.contrib.auth`, a user can hold a model permission (a `Permission` row such as `clinic.view_patient`) in exactly two ways when the default `ModelBackend` is in use: - **Directly**, through the many-to-many field `user.user_permissions`. - **Through a group**, via the many-to-many field `user.groups`. A `Group` is a small model with a unique `name` (up to 150 characters) and its own many-to-many `permissions` field. Every member of the group holds every permission granted to the group. Both fields come from `PermissionsMixin`, which the built-in `User` model and `AbstractUser` include. ## How the effective set is computed When code calls `user.has_perm("clinic.add_prescription")`, `ModelBackend` builds the user's permission set as **the union** of two sets of `"app_label.codename"` strings: | Method | Returns | |---|---| | `user.get_user_permissions()` | strings from `user.user_permissions` | | `user.get_group_permissions()` | strings from every group in `user.groups` | | `user.get_all_permissions()` | the union of the two | `has_perm()` then tests membership in that union. Three consequences follow: 1. **Permissions are purely additive.** There is no "deny" permission and no priority between sources. If either source grants `add_prescription`, the user has it. 2. **Removing a user from a group** removes only what the group gave; a direct grant of the same permission stays in force. 3. **A user in several groups** simply gets the union of them all — a doctor who is also on the admin rota holds both groups' rights. ## Modelling clinic roles as groups A clinic app with `Patient`, `VitalSign`, `Prescription` and `Appointment` models might define: - **Nurses** — `view_patient`, `add_vitalsign`, `change_vitalsign`; - **Doctors** — `view_patient`, `change_patient`, `add_prescription`, `view_vitalsign`; - **Clinic admins** — `add_appointment`, `change_appointment`, `delete_appointment`, `view_patient`. Hiring a nurse becomes one call, `user.groups.add(nurses)`; changing what nurses may do is one edit to the group, applied to every nurse at once. Direct `user_permissions` are then kept for genuine exceptions — a nurse who also covers the pharmacy — so that the rules stay readable. Groups can also serve as a plain label ("on-call staff") that code checks by membership, but for authorization prefer checking a **permission** over checking a group name: renaming a group or splitting a role should not silently break access checks scattered through views. ## Useful queries in both directions - `user.groups.all()` — the groups a user is in; - `group.user_set.all()` — the members of a group (the reverse accessor is named `user_set` on `PermissionsMixin`); - `User.objects.filter(groups__name="Nurses")` — filter users by group (the reverse query name is `user`, so `Group.objects.filter(user=...)` works in the other direction). ## Assigning and revoking in code Membership and grants are ordinary many-to-many operations, available from either side: - `user.groups.add(doctors)` or `doctors.user_set.add(user)` — join a role; - `user.groups.remove(doctors)` — leave it, losing only what that group supplied; - `user.groups.set([nurses])` — replace all memberships at once, handy when a role changes on promotion; - `doctors.permissions.add(perm)` or `.remove(perm)` — change the role for every member at once; - `user.user_permissions.add(perm)` — a one-off exception for one person. The admin exposes the same data: the group change form shows its permissions in a two-pane selector, and the user change form lists the user's groups and direct permissions. Because both screens edit the same tables, a grant made in the admin and one made in code are indistinguishable to `has_perm()`. ## What is not in this picture - **Superusers** bypass all of this: an active superuser passes every `has_perm()` check without holding any rows. - **Inactive users** get an empty set from `ModelBackend`, whatever their groups say. - The permission set is **cached on the user instance** after the first check, so a group change made mid-request is not visible to that same object until it is re-fetched. - Groups are **global**: "Doctors" means doctor everywhere in the application. Per-ward or per-patient scoping needs object-level checks or a different data model. ## Interview angle A good answer names both sources (`user.groups` and `user.user_permissions`), says the effective set is their **union**, and draws the consequence without prompting: there is no deny, so exceptions are handled by shaping groups, not by subtracting rights. Strong candidates add that checks should name permissions rather than groups, and that membership changes made in the middle of a request are not visible to a user object that has already been checked.
- How do you stop one doctor from prescribing when the Doctors group grants add_prescription?Not with the built-in tables: they are additive, with no deny. Either restructure groups — for example split out a Prescribers group and leave that doctor out — or add a custom authentication backend whose `has_perm()` raises `PermissionDenied` for that case, which makes Django stop checking further backends and return `False`.
- Why check has_perm('clinic.add_prescription') rather than whether the user is in the Doctors group?A permission names the action, a group names a role. If nurse practitioners later gain prescribing rights, you grant the permission to their group and every check keeps working; code that tested for membership of Doctors would need editing in every place it appears.
Groups work like staff badges. A nurse's badge opens certain doors, a doctor's badge opens others, and one person can carry several badges plus a personal key. A door opens if any of them fits; there is no badge that stops another from working.
saying these in an interview costs you the question
- A direct grant is overridden when the user's group lacks the permission
- Removing a user from a group removes all their permissions
- Django supports negative permissions to deny one group member
- A user can belong to only one group at a time
- Checking group names in views is equivalent to checking permissions