Rbac permissions
Gates access inside a tenant with a {resource}_{action} role system on Django/DRF — a deny-by-default permission class that maps the ViewSet action to a codename and checks has_permission/has_object_permission, plus explicit guards on every custom @action. Use when adding or reviewing a DRF permission class, wiring permission_classes on a ViewSet, deciding who may list/create/update/delete a resource, guarding a custom @action, or fixing an unguarded endpoint. Not for tenant row-scoping in get_queryset (see multi-tenancy) or generic serializer/router setup (see drf-api).From its SKILL.md
npx -y skills add Deadlymind/nanolama --skill rbac-permissionsAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 1 stars1 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
SKILL.md
5.6 KB, ~1.2k tokens by cl100k_base, as published. Nobody here has run it
RBAC permissions ({resource}_{action}, deny-by-default)
When to use
Deciding who inside a tenant may perform an action on a resource. Tenant scoping
(multi-tenancy) answers "which rows exist for this user"; RBAC answers "may this
user list/create/update/delete them, or fire this custom action". Keep the two
separate — a queryset filter is not an authorization check.
Pattern
Roles grant named permissions of the form {resource}_{action} (e.g.
invoice_create, invoice_delete). One DRF permission class:
- Deny by default. Return
Falseunless the user's roles grant the required codename. No codename resolved → deny; anonymous → deny. - Map the DRF action to the codename.
create/list/retrieve/update/partial_update/destroyand every custom@actionmap to one{resource}_{action}string.has_object_permissionre-checks per object. - Guard every custom
@actionexplicitly. A custom action gets no codename for free; an unmapped action must fail closed, never fall through to allow.
One reusable class carries all three rules; the ViewSet declares its perm_base and
maps each custom @action to its codename half in perm_action_map, so the class-level
check resolves the codename before the handler body runs (DRF checks permissions in
initial(), ahead of your action code):
# accounts/permissions.py — one deny-by-default class + its wiring
from rest_framework.decorators import action
from rest_framework.permissions import BasePermission
_ACTION_MAP = { # DRF action -> {action} half of the codename
"list": "read", "retrieve": "read", "create": "create",
"update": "update", "partial_update": "update", "destroy": "delete",
}
class HasResourcePermission(BasePermission):
def has_permission(self, request, view):
if not request.user.is_authenticated:
return False
base = getattr(view, "perm_base", None) # e.g. "invoice"
# custom @actions declare their half in perm_action_map; else map the DRF action
act = getattr(view, "perm_action_map", {}).get(view.action) or _ACTION_MAP.get(view.action)
if not base or not act:
return False # unmapped -> deny closed
return request.user.has_perm(f"{base}_{act}") # your role lookup
def has_object_permission(self, request, view, obj):
return self.has_permission(request, view) # re-check per object
class InvoiceViewSet(TenantScopedViewSet): # scoping from multi-tenancy
permission_classes = [HasResourcePermission]
perm_base = "invoice"
perm_action_map = {"approve": "approve"} # custom @action -> codename half
@action(detail=True, methods=["post"])
def approve(self, request, pk=None):
invoice = self.get_object() # runs has_object_permission -> invoice_approve
... # class-level check already passed in initial()
Adapt to your repo
Replace has_perm with your actual role lookup (a roles/permissions M2M,
a cached set on the user, a claim in the JWT — resolve it server-side, never from
the request body). Rename perm_base values to your resources and settle the
codename verbs (read/create/update/delete, or view/add/change).
Confirm the tenant FK is still named entreprise (rename to your tenant) and that
scoping lives in get_queryset, not here.
Gotchas
- Row-scoping is not authorization.
get_querysetfiltering byentreprisehides other tenants' rows; it does not stop an in-tenant user without the role. Keep both. - Custom
@actions bypass the default action map — map each one inperm_action_map(resolved at check time, not in the handler body) so an unmapped action denies, and fetch detail objects viaget_object()so the per-object check runs. has_object_permissionruns only on single-object lookups fetched viaget_object(); list endpoints are gated byhas_permissionalone.- Never trust a role/permission name sent in the request — resolve it from
request.userserver-side (seesecurity-review). - Order matters — an empty or missing
perm_base/action must fail closed, so guard theNonecase before building the codename string. - UI-level checks are UX, never access control. Hiding a button or nav item by role only tidies the screen; the API endpoint is directly callable (curl, devtools, a scripted client), so the server must re-authorize every request. A hidden button protects nothing.
- A deny-by-default class returns 403 for a codename that was never registered/seeded.
A new
{resource}_{action}must exist in the permission store before the gate can grant it — otherwisehas_permfinds nothing and denies. This is the number-one cause of "my new endpoint always returns 403"; seed the codename (and grant it to a role) first.
See also
multi-tenancydrf-apisecurity-review
What ships with it
Read from the repository
Just SKILL.md. No reference files, no scripts.