Install
$ agentstack add skill-deadlymind-nanolama-rbac-permissions ✓ scanned · ✓ verified, works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ● Network access Used
- ✓ Filesystem access No
- ✓ Shell / process execution No
- ✓ Environment & secrets No
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
Verified badge
Passed review? Show it. Paste this badge into your README, it links to the public security report.
Reliability & compatibility
Declared compatibility
Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.
We're building live execution health for every listing: tool-call success rate, median latency, uptime, and last-checked timestamps, measured, not self-reported. It isn't live yet, so we don't show numbers we can't stand behind.
How agent discovery & health will work →About
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/destroy and every custom @action map to one {resource}_{action} string. has_object_permission re-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 via get_object() so the per-object check runs.
has_object_permissionruns 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-tenancydrf-apisecurity-review
Source & license
This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.
- Author: Deadlymind
- Source: Deadlymind/nanolama
- License: MIT
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet, be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.