agentsclimarketplace

Rbac permissions

Skill Deadlymind/nanolama/skills/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.

Install
npx -y skills add Deadlymind/nanolama --skill rbac-permissions

Assembled 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:

  1. Deny by default. Return False unless the user's roles grant the required codename. No codename resolved → deny; anonymous → deny.
  2. Map the DRF action to the codename. create/list/retrieve/update/ partial_update/destroy and every custom @action map to one {resource}_{action} string. has_object_permission re-checks per object.
  3. Guard every custom @action explicitly. 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_queryset filtering by entreprise hides 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 in perm_action_map (resolved at check time, not in the handler body) so an unmapped action denies, and fetch detail objects via get_object() so the per-object check runs.
  • has_object_permission runs only on single-object lookups fetched via get_object(); list endpoints are gated by has_permission alone.
  • Never trust a role/permission name sent in the request — resolve it from request.user server-side (see security-review).
  • Order matters — an empty or missing perm_base/action must fail closed, so guard the None case 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 — otherwise has_perm finds 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-tenancy
  • drf-api
  • security-review

Keep looking

Skills are one crate of 328,083. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.