Permission-Aware Affordances
Permission-aware affordances let a user open a PowerCRUD screen without seeing operations they are not allowed to use.
This is an affordance and PowerCRUD-owned endpoint feature. It is not a replacement for your application's authorization layer.
The Four Layers
Keep these concerns separate:
- Screen access: can the user open the list or detail view?
- Operation permission: can the user perform this kind of operation at all?
- Row or workflow state: is the operation valid for this object right now?
- Backend enforcement: does the endpoint or service reject direct unauthorized attempts?
PowerCRUD helps with operation affordances. For PowerCRUD-owned create, detail, update, delete, inline-update, bulk update, and bulk delete endpoints, it also enforces the matching built-in permission hooks.
For custom extra_actions and extra_buttons, PowerCRUD can hide or disable the UI affordance. The endpoint is yours, so it must still enforce its own permission and business rules.
Built-In Create, Detail, Edit, And Delete
Use the built-in hooks when the standard PowerCRUD-owned operations should be available only to some users.
class CaseCRUDView(PowerCRUDMixin, CRUDView):
def has_power_create_permission(self, request):
return request.user.has_perm("cases.add_case")
def has_power_detail_permission(self, request, obj):
return request.user.has_perm("cases.view_case")
def has_power_update_permission(self, request, obj):
return request.user.has_perm("cases.change_case")
def has_power_delete_permission(self, request, obj):
return request.user.has_perm("cases.delete_case")
When these hooks return False:
- Create is removed from the list toolbar.
- Built-in Detail/View, Edit, and Delete are removed from row actions.
- Direct PowerCRUD create, detail, update, delete, and inline-update requests are rejected.
The default implementation returns True, so existing views remain open unless you override the hooks.
has_power_detail_permission() covers the built-in Detail/View row action and PowerCRUD-owned detail endpoint only. It is not a whole-screen list-access hook, and it does not authorize arbitrary linked fields or downstream custom detail-like endpoints.
You may also override the denial response:
def handle_power_permission_denied(self, request, operation, obj=None):
return HttpResponseForbidden(f"You cannot {operation} cases.")
Built-In Bulk Update And Bulk Delete
Use the bulk hooks when PowerCRUD-owned bulk operations should be available only to some users.
class CaseCRUDView(PowerCRUDMixin, CRUDView):
def has_power_bulk_update_permission(self, request):
return request.user.has_perm("cases.change_case")
def has_power_bulk_delete_permission(self, request):
return request.user.has_perm("cases.delete_case")
When these hooks return False:
- bulk update fields and Apply Changes are removed from the bulk modal
- the bulk delete section is removed from the bulk modal
- the bulk modal is denied entirely when neither bulk operation is permitted
- direct PowerCRUD bulk update and bulk delete submissions are rejected
- row selection controls are hidden when no permitted bulk operation or permitted selection-aware extra button needs them
The hooks are operation-level. bulk_fields remains the allow-list for which fields may be bulk edited.
Extra Row Actions
Base API extra_actions dictionaries and PowerAction support the same permission fields.
Base API and Structured API
extra_actions = [
{
"text": "Send for Approval",
"url_name": "cases:send-for-approval",
"permission_check": "can_send_for_approval",
"permission_behavior": "hide",
"disabled_state": "get_send_for_approval_disabled_state",
},
]
def can_send_for_approval(self, request, obj=None):
return CasePermissionService.can_manage_cases(request.user)
def get_send_for_approval_disabled_state(self, obj, request):
if obj.status != "draft":
return "Only draft cases can be sent for approval."
return None
PowerAction(
text="Send for Approval",
url_name="cases:send-for-approval",
permission_check="can_send_for_approval",
permission_behavior="hide",
disabled_state="get_send_for_approval_disabled_state",
)
def can_send_for_approval(self, request, obj=None):
return CasePermissionService.can_manage_cases(request.user)
def get_send_for_approval_disabled_state(self, obj, request):
if obj.status != "draft":
return "Only draft cases can be sent for approval."
return None
Permission is checked before hidden_if and disabled_state. This keeps capability checks separate from row or workflow state.
Extra Toolbar Buttons
Base API extra_buttons dictionaries and PowerButton use the same permission fields.
Base API and Structured API
extra_buttons = [
{
"text": "Selected Summary",
"url_name": "cases:selected-summary",
"uses_selection": True,
"selection_min_count": 1,
"selection_min_behavior": "disable",
"selection_min_reason": "Select at least one case first.",
"permission_check": "can_use_selected_summary",
"permission_behavior": "hide",
},
]
def can_use_selected_summary(self, request, obj=None):
return CasePermissionService.can_manage_cases(request.user)
PowerButton(
text="Selected Summary",
url_name="cases:selected-summary",
uses_selection=True,
selection_min_count=1,
selection_min_behavior="disable",
selection_min_reason="Select at least one case first.",
permission_check="can_use_selected_summary",
permission_behavior="hide",
)
def can_use_selected_summary(self, request, obj=None):
return CasePermissionService.can_manage_cases(request.user)
Permission is checked before selection-state disabling. If permission fails with the default hide behavior, the button is removed instead of showing a disabled selection prompt.
Permission fields
Use one of these per custom action or button:
permission: a Django permission string resolved byhas_power_permission(permission, request, obj=None).permission_check: a named view method with signaturepermission_check(request, obj=None).
Do not set both on the same declaration.
Optional behavior fields:
permission_behavior:"hide"or"disable". Defaults to"hide".permission_denied_reason: tooltip text used only whenpermission_behavior = "disable".
The default has_power_permission(...) delegates to request.user.has_perm(permission). Override it if your project uses a different permission service.
Hide Versus Disable
Use hide for permission failure:
- the user cannot create this model
- the user cannot edit or delete this row type
- the user cannot run an approval operation at all
Use disable for row or workflow state:
- the user can approve cases, but this case is not ready
- the user can edit, but this row is locked
- the user can run a selected-row action, but no rows are selected
This is why hidden_if, disabled_state, can_update_object(), and can_delete_object() remain row or workflow-state hooks. They should compose with permission checks, not replace them.
Backend Responsibility
PowerCRUD enforces permission hooks only for operations it owns:
- standard create
- standard detail
- standard update
- standard delete
- inline update
- bulk update
- bulk delete
Custom extra_actions and extra_buttons call downstream endpoints. Hiding or disabling those controls makes the UI truthful, but the endpoint must still enforce permission directly.
or:
def send_for_approval(request, pk):
if not CasePermissionService.can_manage_cases(request.user):
return HttpResponseForbidden()
...
Sample App Demo
The sample app includes viewer and manager users in the runtime login menu.
On /sample/bigbook/:
- viewer users can open the list but do not see built-in Create, Detail/View, Edit, Delete, the permission-hidden row action, or the permission-hidden toolbar button
- manager users see those affordances
- row and selection state can still disable controls after permission passes
The /sample/powerfield-book/ view mirrors the same behavior through PowerAction and PowerButton.