Permissions¶
Use this guide to enforce Django permissions on reverse relations and craft
custom policies beyond the default change checks. Ready-to-run snippets live
in Permissions on reverse fields.
Permission modes¶
Set reverse_permissions_enabled=True to have the mixin evaluate access
before rendering or saving a virtual field. reverse_permission_mode controls
how denied access surfaces:
"disable"(default) — render the field disabled and ignore submitted changes. The mixin also setsrequired=Falseon disabled reverse fields so that forms do not raise “This field is required.” when no initial value is present and the browser omits the disabled input from POST data."hide"— omit the field entirely.
Note
hide removes the input, so any required=True on the virtual field
does not apply. Use disable if you need the field to remain visible/read-only
while relaxing required semantics.
Use standard admin view guards if you want to block the whole page instead of a single field.
Render-time policies¶
By default, the render gate only consults a base/global permission to decide
visibility and editability. To let per-field (or global) policies influence
visibility at render time, set the class flag
reverse_render_uses_field_policy = True. In this mode the render gate calls
has_reverse_change_permission(request, obj, config, selection=None). Policies
must therefore handle selection=None sensibly.
Example:
class CompanyAdmin(ReverseRelationAdminMixin, admin.ModelAdmin):
reverse_permissions_enabled = True
reverse_render_uses_field_policy = True
reverse_relations = {
"department_binding": ReverseRelationConfig(
model=Department,
fk_field="company",
multiple=False,
permission=lambda request, obj, config, selection: getattr(request.user, "is_staff", False),
)
}
The per-field policy above is evaluated during render with selection=None. If it
returns False, the field will be hidden or disabled according to
reverse_permission_mode.
Custom policies¶
Permissions can be supplied globally via
has_reverse_change_permission()
or per field via ReverseRelationConfig.permission. You may provide policies
in three ergonomic shapes with identical semantics (return True to allow,
False to deny).
A callable with the signature (request, obj, config, selection) -> bool
is ideal for tiny, stateless predicates.
A Policy object implementing __call__ and optionally
permission_denied_message is useful for bundling state or reusable
messages.
An object exposing has_perm(request, obj, config, selection) -> bool
is useful when adapting existing helpers. The permission_denied_message
attribute is honoured if present.
Per-field policies override the global method when provided.
Evaluation flow¶
Permission checks run at three points:
Render gate — decides whether a field is hidden or disabled before templates render. By default, it checks a base permission. If
reverse_render_uses_field_policyisTrue, it consults the full per-field or global policy (withselection=None).Validation gate — once a selection exists, the per-field policy runs (if defined) and otherwise the global policy is invoked. Denials raise a field error using the precedence below. If no custom policy is configured at all (neither per-field nor global), the mixin does not attach validation errors for base permission denials; instead, the UI gating (
hide/disable) applies, and hidden/disabled inputs are ignored on save.Persistence gate — as a safety net, the mixin excludes unauthorized fields from the update payload so crafted POSTs cannot persist changes. This includes hidden and disabled reverse fields.
Error message precedence¶
When a policy denies a selection, the field error message resolves in this order:
ReverseRelationConfig.permission_denied_messagepermission.permission_denied_messageon the per-field policy objectreverse_permission_policy.permission_denied_messageon the global policy objectDefault
"You do not have permission to choose this value."
Source |
Example attribute path |
Precedence |
|---|---|---|
Field override |
|
1 (highest) |
Per-field policy object |
|
2 |
Global policy object |
|
3 |
Built-in default |
|
4 (lowest) |
Visualising the flow¶
flowchart TD
subgraph Render
A[Start] --> B{reverse_permissions_enabled?};
B -- No --> RenderNormal[Render normally];
B -- Yes --> RenderGate{Render gate};
RenderGate --> CheckPolicyMode{reverse_render_uses_field_policy?};
CheckPolicyMode -- Yes --> CheckFieldPolicy{has_reverse_change_permission?};
CheckPolicyMode -- No --> CheckBasePerms{_has_base_permission?};
CheckFieldPolicy -- Deny --> SetVisibility;
CheckBasePerms -- Deny --> SetVisibility{Mode?};
SetVisibility -- hide --> Hide[Hide field];
SetVisibility -- disable --> Disable[Disable field];
CheckFieldPolicy -- Allow --> Visible[Visible/editable];
CheckBasePerms -- Allow --> Visible;
end
subgraph Validate and Persist
RenderNormal --> Clean;
Hide --> Clean;
Disable --> Clean;
Visible --> Clean[clean: run cfg.clean if present];
Clean --> Validate{has_reverse_change_permission?};
Validate -- Deny --> AddError[Add field error];
Validate -- Allow --> OK;
AddError --> Persist;
OK --> Persist[Persistence gate];
Persist --> FilterPayload[Build payload only for allowed fields];
FilterPayload --> Save[save: transaction.atomic; unbind before bind];
end
Minimal examples¶
def only_staff(request, obj, config, selection):
return getattr(request.user, "is_staff", False)
ReverseRelationConfig(..., permission=only_staff)
class OrgPolicy:
def __init__(self, org_id: int):
self.org_id = org_id
permission_denied_message = "You lack access to this organization."
def __call__(self, request, obj, config, selection):
return getattr(request.user, "org_id", None) == self.org_id
ReverseRelationConfig(..., permission=OrgPolicy(org_id=42))
class CanBindAdapter:
permission_denied_message = "Not allowed to bind this item."
def has_perm(self, request, obj, config, selection):
# delegate to some legacy checker
return legacy_can_bind(request.user, selection)
ReverseRelationConfig(..., permission=CanBindAdapter())
See also
Rendering & Visibility — Visibility vs editability and the render gate.
Recipes — End-to-end permission setups in context.
Core Concepts — Where permissions fit in the lifecycle.