Rbac permissions
A portable Claude Code skills library (plugin) for a Django 5.2 + Next.js 16 house stack: 30 model-agnostic, tenant-isolation-and-security-first skills.
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.
2 things to look at
- 21 days oldThe repository was created 21 days ago. New is not bad, but a brand new repository carrying a familiar-sounding name is the shape a typosquat arrives in, and there has been no time for anyone else to find a problem with it.
- 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.
What its author says it does
Copied from the file, not written here
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).
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