AllowAny, IsAuthenticated, IsAdminUser, and the shipped set
standardbeginnerA permission class answers "may this caller do this?" after authentication has already answered "who is this?". DRF ships a handful: `AllowAny` lets everything through, `IsAuthenticated` requires a signed-in user, `IsAdminUser` requires `user.is_staff` (not `is_superuser`, despite the name), and `IsAuthenticatedOrReadOnly` allows anyone to read but only signed-in users to write. `DjangoModelPermissions` maps HTTP methods onto Django's own `add`/`change`/`delete` model permissions. Every class in `permission_classes` must pass — the list is an AND, and the first failure ends the request.
Think of it as
The shipped classes are deliberately coarse: they answer questions about the *caller*, not about the *object*. "Are you signed in?" and "are you staff?" need nothing but `request.user`, which is why they can run before the view has fetched anything. That is also their limit — none of them can express "only the person who created this order", because at the moment they run there is no order yet. Treat them as the outer gate: they keep the obviously-unauthorised out cheaply, and anything that depends on which row is being touched belongs in `has_object_permission` or in `get_queryset()` instead. The default when you set nothing is `AllowAny`, which means an endpoint with no `permission_classes` and no global default is fully public — the failure is silent, so the global default is worth setting to `IsAuthenticated` and opening endpoints up deliberately.
What we're doing: Close the project by default, then open a health endpoint and a staff-only report deliberately.
- 2
- The one line that changes the failure mode of the whole project: a new view that forgets `permission_classes` now inherits "signed in required" instead of "public".
- 6
- Emptying `authentication_classes` too keeps a health check cheap — no session lookup, no token query, on an endpoint a load balancer hits constantly.
- 12
- `IsAdminUser` is `is_staff`. If this report should be superuser-only, the shipped classes do not cover it and a custom class is needed.
Why this works: Defaulting to `IsAuthenticated` and opening endpoints one at a time turns a forgotten permission into a 403 during development rather than a public endpoint discovered in production — the mistake becomes loud instead of silent.
- AllowAny: between anonymous caller and authenticated caller, between safe methods · GET/HEAD/OPTIONS and unsafe methods · POST/PUT/PATCH/DELETE — the default — allows every cell
- IsAuthenticatedOrReadOnly: anonymous caller, safe methods · GET/HEAD/OPTIONS — anonymous reads allowed; writes require a user
- IsAuthenticated: authenticated caller, between safe methods · GET/HEAD/OPTIONS and unsafe methods · POST/PUT/PATCH/DELETE — the whole right column, nothing on the left
- DjangoModelPermissions: authenticated caller, unsafe methods · POST/PUT/PATCH/DELETE — writes need add/change/delete; GET is unrestricted
- IsAdminUser: authenticated caller, unsafe methods · POST/PUT/PATCH/DELETE — is_staff only — the narrowest shipped class
The shipped permission classes
Together
Remember: Permission classes answer "may they?" using only `request.user` — they run before any object is fetched, so none of them can express ownership. The list is AND: every class must pass. `IsAdminUser` means `is_staff`, not superuser. The out-of-the-box default is `AllowAny`, so set `DEFAULT_PERMISSION_CLASSES` to `IsAuthenticated` and open endpoints deliberately.
See also: custom basepermission and object level checks · role based permissions and composition · model permissions and groups

