Chapter 19 · The admin

19.4 Permissions in the admin: has_change_permission and the others

The admin asks a model admin what a request’s user may do with its model, through methods of BaseModelAdmin: has_add_permission, has_change_permission, has_delete_permission, has_view_permission and has_module_permission. The index, a model admin’s pages, actions and inlines, and a related field’s links ask them, and by default each asks the user’s has_perm, or for the module its has_module_perms.

Being staff only gets a user past AdminSite.admin_view (The admin’s URLs: AdminSite.get_urls and AdminSite.admin_view). After that the admin must decide what this user may do with this model’s rows, and a project must be able to decide otherwise: for one object, or never. So the questions are methods, given the request and, for change, delete and view, the object. A model admin’s refused view raises PermissionDenied, a 403 (How an exception becomes a response); a template leaves out what is denied.

The default answers

The defaults for add, change, delete and view join the model’s application label to a codename that get_permission_codename makes of an action and the model’s name, "fleet.change_ship", and ask the user’s has_perm with it, which for auth’s users is PermissionsMixin.has_perm (Permissions: Permission, groups and has_perm). has_view_permission asks for view, then for change: a user who may change may see. BaseModelAdmin.has_view_or_change_permission asks the two methods in turn, so it follows an override of either. has_module_permission asks the user’s has_module_perms with the label; by its docstring it governs the two index pages and restricts no add, change or delete view.

The defaults ignore the object; their docstrings say what an override owes: given an object, whether the request may act on it; given None, whether it may act on any object of the type. The defaults do not hand the object to has_perm, and ModelBackend answers no to any question asked with one: nora’s has_perm for "fleet.change_ship" is true, and with Endeavour as the object false.

ModelAdmin.get_model_perms puts the four, asked with no object, in a dictionary under "add", "change", "delete" and "view". The index leaves out a model whose model admin’s has_module_permission refuses or whose four answers are all false (The site’s own pages: the index, the login and AdminSite.each_context).

Which page asks which

Table 19.3 Where each page asks: ModelAdmin.changelist_view, ModelAdmin._changeform_view, ModelAdmin._delete_view, get_deleted_objects, ModelAdmin.history_view, ModelAdmin._filter_actions_by_permissions, ModelAdmin._get_formset_with_permissions, BaseModelAdmin.formfield_for_dbfield.
page asks when refused
the index, an application’s index asks has_module_permission, then get_model_perms when refused the model left out; an empty application’s index, 404
the change list asks has_view_or_change_permission; has_change_permission for its title, has_add_permission for its add button when refused 403
the add page asks has_add_permission when refused 403
a change page asks has_view_or_change_permission with the object, before its absence is answered when refused 403, for a missing object too
a change page, "POST" of its form asks has_change_permission with the object, before the form is bound; for "_saveasnew", has_add_permission when refused 403
the delete page asks has_delete_permission with the object, before its absence is answered; then, for each registered object the delete would take, its model admin’s when refused 403; a lack is listed, and the delete refused
the history page asks as a change page when refused 403
an action asks for each of its "allowed_permissions", the method named for it, with no object; one true is enough when refused not offered, and not run if posted
list_editable asks has_change_permission, with no object, then for each row when refused no formset, and the "POST" 403; the row’s fields removed, the row not saved
a related field’s widget, unless in raw_id_fields asks the related model admin’s four, with no object when refused the link left out

A user who may view and not change gets a change page to read: ModelAdmin.get_form excludes every field from the form class, ModelAdmin._changeform_view makes every field read-only and titles the page “View”, and Close replaces the save buttons. As nora:

Table 19.4 Nora’s answers. Piet had a 200 or a redirect from each; tomas, not staff, a redirect to the login.
page Ship Port Voyage
change list Ship 200, “Select ship to change” Port 200, “Select port to view” Voyage 403
add page Ship 403 Port 403 Voyage 403
change page of pk 1 Ship 200, “Change ship” Port 200, “View port” Voyage 403
the same, "POST" Ship 302 to the change list Port 403 Voyage 403
delete page Ship 403 Port 403 Voyage 403
history page Ship 200 Port 200 Voyage 403
change page of pk 99 Ship 302 to /admin/ Port 302 to /admin/ Voyage 403

A missing object sends a user who passes the check to the index with a warning; one who fails it gets 403, and cannot tell which keys exist.

Superusers, and staff without permissions

PermissionsMixin.has_perm and PermissionsMixin.has_module_perms answer yes for an active superuser whatever is asked, so every default is true for piet, whom only an override can refuse. Staff with no permission pass admin_view and are refused every model’s pages: tomas, made staff, got an index saying “You don’t have permission to view or edit anything.”, 404 for /admin/fleet/ and 403 for the ship change list.

Inlines

InlineModelAdmin declares the four again; its InlineModelAdmin.has_add_permission takes the parent object. ModelAdmin.get_inline_instances leaves out an inline the user may neither view, change, add to nor delete from, and sets max_num to nought on one the user may not add to. ModelAdmin.get_inline_formsets lets an inline be edited only with the parent’s permission, change on a change page and add on an add page; without it the inline’s add, change and delete are false, and its formset’s extra forms and maximum are set to nought (Inlines: InlineModelAdmin and its formsets). So nora, who may change ships, saw Valletta’s ships read-only: nora may not change the port.

The table a many-to-many field makes has no permissions of its own, as a comment in has_add_permission says, so for an inline of one, add, change and delete ask for change on the model at the other end from the parent, and view for view or change. On PortAdmin, an inline of the table behind Voyage’s calls asks "fleet.change_voyage", and nora did not get it.

Overriding a permission

Two overrides, one per object, one read-only for everyone:

# harbour/fleet/admin.py
@admin.register(Ship)
class ShipAdmin(admin.ModelAdmin):
    ...

    def has_change_permission(self, request, obj=None):
        if obj is not None and obj.tonnage > 300:
            return request.user.is_superuser
        return super().has_change_permission(request, obj)


@admin.register(Voyage)
class VoyageAdmin(admin.ModelAdmin):
    def has_add_permission(self, request):
        return False

    def has_change_permission(self, request, obj=None):
        return False

    def has_delete_permission(self, request, obj=None):
        return False

Nora then got Endeavour’s change page as “View ship” and a 403 for its "POST", while Beagle’s was “Change ship” and saved; the change list, asked with no object, still read “Select ship to change”. Piet got voyages view-only, on the index and the change page, and a 403 from the add page, the delete page and a "POST": has_view_permission, not overridden, still asks has_perm.